perf: 支持@群聊以外成员

This commit is contained in:
kuaifan 2022-06-22 18:31:07 +08:00
parent 3aefe99bd9
commit 822bc3ea69
8 changed files with 287 additions and 185 deletions

View File

@ -268,36 +268,6 @@ class DialogController extends AbstractController
return Base::retSuccess('success', $data);
}
/**
* @api {get} api/dialog/msg/one 06. 获取单个消息
*
* @apiDescription 主要用于获取回复消息的详情需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName msg__one
*
* @apiParam {Number} msg_id 消息ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function msg__one()
{
User::auth();
//
$msg_id = intval(Request::input('msg_id'));
//
$dialogMsg = WebSocketDialogMsg::find($msg_id);
if (empty($dialogMsg)) {
return Base::retError('消息不存在或已被删除');
}
//
WebSocketDialog::checkDialog($dialogMsg->dialog_id);
//
return Base::retSuccess('success', $dialogMsg);
}
/**
* @api {get} api/dialog/msg/unread 07. 获取未读消息数量
*
@ -882,7 +852,8 @@ class DialogController extends AbstractController
return Base::retError('创建群组失败');
}
$data = WebSocketDialog::find($dialog->id)?->formatData($user->userid);
$dialog->pushMsg("groupAdd", $data, $userids);
$userids = array_values(array_diff($userids, [$user->userid]));
$dialog->pushMsg("groupAdd", null, $userids);
return Base::retSuccess('创建成功', $data);
}
@ -956,8 +927,8 @@ class DialogController extends AbstractController
$dialog = WebSocketDialog::checkDialog($dialog_id, "auto");
//
$dialog->checkGroup();
$dialog->joinGroup($userids);
$dialog->pushMsg("groupJoin", $dialog->formatData($user->userid), $userids);
$dialog->joinGroup($userids, $user->userid);
$dialog->pushMsg("groupJoin", null, $userids);
return Base::retSuccess('添加成功');
}

View File

@ -928,11 +928,18 @@ class ProjectTask extends AbstractModel
/**
* 权限版本
* @param int $level 1-负责人2-协助人/负责人3-创建人/协助人/负责人
* @param int $level
* 1:负责人
* 2:协助人/负责人
* 3:创建人/协助人/负责人
* 4:任务群聊成员/3
* @return bool
*/
public function permission($level = 1)
{
if ($level >= 4) {
return $this->permission(3) || $this->existDialogUser();
}
if ($level >= 3 && $this->isCreater()) {
return true;
}
@ -942,6 +949,15 @@ class ProjectTask extends AbstractModel
return $this->isOwner();
}
/**
* 判断是否在任务对话里
* @return bool
*/
public function existDialogUser()
{
return $this->dialog_id && WebSocketDialogUser::whereDialogId($this->dialog_id)->whereUserid(User::userid())->exists();
}
/**
* 判断是否创建者
* @return bool
@ -1218,7 +1234,10 @@ class ProjectTask extends AbstractModel
* @param int $task_id
* @param bool $archived true:仅限未归档, false:仅限已归档, null:不限制
* @param bool $trashed true:仅限未删除, false:仅限已删除, null:不限制
* @param int|bool $permission 0|false:不限制, 1|true:限制项目负责人、任务负责人、协助人员及任务创建者, 2:已有负责人才限制true (子任务时如果是主任务负责人也可以)
* @param int|bool $permission
* - 0|false 限制:项目成员、任务成员、任务群聊成员(任务成员 = 任务创建人+任务协助人+任务负责人)
* - 1|true 限制:项目负责人、任务成员
* - 2 已有负责人才限制true (子任务时如果是主任务负责人也可以)
* @param array $with
* @return self
*/
@ -1245,19 +1264,20 @@ class ProjectTask extends AbstractModel
try {
$project = Project::userProject($task->project_id);
} catch (\Throwable $e) {
if ($task->owner === null) {
if ($task->owner !== null || (!$permission && $task->permission(4))) {
$project = Project::find($task->project_id);
if (empty($project)) {
throw new ApiException('项目不存在或已被删除', [ 'task_id' => $task_id ], -4002);
}
} else {
throw new ApiException($e->getMessage(), [ 'task_id' => $task_id ], -4002);
}
$project = Project::find($task->project_id);
if (empty($project)) {
throw new ApiException('项目不存在或已被删除', [ 'task_id' => $task_id ], -4002);
}
}
//
if ($permission === 2) {
if ($permission >= 2) {
$permission = $task->hasOwner() ? 1 : 0;
}
if (($permission === 1 || $permission === true) && !$project->owner && !$task->permission(3)) {
if ($permission && !$project->owner && !$task->permission(3)) {
throw new ApiException('仅限项目负责人、任务负责人、协助人员或任务创建者操作');
}
//

View File

@ -124,22 +124,27 @@ class WebSocketDialog extends AbstractModel
/**
* 加入聊天室
* @param int|array $userid 加入的会员ID或会员ID组
* @param int $inviter 邀请人
* @return bool
*/
public function joinGroup($userid)
public function joinGroup($userid, $inviter)
{
AbstractModel::transaction(function () use ($userid) {
AbstractModel::transaction(function () use ($inviter, $userid) {
foreach (is_array($userid) ? $userid : [$userid] as $value) {
if ($value > 0) {
WebSocketDialogUser::updateInsert([
'dialog_id' => $this->id,
'userid' => $value,
], [
'inviter' => User::userid(),
'inviter' => $inviter,
]);
}
}
});
$this->pushMsg("groupUpdate", [
'id' => $this->id,
'people' => WebSocketDialogUser::whereDialogId($this->id)->count()
]);
return true;
}
@ -174,6 +179,11 @@ class WebSocketDialog extends AbstractModel
}
});
});
//
$this->pushMsg("groupUpdate", [
'id' => $this->id,
'people' => WebSocketDialogUser::whereDialogId($this->id)->count()
]);
}
/**
@ -244,7 +254,7 @@ class WebSocketDialog extends AbstractModel
/**
* 推送消息
* @param $action
* @param array $data 发送内容,默认为[id=>项目ID]
* @param array $data 发送内容,默认为[id=>会话ID]
* @param array $userid 指定会员,默认为群组所有成员
* @return void
*/

