mirror of
https://github.com/kuaifan/dootask.git
synced 2025-12-12 11:19:56 +00:00
no message
This commit is contained in:
parent
e78513cb80
commit
d5db894891
@ -708,7 +708,9 @@ class DialogController extends AbstractController
|
|||||||
$data[] = [
|
$data[] = [
|
||||||
'id' => $dialogUser->webSocketDialog->id,
|
'id' => $dialogUser->webSocketDialog->id,
|
||||||
'unread' => $dialogUser->webSocketDialog->unread,
|
'unread' => $dialogUser->webSocketDialog->unread,
|
||||||
|
'unread_one' => $dialogUser->webSocketDialog->unread_one,
|
||||||
'mention' => $dialogUser->webSocketDialog->mention,
|
'mention' => $dialogUser->webSocketDialog->mention,
|
||||||
|
'mention_ids' => $dialogUser->webSocketDialog->mention_ids,
|
||||||
'user_at' => Carbon::parse($dialogUser->updated_at)->toDateTimeString('millisecond'),
|
'user_at' => Carbon::parse($dialogUser->updated_at)->toDateTimeString('millisecond'),
|
||||||
'user_ms' => Carbon::parse($dialogUser->updated_at)->valueOf()
|
'user_ms' => Carbon::parse($dialogUser->updated_at)->valueOf()
|
||||||
];
|
];
|
||||||
@ -751,7 +753,9 @@ class DialogController extends AbstractController
|
|||||||
return Base::retSuccess('success', [
|
return Base::retSuccess('success', [
|
||||||
'id' => $dialogUser->webSocketDialog->id,
|
'id' => $dialogUser->webSocketDialog->id,
|
||||||
'unread' => $dialogUser->webSocketDialog->unread,
|
'unread' => $dialogUser->webSocketDialog->unread,
|
||||||
|
'unread_one' => $dialogUser->webSocketDialog->unread_one,
|
||||||
'mention' => $dialogUser->webSocketDialog->mention,
|
'mention' => $dialogUser->webSocketDialog->mention,
|
||||||
|
'mention_ids' => $dialogUser->webSocketDialog->mention_ids,
|
||||||
'user_at' => Carbon::parse($dialogUser->updated_at)->toDateTimeString('millisecond'),
|
'user_at' => Carbon::parse($dialogUser->updated_at)->toDateTimeString('millisecond'),
|
||||||
'user_ms' => Carbon::parse($dialogUser->updated_at)->valueOf()
|
'user_ms' => Carbon::parse($dialogUser->updated_at)->valueOf()
|
||||||
]);
|
]);
|
||||||
@ -1340,6 +1344,7 @@ class DialogController extends AbstractController
|
|||||||
}
|
}
|
||||||
switch ($type) {
|
switch ($type) {
|
||||||
case 'read':
|
case 'read':
|
||||||
|
// 标记已读
|
||||||
$builder = WebSocketDialogMsgRead::whereDialogId($dialog_id)->whereUserid($user->userid)->whereReadAt(null);
|
$builder = WebSocketDialogMsgRead::whereDialogId($dialog_id)->whereUserid($user->userid)->whereReadAt(null);
|
||||||
if ($after_msg_id > 0) {
|
if ($after_msg_id > 0) {
|
||||||
$builder->where('msg_id', '>=', $after_msg_id);
|
$builder->where('msg_id', '>=', $after_msg_id);
|
||||||
@ -1347,32 +1352,28 @@ class DialogController extends AbstractController
|
|||||||
$builder->chunkById(100, function ($list) {
|
$builder->chunkById(100, function ($list) {
|
||||||
WebSocketDialogMsgRead::onlyMarkRead($list);
|
WebSocketDialogMsgRead::onlyMarkRead($list);
|
||||||
});
|
});
|
||||||
//
|
|
||||||
$dialogUser->webSocketDialog->generateUnread($user->userid);
|
|
||||||
$data = [
|
|
||||||
'id' => $dialogUser->webSocketDialog->id,
|
|
||||||
'unread' => $dialogUser->webSocketDialog->unread,
|
|
||||||
'mention' => $dialogUser->webSocketDialog->mention,
|
|
||||||
'mark_unread' => 0,
|
|
||||||
];
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'unread':
|
case 'unread':
|
||||||
$data = [
|
// 标记未读
|
||||||
'id' => $dialogUser->webSocketDialog->id,
|
|
||||||
'mark_unread' => 1,
|
|
||||||
];
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return Base::retError("参数错误");
|
return Base::retError("参数错误");
|
||||||
}
|
}
|
||||||
$dialogUser->mark_unread = $data['mark_unread'];
|
$dialogUser->mark_unread = $type == 'unread' ? 1 : 0;
|
||||||
$dialogUser->save();
|
$dialogUser->save();
|
||||||
return Base::retSuccess("success", array_merge($data, [
|
$dialogUser->webSocketDialog->generateUnread($user->userid);
|
||||||
|
return Base::retSuccess("success", [
|
||||||
|
'id' => $dialogUser->webSocketDialog->id,
|
||||||
|
'unread' => $dialogUser->webSocketDialog->unread,
|
||||||
|
'unread_one' => $dialogUser->webSocketDialog->unread_one,
|
||||||
|
'mention' => $dialogUser->webSocketDialog->mention,
|
||||||
|
'mention_ids' => $dialogUser->webSocketDialog->mention_ids,
|
||||||
'user_at' => Carbon::parse($dialogUser->updated_at)->toDateTimeString('millisecond'),
|
'user_at' => Carbon::parse($dialogUser->updated_at)->toDateTimeString('millisecond'),
|
||||||
'user_ms' => Carbon::parse($dialogUser->updated_at)->valueOf(),
|
'user_ms' => Carbon::parse($dialogUser->updated_at)->valueOf(),
|
||||||
]));
|
'mark_unread' => $dialogUser->mark_unread,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -156,7 +156,7 @@ class WebSocketDialog extends AbstractModel
|
|||||||
$this->last_at = $this->last_msg?->created_at;
|
$this->last_at = $this->last_msg?->created_at;
|
||||||
} else {
|
} else {
|
||||||
// 未读信息
|
// 未读信息
|
||||||
$this->generateUnread($userid, $hasData);
|
$this->generateUnread($userid);
|
||||||
// 未读标记
|
// 未读标记
|
||||||
$this->mark_unread = $this->mark_unread ?? $dialogUserFun('mark_unread');
|
$this->mark_unread = $this->mark_unread ?? $dialogUserFun('mark_unread');
|
||||||
// 是否免打扰
|
// 是否免打扰
|
||||||
@ -250,37 +250,19 @@ class WebSocketDialog extends AbstractModel
|
|||||||
/**
|
/**
|
||||||
* 生成未读数据
|
* 生成未读数据
|
||||||
* @param $userid
|
* @param $userid
|
||||||
* @param $positionData
|
|
||||||
* @return $this
|
* @return $this
|
||||||
*/
|
*/
|
||||||
public function generateUnread($userid, $positionData = false)
|
public function generateUnread($userid)
|
||||||
{
|
{
|
||||||
$builder = WebSocketDialogMsgRead::whereDialogId($this->id)->whereUserid($userid)->whereReadAt(null);
|
$builder = WebSocketDialogMsgRead::whereDialogId($this->id)->whereUserid($userid)->whereReadAt(null);
|
||||||
|
// 未读消息
|
||||||
$this->unread = $builder->count();
|
$this->unread = $builder->count();
|
||||||
|
// 最早一条未读消息
|
||||||
|
$this->unread_one = $this->unread > 0 ? intval($builder->clone()->orderBy('msg_id')->value('msg_id')) : 0;
|
||||||
|
// @我的消息
|
||||||
$this->mention = $this->unread > 0 ? $builder->clone()->whereMention(1)->count() : 0;
|
$this->mention = $this->unread > 0 ? $builder->clone()->whereMention(1)->count() : 0;
|
||||||
if ($positionData) {
|
// @我的消息(id集合)
|
||||||
$array = [];
|
$this->mention_ids = $this->mention > 0 ? $builder->clone()->whereMention(1)->orderByDesc('msg_id')->take(20)->pluck('msg_id')->toArray() : [];
|
||||||
// @我的消息
|
|
||||||
if ($this->mention > 0) {
|
|
||||||
$list = $builder->clone()->whereMention(1)->orderByDesc('msg_id')->take(20)->get();
|
|
||||||
foreach ($list as $item) {
|
|
||||||
$array[] = [
|
|
||||||
'msg_id' => $item->msg_id,
|
|
||||||
'label' => Doo::translate('@我的消息'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 最早一条未读消息
|
|
||||||
if ($this->unread > 0
|
|
||||||
&& $first_id = intval($builder->clone()->orderBy('msg_id')->value('msg_id'))) {
|
|
||||||
$array[] = [
|
|
||||||
'msg_id' => $first_id,
|
|
||||||
'label' => '{UNREAD}'
|
|
||||||
];
|
|
||||||
}
|
|
||||||
//
|
|
||||||
$this->position_msgs = $array;
|
|
||||||
}
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1002,6 +1002,7 @@ export default {
|
|||||||
}
|
}
|
||||||
const notificationFuncB = (title) => {
|
const notificationFuncB = (title) => {
|
||||||
if (this.__notificationId === id) {
|
if (this.__notificationId === id) {
|
||||||
|
this.__notificationId = null
|
||||||
if (this.$isEEUiApp) {
|
if (this.$isEEUiApp) {
|
||||||
this.$refs.mobileNotification.open({
|
this.$refs.mobileNotification.open({
|
||||||
userid: userid,
|
userid: userid,
|
||||||
|
|||||||
@ -101,7 +101,7 @@ export default {
|
|||||||
type: Number,
|
type: Number,
|
||||||
default: 0
|
default: 0
|
||||||
},
|
},
|
||||||
unreadMsgId: {
|
unreadOne: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0
|
default: 0
|
||||||
},
|
},
|
||||||
@ -131,7 +131,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
isUnreadStart() {
|
isUnreadStart() {
|
||||||
return this.unreadMsgId === this.source.id
|
return this.unreadOne === this.source.id
|
||||||
},
|
},
|
||||||
|
|
||||||
hidePercentage() {
|
hidePercentage() {
|
||||||
|
|||||||
@ -181,7 +181,7 @@
|
|||||||
:data-component="msgItem"
|
:data-component="msgItem"
|
||||||
|
|
||||||
:item-class-add="itemClassAdd"
|
:item-class-add="itemClassAdd"
|
||||||
:extra-props="{dialogData, operateVisible, operateItem, isMyDialog, msgId, unreadMsgId, scrollIng, readEnabled}"
|
:extra-props="{dialogData, operateVisible, operateItem, isMyDialog, msgId, unreadOne, scrollIng, readEnabled}"
|
||||||
:estimate-size="dialogData.type=='group' ? 105 : 77"
|
:estimate-size="dialogData.type=='group' ? 105 : 77"
|
||||||
:keeps="keeps"
|
:keeps="keeps"
|
||||||
:disabled="scrollDisabled"
|
:disabled="scrollDisabled"
|
||||||
@ -765,13 +765,13 @@ export default {
|
|||||||
|
|
||||||
observers: [],
|
observers: [],
|
||||||
|
|
||||||
unreadMsgId: 0, // 最早未读消息id
|
unreadOne: 0, // 最早未读消息id
|
||||||
topPosLoad: false, // 置顶跳转加载中
|
topPosLoad: false, // 置顶跳转加载中
|
||||||
positionLoad: 0, // 定位跳转加载中
|
positionLoad: 0, // 定位跳转加载中
|
||||||
positionShow: false, // 定位跳转显示
|
positionShow: false, // 定位跳转显示
|
||||||
renderMsgNum: 0, // 渲染消息数量
|
renderMsgNum: 0, // 渲染消息数量
|
||||||
renderMsgSizes: new Map(), // 渲染消息尺寸
|
renderMsgSizes: new Map(), // 渲染消息尺寸
|
||||||
msgPreparedStatus: false, // 消息准备完成
|
msgActivityStatus: false, // 消息准备完成
|
||||||
listPreparedStatus: false, // 列表准备完成
|
listPreparedStatus: false, // 列表准备完成
|
||||||
selectedTextStatus: false, // 是否选择文本
|
selectedTextStatus: false, // 是否选择文本
|
||||||
scrollToBottomAndRefresh: false, // 滚动到底部重新获取消息
|
scrollToBottomAndRefresh: false, // 滚动到底部重新获取消息
|
||||||
@ -830,7 +830,11 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
dialogData() {
|
dialogData() {
|
||||||
return this.cacheDialogs.find(({id}) => id == this.dialogId) || {};
|
const data = this.cacheDialogs.find(({id}) => id == this.dialogId) || {}
|
||||||
|
if (this.unreadOne === 0) {
|
||||||
|
this.unreadOne = data.unread_one || 0
|
||||||
|
}
|
||||||
|
return data
|
||||||
},
|
},
|
||||||
|
|
||||||
dialogList() {
|
dialogList() {
|
||||||
@ -1082,20 +1086,29 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
positionMsg({msgNew, dialogData, allMsgs}) {
|
positionMsg({msgNew, dialogData, allMsgs}) {
|
||||||
const {mention, unread, position_msgs} = dialogData
|
const {unread, unread_one, mention, mention_ids} = dialogData
|
||||||
if (!position_msgs || position_msgs.length === 0 || (unread - msgNew) <= 0 || allMsgs.length === 0) {
|
const not = unread - msgNew
|
||||||
|
const array = []
|
||||||
|
if (unread_one) {
|
||||||
|
array.push({
|
||||||
|
type: 'unread',
|
||||||
|
label: this.$L(`未读消息${not}条`),
|
||||||
|
msg_id: unread_one
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (mention_ids && mention_ids.length > 0) {
|
||||||
|
array.push(...mention_ids.map(msg_id => {
|
||||||
|
return {
|
||||||
|
type: 'mention',
|
||||||
|
label: this.$L(`@我的消息`),
|
||||||
|
msg_id
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
if (not <= 0 || array.length === 0 || allMsgs.length === 0) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const item = $A.cloneJSON(position_msgs.find(item => {
|
return array.find(item => item.type === (mention === 0 ? 'unread' : 'mention')) || array[0]
|
||||||
if (mention === 0) {
|
|
||||||
return item.label === '{UNREAD}'
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}))
|
|
||||||
if (item.label === '{UNREAD}') {
|
|
||||||
item.label = this.$L(`未读消息${unread - msgNew}条`)
|
|
||||||
}
|
|
||||||
return item
|
|
||||||
},
|
},
|
||||||
|
|
||||||
operateEmojis({cacheEmojis}) {
|
operateEmojis({cacheEmojis}) {
|
||||||
@ -1115,8 +1128,8 @@ export default {
|
|||||||
return 1024000
|
return 1024000
|
||||||
},
|
},
|
||||||
|
|
||||||
readEnabled({msgPreparedStatus, listPreparedStatus}) {
|
readEnabled({msgActivityStatus, listPreparedStatus}) {
|
||||||
return msgPreparedStatus && listPreparedStatus
|
return msgActivityStatus === 0 && listPreparedStatus
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -1143,7 +1156,7 @@ export default {
|
|||||||
if (dialog_id) {
|
if (dialog_id) {
|
||||||
this.msgNew = 0
|
this.msgNew = 0
|
||||||
this.msgType = ''
|
this.msgType = ''
|
||||||
this.unreadMsgId = 0
|
this.unreadOne = 0
|
||||||
this.searchShow = false
|
this.searchShow = false
|
||||||
this.positionShow = false
|
this.positionShow = false
|
||||||
this.listPreparedStatus = false
|
this.listPreparedStatus = false
|
||||||
@ -1164,11 +1177,6 @@ export default {
|
|||||||
this.openId = dialog_id
|
this.openId = dialog_id
|
||||||
this.listPreparedStatus = true
|
this.listPreparedStatus = true
|
||||||
//
|
//
|
||||||
const {position_msgs} = this.dialogData
|
|
||||||
if ($A.isArray(position_msgs)) {
|
|
||||||
this.unreadMsgId = position_msgs.find(item => item.label === '{UNREAD}')?.msg_id || 0
|
|
||||||
}
|
|
||||||
//
|
|
||||||
const tmpMsgB = this.allMsgList.map(({id, msg, emoji}) => {
|
const tmpMsgB = this.allMsgList.map(({id, msg, emoji}) => {
|
||||||
return {id, msg, emoji}
|
return {id, msg, emoji}
|
||||||
})
|
})
|
||||||
@ -1328,10 +1336,11 @@ export default {
|
|||||||
const {tail} = this.scrollInfo();
|
const {tail} = this.scrollInfo();
|
||||||
if ($A.isIos() && newList.length !== oldList.length) {
|
if ($A.isIos() && newList.length !== oldList.length) {
|
||||||
// 隐藏区域,让iOS断触
|
// 隐藏区域,让iOS断触
|
||||||
this.$refs.scroller.$el.style.visibility = 'hidden'
|
const scrollEl = this.$refs.scroller.$el
|
||||||
|
scrollEl.style.visibility = 'hidden'
|
||||||
this.allMsgs = newList;
|
this.allMsgs = newList;
|
||||||
this.$nextTick(_ => {
|
this.$nextTick(_ => {
|
||||||
this.$refs.scroller.$el.style.visibility = 'visible'
|
scrollEl.style.visibility = 'visible'
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.allMsgs = newList;
|
this.allMsgs = newList;
|
||||||
@ -1339,7 +1348,7 @@ export default {
|
|||||||
//
|
//
|
||||||
if (!this.windowActive || (tail > 55 && oldList.length > 0)) {
|
if (!this.windowActive || (tail > 55 && oldList.length > 0)) {
|
||||||
const lastId = oldList[oldList.length - 1] ? oldList[oldList.length - 1].id : 0
|
const lastId = oldList[oldList.length - 1] ? oldList[oldList.length - 1].id : 0
|
||||||
const tmpList = newList.filter(item => item.id && item.id > lastId)
|
const tmpList = newList.filter(item => item.id && item.id > lastId && !item.read_at)
|
||||||
this.msgNew += tmpList.length
|
this.msgNew += tmpList.length
|
||||||
} else {
|
} else {
|
||||||
!this.preventToBottom && this.$nextTick(this.onToBottom)
|
!this.preventToBottom && this.$nextTick(this.onToBottom)
|
||||||
@ -1741,6 +1750,7 @@ export default {
|
|||||||
this.todoViewData = {}
|
this.todoViewData = {}
|
||||||
this.todoViewMid = 0
|
this.todoViewMid = 0
|
||||||
this.todoViewId = 0
|
this.todoViewId = 0
|
||||||
|
this.onFooterResize()
|
||||||
},
|
},
|
||||||
|
|
||||||
onPosTodo() {
|
onPosTodo() {
|
||||||
@ -2128,6 +2138,7 @@ export default {
|
|||||||
setTimeout(_ => {
|
setTimeout(_ => {
|
||||||
scroller.scrollToOffset(offset)
|
scroller.scrollToOffset(offset)
|
||||||
scroller.virtual.handleFront()
|
scroller.virtual.handleFront()
|
||||||
|
// scroller.virtual.handleBehind()
|
||||||
}, 10) // 预防出现白屏的情况
|
}, 10) // 预防出现白屏的情况
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -2518,7 +2529,17 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
onActivity(activity) {
|
onActivity(activity) {
|
||||||
this.msgPreparedStatus = !activity
|
if (this.msgActivityStatus === false) {
|
||||||
|
if (activity) {
|
||||||
|
this.msgActivityStatus = 1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (activity) {
|
||||||
|
this.msgActivityStatus++
|
||||||
|
} else {
|
||||||
|
this.msgActivityStatus--
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onScroll(event) {
|
onScroll(event) {
|
||||||
|
|||||||
27
resources/assets/js/store/actions.js
vendored
27
resources/assets/js/store/actions.js
vendored
@ -2331,7 +2331,9 @@ export default {
|
|||||||
const originalTime = original.user_ms || 0
|
const originalTime = original.user_ms || 0
|
||||||
if (nowTime < originalTime) {
|
if (nowTime < originalTime) {
|
||||||
typeof data.unread !== "undefined" && delete data.unread
|
typeof data.unread !== "undefined" && delete data.unread
|
||||||
|
typeof data.unread_one !== "undefined" && delete data.unread_one
|
||||||
typeof data.mention !== "undefined" && delete data.mention
|
typeof data.mention !== "undefined" && delete data.mention
|
||||||
|
typeof data.mention_ids !== "undefined" && delete data.mention_ids
|
||||||
}
|
}
|
||||||
state.cacheDialogs.splice(index, 1, Object.assign({}, original, data));
|
state.cacheDialogs.splice(index, 1, Object.assign({}, original, data));
|
||||||
} else {
|
} else {
|
||||||
@ -3059,11 +3061,21 @@ export default {
|
|||||||
state.readWaitData[data.id] = data.id;
|
state.readWaitData[data.id] = data.id;
|
||||||
//
|
//
|
||||||
const dialog = state.cacheDialogs.find(({id}) => id == data.dialog_id);
|
const dialog = state.cacheDialogs.find(({id}) => id == data.dialog_id);
|
||||||
if (dialog && $A.isArray(dialog.position_msgs)) {
|
if (dialog) {
|
||||||
const index = dialog.position_msgs.findIndex(({msg_id}) => msg_id == data.id);
|
let mark = false
|
||||||
if (index > -1) {
|
if (data.id == dialog.unread_one) {
|
||||||
|
dialog.unread_one = 0
|
||||||
|
mark = true
|
||||||
|
}
|
||||||
|
if ($A.isArray(dialog.mention_ids)) {
|
||||||
|
const index = dialog.mention_ids.findIndex(id => id == data.id)
|
||||||
|
if (index > -1) {
|
||||||
|
dialog.mention_ids.splice(index, 1)
|
||||||
|
mark = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mark) {
|
||||||
state.readEndMark[data.dialog_id] = Math.max(data.id, $A.runNum(state.readEndMark[data.dialog_id]))
|
state.readEndMark[data.dialog_id] = Math.max(data.id, $A.runNum(state.readEndMark[data.dialog_id]))
|
||||||
dialog.position_msgs.splice(index, 1);
|
|
||||||
dispatch("saveDialog", dialog)
|
dispatch("saveDialog", dialog)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3120,6 +3132,13 @@ export default {
|
|||||||
url: 'dialog/msg/mark',
|
url: 'dialog/msg/mark',
|
||||||
data,
|
data,
|
||||||
}).then(result => {
|
}).then(result => {
|
||||||
|
if (typeof data.after_msg_id !== "undefined") {
|
||||||
|
state.dialogMsgs.some(item => {
|
||||||
|
if (item.dialog_id == data.dialog_id && item.id >= data.after_msg_id) {
|
||||||
|
item.read_at = $A.formatDate()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
dispatch("saveDialog", result.data)
|
dispatch("saveDialog", result.data)
|
||||||
resolve(result)
|
resolve(result)
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user