feat: 新增消息回复表情功能

This commit is contained in:
kuaifan 2022-06-04 09:49:00 +08:00
parent b8234adbc2
commit 2ac7d0bc01
11 changed files with 374 additions and 98 deletions

View File

@ -568,36 +568,6 @@ class DialogController extends AbstractController
return Base::retSuccess("success");
}
/**
* @api {get} api/dialog/top 14. 会话置顶
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName top
*
* @apiParam {Number} dialog_id 会话ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function top()
{
$user = User::auth();
$dialogId = intval(Request::input('dialog_id'));
$dialogUser = WebSocketDialogUser::whereUserid($user->userid)->whereDialogId($dialogId)->first();
if (!$dialogUser) {
return Base::retError("会话不存在");
}
$dialogUser->top_at = $dialogUser->top_at ? null : Carbon::now();
$dialogUser->save();
return Base::retSuccess("success", [
'id' => $dialogUser->dialog_id,
'top_at' => $dialogUser->top_at?->toDateTimeString(),
]);
}
/**
* @api {get} api/dialog/msg/mark 15. 消息标记操作
*
@ -679,6 +649,69 @@ class DialogController extends AbstractController
return $msg->forwardMsg($userids, $user->userid);
}
/**
* @api {get} api/dialog/msg/emoji 13. emoji回复
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName msg__forward
*
* @apiParam {Number} msg_id 消息ID
* @apiParam {String} emoji 回复或取消的emoji表情
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function msg__emoji()
{
$user = User::auth();
//
$msg_id = intval(Request::input("msg_id"));
$emoji = Request::input("emoji");
//
if (!preg_match("/^[\u{d800}-\u{dbff}]|[\u{dc00}-\u{dfff}]$/", $emoji)) {
return Base::retError("参数错误");
}
//
$msg = WebSocketDialogMsg::whereId($msg_id)->whereUserid($user->userid)->first();
if (empty($msg)) {
return Base::retError("消息不存在或已被删除");
}
return $msg->emojiMsg($emoji, $user->userid);
}
/**
* @api {get} api/dialog/top 14. 会话置顶
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName top
*
* @apiParam {Number} dialog_id 会话ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function top()
{
$user = User::auth();
$dialogId = intval(Request::input('dialog_id'));
$dialogUser = WebSocketDialogUser::whereUserid($user->userid)->whereDialogId($dialogId)->first();
if (!$dialogUser) {
return Base::retError("会话不存在");
}
$dialogUser->top_at = $dialogUser->top_at ? null : Carbon::now();
$dialogUser->save();
return Base::retSuccess("success", [
'id' => $dialogUser->dialog_id,
'top_at' => $dialogUser->top_at?->toDateTimeString(),
]);
}
/**
* @api {get} api/dialog/group/add 16. 新增群组
*

View File

@ -19,6 +19,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property int|null $userid 发送会员ID
* @property string|null $type 消息类型
* @property array|mixed $msg 详细消息
* @property array|mixed $emoji emoji回复
* @property int|null $read 已阅数量
* @property int|null $send 发送数量
* @property \Illuminate\Support\Carbon|null $created_at
@ -34,6 +35,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @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 whereDialogType($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereEmoji($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereMsg($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereRead($value)
@ -98,6 +100,19 @@ class WebSocketDialogMsg extends AbstractModel
return $value;
}
/**
* emoji回复格式化
* @param $value
* @return array|mixed
*/
public function getEmojiAttribute($value)
{
if (is_array($value)) {
return $value;
}
return Base::json2array($value);
}
/**
* 获取占比
* @param bool|int $increment 是否新增阅读数
@ -161,6 +176,53 @@ class WebSocketDialogMsg extends AbstractModel
return true;
}
/**
* emoji回复
* @param $emoji
* @param int $sender 发送的会员ID
* @return mixed
*/
public function emojiMsg($emoji, $sender)
{
$exist = false;
$array = $this->emoji;
foreach ($array as $index => &$item) {
if ($item['symbol'] === $emoji) {
if (in_array($sender, $item['userids'])) {
// 已存在 去除
$item['userids'] = array_values(array_diff($item['userids'], [$sender]));
if (empty($item['userids'])) {
unset($array[$index]);
$array = array_values($array);
}
} else {
// 未存在 添加
array_unshift($item['userids'], $sender);
}
$exist = true;
break;
}
}
if (!$exist) {
array_unshift($array, [
'symbol' => $emoji,
'userids' => [$sender]
]);
}
//
$this->emoji = Base::array2json($array);
$this->save();
$resData = [
'id' => $this->id,
'emoji' => $array,
];
//
$dialog = WebSocketDialog::find($this->dialog_id);
$dialog?->pushMsg('update', $resData);
//
return Base::retSuccess('sucess', $resData);
}
/**
* 转发消息
* @param $userids

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddWebSocketDialogMsgsEmoji 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', 'emoji')) {
$table->longText('emoji')->after('msg')->nullable()->comment('emoji回复');
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
$table->dropColumn("emoji");
});
}
}

View File

@ -5,15 +5,9 @@
<UserAvatar :userid="msgData.userid" :show-icon="false" :show-name="true" :tooltip-disabled="true"/>
</div>
<div class="dialog-head">
<div class="dialog-head" v-longpress="{delay: 300, callback: handleLongpress}">
<!--详情-->
<div
class="dialog-content"
:class="contentClass"
v-longpress="{
delay: 300,
callback: handleLongpress
}">
<div class="dialog-content" :class="contentClass">
<!--文本-->
<div v-if="msgData.type === 'text'" class="content-text no-dark-content">
<pre @click="viewText" v-html="textMsg(msgData.msg.text)"></pre>
@ -66,6 +60,17 @@
<!--未知-->
<div v-else class="content-unknown">{{$L("未知的消息类型")}}</div>
</div>
<!--emoji-->
<ul v-if="msgData.emoji.length > 0" class="dialog-emoji">
<li
v-for="(item, index) in msgData.emoji"
:key="index"
:class="{hasme: item.userids.includes(userId)}"
@click="setEmoji(item.symbol)">
<div class="emoji-symbol no-dark-content">{{item.symbol}}</div>
<div class="emoji-num">{{item.userids.length}}</div>
</li>
</ul>
</div>
<!--时间/阅读-->
@ -523,6 +528,20 @@ export default {
this.$store.dispatch('downUrl', $A.apiUrl(`dialog/msg/download?msg_id=${this.msgData.id}`))
}
});
},
setEmoji(emoji) {
this.$store.dispatch("call", {
url: 'dialog/msg/emoji',
data: {
msg_id: this.msgData.id,
emoji,
},
}).then(({data}) => {
this.$store.dispatch("saveDialogMsg", data);
}).catch(({msg}) => {
$A.messageError(msg);
});
}
}
}

View File

@ -139,46 +139,56 @@
<div class="operate-position" :style="operateStyles">
<Dropdown
trigger="custom"
:placement="$isDesktop ? 'bottom' : 'top'"
placement="top"
:visible="operateVisible"
@on-clickoutside="operateVisible = false"
@on-click="onOperate"
transferClassName="dialog-wrapper-operate"
transfer>
<div :style="{userSelect:operateVisible ? 'none' : 'auto', height: operateStyles.height}"></div>
<DropdownMenu slot="list">
<DropdownItem v-if="operateHasText" name="copy">
<div class="operate-item">
<span>{{ $L('复制') }}</span>
<i class="taskfont">&#xe77f;</i>
</div>
<DropdownItem name="action">
<ul class="operate-action">
<template v-if="operateHasText">
<li @click="onOperate('copy')">
<i class="taskfont">&#xe77f;</i>
<span>{{ $L('复制') }}</span>
</li>
<li @click="onOperate('newTask')">
<i class="taskfont">&#xe7b8;</i>
<span>{{ $L('新任务') }}</span>
</li>
</template>
<li @click="onOperate('forward')">
<i class="taskfont">&#xe75e;</i>
<span>{{ $L('转发') }}</span>
</li>
<template v-if="operateItem.userid == userId">
<li @click="onOperate('withdraw')">
<i class="taskfont">&#xe637;</i>
<span>{{ $L('撤回') }}</span>
</li>
</template>
<template v-if="operateItem.type === 'file'">
<li @click="onOperate('view')">
<i class="taskfont">&#xe77b;</i>
<span>{{ $L('查看') }}</span>
</li>
<li @click="onOperate('down')">
<i class="taskfont">&#xe7a8;</i>
<span>{{ $L('下载') }}</span>
</li>
</template>
</ul>
</DropdownItem>
<DropdownItem name="forward">
<div class="operate-item">
<span>{{ $L('转发') }}</span>
<i class="taskfont">&#xe75e;</i>
</div>
<DropdownItem name="emoji">
<ul class="operate-emoji scrollbar-hidden">
<li
v-for="(emoji, key) in operateEmojis"
:key="key"
v-html="emoji"
@click="onOperate('emoji', emoji)"></li>
</ul>
</DropdownItem>
<DropdownItem v-if="operateItem.userid == userId" name="withdraw">
<div class="operate-item">
<span>{{ $L('撤回') }}</span>
<i class="taskfont">&#xe637;</i>
</div>
</DropdownItem>
<template v-if="operateItem.type === 'file'">
<DropdownItem name="view" divided>
<div class="operate-item">
<span>{{ $L('查看') }}</span>
<i class="taskfont">&#xe77b;</i>
</div>
</DropdownItem>
<DropdownItem name="down">
<div class="operate-item">
<span>{{ $L('下载') }}</span>
<i class="taskfont">&#xe7a8;</i>
</div>
</DropdownItem>
</template>
</DropdownMenu>
</Dropdown>
</div>
@ -260,6 +270,7 @@ import ChatInput from "./ChatInput";
import { DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller-hi'
import 'vue-virtual-scroller-hi/dist/vue-virtual-scroller.css'
import {Store} from "le5le-store";
export default {
name: "DialogWrapper",
@ -317,6 +328,7 @@ export default {
operateHasText: false,
operateStyles: {},
operateItem: {},
operateEmojis: ['👌', '🤝', '🤔', '👍', '👎', '👏', '✋', '✅', '❌', '❤️', '❓']
}
},
@ -839,12 +851,11 @@ export default {
})
},
onOperate(name) {
onOperate(name, value = null) {
switch (name) {
case "copy":
if (this.operateHasText) {
const text = this.operateItem.msg.text.replace(/<[^>]+>/g,"");
this.$copyText(text).then(_ => {
this.$copyText(this.operateItem.msg.text.replace(/<[^>]+>/g, "")).then(_ => {
$A.messageSuccess('复制成功');
}).catch(_ => {
$A.messageError('复制失败');
@ -854,6 +865,15 @@ export default {
}
break;
case "newTask":
if (this.operateHasText) {
Store.set('addTask', {
owner: [this.userId],
name: this.operateItem.msg.text.replace(/<[^>]+>/g, "")
});
}
break;
case "forward":
this.onForward('open')
break;
@ -869,6 +889,10 @@ export default {
case "down":
this.$refs[`msg_${this.operateItem.id}`].downFile()
break;
case "emoji":
this.$refs[`msg_${this.operateItem.id}`].setEmoji(value)
break;
}
},
}

View File

@ -2378,8 +2378,9 @@ export default {
// 更新最后消息
dispatch("updateDialogLastMsg", data);
break;
case 'update':
case 'readed':
// 已读回执
// 更新、已读回执
if (state.dialogMsgs.find(({id}) => id == data.id)) {
dispatch("saveDialogMsg", data)
}

View File

@ -179,21 +179,29 @@ body.dark-mode-reverse {
.dialog-item {
.dialog-view {
.dialog-head {
background-color: #e1e1e1;
.dialog-content {
background-color: #e1e1e1;
.content-text,
.content-record,
.content-meeting {
color: #ffffff;
}
}
.dialog-emoji {
> li {
background-color: rgba(#f3f3f3, 0.5);
&.hasme {
background-color: #f3f3f3;
}
}
}
}
}
&.self {
.dialog-view {
.dialog-head {
background-color: $primary-color;
.dialog-content {
background-color: $primary-color;
.content-text {
> pre {
a {
@ -205,6 +213,17 @@ body.dark-mode-reverse {
}
}
}
.dialog-emoji {
> li {
background-color: rgba(#b2ff93, 0.5);
&.hasme {
background-color: #b2ff93;
}
.emoji-num {
color: #000000;
}
}
}
}
}
}

View File

@ -221,9 +221,7 @@
&.operate-action {
.dialog-head {
.dialog-content {
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.2);
}
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.2);
}
}
@ -236,16 +234,16 @@
.dialog-head {
display: flex;
align-items: flex-start;
flex-direction: column;
background-color: #F4F5F7;
padding: 8px;
min-width: 32px;
border-radius: 2px 8px 8px 8px;
transition: box-shadow 0.3s ease;
.dialog-content {
background-color: #F4F5F7;
padding: 8px;
min-width: 32px;
border-radius: 2px 8px 8px 8px;
display: flex;
align-items: flex-start;
transition: box-shadow 0.3s ease;
&.an-emoticon,
&.an-emoji,
@ -558,6 +556,34 @@
text-decoration: underline;
}
}
.dialog-emoji {
display: flex;
align-items: center;
margin-top: 4px;
> li {
list-style: none;
display: flex;
align-items: center;
padding: 2px 7px;
margin-right: 8px;
border-radius: 14px;
line-height: 22px;
cursor: pointer;
background-color: rgba(#e1e1e1, 0.5);
&.hasme {
background-color: #e1e1e1;
}
.emoji-symbol {
font-size: 16px;
}
.emoji-num {
font-size: 12px;
padding-left: 4px;
color: #818181;
}
}
}
}
.dialog-foot {
@ -674,12 +700,10 @@
margin: 0 8px 0 0;
.dialog-head {
flex-direction: row-reverse;
background-color: $primary-color;
border-radius: 8px 2px 8px 8px;
.dialog-content {
background-color: $primary-color;
border-radius: 8px 2px 8px 8px;
.content-text {
color: #ffffff;
@ -738,6 +762,18 @@
}
}
}
.dialog-emoji {
> li {
background-color: rgba(#5ba93c, 0.5);
&.hasme {
background-color: #5ba93c;
}
.emoji-num {
color: #ffffff;
}
}
}
}
.dialog-foot {
@ -842,7 +878,7 @@
.operate-position {
position: absolute;
top: 0;
right: 0;
left: 0;
width: 1px;
opacity: 0;
visibility: hidden;
@ -941,13 +977,61 @@
}
.dialog-wrapper-operate {
.operate-item {
.ivu-dropdown-item {
padding: 0;
&:hover {
background-color: transparent;
}
}
.operate-action {
width: 316px;
padding: 8px;
margin-bottom: -8px;
display: grid;
justify-content: space-between;
grid-template-columns: repeat(auto-fill, 50px);
> li {
list-style: none;
width: 50px;
height: 48px;
margin-bottom: 12px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
.taskfont {
font-size: 20px;
}
> span {
font-size: 12px;
}
}
}
.operate-emoji {
width: 316px;
padding: 8px 4px 2px;
display: flex;
align-items: center;
justify-content: space-between;
min-width: 80px;
.taskfont {
margin-left: 24px;
overflow: auto;
position: relative;
&:after {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 1px;
background-color: #f4f5f5;
}
> li {
list-style: none;
width: 44px;
box-sizing: content-box;
display: flex;
flex-shrink: 0;
font-size: 24px;
justify-content: center;
align-items: center;
}
}
}

View File

@ -185,7 +185,7 @@
.operate-position {
position: absolute;
top: 0;
right: 0;
left: 0;
width: 1px;
opacity: 0;
visibility: hidden;

View File

@ -192,7 +192,7 @@
.operate-position {
position: absolute;
top: 0;
right: 0;
left: 0;
width: 1px;
opacity: 0;
visibility: hidden;

View File

@ -367,7 +367,7 @@
.operate-position {
position: absolute;
top: 0;
right: 0;
left: 0;
width: 1px;
opacity: 0;
visibility: hidden;