View File

@ -13,7 +13,7 @@ use Request;
/**
* 推送话消息
* 推送话消息
* Class WebSocketDialogMsgTask
* @package App\Tasks
*/
@ -38,6 +38,7 @@ class WebSocketDialogMsgTask extends AbstractTask
$_A = [
'__fill_url_remote_url' => true,
];
//
$msg = WebSocketDialogMsg::find($this->id);
if (empty($msg)) {
@ -48,14 +49,31 @@ class WebSocketDialogMsgTask extends AbstractTask
return;
}
// 提及会员
$mentions = [];
if ($msg->type === 'text') {
preg_match_all("/<span class=\"mention user\" data-id=\"(\d+)\">/", $msg->msg['text'], $matchs);
if ($matchs) {
$mentions = array_values(array_filter(array_unique($matchs[1])));
}
}
// 将会话以外的成员加入会话内
$userids = $dialog->dialogUser->pluck('userid')->toArray();
$diffids = array_values(array_diff($mentions, $userids));
if ($diffids) {
$dialog->joinGroup($diffids, $msg->userid);
$dialog->pushMsg("groupJoin", null, $diffids);
$userids = array_values(array_unique(array_merge($mentions, $userids)));
}
// 推送目标①:会话成员/群成员
$array = [];
$userids = $dialog->dialogUser->pluck('userid')->toArray();
foreach ($userids AS $userid) {
if ($userid == $msg->userid) {
$array[$userid] = false;
} else {
$mention = preg_match("/<span class=\"mention user\" data-id=\"[0|{$userid}]\">/", $msg->type === 'text' ? $msg->msg['text'] : '');
$mention = array_intersect([0, $userid], $mentions) ? 1 : 0;
WebSocketDialogMsgRead::createInstance([
'dialog_id' => $msg->dialog_id,
'msg_id' => $msg->id,

View File

@ -202,6 +202,7 @@ export default {
mentionMode: '',
userList: null,
userCache: null,
taskList: null,
showMore: false,
@ -279,7 +280,7 @@ export default {
}
},
computed: {
...mapState(['dialogInputCache', 'cacheProjects', 'cacheTasks', 'cacheUserBasic', 'dialogMsgs']),
...mapState(['dialogInputCache', 'cacheProjects', 'cacheTasks', 'cacheUserBasic', 'dialogMsgs', 'cacheDialogs']),
isEnterSend() {
if (typeof this.enterSend === "boolean") {
@ -350,6 +351,10 @@ export default {
return `${minute}:${seconds}${millisecond}`
},
dialogData() {
return this.dialogId > 0 ? (this.cacheDialogs.find(({id}) => id == this.dialogId) || {}) : {};
},
replyData() {
const {replyId} = this;
if (replyId > 0) {
@ -382,11 +387,13 @@ export default {
// Reset lists
dialogId() {
this.userList = null;
this.userCache = null;
this.taskList = null;
this.$emit('input', this.getInputCache())
},
taskId() {
this.userList = null;
this.userCache = null;
this.taskList = null;
this.$emit('input', this.getInputCache())
},
@ -499,7 +506,7 @@ export default {
return `<div class="mention-item-disabled">${data.value}</div>`;
}
if (data.id === 0) {
return `<div class="mention-item-at">@</div><div class="mention-item-name">${data.value}</div><div class="mention-item-tip">${this.$L('提示所有成员')}</div>`;
return `<div class="mention-item-at">@</div><div class="mention-item-name">${data.value}</div><div class="mention-item-tip">${data.tip}</div>`;
}
if (data.avatar) {
return `<div class="mention-item-img${data.online ? ' online' : ''}"><img src="${data.avatar}"/><em></em></div><div class="mention-item-name">${data.value}</div>`;
@ -518,14 +525,14 @@ export default {
containers[i].classList.add(mentionName);
scrollPreventThrough(containers[i]);
}
this.getSource(mentionChar).then(array => {
let values = [];
this.getMentionSource(mentionChar, searchTerm, array => {
const values = [];
array.some(item => {
let list = item.list;
if (searchTerm && !item.ignoreSearch) {
if (searchTerm) {
list = list.filter(({value}) => $A.strExists(value, searchTerm));
}
if (list.length > 0 || item.ignoreSearch) {
if (list.length > 0) {
item.label && values.push(...item.label)
list.length > 0 && values.push(...list)
}
@ -908,149 +915,210 @@ export default {
return 0;
},
getSource(mentionChar) {
return new Promise(resolve => {
switch (mentionChar) {
case "@": // @
this.mentionMode = "user-mention";
if (this.userList !== null) {
resolve(this.userList)
return;
}
const atCallback = (list) => {
if (list.length > 2) {
this.userList = [{
ignoreSearch: true,
label: null,
list: [{id: 0, value: this.$L('所有人')}]
}, {
ignoreSearch: false,
getMentionSource(mentionChar, searchTerm, resultCallback) {
switch (mentionChar) {
case "@": // @
this.mentionMode = "user-mention";
const atCallback = (list) => {
this.getMoreUser(searchTerm, list.map(item => item.id)).then(moreUser => {
this.userList = list
this.userCache = [];
if (moreUser.length > 0) {
if (list.length > 2) {
this.userCache.push({
label: null,
list: [{id: 0, value: this.$L('所有人'), tip: this.$L('仅提示会话内成员')}]
})
}
this.userCache.push(...[{
label: [{id: 0, value: this.$L('会话内成员'), disabled: true}],
list,
}]
}, {
label: [{id: 0, value: this.$L('会话以外成员'), disabled: true}],
list: moreUser,
}])
} else {
this.userList = [{
ignoreSearch: false,
label: null,
list
}]
if (list.length > 2) {
this.userCache.push(...[{
label: null,
list: [{id: 0, value: this.$L('所有人'), tip: this.$L('提示所有成员')}]
}, {
label: [{id: 0, value: this.$L('会话内成员'), disabled: true}],
list,
}])
} else {
this.userCache.push({
label: null,
list
})
}
}
resolve(this.userList)
}
let array = [];
if (this.dialogId > 0) {
// ID
this.$store.dispatch("call", {
url: 'dialog/user',
data: {
dialog_id: this.dialogId,
getuser: 1
}
}).then(({data}) => {
if (data.length > 0) {
array.push(...data.map(item => {
return {
id: item.userid,
value: item.nickname,
avatar: item.userimg,
online: item.online,
}
}))
}
atCallback(array)
}).catch(_ => {
atCallback(array)
});
} else if (this.taskId > 0) {
// ID
const task = this.cacheTasks.find(({id}) => id == this.taskId)
if (task && $A.isArray(task.task_user)) {
task.task_user.some(tmp => {
let item = this.cacheUserBasic.find(({userid}) => userid == tmp.userid);
if (item) {
array.push({
id: item.userid,
value: item.nickname,
avatar: item.userimg,
online: item.online,
})
}
resultCallback(this.userCache)
})
}
//
if (this.dialogData.people && $A.arrayLength(this.userList) !== this.dialogData.people) {
this.userList = null;
this.userCache = null;
}
if (this.userCache !== null) {
resultCallback(this.userCache)
}
if (this.userList !== null) {
atCallback(this.userList)
return;
}
//
const array = [];
if (this.dialogId > 0) {
// ID
this.$store.dispatch("call", {
url: 'dialog/user',
data: {
dialog_id: this.dialogId,
getuser: 1
}
}).then(({data}) => {
if (this.cacheDialogs.find(({id}) => id == this.dialogId)) {
this.$store.dispatch("saveDialog", {
id: this.dialogId,
people: data.length
})
}
if (data.length > 0) {
array.push(...data.map(item => {
return {
id: item.userid,
value: item.nickname,
avatar: item.userimg,
online: item.online,
}
}))
}
atCallback(array)
}).catch(_ => {
atCallback(array)
});
} else if (this.taskId > 0) {
// ID
const task = this.cacheTasks.find(({id}) => id == this.taskId)
if (task && $A.isArray(task.task_user)) {
task.task_user.some(tmp => {
const item = this.cacheUserBasic.find(({userid}) => userid == tmp.userid);
if (item) {
array.push({
id: item.userid,
value: item.nickname,
avatar: item.userimg,
online: item.online,
})
}
})
}
break;
atCallback(array)
}
break;
case "#": // #
this.mentionMode = "task-mention";
if (this.taskList !== null) {
resolve(this.taskList)
return;
case "#": // #
this.mentionMode = "task-mention";
if (this.taskList !== null) {
resultCallback(this.taskList)
return;
}
const taskCallback = (list) => {
this.taskList = [];
//
if (list.length > 0) {
list = list.map(item => {
return {
id: item.id,
value: item.name
}
})
this.taskList.push({
label: [{id: 0, value: this.$L('项目未完成任务'), disabled: true}],
list,
})
}
const taskCallback = (list) => {
this.taskList = [];
//
if (list.length > 0) {
list = list.map(item => {
//
let data = this.$store.getters.transforTasks(this.$store.getters.dashboardTask['all']);
if (data.length > 0) {
data = data.sort((a, b) => {
return $A.Date(a.end_at || "2099-12-31 23:59:59") - $A.Date(b.end_at || "2099-12-31 23:59:59");
})
this.taskList.push({
label: [{id: 0, value: this.$L('我的待完成任务'), disabled: true}],
list: data.map(item => {
return {
id: item.id,
value: item.name
}
})
this.taskList.push({
ignoreSearch: false,
label: [{id: 0, value: this.$L('项目未完成任务'), disabled: true}],
list,
})
}
//
let data = this.$store.getters.transforTasks(this.$store.getters.dashboardTask['all']);
if (data.length > 0) {
data = data.sort((a, b) => {
return $A.Date(a.end_at || "2099-12-31 23:59:59") - $A.Date(b.end_at || "2099-12-31 23:59:59");
})
this.taskList.push({
ignoreSearch: false,
label: [{id: 0, value: this.$L('我的待完成任务'), disabled: true}],
list: data.map(item => {
return {
id: item.id,
value: item.name
}
}),
})
}
resolve(this.taskList)
}
//
const projectId = this.getProjectId();
if (projectId > 0) {
this.$store.dispatch("getTaskForProject", projectId).then(_ => {
let tasks = this.cacheTasks.filter(task => {
if (task.archived_at) {
return false;
}
return task.project_id == projectId
&& task.parent_id === 0
&& !task.archived_at
&& !task.complete_at
})
if (tasks.length > 0) {
taskCallback(tasks);
} else {
taskCallback([])
}
}).catch(_ => {
taskCallback([])
}),
})
return;
}
taskCallback([])
break;
resultCallback(this.taskList)
}
//
const projectId = this.getProjectId();
if (projectId > 0) {
this.$store.dispatch("getTaskForProject", projectId).then(_ => {
const tasks = this.cacheTasks.filter(task => {
if (task.archived_at) {
return false;
}
return task.project_id == projectId
&& task.parent_id === 0
&& !task.archived_at
&& !task.complete_at
})
if (tasks.length > 0) {
taskCallback(tasks);
} else {
taskCallback([])
}
}).catch(_ => {
taskCallback([])
})
return;
}
taskCallback([])
break;
default:
resolve([])
break;
default:
resultCallback([])
break;
}
},
getMoreUser(key, existIds) {
return new Promise(resolve => {
if (this.dialogId > 0 || this.taskId > 0 || this.dialogData.type === 'group') {
this.__getMoreTimer && clearTimeout(this.__getMoreTimer)
this.__getMoreTimer = setTimeout(_ => {
this.$store.dispatch("call", {
url: 'users/search',
data: {
keys: {
key,
},
take: 30
},
}).then(({data}) => {
const moreUser = data.filter(item => !existIds.includes(item.userid))
resolve(moreUser.map(item => {
return {
id: item.userid,
value: item.nickname,
avatar: item.userimg,
online: !!item.online,
}
}))
}).catch(_ => {
resolve([])
});
}, this.userCache === null ? 0 : 600)
} else {
resolve([])
}
})
},

View File

@ -2510,7 +2510,13 @@ export default {
case 'groupAdd':
case 'groupJoin':
// 群组添加、加入
dispatch("saveDialog", data)
dispatch("getDialogOne", data.id).catch(() => {})
break;
case 'groupUpdate':
// 群组更新
if (state.cacheDialogs.find(({id}) => id == data.id)) {
dispatch("saveDialog", data)
}
break;
case 'groupExit':
case 'groupDelete':

View File

@ -557,6 +557,7 @@
}
.mention-item-at {
flex-shrink: 0;
width: 28px;
height: 28px;
line-height: 28px;
@ -568,6 +569,7 @@
}
.mention-item-img {
flex-shrink: 0;
position: relative;
display: flex;
align-items: center;
@ -601,6 +603,7 @@
}
.mention-item-name {
flex-shrink: 0;
padding: 0 8px;
font-size: 14px;
overflow: hidden;
@ -609,6 +612,8 @@
}
.mention-item-tip {
flex: 1;
text-align: right;
color: #8f8f8e;
font-size: 12px;
font-style: normal;
@ -618,6 +623,7 @@
}
.mention-item-disabled {
flex-shrink: 0;
color: #aaa;
font-size: 12px;
padding: 0 4px;

View File

@ -44,7 +44,7 @@
pointer-events: none;
position: absolute;
top: 50%;
right: 22px;
right: 52px;
transform: translateY(-50%);
font-size: 40px;
color: #19be6b;
@ -151,7 +151,9 @@
line-height: 20px;
padding-top: 2px;
color: #aaaaaa;
white-space: normal;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&.pointer {
cursor: pointer;
@ -1093,7 +1095,8 @@
&.completed {
&:after {
right: 14px;
font-size: 36px;
right: 40px;
}
.dialog-title {
padding-right: 0;