perf: 优化撤回消息

This commit is contained in:
kuaifan 2022-01-29 12:55:44 +08:00
parent 3d04bd4444
commit 9999548bc2
8 changed files with 386 additions and 154 deletions

View File

@ -160,16 +160,28 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/sendtext 05. 未读消息
* @api {get} api/dialog/msg/unread 05. 获取未读消息数量
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName msg__sendtext
* @apiName msg__unread
*
* @apiParam {Number} [dialog_id] 对话ID留空获取总未读消息数量
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function msg__unread()
{
$unread = WebSocketDialogMsgRead::whereUserid(User::userid())->whereReadAt(null)->count();
$dialog_id = intval(Request::input('dialog_id'));
//
$builder = WebSocketDialogMsgRead::whereUserid(User::userid())->whereReadAt(null);
if ($dialog_id > 0) {
$builder->whereDialogId($dialog_id);
}
$unread = $builder->count();
return Base::retSuccess('success', [
'unread' => $unread,
]);
@ -417,7 +429,7 @@ class DialogController extends AbstractController
/**
* @api {get} api/dialog/msg/withdraw 11. 聊天消息撤回
*
* @apiDescription 需要token身份
* @apiDescription 消息撤回限制24小时内需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName msg__withdraw

View File

@ -8,6 +8,7 @@ use App\Tasks\PushTask;
use App\Tasks\WebSocketDialogMsgTask;
use Carbon\Carbon;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* App\Models\WebSocketDialogMsg
@ -21,11 +22,15 @@ use Hhxsv5\LaravelS\Swoole\Task\Task;
* @property int|null $send 发送数量
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property-read int|mixed $percentage
* @property-read \App\Models\WebSocketDialog|null $webSocketDialog
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg newQuery()
* @method static \Illuminate\Database\Query\Builder|WebSocketDialogMsg onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg query()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereDeletedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereDialogId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereMsg($value)
@ -34,10 +39,14 @@ use Hhxsv5\LaravelS\Swoole\Task\Task;
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereType($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereUserid($value)
* @method static \Illuminate\Database\Query\Builder|WebSocketDialogMsg withTrashed()
* @method static \Illuminate\Database\Query\Builder|WebSocketDialogMsg withoutTrashed()
* @mixin \Eloquent
*/
class WebSocketDialogMsg extends AbstractModel
{
use SoftDeletes;
protected $appends = [
'percentage',
];
@ -46,6 +55,14 @@ class WebSocketDialogMsg extends AbstractModel
'updated_at',
];
/**
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function webSocketDialog(): \Illuminate\Database\Eloquent\Relations\HasOne
{
return $this->hasOne(WebSocketDialog::class, 'id', 'dialog_id');
}
/**
* 阅读占比
* @return int|mixed
@ -130,27 +147,39 @@ class WebSocketDialogMsg extends AbstractModel
*/
public function deleteMsg()
{
$send_dt = Carbon::parse($this->created_at)->addMinutes(5);
$send_dt = Carbon::parse($this->created_at)->addDay();
if ($send_dt->lt(Carbon::now())) {
throw new ApiException('已超过5分钟此消息不能撤回');
}
$this->delete();
//
$dialog = WebSocketDialog::find($this->dialog_id);
if ($dialog) {
$userids = $dialog->dialogUser->pluck('userid')->toArray();
PushTask::push([
'userid' => $userids,
'msg' => [
'type' => 'dialog',
'mode' => 'delete',
'data' => [
'id' => $this->id,
'dialog_id' => $this->dialog_id
],
]
]);
throw new ApiException('已超过24小时此消息不能撤回');
}
AbstractModel::transaction(function() {
$deleteRead = WebSocketDialogMsgRead::whereMsgId($this->id)->whereNull('read_at')->delete(); // 未阅读记录不需要软删除,直接删除即可
$this->delete();
//
$last_msg = null;
if ($this->webSocketDialog) {
$last_msg = WebSocketDialogMsg::whereDialogId($this->dialog_id)->orderByDesc('id')->first();
$this->webSocketDialog->last_at = $last_msg->created_at;
$this->webSocketDialog->save();
}
//
$dialog = WebSocketDialog::find($this->dialog_id);
if ($dialog) {
$userids = $dialog->dialogUser->pluck('userid')->toArray();
PushTask::push([
'userid' => $userids,
'msg' => [
'type' => 'dialog',
'mode' => 'delete',
'data' => [
'id' => $this->id,
'dialog_id' => $this->dialog_id,
'last_msg' => $last_msg,
'update_read' => $deleteRead ? 1 : 0
],
]
]);
}
});
}
/**

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class WebSocketDialogMsgsAddDeletes extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
if (!Schema::hasColumn('web_socket_dialog_msgs', 'deleted_at')) {
$table->softDeletes();
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
$table->dropSoftDeletes();
});
}
}

View File

@ -1,41 +1,49 @@
<template>
<div :class="`dialog-view ${msgData.type}`" :data-id="msgData.id">
<!--文本-->
<div v-if="msgData.type === 'text'" class="dialog-content">
<pre class="no-dark-mode">{{textMsg(msgData.msg.text)}}</pre>
</div>
<!--等待-->
<div v-else-if="msgData.type === 'loading'" class="dialog-content loading"><Loading/></div>
<!--文件-->
<div v-else-if="msgData.type === 'file'" :class="['dialog-content', msgData.msg.type]">
<div class="dialog-file">
<img v-if="msgData.msg.type === 'img'" class="file-img" :style="imageStyle(msgData.msg)" :src="msgData.msg.thumb" @click="viewFile"/>
<div v-else class="file-box">
<img class="file-thumb" :src="msgData.msg.thumb"/>
<div class="file-info">
<div class="file-name">{{msgData.msg.name}}</div>
<div class="file-size">{{$A.bytesToSize(msgData.msg.size)}}</div>
<div class="dialog-head">
<!--详情-->
<div class="dialog-content">
<!--文本-->
<div v-if="msgData.type === 'text'" class="content-text">
<pre class="no-dark-mode">{{textMsg(msgData.msg.text)}}</pre>
</div>
<!--文件-->
<div v-else-if="msgData.type === 'file'" :class="`content-file ${msgData.msg.type}`">
<div class="dialog-file">
<img v-if="msgData.msg.type === 'img'" class="file-img" :style="imageStyle(msgData.msg)" :src="msgData.msg.thumb" @click="viewFile"/>
<div v-else class="file-box">
<img class="file-thumb" :src="msgData.msg.thumb"/>
<div class="file-info">
<div class="file-name">{{msgData.msg.name}}</div>
<div class="file-size">{{$A.bytesToSize(msgData.msg.size)}}</div>
</div>
</div>
</div>
</div>
<!--等待-->
<div v-else-if="msgData.type === 'loading'" class="content-loading">
<Loading/>
</div>
<!--未知-->
<div v-else class="content-unknown">{{$L("未知的消息类型")}}</div>
</div>
<div class="dialog-file-menu">
<div class="file-menu-warp"></div>
<div class="file-menu-icon">
<Icon @click="viewFile" type="md-eye" />
<Icon @click="downFile" type="md-arrow-round-down" />
<!--菜单-->
<div v-if="showMenu" class="dialog-menu">
<div class="menu-icon">
<Icon v-if="msgData.userid == userId" @click="withdraw" type="md-undo" :title="$L('撤回')"/>
<template v-if="msgData.type === 'file'">
<Icon @click="viewFile" type="md-eye" :title="$L('查看')"/>
<Icon @click="downFile" type="md-arrow-round-down" :title="$L('下载')"/>
</template>
</div>
</div>
</div>
<!--未知-->
<div v-else class="dialog-content unknown">{{$L("未知的消息类型")}}</div>
<!--时间/阅读-->
<div v-if="msgData.created_at" class="dialog-foot">
<div class="time">{{$A.formatTime(msgData.created_at)}}</div>
<div class="time" :title="msgData.created_at">{{$A.formatTime(msgData.created_at)}}</div>
<Poptip
v-if="msgData.send > 1 || dialogType == 'group'"
class="percent"
@ -95,7 +103,7 @@ export default {
},
computed: {
...mapState(['userToken']),
...mapState(['userToken', 'userId']),
readList() {
return this.read_list.filter(({read_at}) => read_at)
@ -103,6 +111,10 @@ export default {
unreadList() {
return this.read_list.filter(({read_at}) => !read_at)
},
showMenu() {
return this.msgData.userid == this.userId || this.msgData.type === 'file'
}
},
@ -176,6 +188,29 @@ export default {
return {};
},
withdraw() {
$A.modalConfirm({
content: `确定撤回此信息吗?`,
okText: '撤回',
loading: true,
onOk: () => {
this.$store.dispatch("call", {
url: 'dialog/msg/withdraw',
data: {
msg_id: this.msgData.id
},
}).then(() => {
$A.messageSuccess("消息已撤回");
this.$store.dispatch("forgetDialogMsg", this.msgData.id);
this.$Modal.remove();
}).catch(({msg}) => {
$A.messageError(msg, 301);
this.$Modal.remove();
});
}
});
},
viewFile() {
if (this.$Electron) {
this.$Electron.ipcRenderer.send('windowRouter', {

View File

@ -2053,6 +2053,22 @@ export default {
case 'delete':
// 删除消息
dispatch("forgetDialogMsg", data.id)
//
let dialog = state.cacheDialogs.find(({id}) => id == data.dialog_id);
if (dialog) {
// 更新最后消息
dialog.last_at = data.last_msg && data.last_msg.created_at;
dialog.last_msg = data.last_msg;
if (data.update_read) {
// 更新未读数量
dispatch("call", {
url: 'dialog/msg/unread',
dialog_id: data.dialog_id
}).then(result => {
dialog.unread = result.data.unread
}).catch(() => {});
}
}
break;
case 'add':
case 'chat':

View File

@ -140,8 +140,10 @@ body.dark-mode-reverse {
> li {
.dialog-view {
.dialog-content {
color: #ffffff;
background-color: #e1e1e1;
.content-text {
color: #ffffff;
}
}
}
&.self {

View File

@ -8,6 +8,7 @@
flex-direction: column;
background-color: #ffffff;
z-index: 1;
.dialog-title {
display: flex;
flex-direction: column;
@ -15,6 +16,7 @@
padding: 0 30px;
height: 68px;
position: relative;
&:before {
content: "";
position: absolute;
@ -24,6 +26,7 @@
height: 1px;
background-color: #f4f5f5;
}
&.completed {
&:after {
content: "\f373";
@ -39,19 +42,23 @@
z-index: 1;
}
}
.main-title {
display: flex;
align-items: center;
line-height: 22px;
max-width: 100%;
.ivu-tag {
flex-shrink: 0;
margin: 0 6px 0 0;
padding: 0 5px;
&.ivu-tag-success {
padding: 0 6px;
}
}
.ivu-icon {
font-size: 18px;
margin-right: 6px;
@ -60,6 +67,7 @@
color: $primary-color;
}
}
> h2 {
font-size: 17px;
font-weight: 600;
@ -67,6 +75,7 @@
text-overflow: ellipsis;
white-space: nowrap;
}
> em {
font-style: normal;
font-size: 17px;
@ -74,24 +83,29 @@
padding-left: 6px;
}
}
.sub-title {
flex-shrink: 0;
font-size: 12px;
line-height: 20px;
color: #aaaaaa;
&.pointer {
cursor: pointer;
&:hover {
color: #888888;
}
}
}
}
.dialog-scroller {
position: relative;
flex: 1;
padding: 0 32px;
overflow: auto;
.dialog-list {
> ul {
> li {
@ -100,9 +114,11 @@
align-items: flex-end;
list-style: none;
margin-bottom: 16px;
&:first-child {
margin-top: 16px;
}
.dialog-avatar {
position: relative;
margin-bottom: 20px;
@ -110,36 +126,137 @@
width: 30px;
height: 30px;
}
.dialog-view {
display: flex;
flex-direction: column;
align-items: flex-start;
margin: 0 0 0 8px;
position: relative;
&.text {
max-width: 70%;
}
.dialog-content {
color: #333333;
background-color: #F4F5F7;
padding: 8px;
min-width: 32px;
border-radius: 6px 6px 6px 0;
.dialog-file-menu {
opacity: 0;
transition: all 0.3s;
position: absolute;
left: 0;
bottom: -8px;
.file-menu-warp {
width: 100%;
height: 12px;
&:hover {
.dialog-head {
.dialog-menu {
opacity: 1;
}
.file-menu-icon {
}
}
.dialog-head {
display: flex;
align-items: flex-start;
.dialog-content {
background-color: #F4F5F7;
padding: 8px;
min-width: 32px;
border-radius: 6px 6px 6px 0;
display: flex;
align-items: flex-start;
.content-text {
color: #333333;
> pre {
display: block;
margin: 0;
padding: 0;
line-height: 1.5;
white-space: pre-wrap;
word-wrap: break-word;
word-break: break-word;
}
}
.content-file {
&.file {
display: inline-block;
.file-box {
background-color: #ffffff;
display: flex;
align-items: center;
padding: 10px 14px;
border-radius: 3px;
width: 220px;
.file-thumb {
width: 36px;
}
.file-info {
margin-left: 12px;
display: flex;
flex-direction: column;
justify-content: center;
.file-name {
color: #333333;
font-size: 14px;
line-height: 18px;
word-break: break-all;
text-overflow: ellipsis;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.file-size {
padding-top: 4px;
color: #666666;
font-size: 14px;
}
}
}
}
&.img {
padding: 0;
display: flex;
max-width: 220px;
max-height: 220px;
border-radius: 6px;
background-color: transparent;
overflow: hidden;
.file-img {
display: flex;
cursor: pointer;
}
}
}
.content-loading {
display: flex;
.common-loading {
width: 20px;
height: 20px;
margin: 4px;
}
}
.content-unknown {
text-decoration: underline;
}
}
.dialog-menu {
opacity: 0;
margin-left: 6px;
transition: all 0.3s;
.menu-icon {
display: flex;
align-items: center;
border-radius: 4px;
border: 1px solid #ddd;
background-color: #ffffff;
> i {
flex: 1;
display: inline-block;
@ -147,105 +264,37 @@
color: #999;
font-size: 13px;
cursor: pointer;
& + i {
border-left: 1px solid #ddd;
}
&:hover {
color: #777;
}
&+i {
border-left: 1px solid #ddd;
}
}
}
}
> pre {
display: block;
margin: 0;
padding: 0;
line-height: 1.5;
white-space: pre-wrap;
word-wrap: break-word;
word-break: break-word;
}
&:hover {
.dialog-file-menu {
opacity: 1;
}
}
&.loading {
display: flex;
.common-loading {
width: 20px;
height: 20px;
margin: 4px;
}
}
&.file {
display: inline-block;
.file-box {
background-color: #ffffff;
display: flex;
align-items: center;
padding: 10px 14px;
border-radius: 3px;
width: 220px;
.file-thumb {
width: 36px;
}
.file-info {
margin-left: 12px;
display: flex;
flex-direction: column;
justify-content: center;
.file-name {
color: #333333;
font-size: 14px;
line-height: 18px;
word-break: break-all;
text-overflow: ellipsis;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.file-size {
padding-top: 4px;
color: #666666;
font-size: 14px;
}
}
}
}
&.img {
padding: 0;
display: flex;
max-width: 220px;
max-height: 220px;
border-radius: 6px;
background-color: transparent;
overflow: hidden;
.file-img {
display: flex;
cursor: pointer;
}
}
&.unknown {
text-decoration: underline;
}
}
.dialog-foot {
display: flex;
align-items: center;
padding-top: 4px;
height: 21px;
line-height: 1;
.common-loading {
margin: 0 2px;
width: 10px;
height: 10px;
}
.time {
color: #bbbbbb;
font-size: 12px;
}
.done {
display: none;
margin-left: 4px;
@ -253,6 +302,7 @@
font-size: 12px;
color: $primary-color;
}
.percent {
display: none;
margin-left: 4px;
@ -260,6 +310,7 @@
}
}
}
.dialog-action {
align-self: flex-start;
display: flex;
@ -271,6 +322,7 @@
}
}
&.history {
cursor: pointer;
justify-content: center;
@ -279,13 +331,16 @@
margin: 12px 0;
opacity: 0.6;
transition: opacity 0.2s;
&:hover {
opacity: 1;
}
}
&.history-tip {
position: relative;
padding-top: 60px;
.history-text {
font-style: normal;
position: absolute;
@ -301,15 +356,18 @@
transform: translateX(-50%);
}
}
&.loading {
padding: 12px 0;
justify-content: center;
.common-loading {
margin: 0;
width: 18px;
height: 18px;
}
}
&.nothing {
position: absolute;
top: 50%;
@ -317,32 +375,54 @@
transform: translate(-50%, -50%);
color: #999999;
}
&.bottom {
height: 0;
margin: 0;
padding: 0;
}
&.self {
flex-direction: row-reverse;
.dialog-view {
align-items: flex-end;
margin: 0 8px 0 0;
.dialog-content {
color: #ffffff;
background-color: $primary-color;
border-radius: 6px 6px 0 6px;
&.file {
background-color: #F4F5F7;
.dialog-head {
flex-direction: row-reverse;
.dialog-content {
background-color: $primary-color;
border-radius: 6px 6px 0 6px;
.content-text {
color: #ffffff;
}
.content-file {
&.file {
background-color: #F4F5F7;
}
&.img {
border-radius: 6px;
background-color: transparent;
}
}
}
&.img {
border-radius: 6px;
background-color: transparent;
.dialog-menu {
margin-left: 0;
margin-right: 6px;
}
}
.dialog-foot {
.done {
display: inline-block;
}
.percent {
display: inline-block;
}
@ -353,6 +433,7 @@
}
}
}
.dialog-footer {
display: flex;
flex-direction: column;
@ -360,6 +441,7 @@
padding: 0 28px;
margin-bottom: 20px;
position: relative;
.dialog-newmsg {
display: none;
height: 30px;
@ -374,19 +456,23 @@
cursor: pointer;
z-index: 2;;
}
.dialog-input {
background-color: #F4F5F7;
padding: 10px 52px 10px 12px;
border-radius: 10px;
.ivu-input {
border: 0;
resize: none;
background-color: transparent;
&:focus {
box-shadow: none;
}
}
}
.dialog-send {
position: absolute;
top: 0;
@ -398,19 +484,23 @@
align-items: center;
justify-content: center;
}
.chat-upload {
display: none;
width: 0;
height: 0;
overflow: hidden;
}
&.newmsg {
margin-top: -50px;
.dialog-newmsg {
display: block;
}
}
}
.drag-over {
position: absolute;
top: 0;
@ -422,6 +512,7 @@
display: flex;
align-items: center;
justify-content: center;
&:before {
content: "";
position: absolute;
@ -432,6 +523,7 @@
border: 2px dashed #7b7b7b;
border-radius: 12px;
}
.drag-text {
padding: 12px;
font-size: 18px;
@ -439,23 +531,29 @@
}
}
}
.dialog-wrapper-read-poptip-content {
display: flex;
position: relative;
.read,
.unread {
flex: 1;
max-height: 300px;
overflow: auto;
> li {
list-style: none;
margin-bottom: 12px;
.common-avatar {
width: 100%;
}
&:last-child {
margin-bottom: 6px;
}
&.read-title {
> em {
font-size: 18px;
@ -466,11 +564,13 @@
}
}
}
.unread {
> li {
padding-left: 16px;
}
}
&:before {
content: "";
position: absolute;
@ -484,10 +584,12 @@
.dialog-wrapper-paste {
margin-top: -4px;
img {
max-width: 100%;
max-height: 1000px;
}
> div,
> img {
display: flex;
@ -502,6 +604,7 @@
.dialog-footer {
padding: 0 20px;
margin-bottom: 16px;
.dialog-send {
right: 20px;
}

View File

@ -6,7 +6,7 @@
min-height: 120px;
overflow: auto;
.task-info {
flex: 1;
flex: 3;
display: flex;
flex-direction: column;
position: relative;
@ -459,6 +459,7 @@
}
}
.task-dialog {
flex: 2;
flex-shrink: 0;
display: flex;
flex-direction: column;