Merge branch 'master' into dev
# Conflicts: # src/stores/base.ts
This commit is contained in:
217
index.html
217
index.html
@@ -48,75 +48,76 @@
|
||||
<!-- color-scheme 告诉浏览器支持亮/暗模式-->
|
||||
<meta name="color-scheme" content="light dark"/>
|
||||
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-50T6DRD837"></script>
|
||||
<script>
|
||||
if (
|
||||
!location.href.includes('localhost')
|
||||
&& !location.href.includes('192.168')
|
||||
&& !location.href.includes('172.16')
|
||||
&& !location.href.includes('10.0')
|
||||
) {
|
||||
//51.la
|
||||
(function () {
|
||||
window.LA = window.LA || {
|
||||
ids: [{id: "3OH8ITYRgwzo58L2", ck: "3OH8ITYRgwzo58L2"}],
|
||||
id: "3OH8ITYRgwzo58L2",
|
||||
ck: "3OH8ITYRgwzo58L2",
|
||||
hashMode: true
|
||||
};
|
||||
const script = document.createElement('script');
|
||||
script.src = `https://typewords.cc/libs/51.js`;
|
||||
document.head.appendChild(script);
|
||||
})();
|
||||
|
||||
</script>
|
||||
// Cloudflare
|
||||
(function () {
|
||||
var cf = document.createElement("script");
|
||||
cf.src = 'https://static.cloudflareinsights.com/beacon.min.js'
|
||||
cf.setAttribute("data-cf-beacon", '{"token": "e5119992696d4155814400dd69781d68"}');
|
||||
document.head.appendChild(cf);
|
||||
})();
|
||||
|
||||
<script>
|
||||
</script>
|
||||
// google
|
||||
(function () {
|
||||
var ana = document.createElement("script");
|
||||
ana.src = 'https://www.googletagmanager.com/gtag/js?id=G-50T6DRD837'
|
||||
ana.onload = function () {
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
|
||||
<script>
|
||||
if (!location.href.includes('localhost')
|
||||
&& !location.href.includes('192.168')
|
||||
&& !location.href.includes('172.16')
|
||||
&& !location.href.includes('10.0')
|
||||
) {
|
||||
//https://51.la/
|
||||
!function(p){"use strict";!function(t){var s=window,e=document,i=p,c="".concat("https:"===e.location.protocol?"https://":"http://","sdk.51.la/js-sdk-pro.min.js"),n=e.createElement("script"),r=e.getElementsByTagName("script")[0];n.type="text/javascript",n.setAttribute("charset","UTF-8"),n.async=!0,n.src=c,n.id="LA_COLLECT",i.d=n;var o=function(){s.LA.ids.push(i)};s.LA?s.LA.ids&&o():(s.LA=p,s.LA.ids=[],o()),r.parentNode.insertBefore(n,r)}()}({id:"3OH8ITYRgwzo58L2",ck:"3OH8ITYRgwzo58L2",autoTrack:true,hashMode:true});
|
||||
function gtag() {
|
||||
dataLayer.push(arguments);
|
||||
}
|
||||
|
||||
// Cloudflare
|
||||
(function () {
|
||||
var cf = document.createElement("script");
|
||||
cf.src = 'https://static.cloudflareinsights.com/beacon.min.js'
|
||||
cf.setAttribute("data-cf-beacon", '{"token": "e5119992696d4155814400dd69781d68"}');
|
||||
var s = document.getElementsByTagName("script")[0];
|
||||
s.parentNode.insertBefore(cf, s);
|
||||
})();
|
||||
|
||||
// google
|
||||
(function () {
|
||||
var ana = document.createElement("script");
|
||||
ana.src = 'https://www.googletagmanager.com/gtag/js?id=G-50T6DRD837'
|
||||
ana.onload = function () {
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'G-50T6DRD837');
|
||||
}
|
||||
var s = document.getElementsByTagName("script")[0];
|
||||
s.parentNode.insertBefore(ana, s);
|
||||
})();
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'G-50T6DRD837');
|
||||
}
|
||||
document.head.appendChild(ana);
|
||||
})();
|
||||
|
||||
|
||||
// baidu
|
||||
var _hmt = _hmt || [];
|
||||
(function () {
|
||||
var hm = document.createElement("script");
|
||||
hm.src = "https://hm.baidu.com/hm.js?3dae52fcd5375a19905462e4ad3eb54e";
|
||||
var s = document.getElementsByTagName("script")[0];
|
||||
s.parentNode.insertBefore(hm, s);
|
||||
})();
|
||||
// baidu
|
||||
var _hmt = _hmt || [];
|
||||
(function () {
|
||||
var hm = document.createElement("script");
|
||||
hm.src = "https://hm.baidu.com/hm.js?3dae52fcd5375a19905462e4ad3eb54e";
|
||||
document.head.appendChild(hm);
|
||||
})();
|
||||
|
||||
// umami
|
||||
(function () {
|
||||
var umami = document.createElement("script");
|
||||
umami.src = 'https://typewords.cc/libs/s.js'
|
||||
umami.setAttribute("data-website-id", "160308c9-7900-4b1d-a0b1-c3b25a9530f6");
|
||||
var s = document.getElementsByTagName("script")[0];
|
||||
s.parentNode.insertBefore(umami, s);
|
||||
})();
|
||||
// umami
|
||||
// (function () {
|
||||
// var umami = document.createElement("script");
|
||||
// umami.src = 'https://typewords.cc/libs/s.js'
|
||||
// umami.setAttribute("data-website-id", "160308c9-7900-4b1d-a0b1-c3b25a9530f6");
|
||||
// document.head.appendChild(umami);
|
||||
// })();
|
||||
|
||||
// umami-saas
|
||||
(function () {
|
||||
var umami2 = document.createElement("script");
|
||||
umami2.src = 'https://stat.typewords.cc/script.js'
|
||||
umami2.setAttribute("data-website-id", "4d728ae3-5393-4efe-81dc-30dcb4f33c00");
|
||||
var s = document.getElementsByTagName("script")[0];
|
||||
s.parentNode.insertBefore(umami2, s);
|
||||
})();
|
||||
}
|
||||
// umami-saas
|
||||
// (function () {
|
||||
// var umami2 = document.createElement("script");
|
||||
// umami2.src = 'https://stat.typewords.cc/script.js'
|
||||
// umami2.setAttribute("data-website-id", "4d728ae3-5393-4efe-81dc-30dcb4f33c00");
|
||||
// document.head.appendChild(umami2);
|
||||
// })();
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
@@ -126,56 +127,56 @@
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<script>
|
||||
(function () {
|
||||
var ua = navigator.userAgent || ''
|
||||
var isIE = !!document.documentMode || /MSIE|Trident/i.test(ua)
|
||||
if (!isIE) return
|
||||
var style = document.createElement('style')
|
||||
style.type = 'text/css'
|
||||
style.appendChild(document.createTextNode(
|
||||
'.ie-mask{position:fixed;left:0;top:0;right:0;bottom:0;background:rgba(0,0,0,.35);z-index:9998}' +
|
||||
'.ie-dialog{position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);width:28rem;max-width:90vw;background:#fff;color:#111;border-radius:.6rem;box-shadow:0 10px 30px rgba(0,0,0,.15);z-index:9999;padding:1.2rem}' +
|
||||
'.ie-dialog .title{font-size:1.2rem;font-weight:700;margin-bottom:.6rem}' +
|
||||
'.ie-dialog .desc{font-size:.95rem;line-height:1.6;color:#555}' +
|
||||
'.ie-dialog .actions{display:flex;justify-content:flex-end;margin-top:1rem}' +
|
||||
'.ie-dialog .actions > * + *{margin-left:.6rem}' +
|
||||
'.ie-dialog .btn{display:inline-flex;align-items:center;justify-content:center;height:2.2rem;padding:0 1rem;border-radius:.4rem;background:#0C8CE9;color:#fff;text-decoration:none}' +
|
||||
'.ie-dialog .btn-secondary{display:inline-flex;align-items:center;justify-content:center;height:2.2rem;padding:0 .9rem;border-radius:.4rem;background:#eee;color:#333;border:1px solid #ddd}' +
|
||||
'@media (prefers-color-scheme: dark){.ie-dialog{background:#1e1f22;color:#e6e6e6}.ie-dialog .desc{color:#c6c6c6}.ie-dialog .btn-secondary{background:#2a2b2f;color:#e6e6e6;border-color:#3a3b3f}}'
|
||||
))
|
||||
document.head.appendChild(style)
|
||||
var mask = document.createElement('div')
|
||||
mask.className = 'ie-mask'
|
||||
var dialog = document.createElement('div')
|
||||
dialog.className = 'ie-dialog'
|
||||
dialog.innerHTML = '<div class="title">不支持 IE 浏览器</div>' +
|
||||
'<div class="desc">Type Words 使用现代技术构建,请使用 Chrome、Edge、Firefox 或 Safari 等现代浏览器访问。</div>' +
|
||||
'<div class="actions">' +
|
||||
'<a class="btn" href="https://www.google.cn/chrome/" target="_blank" rel="noreferrer">下载 Chrome</a>' +
|
||||
'<button class="btn-secondary" type="button">我知道了</button>' +
|
||||
'</div>'
|
||||
(function () {
|
||||
var ua = navigator.userAgent || ''
|
||||
var isIE = !!document.documentMode || /MSIE|Trident/i.test(ua)
|
||||
if (!isIE) return
|
||||
var style = document.createElement('style')
|
||||
style.type = 'text/css'
|
||||
style.appendChild(document.createTextNode(
|
||||
'.ie-mask{position:fixed;left:0;top:0;right:0;bottom:0;background:rgba(0,0,0,.35);z-index:9998}' +
|
||||
'.ie-dialog{position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);width:28rem;max-width:90vw;background:#fff;color:#111;border-radius:.6rem;box-shadow:0 10px 30px rgba(0,0,0,.15);z-index:9999;padding:1.2rem}' +
|
||||
'.ie-dialog .title{font-size:1.2rem;font-weight:700;margin-bottom:.6rem}' +
|
||||
'.ie-dialog .desc{font-size:.95rem;line-height:1.6;color:#555}' +
|
||||
'.ie-dialog .actions{display:flex;justify-content:flex-end;margin-top:1rem}' +
|
||||
'.ie-dialog .actions > * + *{margin-left:.6rem}' +
|
||||
'.ie-dialog .btn{display:inline-flex;align-items:center;justify-content:center;height:2.2rem;padding:0 1rem;border-radius:.4rem;background:#0C8CE9;color:#fff;text-decoration:none}' +
|
||||
'.ie-dialog .btn-secondary{display:inline-flex;align-items:center;justify-content:center;height:2.2rem;padding:0 .9rem;border-radius:.4rem;background:#eee;color:#333;border:1px solid #ddd}' +
|
||||
'@media (prefers-color-scheme: dark){.ie-dialog{background:#1e1f22;color:#e6e6e6}.ie-dialog .desc{color:#c6c6c6}.ie-dialog .btn-secondary{background:#2a2b2f;color:#e6e6e6;border-color:#3a3b3f}}'
|
||||
))
|
||||
document.head.appendChild(style)
|
||||
var mask = document.createElement('div')
|
||||
mask.className = 'ie-mask'
|
||||
var dialog = document.createElement('div')
|
||||
dialog.className = 'ie-dialog'
|
||||
dialog.innerHTML = '<div class="title">不支持 IE 浏览器</div>' +
|
||||
'<div class="desc">Type Words 使用现代技术构建,请使用 Chrome、Edge、Firefox 或 Safari 等现代浏览器访问。</div>' +
|
||||
'<div class="actions">' +
|
||||
'<a class="btn" href="https://www.google.cn/chrome/" target="_blank" rel="noreferrer">下载 Chrome</a>' +
|
||||
'<button class="btn-secondary" type="button">我知道了</button>' +
|
||||
'</div>'
|
||||
|
||||
function close() {
|
||||
try {
|
||||
document.body.removeChild(mask)
|
||||
} catch (e) {
|
||||
}
|
||||
try {
|
||||
document.body.removeChild(dialog)
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
function close() {
|
||||
try {
|
||||
document.body.removeChild(mask)
|
||||
} catch (e) {
|
||||
}
|
||||
try {
|
||||
document.body.removeChild(dialog)
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
mask.addEventListener('click', close)
|
||||
var btn = null
|
||||
try {
|
||||
btn = dialog.querySelector('.btn-secondary')
|
||||
} catch (e) {
|
||||
}
|
||||
if (btn) btn.addEventListener('click', close)
|
||||
document.body.appendChild(mask)
|
||||
document.body.appendChild(dialog)
|
||||
})()
|
||||
mask.addEventListener('click', close)
|
||||
var btn = null
|
||||
try {
|
||||
btn = dialog.querySelector('.btn-secondary')
|
||||
} catch (e) {
|
||||
}
|
||||
if (btn) btn.addEventListener('click', close)
|
||||
document.body.appendChild(mask)
|
||||
document.body.appendChild(dialog)
|
||||
})()
|
||||
</script>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
||||
@@ -383,6 +383,78 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
if (
|
||||
!location.href.includes('localhost')
|
||||
&& !location.href.includes('192.168')
|
||||
&& !location.href.includes('172.16')
|
||||
&& !location.href.includes('10.0')
|
||||
) {
|
||||
//51.la
|
||||
(function () {
|
||||
window.LA = window.LA || {
|
||||
ids: [{id: "3OH8ITYRgwzo58L2", ck: "3OH8ITYRgwzo58L2"}],
|
||||
id: "3OH8ITYRgwzo58L2",
|
||||
ck: "3OH8ITYRgwzo58L2",
|
||||
hashMode: true
|
||||
};
|
||||
const script = document.createElement('script');
|
||||
script.src = `https://typewords.cc/libs/51.js`;
|
||||
document.head.appendChild(script);
|
||||
})();
|
||||
|
||||
// Cloudflare
|
||||
(function () {
|
||||
var cf = document.createElement("script");
|
||||
cf.src = 'https://static.cloudflareinsights.com/beacon.min.js'
|
||||
cf.setAttribute("data-cf-beacon", '{"token": "e5119992696d4155814400dd69781d68"}');
|
||||
document.head.appendChild(cf);
|
||||
})();
|
||||
|
||||
// google
|
||||
(function () {
|
||||
var ana = document.createElement("script");
|
||||
ana.src = 'https://www.googletagmanager.com/gtag/js?id=G-50T6DRD837'
|
||||
ana.onload = function () {
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
|
||||
function gtag() {
|
||||
dataLayer.push(arguments);
|
||||
}
|
||||
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'G-50T6DRD837');
|
||||
}
|
||||
document.head.appendChild(ana);
|
||||
})();
|
||||
|
||||
|
||||
// baidu
|
||||
var _hmt = _hmt || [];
|
||||
(function () {
|
||||
var hm = document.createElement("script");
|
||||
hm.src = "https://hm.baidu.com/hm.js?3dae52fcd5375a19905462e4ad3eb54e";
|
||||
document.head.appendChild(hm);
|
||||
})();
|
||||
|
||||
// umami
|
||||
// (function () {
|
||||
// var umami = document.createElement("script");
|
||||
// umami.src = 'https://typewords.cc/libs/s.js'
|
||||
// umami.setAttribute("data-website-id", "160308c9-7900-4b1d-a0b1-c3b25a9530f6");
|
||||
// document.head.appendChild(umami);
|
||||
// })();
|
||||
|
||||
// umami-saas
|
||||
// (function () {
|
||||
// var umami2 = document.createElement("script");
|
||||
// umami2.src = 'https://stat.typewords.cc/script.js'
|
||||
// umami2.setAttribute("data-website-id", "4d728ae3-5393-4efe-81dc-30dcb4f33c00");
|
||||
// document.head.appendChild(umami2);
|
||||
// })();
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', () => {
|
||||
|
||||
@@ -129,8 +129,8 @@ async function refreshCDN(domain) {
|
||||
async function main() {
|
||||
const files = getAllFiles('./dist')
|
||||
console.log(`📁 共找到 ${files.length} 个文件,开始上传...`)
|
||||
// await uploadFilesWithClean(files, './dist', ['dicts', 'sound', 'libs','imgs])
|
||||
await uploadFilesWithClean(files, './dist', ['sound','libs','imgs'])
|
||||
await uploadFilesWithClean(files, './dist', ['dicts', 'sound', 'libs','imgs'])
|
||||
// await uploadFilesWithClean(files, './dist', ['sound','libs','imgs'])
|
||||
await refreshCDN('2study.top')
|
||||
await refreshCDN('typewords.cc')
|
||||
}
|
||||
|
||||
@@ -21,7 +21,11 @@ const userStore = useUserStore()
|
||||
const {setTheme} = useTheme()
|
||||
|
||||
let lastAudioFileIdList = []
|
||||
let isInitializing = true // 标记是否正在初始化
|
||||
watch(store.$state, (n: BaseState) => {
|
||||
// 如果正在初始化,不保存数据,避免覆盖
|
||||
if (isInitializing) return
|
||||
console.log('watch')
|
||||
let data = shakeCommonDict(n)
|
||||
set(SAVE_DICT_KEY.key, JSON.stringify({val: data, version: SAVE_DICT_KEY.version}))
|
||||
|
||||
@@ -52,6 +56,7 @@ watch(store.$state, (n: BaseState) => {
|
||||
})
|
||||
|
||||
watch(() => settingStore.$state, (n) => {
|
||||
if (isInitializing) return
|
||||
set(SAVE_SETTING_KEY.key, JSON.stringify({val: n, version: SAVE_SETTING_KEY.version}))
|
||||
if (AppEnv.CAN_REQUEST) {
|
||||
syncSetting(null, settingStore.$state)
|
||||
@@ -59,10 +64,12 @@ watch(() => settingStore.$state, (n) => {
|
||||
}, {deep: true})
|
||||
|
||||
async function init() {
|
||||
isInitializing = true // 开始初始化
|
||||
await userStore.init()
|
||||
await store.init()
|
||||
await settingStore.init()
|
||||
store.load = true
|
||||
isInitializing = false // 初始化完成,允许保存数据
|
||||
|
||||
setTheme(settingStore.theme)
|
||||
|
||||
|
||||
@@ -648,7 +648,7 @@ const currentPractice = inject('currentPractice', [])
|
||||
@input="handleMobileInput"
|
||||
/>
|
||||
<header class="mb-4">
|
||||
<div class="title word"><span class="font-family text-3xl">{{
|
||||
<div class="title"><span class="font-family text-3xl">{{
|
||||
store.sbook.lastLearnIndex + 1
|
||||
}}.</span>{{ props.article.title }}
|
||||
</div>
|
||||
@@ -826,7 +826,7 @@ $article-lh: 2.4;
|
||||
display: inline-block !important;
|
||||
}
|
||||
.translate{
|
||||
color:black;
|
||||
color: var(--color-reverse-black);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -245,6 +245,38 @@ async function onTyping(e: KeyboardEvent) {
|
||||
} else {
|
||||
right = letter === word[input.length]
|
||||
}
|
||||
//针对中文的特殊判断
|
||||
if (e.shiftKey && (
|
||||
'!' === word[input.length] && e.code === 'Digit1' ||
|
||||
'¥' === word[input.length] && e.code === 'Digit4' ||
|
||||
'…' === word[input.length] && e.code === 'Digit6' ||
|
||||
'(' === word[input.length] && e.code === 'Digit9' ||
|
||||
'—' === word[input.length] && e.code === 'Minus' ||
|
||||
'?' === word[input.length] && e.code === 'Slash' ||
|
||||
'》' === word[input.length] && e.code === 'Period' ||
|
||||
'《' === word[input.length] && e.code === 'Comma' ||
|
||||
'“' === word[input.length] && e.code === 'Quote' ||
|
||||
':' === word[input.length] && e.code === 'Semicolon' ||
|
||||
')' === word[input.length] && e.code === 'Digit0')
|
||||
) {
|
||||
right = true
|
||||
letter = word[input.length]
|
||||
}
|
||||
if (!e.shiftKey && (
|
||||
'【' === word[input.length] && e.code === 'BracketLeft' ||
|
||||
'、' === word[input.length] && e.code === 'Slash' ||
|
||||
'。' === word[input.length] && e.code === 'Period' ||
|
||||
',' === word[input.length] && e.code === 'Comma' ||
|
||||
'‘' === word[input.length] && e.code === 'Quote' ||
|
||||
';' === word[input.length] && e.code === 'Semicolon' ||
|
||||
'【' === word[input.length] && e.code === 'BracketLeft' ||
|
||||
'】' === word[input.length] && e.code === 'BracketRight'
|
||||
)) {
|
||||
right = true
|
||||
letter = word[input.length]
|
||||
}
|
||||
console.log('e', e, e.code, e.shiftKey, word[input.length])
|
||||
|
||||
if (right) {
|
||||
input += letter
|
||||
wrong = ''
|
||||
@@ -386,6 +418,7 @@ function checkCursorPosition() {
|
||||
// 选中目标元素
|
||||
const cursorEl = document.querySelector(`.cursor`);
|
||||
const inputList = document.querySelectorAll(`.l`);
|
||||
if (!typingWordRef) return;
|
||||
const typingWordRect = typingWordRef.getBoundingClientRect();
|
||||
|
||||
if (inputList.length) {
|
||||
|
||||
@@ -37,9 +37,15 @@ export function checkAndUpgradeSaveDict(val: any) {
|
||||
} else {
|
||||
data = val
|
||||
}
|
||||
if (!data.version) return defaultState
|
||||
if (!data.version) {
|
||||
console.warn('数据缺少版本号,返回默认状态')
|
||||
return defaultState
|
||||
}
|
||||
let state: any = data.val
|
||||
if (typeof state !== 'object') return defaultState
|
||||
if (typeof state !== 'object') {
|
||||
console.warn('数据格式无效,返回默认状态')
|
||||
return defaultState
|
||||
}
|
||||
state.load = false
|
||||
let version = Number(data.version)
|
||||
// console.log('state', state)
|
||||
@@ -53,10 +59,29 @@ export function checkAndUpgradeSaveDict(val: any) {
|
||||
})
|
||||
return defaultState
|
||||
} else {
|
||||
checkRiskKey(defaultState, state)
|
||||
return defaultState
|
||||
// 版本不匹配时,尽量保留数据而不是直接返回默认状态
|
||||
console.warn(`数据版本不匹配: 当前版本 ${version}, 期望版本 ${SAVE_DICT_KEY.version},尝试保留数据`)
|
||||
try {
|
||||
checkRiskKey(defaultState, state)
|
||||
// 尝试保留 bookList 数据
|
||||
if (state.word && state.word.bookList && Array.isArray(state.word.bookList)) {
|
||||
defaultState.word.bookList = state.word.bookList.map((v: any) => {
|
||||
return getDefaultDict(checkRiskKey(getDefaultDict(), v))
|
||||
})
|
||||
}
|
||||
if (state.article && state.article.bookList && Array.isArray(state.article.bookList)) {
|
||||
defaultState.article.bookList = state.article.bookList.map((v: any) => {
|
||||
return getDefaultDict(checkRiskKey(getDefaultDict(), v))
|
||||
})
|
||||
}
|
||||
return defaultState
|
||||
} catch (upgradeError) {
|
||||
console.error('数据升级失败,返回默认状态', upgradeError)
|
||||
return defaultState
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('数据解析异常,返回默认状态', e)
|
||||
return defaultState
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user