koishi-plugin-chat-patch 3.0.2 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/client/vue/index.vue +60 -634
  2. package/client/web/dist/assets/AppIcon-DIku6-h0.png +0 -0
  3. package/client/web/dist/assets/AppIconDark-BYymN71t.png +0 -0
  4. package/client/web/dist/assets/CapacitorAppIcon-CSZwNhvi.png +0 -0
  5. package/client/web/dist/assets/CapacitorAppIconDark-BMvMOzsg.png +0 -0
  6. package/client/web/dist/assets/Chat-CTdyeaX2.css +1 -0
  7. package/client/web/dist/assets/Chat-DkZvD7d8.js +28 -0
  8. package/client/web/dist/assets/MealHungryPan-Beos3lRI.js +1 -0
  9. package/client/web/dist/assets/MsgBody-aZ4_aija.js +1 -0
  10. package/client/web/dist/assets/MsgBody.vue_vue_type_script_setup_true_lang-CPZjRLp6.js +67 -0
  11. package/client/web/dist/assets/NapcatAppIcon-C7VOAkW9.png +0 -0
  12. package/client/web/dist/assets/Vue-lEhHouxB.png +0 -0
  13. package/client/web/dist/assets/append_darwin-CqBNU62J.css +1 -0
  14. package/client/web/dist/assets/append_full_vibrancy-j0D2LM9m.css +1 -0
  15. package/client/web/dist/assets/append_linux-uhxeBqeC.css +1 -0
  16. package/client/web/dist/assets/append_linux_vibrancy-COiZq8MS.css +1 -0
  17. package/client/web/dist/assets/append_napcat-DV8CAyAd.css +1 -0
  18. package/client/web/dist/assets/append_new-407Lo5_V.css +1 -0
  19. package/client/web/dist/assets/append_vibrancy-6FHctkOY.css +1 -0
  20. package/client/web/dist/assets/append_win32-srmD-78y.css +1 -0
  21. package/client/web/dist/assets/cakes-CxTVKdnK.svg +189 -0
  22. package/client/web/dist/assets/core-DhEqZVGG.js +1 -0
  23. package/client/web/dist/assets/event-Cadp6m6J.js +1 -0
  24. package/client/web/dist/assets/index-CPTXiemn.js +4045 -0
  25. package/client/web/dist/assets/index-Cg_esKB0.js +1 -0
  26. package/client/web/dist/assets/index-Cln012sP.css +1 -0
  27. package/client/web/dist/assets//346/226/271/346/255/243/345/226/265/345/221/234/344/275/223-CJKIKBn3.ttf +0 -0
  28. package/client/web/dist/assets//346/234/237/345/276/205-C90_OY60.png +0 -0
  29. package/client/web/dist/bcui/css/color-dark.css +35 -0
  30. package/client/web/dist/bcui/css/color-light.css +35 -0
  31. package/client/web/dist/bcui/css/style.css +468 -0
  32. package/client/web/dist/bcui/js/main.js +193 -0
  33. package/client/web/dist/css/append-dark.css +13 -0
  34. package/client/web/dist/css/append-light.css +13 -0
  35. package/client/web/dist/css/append-napcat-dark.css +14 -0
  36. package/client/web/dist/css/append-napcat-light.css +14 -0
  37. package/client/web/dist/favicon.ico +0 -0
  38. package/client/web/dist/img/icons/icon-for-fuck-apple.png +0 -0
  39. package/client/web/dist/img/icons/icon-maskable.png +0 -0
  40. package/client/web/dist/img/icons/icon.png +0 -0
  41. package/client/web/dist/img/icons/icon.svg +10 -0
  42. package/client/web/dist/index.html +22 -0
  43. package/client/web/dist/robots.txt +2 -0
  44. package/client/web/dist/sw.js +58 -0
  45. package/client/web/env.d.ts +56 -0
  46. package/client/web/index.html +21 -0
  47. package/client/web/package-lock.json +3419 -0
  48. package/client/web/package.json +62 -0
  49. package/client/web/public/bcui/css/color-dark.css +35 -0
  50. package/client/web/public/bcui/css/color-light.css +35 -0
  51. package/client/web/public/bcui/css/style.css +468 -0
  52. package/client/web/public/bcui/js/main.js +193 -0
  53. package/client/web/public/css/append-dark.css +13 -0
  54. package/client/web/public/css/append-light.css +13 -0
  55. package/client/web/public/css/append-napcat-dark.css +14 -0
  56. package/client/web/public/css/append-napcat-light.css +14 -0
  57. package/client/web/public/favicon.ico +0 -0
  58. package/client/web/public/img/icons/icon-for-fuck-apple.png +0 -0
  59. package/client/web/public/img/icons/icon-maskable.png +0 -0
  60. package/client/web/public/img/icons/icon.png +0 -0
  61. package/client/web/public/img/icons/icon.svg +10 -0
  62. package/client/web/public/robots.txt +2 -0
  63. package/client/web/public/sw.js +58 -0
  64. package/client/web/scripts/copy-bcui.mjs +17 -0
  65. package/client/web/src/App.vue +1127 -0
  66. package/client/web/src/assets/css/append/append_darwin.css +68 -0
  67. package/client/web/src/assets/css/append/append_full_vibrancy.css +290 -0
  68. package/client/web/src/assets/css/append/append_linux.css +125 -0
  69. package/client/web/src/assets/css/append/append_linux_vibrancy.css +22 -0
  70. package/client/web/src/assets/css/append/append_napcat.css +35 -0
  71. package/client/web/src/assets/css/append/append_new.css +843 -0
  72. package/client/web/src/assets/css/append/append_vibrancy.css +113 -0
  73. package/client/web/src/assets/css/append/append_win32.css +109 -0
  74. package/client/web/src/assets/css/append/mobile/append_mobile_horizontal.css +349 -0
  75. package/client/web/src/assets/css/append/mobile/append_mobile_vertical.css +96 -0
  76. package/client/web/src/assets/css/chat.css +2104 -0
  77. package/client/web/src/assets/css/msg.css +941 -0
  78. package/client/web/src/assets/css/options.css +811 -0
  79. package/client/web/src/assets/css/sys_notice.css +62 -0
  80. package/client/web/src/assets/css/view.css +2105 -0
  81. package/client/web/src/assets/font//346/226/271/346/255/243/345/226/265/345/221/234/344/275/223.ttf +0 -0
  82. package/client/web/src/assets/img/Vue.png +0 -0
  83. package/client/web/src/assets/img/cakes.svg +189 -0
  84. package/client/web/src/assets/img/icons/AppIcon.png +0 -0
  85. package/client/web/src/assets/img/icons/AppIconDark.png +0 -0
  86. package/client/web/src/assets/img/icons/CapacitorAppIcon.png +0 -0
  87. package/client/web/src/assets/img/icons/CapacitorAppIconDark.png +0 -0
  88. package/client/web/src/assets/img/icons/NapcatAppIcon.png +0 -0
  89. package/client/web/src/assets/img/icons/NapcatAppIconDark.png +0 -0
  90. package/client/web/src/assets/img/icons/exit.png +0 -0
  91. package/client/web/src/assets/img/icons/pen.png +0 -0
  92. package/client/web/src/assets/img/stars.svg +89 -0
  93. package/client/web/src/assets/img//346/234/237/345/276/205.png +0 -0
  94. package/client/web/src/assets/l10n/_About I10n.md +35 -0
  95. package/client/web/src/assets/l10n/_l10nconfig.json +7 -0
  96. package/client/web/src/assets/l10n/zh-CN.po +1941 -0
  97. package/client/web/src/assets/logo.png +0 -0
  98. package/client/web/src/assets/pathMap/LLOneBot.yaml +2 -0
  99. package/client/web/src/assets/pathMap/Lagrange.OneBot.yaml +180 -0
  100. package/client/web/src/assets/pathMap/NapCat.Onebot.yaml +224 -0
  101. package/client/web/src/assets/pathMap/SnowLuma.yaml +80 -0
  102. package/client/web/src/assets/qed.txt +1 -0
  103. package/client/web/src/components/AboutPan.vue +197 -0
  104. package/client/web/src/components/BulletinBody.vue +129 -0
  105. package/client/web/src/components/DepPan.vue +310 -0
  106. package/client/web/src/components/EmojiFace.vue +32 -0
  107. package/client/web/src/components/FacePan.vue +461 -0
  108. package/client/web/src/components/FileBody.vue +129 -0
  109. package/client/web/src/components/FileManager.vue +532 -0
  110. package/client/web/src/components/FriendBody.vue +54 -0
  111. package/client/web/src/components/GlobalSessionSearchBar.vue +301 -0
  112. package/client/web/src/components/History.vue +49 -0
  113. package/client/web/src/components/LazyLottie.vue +82 -0
  114. package/client/web/src/components/MergePan.vue +127 -0
  115. package/client/web/src/components/MsgBody.vue +1478 -0
  116. package/client/web/src/components/MusicPlayer.vue +549 -0
  117. package/client/web/src/components/NoticeBody.vue +168 -0
  118. package/client/web/src/components/QzonePermissionPan.vue +150 -0
  119. package/client/web/src/components/RawMsgRenderPreviewPan.vue +190 -0
  120. package/client/web/src/components/ThemeColorPickerPan.vue +420 -0
  121. package/client/web/src/components/TinySessionBody.vue +51 -0
  122. package/client/web/src/components/UmamiInfoPan.vue +1549 -0
  123. package/client/web/src/components/UpdatePan.vue +263 -0
  124. package/client/web/src/components/ViewerCom.vue +1360 -0
  125. package/client/web/src/components/VoiceMsg.vue +626 -0
  126. package/client/web/src/components/WelPan.vue +894 -0
  127. package/client/web/src/components/msg-body/.gitkeep +0 -0
  128. package/client/web/src/components/msg-component/JsonSegComp.vue +138 -0
  129. package/client/web/src/components/msg-component/XmlSegComp.vue +132 -0
  130. package/client/web/src/components/msg-component/jsonComp/AutoReply.vue +97 -0
  131. package/client/web/src/components/msg-component/jsonComp/Contact.lua.vue +129 -0
  132. package/client/web/src/components/msg-component/jsonComp/Feed.lua.vue +94 -0
  133. package/client/web/src/components/msg-component/jsonComp/Forum.vue +84 -0
  134. package/client/web/src/components/msg-component/jsonComp/Mannounce.vue +111 -0
  135. package/client/web/src/components/msg-component/jsonComp/Map.vue +52 -0
  136. package/client/web/src/components/msg-component/jsonComp/Miniapp.lua.vue +53 -0
  137. package/client/web/src/components/msg-component/jsonComp/Miniapp.vue +69 -0
  138. package/client/web/src/components/msg-component/jsonComp/Music.lua.vue +135 -0
  139. package/client/web/src/components/msg-component/jsonComp/Tuwen.lua.vue +56 -0
  140. package/client/web/src/components/notice-component/.gitkeep +0 -0
  141. package/client/web/src/components/notice-component/MealHungryPan.vue +119 -0
  142. package/client/web/src/components/tooltip/CustomFaceTooltip.vue +22 -0
  143. package/client/web/src/components/tooltip/Tooltip.vue +50 -0
  144. package/client/web/src/components/tooltip/Tooltips.vue +14 -0
  145. package/client/web/src/components/tooltip/UserInfoTooltip.vue +69 -0
  146. package/client/web/src/function/base.ts +238 -0
  147. package/client/web/src/function/connect.ts +2042 -0
  148. package/client/web/src/function/debug.ts +10 -0
  149. package/client/web/src/function/elements/information.ts +141 -0
  150. package/client/web/src/function/elements/system.ts +101 -0
  151. package/client/web/src/function/elements/vueComp.ts +62 -0
  152. package/client/web/src/function/favicon.ts +76 -0
  153. package/client/web/src/function/input.ts +69 -0
  154. package/client/web/src/function/model/emoji.ts +127 -0
  155. package/client/web/src/function/model/img.ts +150 -0
  156. package/client/web/src/function/model/msg-body.ts +51 -0
  157. package/client/web/src/function/msg.ts +2674 -0
  158. package/client/web/src/function/notify.ts +225 -0
  159. package/client/web/src/function/option.ts +749 -0
  160. package/client/web/src/function/satori-model.ts +818 -0
  161. package/client/web/src/function/satori.ts +325 -0
  162. package/client/web/src/function/sender.ts +178 -0
  163. package/client/web/src/function/tooltip.ts +49 -0
  164. package/client/web/src/function/utils/appUtil.ts +2288 -0
  165. package/client/web/src/function/utils/avatarUtil.ts +31 -0
  166. package/client/web/src/function/utils/backgroundUtil.ts +110 -0
  167. package/client/web/src/function/utils/linkViewUtil.ts +101 -0
  168. package/client/web/src/function/utils/localHistoryUtil.ts +517 -0
  169. package/client/web/src/function/utils/msgUtil.ts +1242 -0
  170. package/client/web/src/function/utils/pinyin.ts +141 -0
  171. package/client/web/src/function/utils/recordUtil.ts +187 -0
  172. package/client/web/src/function/utils/sessionUtil.ts +114 -0
  173. package/client/web/src/function/utils/systemUtil.ts +664 -0
  174. package/client/web/src/main.ts +91 -0
  175. package/client/web/src/pages/Chat.vue +3453 -0
  176. package/client/web/src/pages/Friends.vue +636 -0
  177. package/client/web/src/pages/Info.vue +717 -0
  178. package/client/web/src/pages/Messages.vue +875 -0
  179. package/client/web/src/pages/Options.vue +82 -0
  180. package/client/web/src/pages/Qzone.vue +1178 -0
  181. package/client/web/src/pages/chat-view/ChatDanmaku.vue +759 -0
  182. package/client/web/src/pages/chat-view/ChatGlagame.vue +642 -0
  183. package/client/web/src/pages/chat-view/ChatTerminal.vue +1044 -0
  184. package/client/web/src/pages/chat-view/SystemNotice.vue +171 -0
  185. package/client/web/src/pages/options/OptAccount.vue +107 -0
  186. package/client/web/src/pages/options/OptAddon.vue +185 -0
  187. package/client/web/src/pages/options/OptDev.vue +704 -0
  188. package/client/web/src/pages/options/OptFunction.vue +403 -0
  189. package/client/web/src/pages/options/OptInfo.vue +165 -0
  190. package/client/web/src/pages/options/OptView.vue +846 -0
  191. package/client/web/src/runtime/backend.ts +310 -0
  192. package/client/web/src/state/auth.ts +14 -0
  193. package/client/web/src/state/chat.ts +36 -0
  194. package/client/web/src/state/connection.ts +20 -0
  195. package/client/web/src/state/contact.ts +37 -0
  196. package/client/web/src/state/qzone.ts +18 -0
  197. package/client/web/src/state/sessionHistory.ts +25 -0
  198. package/client/web/src/state/settings.ts +18 -0
  199. package/client/web/src/state/sticker.ts +10 -0
  200. package/client/web/src/state/ui.ts +56 -0
  201. package/client/web/tsconfig.json +27 -0
  202. package/client/web/tsconfig.tsbuildinfo +1 -0
  203. package/client/web/vite.config.ts +33 -0
  204. package/dist/index.js +1 -12
  205. package/dist/style.css +1 -1
  206. package/lib/index.js +2070 -2077
  207. package/package.json +15 -2
  208. package/readme.md +0 -22
  209. package/src/bootstrap.ts +71 -0
  210. package/src/cache.ts +319 -0
  211. package/src/config.ts +23 -35
  212. package/src/database.ts +650 -0
  213. package/src/index.ts +27 -59
  214. package/src/media.ts +302 -0
  215. package/src/recorder.ts +175 -0
  216. package/src/self-message.ts +456 -0
  217. package/src/server.d.ts +26 -0
  218. package/src/types.ts +108 -48
  219. package/src/web.ts +712 -0
  220. package/client/vue/chat-logic.ts +0 -683
  221. package/client/vue/composables/useChatActions.ts +0 -61
  222. package/client/vue/composables/useChatData.ts +0 -185
  223. package/client/vue/composables/useImageCache.ts +0 -281
  224. package/client/vue/composables/useVideoCache.ts +0 -125
  225. package/client/vue/types.ts +0 -72
  226. package/lib/api-handlers.d.ts +0 -25
  227. package/lib/config.d.ts +0 -14
  228. package/lib/file-manager.d.ts +0 -124
  229. package/lib/index.d.ts +0 -17
  230. package/lib/logger.d.ts +0 -9
  231. package/lib/message-handler.d.ts +0 -35
  232. package/lib/types.d.ts +0 -57
  233. package/lib/utils.d.ts +0 -18
  234. package/src/api-handlers.ts +0 -646
  235. package/src/file-manager.ts +0 -1436
  236. package/src/message-handler.ts +0 -507
  237. package/src/utils.ts +0 -189
@@ -0,0 +1,3453 @@
1
+ <!--
2
+ * @FileDescription: 聊天面板页面
3
+ * @Author: Stapxs
4
+ * @Date:
5
+ * 2022/08/14
6
+ * 2022/12/12
7
+ * @Version:
8
+ * 1.0 - 初始版本
9
+ * 1.5 - 重构为 ts 版本,代码格式优化
10
+ -->
11
+
12
+ <template>
13
+ <div id="chat-pan"
14
+ v-move="chatMoveOptions"
15
+ :class="'chat-pan' +
16
+ (uiStore.openSideBar ? ' open' : '') +
17
+ (['linux', 'win32'].includes(backend.platform ?? '') ? ' withBar' : '')"
18
+ :style="{
19
+ 'background-image': toBackgroundImageStyle(!settingsStore.sysConfig.chat_more_blur ? settingsStore.sysConfig.chat_background : ''),
20
+ 'background-position': settingsStore.sysConfig.chat_background_align ?? 'center',
21
+ 'background-size': settingsStore.sysConfig.chat_background_fit ?? 'cover'
22
+ }"
23
+ @v-move-right.prevent="exitWin()">
24
+ <slot name="chat-extra" />
25
+ <!-- 聊天基本信息 -->
26
+ <div class="info">
27
+ <font-awesome-icon class="back" :icon="['fas', 'angle-left']" @click="exitWin" />
28
+ <img :src="chat.show.avatar || '/img/icons/icon.svg'" @error="avatarError">
29
+ <div class="info">
30
+ <p>
31
+ {{ chat.show.name }}
32
+ <template
33
+ v-if="chat.show.type == 'group'">
34
+ ({{
35
+ chat.info.group_members.length
36
+ }})
37
+ </template>
38
+ </p>
39
+ <span v-if="chat.show.temp">
40
+ {{ $t('来自群聊:{group}', { group: chat.show.temp }) }}
41
+ </span>
42
+ <span v-else>
43
+ <template v-if="chat.show.appendInfo">
44
+ {{ chat.show.appendInfo }}
45
+ </template>
46
+ <template v-else>
47
+ {{
48
+ list[list.length - 1] ? $t('上次消息 - {time}', {
49
+ time: Intl.DateTimeFormat(trueLang, {
50
+ hour: 'numeric',
51
+ minute: 'numeric',
52
+ second: 'numeric',
53
+ }).format(new Date(list[list.length - 1].time * 1000)),
54
+ }) : $t('暂无消息')
55
+ }}
56
+ </template>
57
+ </span>
58
+ </div>
59
+ <div class="space" />
60
+ <div class="more">
61
+ <font-awesome-icon :icon="['fas', 'ellipsis-vertical']" @click="openChatInfoPan" />
62
+ </div>
63
+ </div>
64
+ <!-- 加载中指示器 -->
65
+ <div :class=" 'loading' + (uiStore.nowGetHistory && uiStore.canLoadHistory ? ' show' : '')">
66
+ <font-awesome-icon :icon="['fas', 'spinner']" />
67
+ <span>{{ $t('加载中') }}</span>
68
+ </div>
69
+ <!-- 消息显示区 -->
70
+ <div id="msgPan" ref="msgPan" class="chat"
71
+ style="scroll-behavior: smooth"
72
+ @scroll="chatScroll($event, details[3].open)">
73
+ <template v-if="!details[3].open">
74
+ <div v-if="!uiStore.canLoadHistory" class="note note-nomsg">
75
+ <hr>
76
+ <a>{{ $t('没有更多消息了') }}</a>
77
+ </div>
78
+ <div v-if="uiStore.loadHistoryFail" class="note note-nomsg">
79
+ <hr>
80
+ <a>{{ $t('获取历史记录失败') }}</a>
81
+ </div>
82
+ <!-- 时间戳,在下滑加载的时候会显示,方便在大段的相连消息上让用户知道消息时间 -->
83
+ <NoticeBody v-if="uiStore.nowGetHistory && list.length > 0"
84
+ :data="{ sub_type: 'time', time: list[0].time }" />
85
+ <TransitionGroup :name="settingsStore.sysConfig.opt_fast_animation ? '' : 'msglist'" tag="div">
86
+ <template v-for="(msgIndex, index) in list">
87
+ <!-- 时间戳 -->
88
+ <NoticeBody
89
+ v-if="isShowTime(list[Number(index) - 1] ? list[Number(index) - 1].time : undefined, msgIndex.time)"
90
+ :key="'notice-time-' + (msgIndex.time / ( 4 * 60 )).toFixed(0)"
91
+ :data="{ sub_type: 'time', time: msgIndex.time }" />
92
+ <!-- [已删除]消息 -->
93
+ <NoticeBody
94
+ v-if="isDeleteMsg(msgIndex)"
95
+ :key="'delete-' + msgIndex.message_id"
96
+ :data="{ sub_type: 'delete' }" />
97
+ <!-- 消息体 -->
98
+ <MsgBody v-else-if="(msgIndex.post_type === 'message' ||
99
+ msgIndex.post_type === 'message_sent') &&
100
+ msgIndex.message.length > 0"
101
+ :key="msgIndex.fake_message_id ?? msgIndex.message_id"
102
+ :selected="multipleSelectList.includes(msgIndex.message_id) || tags.menuDisplay.menuSelectedMsgId == msgIndex.message_id"
103
+ :selecting="multipleSelectList.length > 0"
104
+ :data="msgIndex"
105
+ :image-list-header="chatImg"
106
+ @click="msgClick($event, msgIndex)"
107
+ @show-menu="showMsgMeun"
108
+ @scroll-to-msg="scrollToMsg"
109
+ @image-loaded="imgLoadedScroll"
110
+ @left-move="replyMsg"
111
+ @send-poke="sendPoke"
112
+ @content-rendered="scrollBottomAfterLayout" />
113
+ <!-- 其他通知消息 -->
114
+ <NoticeBody v-else-if="msgIndex.post_type === 'notice'"
115
+ :id="uuid()"
116
+ :key="'notice-' + index"
117
+ :data="msgIndex" />
118
+ </template>
119
+ </TransitionGroup>
120
+ </template>
121
+ <template v-else>
122
+ <!-- 搜索消息结果显示 -->
123
+ <TransitionGroup
124
+ :name="settingsStore.sysConfig.opt_fast_animation ? '' : 'msglist'"
125
+ tag="div">
126
+ <template v-for="(msgIndex, index) in tags.search.list">
127
+ <!-- 时间戳 -->
128
+ <NoticeBody
129
+ v-if="isShowTime(list[Number(index) - 1] ? list[Number(index) - 1].time : undefined, msgIndex.time)"
130
+ :key="'notice-time-' + index"
131
+ :data="{ sub_type: 'time', time: msgIndex.time }" />
132
+ <!-- 消息体 -->
133
+ <MsgBody v-if=" (msgIndex.post_type === 'message' ||
134
+ msgIndex.post_type === 'message_sent') &&
135
+ msgIndex.message.length > 0"
136
+ :key="msgIndex.fake_message_id ?? msgIndex.message_id"
137
+ :selected="multipleSelectList.includes(msgIndex.message_id) || tags.menuDisplay.menuSelectedMsgId == msgIndex.message_id"
138
+ :selecting="multipleSelectList.length > 0"
139
+ :data="msgIndex"
140
+ @scroll-to-msg="scrollToMsg"
141
+ @show-menu="showMsgMeun"
142
+ @image-loaded="imgLoadedScroll"
143
+ @left-move="replyMsg"
144
+ @content-rendered="scrollBottomAfterLayout" />
145
+ </template>
146
+ </TransitionGroup>
147
+ </template>
148
+ <span ref="chatPadding" class="chat-padding">&nbsp;</span>
149
+ </div>
150
+ <!-- 滚动到底部悬浮标志 -->
151
+ <div class="new-msg"
152
+ :style="{ 'opacity': tags.showBottomButton ? 1 : 0 }"
153
+ @click="scrollBottom(true)">
154
+ <div class="ss-card">
155
+ <font-awesome-icon :icon="['fas', 'angle-down']" />
156
+ <span v-if="NewMsgNum > 0">{{ NewMsgNum }}</span>
157
+ </div>
158
+ </div>
159
+ <!-- 底部区域 -->
160
+ <div id="send-more" ref="sendMore" class="more">
161
+ <!-- 功能附加 -->
162
+ <div>
163
+ <div>
164
+ <!-- 表情面板 -->
165
+ <Transition name="pan">
166
+ <FacePan v-show="details[1].open"
167
+ @add-special-msg="addSpecialMsg" @send-msg="sendMsg" />
168
+ </Transition>
169
+ <!-- 精华消息 -->
170
+ <Transition name="pan">
171
+ <div v-show="details[2].open && chat.info.jin_info.list.length > 0"
172
+ class="ss-card jin-pan">
173
+ <div>
174
+ <font-awesome-icon :icon="['fas', 'message']" />
175
+ <span>{{ $t('精华消息') }}</span>
176
+ <font-awesome-icon :icon="['fas', 'xmark']" @click="details[2].open = !details[2].open" />
177
+ </div>
178
+ <div
179
+ class="jin-pan-body"
180
+ @scroll="jinScroll">
181
+ <div v-for="(item, index) in chat.info.jin_info.list"
182
+ :key="'jin-' + index">
183
+ <div>
184
+ <img :src="item.sender_avatar || '/img/icons/icon.svg'">
185
+ <div>
186
+ <a>{{ item.sender_nick }}</a>
187
+ <span>{{ item.sender_time ? Intl.DateTimeFormat(
188
+ trueLang,
189
+ {
190
+ hour: 'numeric',
191
+ minute: 'numeric',
192
+ },
193
+ ).format(new Date(item.sender_time * 1000))
194
+ : '' }}
195
+ {{ $t('发送') }}</span>
196
+ </div>
197
+ <span>{{
198
+ $t('{time},由 {name} 设置', {
199
+ time: item.sender_time ? Intl.DateTimeFormat(
200
+ trueLang,
201
+ {
202
+ hour: 'numeric',
203
+ minute: 'numeric',
204
+ },
205
+ ).format(new Date(item.sender_time * 1000)) : '',
206
+ name: item.add_digest_nick,
207
+ })
208
+ }}</span>
209
+ </div>
210
+ <div class="context">
211
+ <template
212
+ v-for="(context, indexc) in item.msg_content"
213
+ :key="'jinc-' + index + '-' + indexc">
214
+ <span v-if="context.type === 'text'">
215
+ {{ context.data.text }}
216
+ </span>
217
+ <EmojiFace v-if="context.type === 'face'"
218
+ :emoji="Emoji.get(Number(context.data.id))" />
219
+ <img v-if="context.type === 'image'"
220
+ :src="context.data.url"
221
+ @click="viewerEssImg(context.data.url)">
222
+ </template>
223
+ </div>
224
+ </div>
225
+ <div v-show="tags.isJinLoading" class="jin-pan-load">
226
+ <font-awesome-icon :icon="['fas', 'spinner']" />
227
+ </div>
228
+ </div>
229
+ </div>
230
+ </Transition>
231
+ </div>
232
+ <!-- 多选指示器 -->
233
+ <div :class=" multipleSelectList.length > 0 ? 'select-tag show' : 'select-tag'">
234
+ <div>
235
+ <font-awesome-icon :icon="['fas', 'share-from-square']" @click="showForWard('individual-messages')" />
236
+ <span>{{ $t('逐条转发') }}</span>
237
+ </div>
238
+ <div>
239
+ <font-awesome-icon :icon="['fas', 'share']" @click="showForWard('merged-messages')" />
240
+ <span>{{ $t('合并转发') }}</span>
241
+ </div>
242
+ <div>
243
+ <font-awesome-icon :icon="['fas', 'scissors']" />
244
+ <span>{{ $t('截图') }}</span>
245
+ </div>
246
+ <div>
247
+ <font-awesome-icon :icon="['fas', 'trash-can']" @click="delMsgs" />
248
+ <span>{{ $t('删除') }}</span>
249
+ </div>
250
+ <div>
251
+ <font-awesome-icon :icon="['fas', 'copy']" @click="copyMsgs" />
252
+ <span>{{ $t('复制') }}</span>
253
+ </div>
254
+ <div>
255
+ <font-awesome-icon :icon="['fas', 'xmark']" @click="recallMsgs" />
256
+ <span>{{ $t('撤回') }}</span>
257
+ </div>
258
+ <div>
259
+ <span @click="multipleSelectList = []">{{ multipleSelectList.length }}</span>
260
+ <span>{{ $t('取消') }}</span>
261
+ </div>
262
+ </div>
263
+ <!-- 图片指示器 -->
264
+ <Transition name="img-pan">
265
+ <div v-show="imgCache.size > 0"
266
+ :class="{
267
+ 'img-pan': true,
268
+ 'ss-card': true,
269
+ }"
270
+ @wheel="($event.currentTarget as HTMLElement).scrollLeft += $event.deltaY">
271
+ <div class="imgs">
272
+ <div v-for="[key, value] in imgCache"
273
+ :key="'imgCache-' + key">
274
+ <div class="img-btns">
275
+ <div @click="editImg(key)">
276
+ <font-awesome-icon :icon="['fas', 'pencil']" />
277
+ </div>
278
+ <hr>
279
+ <div @click="deleteImg(key)">
280
+ <font-awesome-icon style="color: var(--color-red)" :icon="['fas', 'xmark']" />
281
+ </div>
282
+ </div>
283
+ <div class="img">
284
+ <img :src="value" alt="[图片]">
285
+ </div>
286
+ <span>#{{ key }}</span>
287
+ </div>
288
+ </div>
289
+ </div>
290
+ </Transition>
291
+ <!-- 搜索指示器 -->
292
+ <div :class="details[3].open ? 'search-tag show' : 'search-tag'">
293
+ <font-awesome-icon :icon="['fas', 'search']" />
294
+ <span>{{ settingsStore.sysConfig.enable_local_history ? $t('搜索已保存的消息') : $t('搜索已加载的消息') }}</span>
295
+ <div @click="closeSearch">
296
+ <font-awesome-icon :icon="['fas', 'xmark']" />
297
+ </div>
298
+ </div>
299
+ <!-- 回复指示器 -->
300
+ <div :class="tags.isReply ? 'replay-tag show' : 'replay-tag'">
301
+ <font-awesome-icon :icon="['fas', 'reply']" />
302
+ <span>{{
303
+ selectedMsg === null ?
304
+ '' : selectedMsg.sender.nickname + ': ' + getMsgRawTxt(selectedMsg)
305
+ }}</span>
306
+ <div @click="cancelReply">
307
+ <font-awesome-icon :icon="['fas', 'xmark']" />
308
+ </div>
309
+ </div>
310
+ <!-- At 指示器 -->
311
+ <div
312
+ :class="atFindList != null ? 'at-tag show' : 'at-tag'"
313
+ contenteditable="true"
314
+ @blur="choiceAt(undefined)">
315
+ <div v-for="(item, index) in atFindList != null ? atFindList : []"
316
+ :key="'atFind-' + item.user_id"
317
+ :class="{ selected: index === atSelectedIndex }"
318
+ @click="choiceAt(item.user_id)">
319
+ <img :src="'/img/icons/icon.svg'">
320
+ <span>{{
321
+ item.card != '' && item.card != null ? item.card : item.nickname
322
+ }}</span>
323
+ <a>{{ item.user_id }}</a>
324
+ </div>
325
+ <div v-if="atFindList?.length == 0" class="emp">
326
+ <span>{{ $t('没有找到匹配的群成员') }}</span>
327
+ </div>
328
+ </div>
329
+ <!-- 更多功能 -->
330
+ <div :class="(tags.showMoreDetail ? 'more-detail show' : 'more-detail') +
331
+ (voiceMenuShow ? ' voice-menu-open' : '')"
332
+ @click="voiceMenuShow = false">
333
+ <div
334
+ :title="$t('图片')"
335
+ @click="runSelectImg">
336
+ <font-awesome-icon :icon="['fas', 'image']" />
337
+ <input id="choice-pic" type="file" accept="image/*" style="display: none"
338
+ @change="selectImg">
339
+ <label for="choice-pic" class="sr-only">{{ $t('选择图片') }}</label>
340
+ </div>
341
+ <div
342
+ :title="$t('文件')"
343
+ @click="runSelectFile">
344
+ <font-awesome-icon :icon="['fas', 'folder']" />
345
+ <input id="choice-file" type="file"
346
+ style="display: none" @change="selectFile">
347
+ <label for="choice-file" class="sr-only">{{ $t('选择文件') }}</label>
348
+ </div>
349
+ <div
350
+ :title="$t('视频')"
351
+ @click="runSelectVideo">
352
+ <font-awesome-icon :icon="['fas', 'video']" />
353
+ <input id="choice-video" type="file" accept="video/*"
354
+ style="display: none" @change="selectVideo">
355
+ <label for="choice-video" class="sr-only">{{ $t('选择视频') }}</label>
356
+ </div>
357
+ <div class="voice-menu-wrap">
358
+ <div
359
+ :title="voiceRecording ? $t('停止录音') : $t('发送语音')"
360
+ :class="{ active: voiceRecording }"
361
+ @click.stop="toggleVoiceMenu">
362
+ <font-awesome-icon :icon="['fas', voiceRecording ? 'stop' : 'microphone']" />
363
+ </div>
364
+ <div v-if="voiceMenuShow" class="voice-menu" @click.stop>
365
+ <div @click="startVoiceRecord">{{ $t('浏览器录音') }}</div>
366
+ <div @click="selectVoiceFile">{{ $t('音频文件') }}</div>
367
+ </div>
368
+ <input id="choice-audio" type="file" accept="audio/*"
369
+ style="display: none" @change="selectAudioFile">
370
+ </div>
371
+ <div class="space" />
372
+ <div :title="$t('搜索消息')" @click="openSearch">
373
+ <font-awesome-icon :icon="['fas', 'search']" />
374
+ </div>
375
+ </div>
376
+ </div>
377
+ <!-- 消息发送框 -->
378
+ <div>
379
+ <div v-menu.prevent="_=>moreFunClick()"
380
+ @click="moreFunClick(settingsStore.sysConfig.quick_send)">
381
+ <font-awesome-icon v-if="tags.showMoreDetail || details.find(item => item.open)" :icon="['fas', 'minus']" />
382
+ <font-awesome-icon v-else-if="settingsStore.sysConfig.quick_send == 'default'" :icon="['fas', 'plus']" />
383
+ <font-awesome-icon v-else-if="settingsStore.sysConfig.quick_send == 'img'" :icon="['fas', 'image']" />
384
+ <font-awesome-icon v-else-if="settingsStore.sysConfig.quick_send == 'file'" :icon="['fas', 'folder']" />
385
+ </div>
386
+ <div>
387
+ <form @submit="mainSubmit">
388
+ <template v-if="pendingVoice">
389
+ <div id="voice-pending-input" class="voice-pending" role="button"
390
+ @click="cancelPendingVoice">
391
+ <span>[{{ $t('语音') }}]</span>
392
+ <font-awesome-icon :icon="['fas', 'xmark']" />
393
+ </div>
394
+ </template>
395
+ <template v-else>
396
+ <template v-if="settingsStore.sysConfig.send_key === 'none'">
397
+ <label for="main-input" class="sr-only">{{ $t('消息输入框') }}</label>
398
+ <input
399
+ id="main-input"
400
+ ref="mainInput"
401
+ v-model="msg"
402
+ type="text"
403
+ autocomplete="off"
404
+ :disabled="uiStore.openSideBar || chat.info.me_info.shut_up_timestamp > 0"
405
+ :placeholder="
406
+ chat.info.me_info.shut_up_timestamp > 0
407
+ ? $t('已被禁言至:{time}', {
408
+ time: Intl.DateTimeFormat(
409
+ trueLang, getTimeConfig(
410
+ new Date(chat.info.me_info.shut_up_timestamp * 1000),
411
+ ),
412
+ ).format(new Date(chat.info.me_info.shut_up_timestamp * 1000)),
413
+ }) : ''"
414
+ @paste="addImg"
415
+ @keydown="mainAtKey"
416
+ @keyup="mainKeyUp"
417
+ @click="selectSQIn"
418
+ @input="handleInput">
419
+ </template>
420
+ <template v-else>
421
+ <label for="main-input-ex" class="sr-only">{{ $t('消息输入框') }}</label>
422
+ <textarea id="main-input-ex"
423
+ ref="mainInput"
424
+ v-model="msg"
425
+ type="text"
426
+ :disabled="uiStore.openSideBar"
427
+ @paste="addImg"
428
+ @keydown="mainKey"
429
+ @keyup="mainKeyUp"
430
+ @click="selectSQIn"
431
+ @input="handleInput"
432
+ @compositionstart="handleCompositionStart"
433
+ @compositionend="handleCompositionEnd" />
434
+ </template>
435
+ </template>
436
+ </form>
437
+ <slot name="main-input-button" />
438
+ <div @click="sendMsg('sendMsgBack')">
439
+ <font-awesome-icon v-if="details[3].open" :icon="['fas', 'search']" />
440
+ <font-awesome-icon v-else :icon="['fas', 'angle-right']" />
441
+ </div>
442
+ </div>
443
+ </div>
444
+ <div />
445
+ </div>
446
+ <!-- 合并转发消息预览器 -->
447
+ <MergePan ref="mergePan" />
448
+ <!-- 消息右击菜单 -->
449
+ <Teleport to="body">
450
+ <div :class="'msg-menu' + (['linux', 'win32'].includes(backend.platform ?? '') ? ' withBar' : '')">
451
+ <div v-show="tags.showMsgMenu" class="msg-menu-bg" @click="closeMsgMenu" />
452
+ <div id="msgMenu" :class="tags.showMsgMenu ?
453
+ 'ss-card msg-menu-body show' : 'ss-card msg-menu-body'">
454
+ <div v-show="tags.menuDisplay.add" @click="forwardSelf()">
455
+ <div><font-awesome-icon :icon="['fas', 'plus']" /></div>
456
+ <a>{{ $t('+ 1') }}</a>
457
+ </div>
458
+ <div v-show="tags.menuDisplay.relpy" @click="menuReplyMsg(true)">
459
+ <div><font-awesome-icon :icon="['fas', 'message']" /></div>
460
+ <a>{{ $t('回复') }}</a>
461
+ </div>
462
+ <div v-show="tags.menuDisplay.forward" @click="showForWard()">
463
+ <div><font-awesome-icon :icon="['fas', 'share']" /></div>
464
+ <a>{{ $t('转发') }}</a>
465
+ </div>
466
+ <div v-show="tags.menuDisplay.select" @click="intoMultipleSelect()">
467
+ <div><font-awesome-icon :icon="['fas', 'circle-check']" /></div>
468
+ <a>{{ $t('多选') }}</a>
469
+ </div>
470
+ <div v-show="tags.menuDisplay.copy" @click="copyMsg">
471
+ <div><font-awesome-icon :icon="['fas', 'clipboard']" /></div>
472
+ <a>{{ $t('复制') }}</a>
473
+ </div>
474
+ <div v-show="tags.menuDisplay.copySelect" @click="copySelectMsg">
475
+ <div><font-awesome-icon :icon="['fas', 'code']" /></div>
476
+ <a>{{ $t('复制选中文本') }}</a>
477
+ </div>
478
+ <div v-show="tags.menuDisplay.copyImg" @click="copyImg">
479
+ <div><font-awesome-icon :icon="['fas', 'object-ungroup']" /></div>
480
+ <a>{{ $t('复制图片') }}</a>
481
+ </div>
482
+ <div v-show="tags.menuDisplay.downloadImg != false" @click="downloadImg">
483
+ <div><font-awesome-icon :icon="['fas', 'floppy-disk']" /></div>
484
+ <a>{{ $t('下载图片') }}</a>
485
+ </div>
486
+ <div v-show="tags.menuDisplay.revoke" @click="revokeMsg">
487
+ <div><font-awesome-icon :icon="['fas', 'xmark']" /></div>
488
+ <a>{{ $t('撤回') }}</a>
489
+ </div>
490
+ <div v-show="tags.menuDisplay.reedit" @click="reeditMsg">
491
+ <div><font-awesome-icon :icon="['fas', 'pencil']" /></div>
492
+ <a>{{ $t('重新编辑') }}</a>
493
+ </div>
494
+ <div v-show="tags.menuDisplay.at"
495
+ @click="selectedMsg ? addSpecialMsg({ msgObj: { type: 'at', qq: Number(selectedMsg.sender.user_id) }, addText: true, }): '';
496
+ toMainInput();
497
+ closeMsgMenu()">
498
+ <div><font-awesome-icon :icon="['fas', 'at']" /></div>
499
+ <a>{{ $t('提及') }}</a>
500
+ </div>
501
+ <div v-show="tags.menuDisplay.poke" @click="sendPoke(selectedMsg ? selectedMsg.sender.user_id : undefined)">
502
+ <div><font-awesome-icon :icon="['fas', 'fa-hand-point-up']" /></div>
503
+ <a>{{ $t('戳一戳') }}</a>
504
+ </div>
505
+ <div v-show="tags.menuDisplay.remove" @click="removeUser">
506
+ <div><font-awesome-icon :icon="['fas', 'trash-can']" /></div>
507
+ <a>{{ $t('移出群聊') }}</a>
508
+ </div>
509
+ <div v-show="tags.menuDisplay.config"
510
+ @click="openChatInfoPan();
511
+ ($refs.infoRef as any).openMoreConfig(selectedMsg?.sender.user_id);
512
+ closeMsgMenu();">
513
+ <div><font-awesome-icon :icon="['fas', 'cog']" /></div>
514
+ <a>{{ $t('成员设置') }}</a>
515
+ </div>
516
+ <div v-show="tags.menuDisplay.jumpToMsg" @click="jumpSearchMsg">
517
+ <div><font-awesome-icon :icon="['fas', 'arrow-up-right-from-square']" /></div>
518
+ <a>{{ $t('跳转到消息') }}</a>
519
+ </div>
520
+ <div v-show="isDev" @click="consoleLogMsg">
521
+ <div><font-awesome-icon :icon="['fas', 'screwdriver-wrench']" /></div>
522
+ <a>{{ $t('调试信息') }}</a>
523
+ </div>
524
+ </div>
525
+ </div>
526
+ </Teleport>
527
+ <!-- 群 / 好友信息弹窗 -->
528
+ <Transition name="chat-info-float" :duration="{ enter: 300, leave: 200 }">
529
+ <Info ref="infoRef" :chat="chat" :tags="tags"
530
+ @close="openChatInfoPan" />
531
+ </Transition>
532
+ <!-- 转发面板 -->
533
+ <Transition>
534
+ <div v-if="tags.showForwardPan" class="forward-pan">
535
+ <div class="ss-card card">
536
+ <header>
537
+ <span>{{ $t('转发消息') }}</span>
538
+ <font-awesome-icon :icon="['fas', 'xmark']" @click="cancelForward" />
539
+ </header>
540
+ <label for="chat-forward-search" class="sr-only">{{ $t('搜索转发对象') }}</label>
541
+ <input id="chat-forward-search" :placeholder="$t('搜索 ……')" @input="searchForward">
542
+ <div>
543
+ <div v-for="data in forwardList"
544
+ :key=" 'forwardList-' + (data.user_id ?? data.group_id)"
545
+ @click="forwardMsg(data)">
546
+ <img loading="lazy"
547
+ :title="forwardDisplayName(data)"
548
+ :src="data.avatar || '/img/icons/icon.svg'">
549
+ <div>
550
+ <p>
551
+ {{ forwardDisplayName(data) }}
552
+ </p>
553
+ <span>{{ data.group_id ? $t('群组') : $t('好友') }}</span>
554
+ </div>
555
+ </div>
556
+ </div>
557
+ </div>
558
+ <div class="bg" @click="cancelForward" />
559
+ </div>
560
+ </Transition>
561
+ <div class="bg" :style="{
562
+ 'backdrop-filter': `blur(${!settingsStore.sysConfig.chat_more_blur ? settingsStore.sysConfig .chat_background_blur : 0}px)`
563
+ }" />
564
+ </div>
565
+ </template>
566
+
567
+ <script setup lang="ts">
568
+ import app from '../main'
569
+ import { i18n } from '../main'
570
+ import SendUtil from '../function/sender'
571
+ import Option, { get } from '../function/option'
572
+ import Info from '../pages/Info.vue'
573
+ import MsgBody from '../components/MsgBody.vue'
574
+ import NoticeBody from '../components/NoticeBody.vue'
575
+ import FacePan from '../components/FacePan.vue'
576
+ import MergePan from '../components/MergePan.vue'
577
+ import imageCompression from 'browser-image-compression'
578
+
579
+ import {
580
+ ref,
581
+ watch,
582
+ onMounted,
583
+ onBeforeUnmount,
584
+ markRaw,
585
+ nextTick,
586
+ reactive,
587
+ inject,
588
+ toRaw,
589
+ useTemplateRef,
590
+ } from 'vue'
591
+ import { v4 as uuid } from 'uuid'
592
+ import {
593
+ scrollToMsg,
594
+ downloadFile,
595
+ loadHistory as loadHistoryFirst,
596
+ shouldAutoFocus,
597
+ vMenu,
598
+ vMove,
599
+ VMoveOptions,
600
+ } from '../function/utils/appUtil'
601
+ import {
602
+ copyToClipboard,
603
+ getTimeConfig,
604
+ getTrueLang,
605
+ getViewTime,
606
+ } from '../function/utils/systemUtil'
607
+ import {
608
+ getMsgRawTxt,
609
+ sendMsgRaw,
610
+ isShowTime,
611
+ isDeleteMsg,
612
+ getImageUrlData,
613
+ getDifferencesWithRanges,
614
+ updateBaseOnMsgList,
615
+ } from '../function/utils/msgUtil'
616
+ import { Logger, LogType, PopInfo, PopType } from '../function/base'
617
+ import { Connector, loadChatHistoryFromCache, saveSentSelfMessage, sendForwardMessage } from '../function/connect'
618
+ import {
619
+ BaseChatInfoElem,
620
+ MsgItemElem,
621
+ SQCodeElem,
622
+ GroupMemberInfoElem,
623
+ UserFriendElem,
624
+ UserGroupElem,
625
+ MenuEventData,
626
+ } from '../function/elements/information'
627
+ import { backend } from '../runtime/backend'
628
+ import { toBackgroundImageStyle } from '../function/utils/backgroundUtil'
629
+ import { dbGetBefore, dbGetBeforeByTime, dbSearchMessages } from '../function/utils/localHistoryUtil'
630
+ import { normalizeSessionId } from '../function/utils/sessionUtil'
631
+ import Emoji from '../function/model/emoji'
632
+ import EmojiFace from '../components/EmojiFace.vue'
633
+ import { Img } from '../function/model/img'
634
+ import { useSessionHistoryStore } from '../state/sessionHistory'
635
+ import { useConnectionStore } from '../state/connection'
636
+ import { useUIStore } from '../state/ui'
637
+ import { useSettingsStore } from '../state/settings'
638
+ import { useAuthStore } from '../state/auth'
639
+ import { useChatStore } from '../state/chat'
640
+ import { useContactStore } from '../state/contact'
641
+ import { addUploadTask, failUploadTask } from '../components/FileManager.vue'
642
+ import { avatarError } from '../function/utils/avatarUtil'
643
+
644
+ defineOptions({ name: 'ViewChat' })
645
+
646
+ const $t = i18n.global.t
647
+ const { viewer: viewerRef } = inject<{ viewer: any }>('viewer', { viewer: null })
648
+
649
+ const { chat, list } = defineProps<{
650
+ chat: any
651
+ list: any[]
652
+ imgView?: any
653
+ }>()
654
+
655
+ const connectionStore = useConnectionStore()
656
+ const uiStore = useUIStore()
657
+ const settingsStore = useSettingsStore()
658
+ const authStore = useAuthStore()
659
+ const chatStore = useChatStore()
660
+ const contactStore = useContactStore()
661
+ const mergePan = useTemplateRef<InstanceType<typeof MergePan>>('mergePan')
662
+ const msgPan = useTemplateRef<HTMLDivElement>('msgPan')
663
+ const chatPadding = useTemplateRef<HTMLSpanElement>('chatPadding')
664
+ const sendMore = useTemplateRef<HTMLDivElement>('sendMore')
665
+ const mainInput = useTemplateRef<HTMLInputElement | HTMLTextAreaElement>('mainInput')
666
+
667
+ type ForwardAction = 'single-message' | 'individual-messages' | 'merged-messages'
668
+
669
+ const multipleSelectList = ref<string[]>([])
670
+ const selectedForwardAction = ref<ForwardAction>('single-message')
671
+ const tags = ref({
672
+ sendTag: 'REFUSE' as 'READY' | 'PASS' | 'REFUSE',
673
+ showBottomButton: false,
674
+ showMoreDetail: false,
675
+ showMsgMenu: false,
676
+ showForwardPan: false,
677
+ openChatInfo: false,
678
+ isReply: false,
679
+ isJinLoading: false,
680
+ onAtFind: false,
681
+ menuDisplay: {
682
+ menuSelectedMsgId: null as string | null,
683
+ jumpToMsg: false,
684
+ add: true,
685
+ relpy: true,
686
+ forward: true,
687
+ select: true,
688
+ copy: true,
689
+ copySelect: false,
690
+ copyImg: false,
691
+ downloadImg: false as string | false,
692
+ revoke: false,
693
+ reedit: false,
694
+ at: true,
695
+ poke: false,
696
+ remove: false,
697
+ respond: false,
698
+ showRespond: true,
699
+ config: false,
700
+ },
701
+ search: {
702
+ userId: -1,
703
+ list: reactive(list),
704
+ },
705
+ msgTouch: {
706
+ x: -1,
707
+ y: -1,
708
+ msgOnTouchDown: false,
709
+ onMove: 'no',
710
+ },
711
+ checkNewLineFlag: false,
712
+ })
713
+ const details = ref([
714
+ { open: false },
715
+ { open: false },
716
+ { open: false },
717
+ { open: false },
718
+ ])
719
+ const msgMenus = ref<any[]>([])
720
+ const NewMsgNum = ref(0)
721
+ const msg = ref('')
722
+ const oldMsg = ref('')
723
+ const imgCache = ref(new Map<number, string>())
724
+ const voiceRecording = ref(false)
725
+ const pendingVoice = ref<MsgItemElem | null>(null)
726
+ const voiceMenuShow = ref(false)
727
+ const voiceRecorder = ref<MediaRecorder | null>(null)
728
+ let voiceChunks: Blob[] = []
729
+ const sendCache = ref<MsgItemElem[]>([])
730
+ const selectedMsg = ref<{ [key: string]: any } | null>(null)
731
+ const selectCache = ref('')
732
+ const atFindList = ref<GroupMemberInfoElem[] | null>(null)
733
+ const atSelectedIndex = ref(0)
734
+ const atScrollTimer = ref<NodeJS.Timeout | null>(null)
735
+ const atScrollInterval = ref<NodeJS.Timeout | null>(null)
736
+ const searchDebounceTimer = ref<NodeJS.Timeout | null>(null)
737
+ const searchRequestId = ref(0)
738
+ const forwardList = ref(contactStore.userList)
739
+ const chatImg = ref<any>(undefined)
740
+ const trueLang = getTrueLang()
741
+ const isDev = import.meta.env.DEV
742
+ let selfRefreshTimer: number | null = null
743
+
744
+ function ensureChannelPrefix(channelId: string, prefix: string): string {
745
+ if (/^(?:group|room|chat|channel|guild|private):/i.test(channelId)) return channelId
746
+ return `${prefix}:${channelId}`
747
+ }
748
+
749
+ function messageLocalTimeMs(item: any): number {
750
+ const local = Number(item?.local_time ?? item?.timestamp_ms ?? item?.time_ms ?? 0)
751
+ if (local) return local
752
+ const time = Number(item?.time ?? 0)
753
+ return Number.isFinite(time) && time > 0 ? time * 1000 : 0
754
+ }
755
+
756
+ async function refreshSelfHistory() {
757
+ const id = chat.show.id
758
+ if (!id || id === 0 || details.value[3].open || tags.value.showForwardPan) return
759
+ const channelId = chat.show.type === 'group'
760
+ ? ensureChannelPrefix(String(chat.show.channel_id ?? id), 'group')
761
+ : ensureChannelPrefix(String(chat.show.channel_id ?? id), 'private')
762
+ let cached: Record<string, unknown>[] = []
763
+ try {
764
+ cached = await loadChatHistoryFromCache({
765
+ platform: String(authStore.loginInfo.platform ?? ''),
766
+ selfId: String(authStore.loginInfo.uin ?? ''),
767
+ channelId,
768
+ limit: 30,
769
+ })
770
+ } catch {
771
+ return
772
+ }
773
+ if (!cached.length || String(chat.show.id) !== String(id)) return
774
+
775
+ const seen = new Set<string>()
776
+ const addSeen = (item: any) => {
777
+ const messageId = String(item?.message_id ?? '')
778
+ const fakeId = String(item?.fake_message_id ?? '')
779
+ if (messageId) seen.add(messageId)
780
+ if (fakeId) seen.add(fakeId)
781
+ }
782
+ const merged = [...chatStore.messageList]
783
+ for (const item of merged) addSeen(item)
784
+
785
+ let added = false
786
+ for (const item of cached) {
787
+ const messageId = String(item.message_id ?? '')
788
+ const fakeId = String(item.fake_message_id ?? '')
789
+ if ((messageId && seen.has(messageId)) || (fakeId && seen.has(fakeId))) continue
790
+ if (!messageId && !fakeId) continue
791
+ addSeen(item)
792
+ merged.push(item)
793
+ added = true
794
+ }
795
+ if (!added) return
796
+
797
+ merged.sort((a, b) => messageLocalTimeMs(a) - messageLocalTimeMs(b))
798
+ chatStore.messageList.splice(0, chatStore.messageList.length, ...merged)
799
+ }
800
+
801
+ function startSelfRefresh() {
802
+ if (selfRefreshTimer !== null) {
803
+ window.clearInterval(selfRefreshTimer)
804
+ selfRefreshTimer = null
805
+ }
806
+ if (!chat.show.id) return
807
+ void refreshSelfHistory()
808
+ selfRefreshTimer = window.setInterval(() => {
809
+ void refreshSelfHistory()
810
+ }, 2500)
811
+ }
812
+
813
+ function stopSelfRefresh() {
814
+ if (selfRefreshTimer !== null) {
815
+ window.clearInterval(selfRefreshTimer)
816
+ selfRefreshTimer = null
817
+ }
818
+ }
819
+
820
+ //#region == 窗口移动相关 ==================================================
821
+ const chatMoveOptions: VMoveOptions<HTMLDivElement> = {
822
+ beforeHook: (_) => {
823
+ const target = getTargetWin()
824
+ if (!target) return
825
+ target.style.transition = 'all 0s'
826
+ const pan = document.getElementById('chat-pan')
827
+ if (!pan) return
828
+ const chatEl = pan.getElementsByClassName('chat')[0] as HTMLDivElement
829
+ if(chatEl)
830
+ chatEl.style.overflowY = 'hidden'
831
+ },
832
+ moveHook: (_, move: number) => {
833
+ const target = getTargetWin()
834
+ if (!target) return
835
+ target.style.transform = 'translateX(' + move + 'px)'
836
+ },
837
+ endHook: (_) => {
838
+ const pan = document.getElementById('chat-pan')
839
+ const chatEl = pan?.getElementsByClassName('chat')[0] as HTMLDivElement
840
+ if(chatEl) {
841
+ chatEl.style.overflowY = 'scroll'
842
+ }
843
+ const target = getTargetWin()
844
+ if (!target) return
845
+ target.style.transition = 'transform 0.3s'
846
+ target.style.transform = ''
847
+ },
848
+ rightLimit: {
849
+ value: 100,
850
+ type: '%',
851
+ },
852
+ speedCondition: {
853
+ minMove: {
854
+ value: 0.5 * uiStore.inch,
855
+ type: 'px',
856
+ },
857
+ minSpeed: 5 * uiStore.inch,
858
+ },
859
+ moveCondition: {
860
+ minMove: {
861
+ value: 33,
862
+ type: '%',
863
+ }
864
+ },
865
+ }
866
+ //#endregion
867
+
868
+ function resetState() {
869
+ NewMsgNum.value = 0
870
+ tags.value = {
871
+ sendTag: 'REFUSE',
872
+ showBottomButton: false,
873
+ showMoreDetail: false,
874
+ showMsgMenu: false,
875
+ showForwardPan: false,
876
+ openChatInfo: false,
877
+ isReply: false,
878
+ isJinLoading: false,
879
+ onAtFind: false,
880
+ menuDisplay: {
881
+ menuSelectedMsgId: null,
882
+ jumpToMsg: false,
883
+ add: true,
884
+ relpy: true,
885
+ forward: true,
886
+ select: true,
887
+ copy: true,
888
+ copySelect: false,
889
+ copyImg: false,
890
+ downloadImg: false,
891
+ revoke: false,
892
+ reedit: false,
893
+ at: true,
894
+ poke: false,
895
+ remove: false,
896
+ respond: false,
897
+ showRespond: true,
898
+ config: false,
899
+ },
900
+ search: {
901
+ userId: -1,
902
+ list: reactive(list),
903
+ },
904
+ msgTouch: {
905
+ x: -1,
906
+ y: -1,
907
+ msgOnTouchDown: false,
908
+ onMove: 'no',
909
+ },
910
+ checkNewLineFlag: false,
911
+ }
912
+ msgMenus.value = []
913
+ shouldKeepChatAtBottom = true
914
+ }
915
+
916
+ watch(() => chat, () => {
917
+ resetState()
918
+ sendCache.value = []
919
+ imgCache.value.clear()
920
+ multipleSelectList.value = []
921
+ initMenuDisplay()
922
+ nextTick(() => {
923
+ scheduleResizeMainInput()
924
+ scrollBottomAfterLayout()
925
+ updateBottomButtonVisibility()
926
+ })
927
+ startSelfRefresh()
928
+ const history = useSessionHistoryStore()
929
+ const sessionId = chat.show.id
930
+ const session = [...contactStore.userList].find(i => (i.user_id ?? i.group_id) === sessionId)
931
+ if (session) history.add(session)
932
+ })
933
+
934
+ watch(() => msg.value, (_newMsg, oldMsgVal) => {
935
+ oldMsg.value = oldMsgVal
936
+ scheduleResizeMainInput()
937
+ })
938
+
939
+ onMounted(() => {
940
+ startSelfRefresh()
941
+ const history = useSessionHistoryStore()
942
+ const sessionId = chat.show.id
943
+ const session = [...contactStore.userList].find(i => (i.user_id ?? i.group_id) === sessionId)
944
+ if (session) history.add(session)
945
+
946
+ updateList(list.length, 0)
947
+ watch(() => list.map((item) => item.message_id + '_' + item.fake_msg),
948
+ (newIds, oldIds = []) => {
949
+ updateList(newIds.length, oldIds.length)
950
+ if (newIds.length > 0 && oldIds.length === 0) {
951
+ nextTick(scrollBottom)
952
+ }
953
+ },
954
+ )
955
+ watch(() => chat.info.jin_info.list.length, () => {
956
+ tags.value.isJinLoading = false
957
+ },
958
+ )
959
+ if(backend.type == 'capacitor' && backend.platform === 'android') {
960
+ backend.addListener('App', 'backButton', () => {
961
+ exitWin()
962
+ })
963
+ }
964
+ watch(() => connectionStore.backTimes, () => {
965
+ exitWin()
966
+ })
967
+ nextTick(() => {
968
+ setupChatPaddingObserver()
969
+ setupChatLayoutObserver()
970
+ scheduleResizeMainInput()
971
+ scrollBottomAfterLayout()
972
+ updateBottomButtonVisibility()
973
+ })
974
+ })
975
+
976
+ onBeforeUnmount(() => {
977
+ stopSelfRefresh()
978
+ if (resizeMainInputFrame !== null) {
979
+ cancelAnimationFrame(resizeMainInputFrame)
980
+ resizeMainInputFrame = null
981
+ }
982
+ if (chatPaddingFrame !== null) {
983
+ cancelAnimationFrame(chatPaddingFrame)
984
+ chatPaddingFrame = null
985
+ }
986
+ if (sendMoreResizeObserver !== null) {
987
+ sendMoreResizeObserver.disconnect()
988
+ sendMoreResizeObserver = null
989
+ }
990
+ if (chatLayoutObserver !== null) {
991
+ chatLayoutObserver.disconnect()
992
+ chatLayoutObserver = null
993
+ }
994
+ if (chatMediaLoadHandler !== null) {
995
+ const pan = msgPan.value
996
+ pan?.removeEventListener('load', chatMediaLoadHandler, true)
997
+ pan?.removeEventListener('error', chatMediaLoadHandler, true)
998
+ pan?.removeEventListener('loadedmetadata', chatMediaLoadHandler, true)
999
+ pan?.removeEventListener('loadeddata', chatMediaLoadHandler, true)
1000
+ chatMediaLoadHandler = null
1001
+ }
1002
+ })
1003
+
1004
+ let resizeMainInputFrame: number | null = null
1005
+ let chatPaddingFrame: number | null = null
1006
+ let sendMoreResizeObserver: ResizeObserver | null = null
1007
+ let chatLayoutObserver: ResizeObserver | null = null
1008
+ let chatMediaLoadHandler: ((event: Event) => void) | null = null
1009
+ let shouldKeepChatAtBottom = true
1010
+ let chatPaddingAfterUpdate: Array<() => void> = []
1011
+ // scrollHeight includes a small browser-dependent inner gap for this textarea style.
1012
+ // Keep the existing compact visual height, but make the adjustment explicit.
1013
+ const TEXTAREA_SCROLL_HEIGHT_COMPACT_OFFSET = 4
1014
+
1015
+ function scheduleResizeMainInput(target?: HTMLTextAreaElement | HTMLInputElement | null, keepBottom = false) {
1016
+ // The template switches between input and textarea, so measure only after Vue
1017
+ // has applied the branch and coalesce rapid input changes into one frame.
1018
+ nextTick(() => {
1019
+ if (resizeMainInputFrame !== null) {
1020
+ cancelAnimationFrame(resizeMainInputFrame)
1021
+ }
1022
+ resizeMainInputFrame = requestAnimationFrame(() => {
1023
+ resizeMainInputFrame = null
1024
+ resizeMainInput(target ?? mainInput.value)
1025
+ scheduleChatPaddingUpdate(keepBottom ? () => scrollBottom() : undefined)
1026
+ })
1027
+ })
1028
+ }
1029
+
1030
+ function updateChatPadding() {
1031
+ const morePan = sendMore.value
1032
+ const padding = chatPadding.value
1033
+ const chatPan = msgPan.value
1034
+ if (!morePan || !padding || !chatPan) return
1035
+
1036
+ const contentBlocks = Array.from(morePan.children)
1037
+ .flatMap(child => Array.from(child.children))
1038
+ .filter((child): child is HTMLElement =>
1039
+ child instanceof HTMLElement && child.offsetHeight > 0,
1040
+ )
1041
+ const contentTop = contentBlocks.length > 0? contentBlocks.reduce(
1042
+ (top, child) => Math.min(top, child.getBoundingClientRect().top),
1043
+ Number.POSITIVE_INFINITY,
1044
+ ): morePan.getBoundingClientRect().top
1045
+ const chatBottom = chatPan.getBoundingClientRect().bottom
1046
+ padding.style.height = Math.max(0, chatBottom - contentTop) + 'px'
1047
+ }
1048
+
1049
+ function scheduleChatPaddingUpdate(afterUpdate?: () => void) {
1050
+ if (afterUpdate) {
1051
+ chatPaddingAfterUpdate.push(afterUpdate)
1052
+ }
1053
+ if (chatPaddingFrame !== null) {
1054
+ return
1055
+ }
1056
+ chatPaddingFrame = requestAnimationFrame(() => {
1057
+ chatPaddingFrame = null
1058
+ updateChatPadding()
1059
+ const callbacks = chatPaddingAfterUpdate
1060
+ chatPaddingAfterUpdate = []
1061
+ callbacks.forEach(callback => callback())
1062
+ })
1063
+ }
1064
+
1065
+ function setupChatPaddingObserver() {
1066
+ if (sendMoreResizeObserver !== null) return
1067
+ const morePan = sendMore.value
1068
+ if (!morePan || typeof ResizeObserver === 'undefined') {
1069
+ scheduleChatPaddingUpdate()
1070
+ return
1071
+ }
1072
+ sendMoreResizeObserver = new ResizeObserver(() => {
1073
+ scheduleChatPaddingUpdate()
1074
+ })
1075
+ sendMoreResizeObserver.observe(morePan)
1076
+ scheduleChatPaddingUpdate()
1077
+ }
1078
+
1079
+ function setupChatLayoutObserver() {
1080
+ const pan = msgPan.value
1081
+ if (!pan || chatLayoutObserver !== null || chatMediaLoadHandler !== null) return
1082
+ // 图片/视频加载后会改变消息高度,捕获阶段补一次贴底定位
1083
+ const keepBottom = () => {
1084
+ updateBottomButtonVisibility()
1085
+ scrollBottomAfterLayout()
1086
+ }
1087
+ chatMediaLoadHandler = keepBottom
1088
+ pan.addEventListener('load', keepBottom, true)
1089
+ pan.addEventListener('error', keepBottom, true)
1090
+ pan.addEventListener('loadedmetadata', keepBottom, true)
1091
+ pan.addEventListener('loadeddata', keepBottom, true)
1092
+ if (typeof ResizeObserver === 'undefined') return
1093
+ // 布局尺寸变化时也保持贴底,避免固定延迟漏掉异步内容
1094
+ chatLayoutObserver = new ResizeObserver(() => {
1095
+ keepBottom()
1096
+ })
1097
+ chatLayoutObserver.observe(pan)
1098
+ if (pan.firstElementChild) chatLayoutObserver.observe(pan.firstElementChild)
1099
+ }
1100
+
1101
+ function scrollBottomAfterLayout() {
1102
+ if (!shouldKeepChatAtBottom || !msgPan.value) return
1103
+ scrollBottom(false)
1104
+ requestAnimationFrame(() => {
1105
+ if (!shouldKeepChatAtBottom || !msgPan.value) return
1106
+ scrollBottom(false)
1107
+ requestAnimationFrame(() => {
1108
+ if (shouldKeepChatAtBottom && msgPan.value) scrollBottom(false)
1109
+ })
1110
+ })
1111
+ }
1112
+
1113
+ function resizeMainInput(target?: HTMLTextAreaElement | HTMLInputElement | null) {
1114
+ const input = target ?? mainInput.value
1115
+ if (!input) return
1116
+ if (settingsStore.sysConfig.send_key === 'none') {
1117
+ input.style.height = ''
1118
+ return
1119
+ }
1120
+ if (!(input instanceof HTMLTextAreaElement)) {
1121
+ input.style.height = ''
1122
+ return
1123
+ }
1124
+ const computed = getComputedStyle(input)
1125
+ const lineHeight = Number.parseFloat(computed.lineHeight)
1126
+ const fontSize = Number.parseFloat(computed.fontSize)
1127
+ const baseLineHeight = Number.isFinite(lineHeight) ? lineHeight : fontSize
1128
+ const paddingTop = Number.parseFloat(computed.paddingTop) || 0
1129
+ const paddingBottom = Number.parseFloat(computed.paddingBottom) || 0
1130
+ const borderTop = Number.parseFloat(computed.borderTopWidth) || 0
1131
+ const borderBottom = Number.parseFloat(computed.borderBottomWidth) || 0
1132
+ let minHeight = (Number.isFinite(baseLineHeight) ? baseLineHeight : 0) + paddingTop + paddingBottom + borderTop + borderBottom
1133
+ if (minHeight <= 0) {
1134
+ const fallback = input.offsetHeight || Number.parseFloat(computed.height) || fontSize
1135
+ if (fallback && Number.isFinite(fallback)) {
1136
+ minHeight = fallback
1137
+ }
1138
+ }
1139
+ if (!input.dataset.baseHeight) {
1140
+ input.dataset.baseHeight = String(minHeight)
1141
+ }
1142
+
1143
+ const oldTransition = input.style.transition
1144
+ input.style.transition = 'none'
1145
+ const oldOverflow = input.style.overflow
1146
+
1147
+ const baseHeight = Number.parseFloat(input.dataset.baseHeight) || minHeight
1148
+ // Fast-path: if content is empty, reset directly to baseHeight without measuring
1149
+ if (input.value === '') {
1150
+ input.style.height = baseHeight + 'px'
1151
+ } else {
1152
+ // Set overflow:hidden so scrollHeight correctly reflects content height
1153
+ input.style.overflow = 'hidden'
1154
+ input.style.height = '0px'
1155
+ const targetHeight = Math.max(input.scrollHeight - TEXTAREA_SCROLL_HEIGHT_COMPACT_OFFSET, baseHeight)
1156
+ input.style.height = targetHeight + 'px'
1157
+ input.style.overflow = oldOverflow
1158
+ }
1159
+
1160
+ input.getBoundingClientRect()
1161
+ input.style.transition = oldTransition
1162
+ }
1163
+ function jumpSearchMsg() {
1164
+ closeSearch()
1165
+ // 搜索面板切换后再定位目标消息
1166
+ nextTick(() => {
1167
+ if (!selectedMsg.value) return
1168
+ scrollToMsg('chat-' + selectedMsg.value?.message_id, true)
1169
+ closeMsgMenu()
1170
+ })
1171
+ }
1172
+
1173
+ function updateBottomButtonVisibility() {
1174
+ const pan = msgPan.value
1175
+ if (!pan) return
1176
+ const scrollable = pan.scrollHeight > pan.clientHeight + 1
1177
+ const atBottom = pan.scrollTop + pan.clientHeight + 10 >= pan.scrollHeight
1178
+ tags.value.showBottomButton = scrollable && !atBottom
1179
+ }
1180
+
1181
+ function chatScroll(event: Event, pass: boolean) {
1182
+ if(pass) return
1183
+
1184
+ const body = event.target as HTMLDivElement
1185
+ if (body.scrollTop === 0 && list.length > 0) {
1186
+ loadMoreHistory()
1187
+ }
1188
+ const atBottom = (body.scrollTop + body.clientHeight + 10) >= body.scrollHeight
1189
+ if (atBottom) {
1190
+ shouldKeepChatAtBottom = true
1191
+ NewMsgNum.value = 0
1192
+ tags.value.showBottomButton = false
1193
+ } else {
1194
+ if (shouldKeepChatAtBottom) shouldKeepChatAtBottom = false
1195
+ updateBottomButtonVisibility()
1196
+ }
1197
+ }
1198
+
1199
+ async function loadMoreHistory() {
1200
+ if (
1201
+ !uiStore.nowGetHistory &&
1202
+ uiStore.canLoadHistory !== false
1203
+ ) {
1204
+ const firstMsg = list[0]
1205
+ const firstMsgId = firstMsg.message_id
1206
+ const firstMsgTime = Number(firstMsg?.time)
1207
+ const cacheBeforeTime = Number(
1208
+ firstMsg?.local_time ?? firstMsg?.timestamp_ms ?? firstMsg?.time_ms ?? 0
1209
+ ) || (Number.isFinite(firstMsgTime) ? firstMsgTime * 1000 : 0)
1210
+ const useMixedHistory =
1211
+ settingsStore.sysConfig.enable_local_history &&
1212
+ settingsStore.sysConfig.mixed_load_messages !== false
1213
+ uiStore.nowGetHistory = true
1214
+ if (useMixedHistory && Number.isFinite(firstMsgTime)) {
1215
+ uiStore.historyBeforeTime = firstMsgTime
1216
+ } else {
1217
+ uiStore.historyBeforeTime = undefined
1218
+ }
1219
+ uiStore.loadHistoryFail = false
1220
+
1221
+ if (useMixedHistory) {
1222
+ let localMsgs = [] as any[]
1223
+ if (Number.isFinite(firstMsgTime)) {
1224
+ localMsgs = await dbGetBeforeByTime(
1225
+ authStore.loginInfo.uin,
1226
+ chatStore.chatInfo.show.id,
1227
+ firstMsgTime,
1228
+ 20,
1229
+ )
1230
+ } else {
1231
+ localMsgs = await dbGetBefore(
1232
+ authStore.loginInfo.uin,
1233
+ chatStore.chatInfo.show.id,
1234
+ firstMsgId,
1235
+ 20,
1236
+ )
1237
+ }
1238
+ if (localMsgs.length > 0) {
1239
+ const existingIds = new Set(chatStore.messageList.map((m) => String(m.message_id ?? '')))
1240
+ const addList = localMsgs.filter((m) => {
1241
+ const msgId = String(m?.message_id ?? '')
1242
+ return msgId.length === 0 || !existingIds.has(msgId)
1243
+ })
1244
+ if (addList.length > 0) {
1245
+ chatStore.messageList.splice(0, 0, ...addList)
1246
+ }
1247
+ const boundary = list[addList.length] ?? list[addList.length - 1]
1248
+ const seqGapAnchors = detectSeqGaps([...addList, boundary])
1249
+ if (seqGapAnchors.length > 0) {
1250
+ fillSeqGaps(seqGapAnchors)
1251
+ }
1252
+ }
1253
+ }
1254
+
1255
+ const type = chatStore.chatInfo.show.type
1256
+ const id = chatStore.chatInfo.show.id
1257
+ const rawChannelId = String(chatStore.chatInfo.show.channel_id ?? '')
1258
+ const channelId = type === 'group'
1259
+ ? ensureChannelPrefix(rawChannelId || String(id), 'group')
1260
+ : ensureChannelPrefix(rawChannelId || String(id), 'private')
1261
+ const cached = await loadChatHistoryFromCache({
1262
+ platform: String(authStore.loginInfo.platform ?? ''),
1263
+ selfId: String(authStore.loginInfo.uin ?? ''),
1264
+ channelId,
1265
+ limit: 20,
1266
+ beforeTimeMs: cacheBeforeTime > 0 ? cacheBeforeTime : undefined,
1267
+ })
1268
+ const existingIds = new Set(chatStore.messageList.map((m) => String(m.message_id ?? '')))
1269
+ const addList = cached.filter((m) => {
1270
+ const msgId = String(m.message_id ?? '')
1271
+ return msgId.length === 0 || !existingIds.has(msgId)
1272
+ })
1273
+ if (addList.length > 0) {
1274
+ chatStore.messageList.splice(0, 0, ...addList)
1275
+ }
1276
+ if (cached.length < 20) {
1277
+ uiStore.canLoadHistory = false
1278
+ }
1279
+ uiStore.nowGetHistory = false
1280
+ }
1281
+ }
1282
+
1283
+ function detectSeqGaps(msgs: any[]): string[] {
1284
+ const gaps: string[] = []
1285
+ for (let i = 0; i < msgs.length - 1; i++) {
1286
+ const seqA: number | null = msgs[i].message_seq ?? msgs[i].seq ?? null
1287
+ const seqB: number | null = msgs[i + 1].message_seq ?? msgs[i + 1].seq ?? null
1288
+ if (seqA == null || seqB == null) return []
1289
+ if (seqB - seqA > 1) {
1290
+ gaps.push(msgs[i + 1].message_id)
1291
+ }
1292
+ }
1293
+ return gaps
1294
+ }
1295
+
1296
+ function fillSeqGaps(anchorMsgIds: string[]) {
1297
+ const type = chatStore.chatInfo.show.type
1298
+ const id = chatStore.chatInfo.show.id
1299
+ const channelId = String(chatStore.chatInfo.show.channel_id ?? id)
1300
+ const guildId = String(chatStore.chatInfo.show.guild_id ?? id)
1301
+ let name: string
1302
+ if (authStore.jsonMap.message_list && type != 'group') {
1303
+ name = authStore.jsonMap.message_list.private_name
1304
+ } else {
1305
+ name = authStore.jsonMap.message_list?.name
1306
+ }
1307
+ for (const anchorMsgId of anchorMsgIds) {
1308
+ Connector.send(
1309
+ name ?? 'get_chat_history',
1310
+ {
1311
+ group_id: type == 'group' ? (guildId || id) : undefined,
1312
+ guild_id: type == 'group' ? (guildId || id) : undefined,
1313
+ channel_id: channelId,
1314
+ user_id: type != 'group' ? id : undefined,
1315
+ message_id: anchorMsgId,
1316
+ count: 20,
1317
+ },
1318
+ 'getChatHistoryGapFill_' + anchorMsgId,
1319
+ )
1320
+ }
1321
+ }
1322
+
1323
+ function scrollTo(where: number | undefined, showAnimation = true) {
1324
+ const pan = document.getElementById('msgPan')
1325
+ if (pan !== null && where) {
1326
+ if (showAnimation === false) {
1327
+ pan.style.scrollBehavior = 'unset'
1328
+ } else {
1329
+ pan.style.scrollBehavior = 'smooth'
1330
+ }
1331
+ pan.scrollTop = where
1332
+ pan.style.scrollBehavior = 'smooth'
1333
+ }
1334
+ }
1335
+
1336
+ function scrollBottom(showAnimation = false) {
1337
+ const pan = document.getElementById('msgPan')
1338
+ if (pan !== null) {
1339
+ scrollTo(pan.scrollHeight, showAnimation)
1340
+ }
1341
+ }
1342
+
1343
+ function scrollToMsgLocal(message_id: string) {
1344
+ if (!scrollToMsg(message_id, true)) {
1345
+ new PopInfo().add(PopType.INFO, $t('无法定位上下文'))
1346
+ }
1347
+ }
1348
+
1349
+ function imgLoadedScroll(height: number) {
1350
+ const pan = document.getElementById('msgPan')
1351
+ if(pan) {
1352
+ if(list.length <= 20 && !tags.value.showBottomButton) {
1353
+ scrollBottom()
1354
+ } else {
1355
+ scrollTo(pan.scrollTop + height, false)
1356
+ }
1357
+ }
1358
+ }
1359
+
1360
+ function mainKey(event: KeyboardEvent) {
1361
+ if (mainAtKey(event)) return
1362
+
1363
+ if(tags.value.onAtFind) return
1364
+ if (event.key !== 'Enter') return
1365
+ let canSend = false
1366
+ switch (settingsStore.sysConfig.send_key) {
1367
+ case 'none':
1368
+ if (event.shiftKey) break
1369
+ if (event.ctrlKey) break
1370
+ if (event.altKey) break
1371
+ if (event.metaKey) break
1372
+ canSend = true
1373
+ break
1374
+ case 'shift':
1375
+ if (!event.shiftKey) break
1376
+ canSend = true
1377
+ break
1378
+ case 'ctrl':
1379
+ if (!event.ctrlKey) break
1380
+ canSend = true
1381
+ break
1382
+ case 'alt':
1383
+ if (!event.altKey) break
1384
+ canSend = true
1385
+ break
1386
+ case 'meta':
1387
+ if (!event.metaKey) break
1388
+ canSend = true
1389
+ break
1390
+ }
1391
+
1392
+ if(canSend && tags.value.sendTag != 'PASS') {
1393
+ tags.value.sendTag = 'READY'
1394
+ }
1395
+
1396
+ if (tags.value.sendTag == 'READY' && msg.value !== '') {
1397
+ event.preventDefault()
1398
+ sendMsg()
1399
+ } else {
1400
+ if(event.key === 'Enter' &&
1401
+ (event.ctrlKey || event.metaKey || event.altKey)) {
1402
+ msg.value += '\n'
1403
+ }
1404
+ }
1405
+
1406
+ tags.value.sendTag = 'REFUSE'
1407
+ }
1408
+
1409
+ function mainAtKey(event: KeyboardEvent) {
1410
+ if (!tags.value.onAtFind) return false
1411
+
1412
+ if (event.keyCode === 38 || event.keyCode === 40) {
1413
+ event.preventDefault()
1414
+ const direction = event.keyCode === 38 ? -1 : 1
1415
+ moveAtSelection(direction)
1416
+ if (atScrollTimer.value !== null) return true
1417
+ atScrollTimer.value = setTimeout(() => {
1418
+ atScrollInterval.value = setInterval(() => {
1419
+ moveAtSelection(direction)
1420
+ }, 50)
1421
+ }, 300)
1422
+ return true
1423
+ }
1424
+
1425
+ if (event.keyCode === 13) {
1426
+ event.preventDefault()
1427
+ const selectedMember = atFindList.value?.[atSelectedIndex.value]
1428
+ if (selectedMember) {
1429
+ choiceAt(selectedMember.user_id)
1430
+ }
1431
+ return true
1432
+ }
1433
+
1434
+ if (event.keyCode === 27) {
1435
+ event.preventDefault()
1436
+ tags.value.onAtFind = false
1437
+ atFindList.value = null
1438
+ atSelectedIndex.value = 0
1439
+ return true
1440
+ }
1441
+
1442
+ return false
1443
+ }
1444
+
1445
+ function handleCompositionStart() {
1446
+ tags.value.sendTag = 'REFUSE'
1447
+ }
1448
+
1449
+ function handleCompositionEnd() {
1450
+ tags.value.sendTag = 'PASS'
1451
+ setTimeout(() => { tags.value.sendTag = 'REFUSE' }, 50)
1452
+ }
1453
+
1454
+ function mainKeyUp(event: KeyboardEvent) {
1455
+ const logger = new Logger()
1456
+
1457
+ if (event.keyCode === 27) {
1458
+ return
1459
+ }
1460
+
1461
+ if (event.keyCode === 38 || event.keyCode === 40) {
1462
+ if (atScrollTimer.value !== null) {
1463
+ clearTimeout(atScrollTimer.value)
1464
+ atScrollTimer.value = null
1465
+ }
1466
+ if (atScrollInterval.value !== null) {
1467
+ clearInterval(atScrollInterval.value)
1468
+ atScrollInterval.value = null
1469
+ }
1470
+ }
1471
+
1472
+ if (tags.value.checkNewLineFlag){
1473
+ tags.value.checkNewLineFlag = false
1474
+ if (msg.value == '\n'){
1475
+ msg.value = ''
1476
+ scheduleResizeMainInput()
1477
+ }
1478
+ }
1479
+
1480
+ if (tags.value.onAtFind && atFindList.value && atFindList.value.length > 0) {
1481
+ if (event.keyCode === 38 || event.keyCode === 40 || event.keyCode === 13 || event.keyCode === 27) {
1482
+ return
1483
+ }
1484
+ }
1485
+
1486
+ if (event.keyCode != 13) {
1487
+ const lastInput = msg.value.substring(msg.value.length - 1)
1488
+ if (
1489
+ !tags.value.onAtFind &&
1490
+ lastInput == '@' &&
1491
+ chatStore.chatInfo.info.group_members.length > 0 &&
1492
+ chatStore.chatInfo.show.type == 'group'
1493
+ ) {
1494
+ logger.add(LogType.UI, '开始匹配群成员列表 ……')
1495
+ tags.value.onAtFind = true
1496
+ atSelectedIndex.value = 0
1497
+ }
1498
+ if (tags.value.onAtFind) {
1499
+ if (msg.value.lastIndexOf('@') < 0) {
1500
+ logger.add(LogType.UI, '匹配群成员列表被打断 ……')
1501
+ tags.value.onAtFind = false
1502
+ atFindList.value = null
1503
+ atSelectedIndex.value = 0
1504
+ } else {
1505
+ const atInfo = msg.value
1506
+ .substring(msg.value.lastIndexOf('@') + 1)
1507
+ .toLowerCase()
1508
+ atFindList.value = chatStore.chatInfo.info.group_members
1509
+ .filter((item) => { return (
1510
+ (item.card != '' && item.card != null && item.card.toLowerCase().indexOf(atInfo) >=0) ||
1511
+ item.nickname.toLowerCase().indexOf(atInfo) >= 0 ||
1512
+ atInfo ==item.user_id.toString()
1513
+ )
1514
+ },
1515
+ )
1516
+ if (atFindList.value.length == 0) {
1517
+ atFindList.value = chatStore.chatInfo.info.group_members
1518
+ }
1519
+ atSelectedIndex.value = 0
1520
+ }
1521
+ }
1522
+ }
1523
+ }
1524
+
1525
+ function mainSubmit(event: Event) {
1526
+ event.preventDefault()
1527
+ if (msg.value != '') {
1528
+ sendMsg()
1529
+ }
1530
+ }
1531
+
1532
+ function choiceAt(id: number | string | undefined) {
1533
+ if (id != undefined) {
1534
+ msg.value = msg.value.substring(0, msg.value.lastIndexOf('@'))
1535
+ addSpecialMsg({
1536
+ msgObj: { type: 'at', qq: String(id) },
1537
+ addText: true,
1538
+ })
1539
+ }
1540
+ toMainInput()
1541
+ tags.value.onAtFind = false
1542
+ atFindList.value = null
1543
+ atSelectedIndex.value = 0
1544
+ }
1545
+
1546
+ function scrollAtListToSelected() {
1547
+ nextTick(() => {
1548
+ const container = document.querySelector('.at-tag.show')
1549
+ const selectedItem = document.querySelector('.at-tag.show > div.selected')
1550
+ if (container && selectedItem) {
1551
+ const containerRect = container.getBoundingClientRect()
1552
+ const itemRect = selectedItem.getBoundingClientRect()
1553
+ if (itemRect.top < containerRect.top) {
1554
+ selectedItem.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
1555
+ } else if (itemRect.bottom > containerRect.bottom) {
1556
+ selectedItem.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
1557
+ }
1558
+ }
1559
+ })
1560
+ }
1561
+
1562
+ function moveAtSelection(direction: number) {
1563
+ if (!atFindList.value || atFindList.value.length === 0) return
1564
+ if (direction === -1) {
1565
+ atSelectedIndex.value = atSelectedIndex.value > 0? atSelectedIndex.value - 1: atFindList.value.length - 1
1566
+ } else {
1567
+ atSelectedIndex.value = atSelectedIndex.value < atFindList.value.length - 1? atSelectedIndex.value + 1: 0
1568
+ }
1569
+ scrollAtListToSelected()
1570
+ }
1571
+
1572
+ function selectSQIn() {
1573
+ const input = (document.getElementById( 'main-input') as HTMLTextAreaElement | HTMLInputElement | null) ??
1574
+ (document.getElementById( 'main-input-ex') as HTMLTextAreaElement | HTMLInputElement | null)
1575
+ if (
1576
+ input !== null &&
1577
+ input.selectionStart === input.selectionEnd
1578
+ ) {
1579
+ let cursurPosition = -1
1580
+ if (typeof input.selectionStart === 'number') {
1581
+ cursurPosition = input.selectionStart
1582
+ }
1583
+ const getSQCode = SendUtil.getSQList(msg.value)
1584
+ if (getSQCode != null) {
1585
+ getSQCode.forEach((item) => {
1586
+ const start = msg.value.indexOf(item)
1587
+ const end = start + item.length
1588
+ if (
1589
+ start !== -1 &&
1590
+ cursurPosition > start &&
1591
+ cursurPosition < end
1592
+ ) {
1593
+ nextTick(() => {
1594
+ input.selectionStart = start
1595
+ input.selectionEnd = end
1596
+ })
1597
+ }
1598
+ })
1599
+ }
1600
+ }
1601
+ }
1602
+
1603
+ function showMsgMeun(event: MenuEventData, data: any) {
1604
+ selectedMsg.value = data
1605
+ tags.value.menuDisplay.menuSelectedMsgId = data.message_id
1606
+
1607
+ if (Option.get('log_level') === 'debug') {
1608
+ new Logger().debug('右击消息:' + data)
1609
+ }
1610
+ if (multipleSelectList.value.length > 0) {
1611
+ return
1612
+ }
1613
+
1614
+ const menu = document.getElementById('msgMenu')
1615
+ const select = event.target as HTMLElement
1616
+ let selectUserType = 'member'
1617
+ if (
1618
+ chatStore.chatInfo.show.type == 'group' &&
1619
+ chatStore.chatInfo.info.group_members
1620
+ ) {
1621
+ chatStore.chatInfo.info.group_members.forEach(
1622
+ (item: any) => {
1623
+ if (item.user_id == data.sender.user_id) {
1624
+ selectUserType = item.role
1625
+ }
1626
+ },
1627
+ )
1628
+ }
1629
+
1630
+ if (menu !== null && data !== null) {
1631
+ if (get('close_respond') == true) {
1632
+ tags.value.menuDisplay.showRespond = false
1633
+ }
1634
+ if (
1635
+ select.nodeName == 'IMG' &&
1636
+ (select as HTMLImageElement).name == 'avatar'
1637
+ ) {
1638
+ Object.keys(tags.value.menuDisplay).forEach(
1639
+ (name: string) => {
1640
+ (tags.value.menuDisplay as any)[name] = false
1641
+ },
1642
+ )
1643
+ tags.value.menuDisplay.showRespond = false
1644
+ tags.value.menuDisplay.at = true
1645
+ tags.value.menuDisplay.poke = true
1646
+ tags.value.menuDisplay.remove = true
1647
+ if (
1648
+ chatStore.chatInfo.show.type != 'group' ||
1649
+ data.sender.user_id === authStore.loginInfo.uin ||
1650
+ chatStore.chatInfo.info.me_info.role === 'member' ||
1651
+ selectUserType == 'owner' ||
1652
+ (selectUserType == 'admin' && chatStore.chatInfo.info.me_info.role != 'owner')
1653
+ ) {
1654
+ tags.value.menuDisplay.remove = false
1655
+ }
1656
+ if (data.sender.user_id === authStore.loginInfo.uin) {
1657
+ tags.value.menuDisplay.at = false
1658
+ }
1659
+ if(chatStore.chatInfo.show.type == 'group' &&
1660
+ chatStore.chatInfo.info.me_info.role != 'member') {
1661
+ tags.value.menuDisplay.config = true
1662
+ }
1663
+ } else {
1664
+ if (
1665
+ data.sender.user_id === authStore.loginInfo.uin ||
1666
+ chatStore.chatInfo.info.me_info.role ===
1667
+ 'admin' ||
1668
+ chatStore.chatInfo.info.me_info.role === 'owner'
1669
+ ) {
1670
+ tags.value.menuDisplay.revoke = true
1671
+ }
1672
+ tags.value.menuDisplay.reedit = tags.value.menuDisplay.revoke && data.sender.user_id === authStore.loginInfo.uin
1673
+ if (data.revoke === true) {
1674
+ tags.value.menuDisplay.relpy = false
1675
+ tags.value.menuDisplay.forward = false
1676
+ tags.value.menuDisplay.revoke = false
1677
+ tags.value.menuDisplay.select = false
1678
+ }
1679
+ if (details.value[3].open) {
1680
+ Object.keys(tags.value.menuDisplay).forEach(
1681
+ (name: string) => {
1682
+ (tags.value.menuDisplay as any)[name] = false
1683
+ },
1684
+ )
1685
+ tags.value.menuDisplay.jumpToMsg = true
1686
+ }
1687
+ const selection = document.getSelection()
1688
+ const textBody = selection?.anchorNode?.parentElement
1689
+ let textMsg = null as HTMLElement | null
1690
+ let msgParent = textBody
1691
+ if (msgParent) {
1692
+ while (msgParent.className != 'chat') {
1693
+ if (
1694
+ msgParent.className.startsWith('message') &&
1695
+ msgParent.className.indexOf('-') < 0
1696
+ ) {
1697
+ textMsg = msgParent
1698
+ break
1699
+ }
1700
+ msgParent =
1701
+ msgParent.parentElement as HTMLDivElement
1702
+ if (!msgParent) {
1703
+ break
1704
+ }
1705
+ }
1706
+ }
1707
+ if (
1708
+ textBody &&
1709
+ textBody.className.indexOf('msg-text') > -1 &&
1710
+ selection.focusNode == selection.anchorNode &&
1711
+ textMsg &&
1712
+ textMsg.id == data.message_id
1713
+ ) {
1714
+ selectCache.value = selection.toString()
1715
+ if (selectCache.value.length > 0) {
1716
+ tags.value.menuDisplay.copySelect = true
1717
+ }
1718
+ }
1719
+ const nList = ['xml', 'json']
1720
+ data.message.forEach((item: any) => {
1721
+ if (nList.indexOf(item.type as string) > 0) {
1722
+ tags.value.menuDisplay.forward = false
1723
+ tags.value.menuDisplay.add = false
1724
+ }
1725
+ })
1726
+ if (select.nodeName == 'IMG' && (select as HTMLImageElement).src.length > 0) {
1727
+ tags.value.menuDisplay.downloadImg = (
1728
+ select as HTMLImageElement
1729
+ ).src
1730
+ if (backend.isDesktop())
1731
+ tags.value.menuDisplay.copyImg = true
1732
+ }
1733
+ }
1734
+ const pointX = event.x
1735
+ const pointY = event.y
1736
+ menu.style.marginLeft = pointX + 'px'
1737
+ menu.style.marginTop = pointY + 'px'
1738
+ let menuWidth = menu.clientWidth
1739
+ if (tags.value.menuDisplay.showRespond) {
1740
+ const item = menu.children[0] as HTMLDivElement
1741
+ menuWidth = item.clientWidth
1742
+ }
1743
+ const maxWidth = window.innerWidth
1744
+ if (pointX + menuWidth > maxWidth + 27) {
1745
+ menu.style.marginLeft = maxWidth + 7 - menuWidth + 'px'
1746
+ }
1747
+ tags.value.showMsgMenu = true
1748
+ // 菜单真正显示后再测量,避免固定延迟漏掉布局变化
1749
+ nextTick(() => {
1750
+ const menuHeight = menu.clientHeight
1751
+ const bodyHeight = document.body.clientHeight
1752
+ if (pointY + menuHeight > bodyHeight - 20) {
1753
+ menu.classList.add('topOut')
1754
+ menu.style.marginTop =
1755
+ bodyHeight - menuHeight - 10 + 'px'
1756
+ }
1757
+ })
1758
+ }
1759
+ }
1760
+
1761
+ function initMenuDisplay() {
1762
+ tags.value.menuDisplay = {
1763
+ menuSelectedMsgId : null,
1764
+ jumpToMsg: false,
1765
+ add: true,
1766
+ relpy: true,
1767
+ forward: true,
1768
+ select: true,
1769
+ copy: true,
1770
+ copySelect: false,
1771
+ downloadImg: false,
1772
+ copyImg: false,
1773
+ revoke: false,
1774
+ reedit: false,
1775
+ at: false,
1776
+ poke: false,
1777
+ remove: false,
1778
+ respond: false,
1779
+ showRespond: true,
1780
+ config: false,
1781
+ }
1782
+ }
1783
+
1784
+ function menuReplyMsg(closeMenu = true) {
1785
+ const msgData = selectedMsg.value
1786
+ if (!msgData) return
1787
+ replyMsg(msgData)
1788
+ if (closeMenu) {
1789
+ closeMsgMenu()
1790
+ }
1791
+ }
1792
+
1793
+ function replyMsg(msgData: any) {
1794
+ const msgId = msgData.message_id
1795
+ selectedMsg.value = msgData
1796
+ addSpecialMsg({
1797
+ msgObj: { type: 'reply', id: String(msgId) },
1798
+ addText: false,
1799
+ addTop: true,
1800
+ })
1801
+ tags.value.isReply = true
1802
+ toMainInput()
1803
+ }
1804
+
1805
+ function cancelReply() {
1806
+ sendCache.value = sendCache.value.filter((item) => {
1807
+ return item.type !== 'reply'
1808
+ })
1809
+ tags.value.isReply = false
1810
+ }
1811
+
1812
+ function consoleLogMsg() {
1813
+ if (!selectedMsg.value) return
1814
+ const raw = selectedMsg.value?._rawSatori ?? selectedMsg.value
1815
+ try {
1816
+ // eslint-disable-next-line no-console
1817
+ console.log(JSON.stringify(raw, null, 2))
1818
+ } catch {
1819
+ // eslint-disable-next-line no-console
1820
+ console.dir(raw, { depth: null })
1821
+ }
1822
+ closeMsgMenu()
1823
+ }
1824
+
1825
+ function cancelForward() {
1826
+ forwardList.value = contactStore.userList
1827
+ tags.value.showForwardPan = false
1828
+ selectedForwardAction.value = 'single-message'
1829
+ closeMsgMenu()
1830
+ }
1831
+
1832
+ function contactField(data: UserFriendElem & UserGroupElem, ...keys: string[]): string {
1833
+ const record = data as unknown as Record<string, unknown>
1834
+ for (const key of keys) {
1835
+ const value = record[key]
1836
+ const text = typeof value === 'string' || typeof value === 'number' ? String(value) : ''
1837
+ if (text && text !== 'undefined') return text
1838
+ }
1839
+ return ''
1840
+ }
1841
+
1842
+ function forwardDisplayName(data: UserFriendElem & UserGroupElem): string {
1843
+ if (data.group_id) {
1844
+ return contactField(data, 'group_name', 'name') || String(data.group_id)
1845
+ }
1846
+ const name = contactField(data, 'nickname', 'name', 'nick', 'username', 'remark')
1847
+ || String(data.user_id ?? '')
1848
+ const id = String(data.user_id ?? '')
1849
+ return id ? `${name}(${id})` : name
1850
+ }
1851
+
1852
+ function searchForward(event: Event) {
1853
+ const value = (event.target as HTMLInputElement).value
1854
+ forwardList.value = contactStore.userList.filter(
1855
+ (item: UserFriendElem & UserGroupElem) => {
1856
+ const rawName = item.user_id
1857
+ ? contactField(item, 'nickname', 'name', 'nick', 'remark') || String(item.user_id)
1858
+ : contactField(item, 'group_name', 'name') || String(item.group_id || '')
1859
+ const id = String(item.user_id ?? item.group_id ?? '')
1860
+ return (
1861
+ rawName.toLowerCase().indexOf(value.toLowerCase()) !== -1 ||
1862
+ (id.length > 0 && id === value)
1863
+ )
1864
+ },
1865
+ )
1866
+ }
1867
+
1868
+ function showForWard(action: ForwardAction = 'single-message') {
1869
+ selectedForwardAction.value = action
1870
+ tags.value.showForwardPan = true
1871
+ const showList = [...contactStore.onMsgList].reverse()
1872
+ showList.forEach((item: any) => {
1873
+ const index = forwardList.value.indexOf(item)
1874
+ if (index > -1) {
1875
+ forwardList.value.splice(index, 1)
1876
+ forwardList.value.unshift(item)
1877
+ }
1878
+ })
1879
+ closeMsgMenu()
1880
+ }
1881
+
1882
+ function forwardSelf() {
1883
+ if (selectedMsg.value) {
1884
+ const msgData = JSON.parse(JSON.stringify(selectedMsg.value))
1885
+ sendMsgRaw(
1886
+ chat.show.id,
1887
+ chat.show.type,
1888
+ msgData.message,
1889
+ true,
1890
+ )
1891
+ }
1892
+ closeMsgMenu()
1893
+ }
1894
+
1895
+ function intoMultipleSelect() {
1896
+ if (selectedMsg.value) {
1897
+ multipleSelectList.value.push(selectedMsg.value.message_id)
1898
+ }
1899
+ closeMsgMenu()
1900
+ }
1901
+
1902
+ function cloneMessagePayload<T>(payload: T): T {
1903
+ const rawPayload = toRaw(payload)
1904
+ if (typeof structuredClone === 'function') {
1905
+ try {
1906
+ return structuredClone(rawPayload)
1907
+ } catch {
1908
+ return JSON.parse(JSON.stringify(rawPayload))
1909
+ }
1910
+ }
1911
+ return JSON.parse(JSON.stringify(rawPayload))
1912
+ }
1913
+
1914
+ function forwardMsg(data: UserFriendElem & UserGroupElem) {
1915
+ const forwardAction = selectedForwardAction.value
1916
+ const msgData = selectedMsg.value ? cloneMessagePayload(selectedMsg.value) : null
1917
+ const id = data.group_id ? data.group_id : data.user_id
1918
+ const targetId = String(id)
1919
+ const targetType = data.group_id ? 'group' : 'user'
1920
+ const targetChannelId = String(data.channel_id ?? data.channelId ?? '')
1921
+ const targetGuildId = String(data.guild_id ?? data.guildId ?? '')
1922
+ const msgList = chatStore.messageList.filter((item) => {
1923
+ return multipleSelectList.value.includes(item.message_id)
1924
+ })
1925
+ const shouldPreShow = () =>
1926
+ String(chat.show.id) === targetId && chat.show.type === targetType
1927
+
1928
+ if (forwardAction !== 'single-message' && msgList.length === 0) {
1929
+ cancelForward()
1930
+ return
1931
+ }
1932
+
1933
+ if (forwardAction === 'individual-messages') {
1934
+ const popInfo = {
1935
+ title: $t('逐条转发'),
1936
+ html: $t('将按顺序逐条转发 {count} 条消息,是否继续?', {
1937
+ count: msgList.length,
1938
+ }),
1939
+ button: [
1940
+ {
1941
+ text: $t('取消'),
1942
+ fun: () => {
1943
+ uiStore.popBoxList.shift()
1944
+ },
1945
+ },
1946
+ {
1947
+ text: $t('确定'),
1948
+ master: true,
1949
+ fun: () => {
1950
+ msgList.forEach((item) => {
1951
+ sendMsgRaw(
1952
+ targetId,
1953
+ targetType,
1954
+ cloneMessagePayload(item.message),
1955
+ shouldPreShow(),
1956
+ 'sendMsgBack',
1957
+ targetChannelId || undefined,
1958
+ targetGuildId || undefined,
1959
+ )
1960
+ })
1961
+ multipleSelectList.value = []
1962
+ uiStore.popBoxList.shift()
1963
+ finishForward(data, id)
1964
+ },
1965
+ },
1966
+ ],
1967
+ }
1968
+ uiStore.popBoxList.push(popInfo)
1969
+ } else if (forwardAction === 'merged-messages') {
1970
+ const jsonMsg = {
1971
+ app: 'com.tencent.multimsg',
1972
+ meta: {
1973
+ detail: {
1974
+ source: $t('合并转发消息'),
1975
+ news: [
1976
+ ...msgList.slice(0, 3).map((item) => {
1977
+ const name =
1978
+ item.sender.card &&
1979
+ item.sender.card != ''? item.sender.card: item.sender.nickname
1980
+ return {
1981
+ text:
1982
+ name +
1983
+ ': ' +
1984
+ getMsgRawTxt(item),
1985
+ }
1986
+ }),
1987
+ ],
1988
+ summary: $t('查看 {count} 条转发消息', { count: msgList.length }),
1989
+ resid: '',
1990
+ },
1991
+ },
1992
+ }
1993
+ const previewMsg = {
1994
+ message: [
1995
+ { type: 'json', data: JSON.stringify(jsonMsg), id: '' },
1996
+ ],
1997
+ sender: {
1998
+ user_id: authStore.loginInfo.uin,
1999
+ nickname: authStore.loginInfo.nickname,
2000
+ avatar: authStore.loginInfo.avatar,
2001
+ }
2002
+ }
2003
+ const popInfo = {
2004
+ title: $t('合并转发消息'),
2005
+ template: markRaw(MsgBody),
2006
+ templateValue: markRaw({ data: previewMsg, type: 'forward' }),
2007
+ button: [
2008
+ {
2009
+ text: $t('取消'),
2010
+ fun: () => {
2011
+ uiStore.popBoxList.shift()
2012
+ },
2013
+ },
2014
+ {
2015
+ text: $t('确定'),
2016
+ master: true,
2017
+ fun: async () => {
2018
+ const msgBody = msgList.map((item) => {
2019
+ return {
2020
+ type: 'node',
2021
+ id: item.message_id,
2022
+ user_id: String(item.sender?.user_id ?? ''),
2023
+ nickname: String(item.sender?.card || item.sender?.nickname || ''),
2024
+ time: item.time,
2025
+ content: cloneMessagePayload(item.message),
2026
+ }
2027
+ })
2028
+ const result = await sendForwardMessage(
2029
+ String(authStore.loginInfo.platform ?? ''),
2030
+ String(authStore.loginInfo.uin ?? ''),
2031
+ targetType as 'group' | 'user',
2032
+ targetId,
2033
+ msgBody,
2034
+ )
2035
+ if (!result.ok) {
2036
+ new PopInfo().add(PopType.ERR, $t('合并转发发送失败'))
2037
+ } else {
2038
+ // 部分 adapter 不广播机器人自身的发送事件,这里先本地补一条渲染
2039
+ const sentTime = Date.now()
2040
+ const sentMsg = {
2041
+ fake_msg: 'pending',
2042
+ message_id: 'forward-' + sentTime,
2043
+ fake_message_id: 'forward-' + sentTime,
2044
+ message_type: targetType,
2045
+ channel_type: targetType === 'group' ? 0 : 1,
2046
+ time: Math.floor(sentTime / 1000),
2047
+ local_time: sentTime,
2048
+ timestamp_ms: sentTime,
2049
+ post_type: 'message',
2050
+ sender: {
2051
+ user_id: String(authStore.loginInfo.uin ?? ''),
2052
+ nickname: String(authStore.loginInfo.nickname ?? ''),
2053
+ avatar: String(authStore.loginInfo.avatar ?? ''),
2054
+ },
2055
+ message: [{
2056
+ type: 'forward',
2057
+ id: '',
2058
+ content: msgBody.map((node) => ({
2059
+ message_id: String(node.id ?? ''),
2060
+ time: Number(node.time ?? Math.floor(sentTime / 1000)),
2061
+ sender: {
2062
+ user_id: String(node.user_id ?? ''),
2063
+ nickname: String(node.nickname ?? ''),
2064
+ },
2065
+ message: node.content ?? [],
2066
+ })),
2067
+ }],
2068
+ raw_message: `[${$t('聊天记录')}]`,
2069
+ }
2070
+ if (shouldPreShow()) {
2071
+ chatStore.messageList.push(sentMsg)
2072
+ nextTick(() => {
2073
+ scrollToMsg('chat-' + sentMsg.message_id, true)
2074
+ })
2075
+ }
2076
+ const sentChannelId = targetType === 'group'
2077
+ ? (targetChannelId || targetId)
2078
+ : /^(?:private|direct):/i.test(targetChannelId || '')
2079
+ ? targetChannelId
2080
+ : `private:${targetId}`
2081
+ if (result.native && !result.messageId) {
2082
+ // 原生路径已经由后端 before-send/发送拦截写入,缺少消息 id 时不再重复写入
2083
+ } else {
2084
+ await saveSentSelfMessage({
2085
+ id: String(sentMsg.fake_message_id ?? ''),
2086
+ platform: String(authStore.loginInfo.platform ?? ''),
2087
+ selfId: String(authStore.loginInfo.uin ?? ''),
2088
+ channelId: sentChannelId,
2089
+ guildId: targetGuildId || undefined,
2090
+ channelType: targetType,
2091
+ messageId: result.messageId,
2092
+ content: String(sentMsg.raw_message ?? ''),
2093
+ message: sentMsg.message,
2094
+ forwardContent: msgBody,
2095
+ source: 'webui',
2096
+ sentAt: sentTime,
2097
+ kind: 'forward',
2098
+ })
2099
+ }
2100
+ const targetSession = contactStore.baseOnMsgList.get(normalizeSessionId(targetId))
2101
+ if (targetSession) {
2102
+ const preview = `[${$t('聊天记录')}]`
2103
+ targetSession.raw_msg_base = preview
2104
+ targetSession.raw_msg = targetSession.group_id
2105
+ ? `${String(authStore.loginInfo.nickname ?? '')}: ${preview}`
2106
+ : preview
2107
+ targetSession.time = Math.floor(sentTime / 1000)
2108
+ contactStore.baseOnMsgList.set(normalizeSessionId(targetId), targetSession)
2109
+ updateBaseOnMsgList()
2110
+ }
2111
+ }
2112
+ multipleSelectList.value = []
2113
+ uiStore.popBoxList.shift()
2114
+ finishForward(data, id)
2115
+ },
2116
+ },
2117
+ ],
2118
+ }
2119
+ uiStore.popBoxList.push(popInfo)
2120
+ } else if (selectedMsg.value && msgData) {
2121
+ const popInfo = {
2122
+ title: $t('转发消息'),
2123
+ template: markRaw(MsgBody),
2124
+ templateValue: markRaw({ data: msgData, type: 'forward' }),
2125
+ button: [
2126
+ {
2127
+ text: $t('取消'),
2128
+ fun: () => {
2129
+ uiStore.popBoxList.shift()
2130
+ },
2131
+ },
2132
+ {
2133
+ text: $t('确定'),
2134
+ master: true,
2135
+ fun: () => {
2136
+ sendMsgRaw(
2137
+ targetId,
2138
+ targetType,
2139
+ cloneMessagePayload(msgData.message),
2140
+ shouldPreShow(),
2141
+ 'sendMsgBack',
2142
+ targetChannelId || undefined,
2143
+ targetGuildId || undefined,
2144
+ )
2145
+ uiStore.popBoxList.shift()
2146
+ finishForward(data, id)
2147
+ },
2148
+ },
2149
+ ],
2150
+ }
2151
+ uiStore.popBoxList.push(popInfo)
2152
+ }
2153
+ }
2154
+
2155
+ function finishForward(data: UserFriendElem & UserGroupElem, id: string | number) {
2156
+ cancelForward()
2157
+ if(contactStore.baseOnMsgList.get(normalizeSessionId(id)) == undefined) {
2158
+ contactStore.baseOnMsgList.set(normalizeSessionId(id), data)
2159
+ }
2160
+ nextTick(() => {
2161
+ const user = document.getElementById('user-' + id)
2162
+ if (user) {
2163
+ user.click()
2164
+ }
2165
+ })
2166
+ }
2167
+
2168
+ function sendRespond(num: number) {
2169
+ const msgData = selectedMsg.value
2170
+ if (msgData !== null) {
2171
+ const msgId = msgData.message_id
2172
+ Connector.send(
2173
+ authStore.jsonMap.send_respond?.name ?? 'send_respond',
2174
+ {
2175
+ group_id: chat.show.id,
2176
+ message_id: msgId,
2177
+ emoji_id: String(num),
2178
+ code: String(num),
2179
+ },
2180
+ 'SendRespondBack_' + msgId + '_' + num,
2181
+ )
2182
+ }
2183
+ closeMsgMenu()
2184
+ }
2185
+
2186
+ function copyMsg() {
2187
+ const msgData = selectedMsg.value
2188
+ if (msgData !== null) {
2189
+ if (!msgData.raw_message) {
2190
+ msgData.raw_message = getMsgRawTxt(msgData)
2191
+ }
2192
+ const popInfo = new PopInfo()
2193
+ app.config.globalProperties.$copyText(msgData.raw_message).then(
2194
+ () => {
2195
+ popInfo.add(PopType.INFO, $t('复制成功'), true)
2196
+ },
2197
+ () => {
2198
+ popInfo.add(PopType.ERR, $t('复制失败'), true)
2199
+ },
2200
+ )
2201
+ }
2202
+ closeMsgMenu()
2203
+ }
2204
+
2205
+ function copySelectMsg() {
2206
+ if (selectCache.value != '') {
2207
+ const popInfo = new PopInfo()
2208
+ app.config.globalProperties
2209
+ .$copyText(selectCache.value)
2210
+ .then(
2211
+ () => {
2212
+ popInfo.add(PopType.INFO, $t('复制成功'), true)
2213
+ },
2214
+ () => {
2215
+ popInfo.add(PopType.ERR, $t('复制失败'), true)
2216
+ },
2217
+ )
2218
+ }
2219
+ closeMsgMenu()
2220
+ }
2221
+
2222
+ async function copyImg() {
2223
+ const url = tags.value.menuDisplay.downloadImg
2224
+ if (!url) return
2225
+ closeMsgMenu()
2226
+ const { blob, buffer } = await getImageUrlData(url)
2227
+ const popInfo = new PopInfo()
2228
+ if(backend.type === 'tauri') {
2229
+ try {
2230
+ const Clipboard = await import('@tauri-apps/plugin-clipboard-manager')
2231
+ await Clipboard.writeImage(buffer)
2232
+ popInfo.add(PopType.INFO, $t('复制成功'))
2233
+ } catch(e) {
2234
+ popInfo.add(PopType.ERR, $t('复制失败'))
2235
+ new Logger().error(e as unknown as Error, '复制图片失败')
2236
+ }
2237
+ } else {
2238
+ const item = new ClipboardItem({ [blob.type]: blob })
2239
+ try {
2240
+ await copyToClipboard([item])
2241
+ popInfo.add(PopType.INFO, $t('复制成功'))
2242
+ } catch (e) {
2243
+ popInfo.add(PopType.ERR, $t('复制失败'))
2244
+ new Logger().error(e as unknown as Error, '复制图片失败')
2245
+ }
2246
+ }
2247
+ }
2248
+
2249
+ function downloadImg() {
2250
+ const url = tags.value.menuDisplay.downloadImg
2251
+ if (url != false) {
2252
+ downloadFile(url as string, `img_${new Date().getTime()}.png`, () => undefined, () => undefined)
2253
+ }
2254
+ closeMsgMenu()
2255
+ }
2256
+
2257
+ async function revokeMsg() {
2258
+ const msgData = selectedMsg.value
2259
+ closeMsgMenu()
2260
+ if (!msgData) {
2261
+ new PopInfo().add(PopType.ERR, $t('获取选中消息失败'))
2262
+ return
2263
+ }
2264
+ const msgId = msgData.message_id
2265
+ const channelId = String(
2266
+ msgData.channel_id
2267
+ ?? msgData.channelId
2268
+ ?? chatStore.chatInfo.show.channel_id
2269
+ ?? chat.show.id,
2270
+ )
2271
+ await Connector.callApi('delete_msg', { channel_id: channelId, message_id: msgId })
2272
+ }
2273
+
2274
+ async function reeditMsg() {
2275
+ const msgData = selectedMsg.value
2276
+ closeMsgMenu()
2277
+ if (!msgData) {
2278
+ new PopInfo().add(PopType.ERR, $t('获取选中消息失败'))
2279
+ return
2280
+ }
2281
+ const msgId = msgData.message_id
2282
+ const channelId = String(
2283
+ msgData.channel_id
2284
+ ?? msgData.channelId
2285
+ ?? chatStore.chatInfo.show.channel_id
2286
+ ?? chat.show.id,
2287
+ )
2288
+ await Connector.callApi('delete_msg', { channel_id: channelId, message_id: msgId })
2289
+ reedit(msgData)
2290
+ }
2291
+
2292
+ function removeUser() {
2293
+ const msgData = selectedMsg.value
2294
+ if (msgData !== null) {
2295
+ const popInfo = {
2296
+ title: $t('提醒'),
2297
+ html: `<span>${$t('真的要将 {user} 移出群聊吗', { user: msgData.sender.nickname })}</span>`,
2298
+ button: [
2299
+ {
2300
+ text: $t('确定'),
2301
+ fun: () => {
2302
+ if (msgData) {
2303
+ Connector.send(
2304
+ 'set_group_kick',
2305
+ {
2306
+ group_id:
2307
+ chatStore.chatInfo.show
2308
+ .id,
2309
+ user_id: msgData.sender.user_id,
2310
+ },
2311
+ 'setGroupKick',
2312
+ )
2313
+ closeMsgMenu()
2314
+ uiStore.popBoxList.shift()
2315
+ }
2316
+ },
2317
+ },
2318
+ {
2319
+ text: $t('取消'),
2320
+ master: true,
2321
+ fun: () => {
2322
+ uiStore.popBoxList.shift()
2323
+ },
2324
+ },
2325
+ ],
2326
+ }
2327
+ uiStore.popBoxList.push(popInfo)
2328
+ }
2329
+ }
2330
+
2331
+ function closeMsgMenu() {
2332
+ tags.value.showMsgMenu = false
2333
+ tags.value.menuDisplay.menuSelectedMsgId = null
2334
+ setTimeout(() => {
2335
+ initMenuDisplay()
2336
+ }, 300)
2337
+ }
2338
+
2339
+ function openChatInfoPan() {
2340
+ tags.value.openChatInfo = !tags.value.openChatInfo
2341
+ if (tags.value.openChatInfo) {
2342
+ if (
2343
+ chat.show.type === 'group' &&
2344
+ chat.info.group_info?.gc !== chat.show.id
2345
+ ) {
2346
+ const url = `https://qinfo.clt.qq.com/cgi-bin/qun_info/get_group_info_all?gc=${chat.show.id}&bkn=${authStore.loginInfo.bkn}`
2347
+ Connector.send(
2348
+ 'http_proxy',
2349
+ { url: url },
2350
+ 'getMoreGroupInfo',
2351
+ )
2352
+ } else if (
2353
+ chat.show.type === 'user' &&
2354
+ chat.info.user_info?.uin !== chat.show.id
2355
+ ) {
2356
+ const userInfo = authStore.jsonMap?.friend_info?.name
2357
+ if(userInfo != undefined) {
2358
+ Connector.send(
2359
+ userInfo,
2360
+ { user_id: chat.show.id },
2361
+ 'getMoreUserInfo',
2362
+ )
2363
+ }
2364
+ }
2365
+ const noticeName = authStore.jsonMap?.group_notices?.name
2366
+ if (
2367
+ chat.show.type === 'group' &&
2368
+ (chat.info.group_notices === undefined ||
2369
+ Object.keys(chat.info.group_notices ?? {}).length ===
2370
+ 0)
2371
+ ) {
2372
+ if (noticeName) {
2373
+ Connector.send(
2374
+ noticeName,
2375
+ { group_id: chat.show.id },
2376
+ 'getGroupNotices',
2377
+ )
2378
+ }
2379
+ }
2380
+ if (chat.show.type === 'group' && Object.keys(chat.info.group_files ?? {}).length === 0) {
2381
+ const name = authStore.jsonMap.group_files?.name
2382
+ if(name) {
2383
+ Connector.send(name, {
2384
+ group_id: chat.show.id
2385
+ }, 'getGroupFiles')
2386
+ }
2387
+ }
2388
+ }
2389
+ }
2390
+
2391
+ function removeNthOccurrence(source: string, token: string, ordinal: number): string {
2392
+ let index = -1
2393
+ for (let i = 0; i < ordinal; i++) {
2394
+ index = source.indexOf(token, index + 1)
2395
+ if (index < 0) return source
2396
+ }
2397
+ if (index < 0) return source
2398
+ return source.slice(0, index) + source.slice(index + token.length)
2399
+ }
2400
+
2401
+ function mediaToken(index: number): string {
2402
+ const segment = sendCache.value[index]
2403
+ if (segment?.type === 'image') return '[图片]'
2404
+ if (segment?.type === 'at') {
2405
+ const id = String(segment.qq ?? segment.id ?? '')
2406
+ const member = chatStore.chatInfo.info.group_members.find((item) => {
2407
+ return String(item.user_id) === id
2408
+ })
2409
+ const name = String(segment.text || member?.card || member?.nickname || id)
2410
+ return '[@' + name + ']'
2411
+ }
2412
+ return '[SQ:' + index + ']'
2413
+ }
2414
+
2415
+ function deleteImg(index: number) {
2416
+ imgCache.value.delete(index)
2417
+ const segment = sendCache.value[index]
2418
+ if (segment?.type === 'image') {
2419
+ let imageOrdinal = 0
2420
+ for (let i = 0; i <= index; i++) {
2421
+ if (sendCache.value[i]?.type === 'image') imageOrdinal++
2422
+ }
2423
+ msg.value = removeNthOccurrence(msg.value, '[图片]', imageOrdinal)
2424
+ } else {
2425
+ msg.value = msg.value.replace(
2426
+ '[SQ:' + index + ']',
2427
+ '',
2428
+ )
2429
+ msg.value = msg.value.replace(
2430
+ '[SQ:' + index,
2431
+ '',
2432
+ )
2433
+ }
2434
+ }
2435
+
2436
+ async function editImg(key: number) {
2437
+ const img = imgCache.value.get(key)
2438
+ if (!img) return
2439
+ if (!viewerRef?.value) return
2440
+ const dataurl = await viewerRef.value.edit(img)
2441
+ imgCache.value.set(key, dataurl)
2442
+ }
2443
+
2444
+ function addSpecialMsg(data: SQCodeElem) {
2445
+ const input = (document.getElementById( 'main-input') as HTMLTextAreaElement | HTMLInputElement) ??
2446
+ (document.getElementById( 'main-input-ex') as HTMLTextAreaElement | HTMLInputElement)
2447
+ if (data !== undefined) {
2448
+ if (data.msgObj.type === 'at') {
2449
+ const id = String(data.msgObj.qq ?? data.msgObj.id ?? '')
2450
+ const member = chatStore.chatInfo.info.group_members.find((item) => {
2451
+ return String(item.user_id) === id
2452
+ })
2453
+ data.msgObj = {
2454
+ ...data.msgObj,
2455
+ qq: id,
2456
+ text: String(member?.card || member?.nickname || id),
2457
+ }
2458
+ }
2459
+ const index = sendCache.value.length
2460
+ sendCache.value.push(data.msgObj)
2461
+ if (!data.addText) return index
2462
+
2463
+ const sqCode = mediaToken(index)
2464
+ const atSpace = data.msgObj.type === 'at' ? ' ' : ''
2465
+
2466
+ if (data.addTop === true) {
2467
+ msg.value = sqCode + atSpace + msg.value
2468
+ } else {
2469
+ const selectionStart = input?.selectionStart
2470
+ const selectionEnd = input?.selectionEnd ?? selectionStart
2471
+ if(selectionStart != null) {
2472
+ const first = msg.value.substring(0, selectionStart)
2473
+ const last = msg.value.substring(selectionEnd!, msg.value.length)
2474
+ msg.value = first + sqCode + atSpace + last
2475
+ nextTick(()=>{
2476
+ const cursor = selectionStart + sqCode.length + atSpace.length
2477
+ input.selectionStart = cursor
2478
+ input.selectionEnd = cursor
2479
+ })
2480
+ } else {
2481
+ msg.value += sqCode + atSpace
2482
+ }
2483
+ }
2484
+ return index
2485
+ }
2486
+ return -1
2487
+ }
2488
+
2489
+ function addImg(event: ClipboardEvent) {
2490
+ if (!(event.clipboardData && event.clipboardData.items)) {
2491
+ return
2492
+ }
2493
+ for (
2494
+ let i = 0, len = event.clipboardData.items.length;
2495
+ i < len;
2496
+ i++
2497
+ ) {
2498
+ const item = event.clipboardData.items[i]
2499
+ if (item.kind === 'file') {
2500
+ setImg(item.getAsFile())
2501
+ event.preventDefault()
2502
+ }
2503
+ }
2504
+ }
2505
+
2506
+ function runSelectImg() {
2507
+ const input = document.getElementById('choice-pic')
2508
+ if (input) {
2509
+ input.click()
2510
+ }
2511
+ }
2512
+
2513
+ function selectImg(event: Event) {
2514
+ tags.value.showMoreDetail = false
2515
+ const sender = event.target as HTMLInputElement
2516
+ if (sender && sender.files) {
2517
+ setImg(sender.files[0])
2518
+ }
2519
+ }
2520
+
2521
+ function runSelectFile() {
2522
+ const input = document.getElementById('choice-file')
2523
+ if (input) {
2524
+ input.click()
2525
+ }
2526
+ }
2527
+
2528
+ function runSelectVideo() {
2529
+ const input = document.getElementById('choice-video')
2530
+ if (input) {
2531
+ input.click()
2532
+ }
2533
+ }
2534
+
2535
+ function selectFile(event: Event) {
2536
+ tags.value.showMoreDetail = false
2537
+ const sender = event.target as HTMLInputElement
2538
+ if (sender.files != null) {
2539
+ const file = sender.files[0]
2540
+ const fileName = file.name
2541
+ const size = file.size
2542
+ if (size > 500 * 1024 * 1024) {
2543
+ const popInfo = {
2544
+ title: $t('提醒'),
2545
+ html: `<span>${$t('文件大于 500MB。发送速度可能会非常缓慢;确认要发送吗?')}</span>`,
2546
+ button: [
2547
+ {
2548
+ text: $t('发送'),
2549
+ fun: () => {
2550
+ uiStore.popBoxList.shift()
2551
+ },
2552
+ },
2553
+ {
2554
+ text: $t('取消'),
2555
+ master: true,
2556
+ fun: () => {
2557
+ uiStore.popBoxList.shift()
2558
+ },
2559
+ },
2560
+ ],
2561
+ }
2562
+ uiStore.popBoxList.push(popInfo)
2563
+ } else {
2564
+ sendFile(file, fileName)
2565
+ }
2566
+ sender.value = ''
2567
+ }
2568
+ }
2569
+
2570
+ function selectVideo(event: Event) {
2571
+ tags.value.showMoreDetail = false
2572
+ const sender = event.target as HTMLInputElement
2573
+ if (sender.files != null) {
2574
+ const file = sender.files[0]
2575
+ const fileName = file.name
2576
+ const size = file.size
2577
+ if (size > 500 * 1024 * 1024) {
2578
+ new PopInfo().add(PopType.ERR, $t('视频文件不能超过 500MB'))
2579
+ } else {
2580
+ sendVideo(file, fileName)
2581
+ }
2582
+ sender.value = ''
2583
+ }
2584
+ }
2585
+
2586
+ function sendMedia(file: File, fileName: string | null, type: string) {
2587
+ const displayName = fileName ?? file.name ?? $t('未知文件')
2588
+ const taskId = addUploadTask({
2589
+ fileName: displayName,
2590
+ fileSize: file.size,
2591
+ execute: (onProgress) => {
2592
+ const reader = new FileReader()
2593
+ reader.onprogress = (event) => {
2594
+ if (event.lengthComputable) {
2595
+ onProgress(event.loaded, event.total)
2596
+ }
2597
+ }
2598
+ reader.readAsDataURL(file)
2599
+ reader.onloadend = () => {
2600
+ let base64data = reader.result as string
2601
+ base64data = base64data.substring(
2602
+ base64data.indexOf('base64,') + 7,
2603
+ base64data.length,
2604
+ )
2605
+ sendCache.value = []
2606
+ imgCache.value.clear()
2607
+ msg.value = ''
2608
+ addSpecialMsg({
2609
+ addText: true,
2610
+ msgObj: {
2611
+ type,
2612
+ file: 'base64://' + base64data,
2613
+ name: displayName,
2614
+ },
2615
+ })
2616
+ sendMsg('sendFileBack_' + taskId)
2617
+ }
2618
+ reader.onerror = () => {
2619
+ failUploadTask(taskId, '文件读取失败')
2620
+ }
2621
+ }
2622
+ })
2623
+ }
2624
+
2625
+ function sendFile(file: File, fileName: string | null) {
2626
+ sendMedia(file, fileName, 'file')
2627
+ }
2628
+
2629
+ function sendVideo(file: File, fileName: string | null) {
2630
+ sendMedia(file, fileName, 'video')
2631
+ }
2632
+
2633
+ async function setImg(file: File | null) {
2634
+ const popInfo = new PopInfo()
2635
+ if (!file) return
2636
+ if (!file.type.includes('image/')) return
2637
+ if (file.size === 0) return
2638
+
2639
+ if (file.size > 3145728) {
2640
+ const options = { maxSizeMB: 3, useWebWorker: true }
2641
+ try {
2642
+ popInfo.add(PopType.INFO, $t('正在压缩图片 ……'))
2643
+ const compressedFile = await imageCompression(file, options)
2644
+ new Logger().add(
2645
+ LogType.INFO,
2646
+ '图片压缩成功,原大小:' +
2647
+ file.size / 1024 / 1024 +
2648
+ ' MB,压缩后大小:' +
2649
+ compressedFile.size / 1024 / 1024 +
2650
+ ' MB',
2651
+ )
2652
+ setImg(compressedFile)
2653
+ } catch (error) {
2654
+ new Logger().error(error as Error, '图片压缩失败')
2655
+ popInfo.add(PopType.INFO, $t('压缩图片失败'))
2656
+ }
2657
+ return
2658
+ }
2659
+
2660
+ const dataUrl = await fileToDataURL(file)
2661
+ const id = sendCache.value.length
2662
+ const data = {
2663
+ type: 'image',
2664
+ text: `[${$t('图片')}]`,
2665
+ }
2666
+ imgCache.value.set(id, dataUrl)
2667
+ addSpecialMsg({
2668
+ addText: true,
2669
+ msgObj: data,
2670
+ })
2671
+ }
2672
+
2673
+ async function fileToDataURL(file: File): Promise<string> {
2674
+ return new Promise((resolve, reject) => {
2675
+ const reader = new FileReader()
2676
+ reader.onload = function(event) {
2677
+ if (!event.target) reject(new Error('读取文件失败'))
2678
+ else resolve(event.target.result as string)
2679
+ }
2680
+ reader.onerror = function(error) {
2681
+ reject(error)
2682
+ }
2683
+ reader.readAsDataURL(file)
2684
+ })
2685
+ }
2686
+
2687
+ function blobToBase64(blob: Blob): Promise<string> {
2688
+ return new Promise((resolve, reject) => {
2689
+ const reader = new FileReader()
2690
+ reader.onload = function(event) {
2691
+ const dataUrl = String(event.target?.result ?? '')
2692
+ const index = dataUrl.indexOf('base64,')
2693
+ resolve(index >= 0 ? dataUrl.slice(index + 7) : dataUrl)
2694
+ }
2695
+ reader.onerror = function(error) {
2696
+ reject(error)
2697
+ }
2698
+ reader.readAsDataURL(blob)
2699
+ })
2700
+ }
2701
+
2702
+ function toggleVoiceMenu() {
2703
+ if (voiceRecording.value) {
2704
+ voiceRecorder.value?.stop()
2705
+ voiceMenuShow.value = false
2706
+ return
2707
+ }
2708
+ voiceMenuShow.value = !voiceMenuShow.value
2709
+ }
2710
+
2711
+ async function startVoiceRecord() {
2712
+ voiceMenuShow.value = false
2713
+ if (voiceRecording.value) return
2714
+ if (!navigator.mediaDevices?.getUserMedia || typeof MediaRecorder === 'undefined') {
2715
+ new PopInfo().add(PopType.ERR, $t('当前浏览器不支持录音'))
2716
+ return
2717
+ }
2718
+ try {
2719
+ if (navigator.permissions?.query) {
2720
+ try {
2721
+ const status = await navigator.permissions.query({
2722
+ name: 'microphone' as PermissionName,
2723
+ })
2724
+ if (status.state === 'denied') {
2725
+ new PopInfo().add(PopType.ERR, $t('麦克风权限被拒绝,请在浏览器设置中允许访问麦克风'))
2726
+ return
2727
+ }
2728
+ } catch {
2729
+ // 部分浏览器不支持 micro 权限查询,继续用 getUserMedia 尝试
2730
+ }
2731
+ }
2732
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
2733
+ voiceChunks = []
2734
+ const recorder = new MediaRecorder(stream)
2735
+ voiceRecorder.value = recorder
2736
+ voiceRecording.value = true
2737
+ recorder.ondataavailable = (event) => {
2738
+ if (event.data.size > 0) voiceChunks.push(event.data)
2739
+ }
2740
+ recorder.onstop = async () => {
2741
+ voiceRecording.value = false
2742
+ voiceRecorder.value = null
2743
+ stream.getTracks().forEach((track) => track.stop())
2744
+ const blob = new Blob(voiceChunks, {
2745
+ type: recorder.mimeType || 'audio/webm',
2746
+ })
2747
+ const base64 = await blobToBase64(blob)
2748
+ const voiceName = recorder.mimeType.includes('mp4') ? 'voice.m4a' : 'voice.webm'
2749
+ const audioMime = recorder.mimeType.includes('mp4') ? 'audio/mp4' : 'audio/webm'
2750
+ if (voiceChunks.length > 0) {
2751
+ pendingVoice.value = {
2752
+ type: 'record',
2753
+ file: 'base64://' + base64,
2754
+ url: `data:${audioMime};base64,${base64}`,
2755
+ fileName: voiceName,
2756
+ name: $t('语音'),
2757
+ } as MsgItemElem
2758
+ msg.value = ''
2759
+ sendCache.value = []
2760
+ imgCache.value.clear()
2761
+ }
2762
+ voiceChunks = []
2763
+ }
2764
+ recorder.start()
2765
+ } catch (error) {
2766
+ voiceRecording.value = false
2767
+ voiceRecorder.value = null
2768
+ if (error instanceof DOMException && error.name === 'NotAllowedError') {
2769
+ new PopInfo().add(PopType.ERR, $t('麦克风权限被拒绝,请在浏览器设置中允许访问麦克风'))
2770
+ }
2771
+ new Logger().error(error as Error, $t('录音失败'))
2772
+ }
2773
+ }
2774
+
2775
+ function cancelPendingVoice() {
2776
+ pendingVoice.value = null
2777
+ voiceMenuShow.value = false
2778
+ voiceChunks = []
2779
+ msg.value = ''
2780
+ toMainInput()
2781
+ }
2782
+
2783
+ function selectVoiceFile() {
2784
+ voiceMenuShow.value = false
2785
+ const input = document.getElementById('choice-audio') as HTMLInputElement | null
2786
+ input?.click()
2787
+ }
2788
+
2789
+ async function selectAudioFile(event: Event) {
2790
+ const input = event.target as HTMLInputElement
2791
+ const file = input.files?.[0]
2792
+ input.value = ''
2793
+ if (!file) return
2794
+ if (!file.type.startsWith('audio/') && !/\.(mp3|wav|m4a|aac|ogg|webm|flac)$/i.test(file.name)) {
2795
+ new PopInfo().add(PopType.ERR, $t('请选择音频文件'))
2796
+ return
2797
+ }
2798
+ const dataUrl = await fileToDataURL(file)
2799
+ pendingVoice.value = {
2800
+ type: 'record',
2801
+ file: dataUrl,
2802
+ url: dataUrl,
2803
+ fileName: file.name,
2804
+ name: $t('语音'),
2805
+ } as MsgItemElem
2806
+ msg.value = ''
2807
+ sendCache.value = []
2808
+ imgCache.value.clear()
2809
+ voiceChunks = []
2810
+ }
2811
+
2812
+ function toMainInput() {
2813
+ const input = (document.getElementById( 'main-input') as HTMLTextAreaElement | HTMLInputElement) ??
2814
+ (document.getElementById( 'main-input-ex') as HTMLTextAreaElement | HTMLInputElement)
2815
+ if (input !== null) {
2816
+ input.focus()
2817
+ }
2818
+ }
2819
+
2820
+ function sendMsg(echo = 'sendMsgBack') {
2821
+ if (details.value[3].open) {
2822
+ return
2823
+ }
2824
+ details.value.forEach((item) => {
2825
+ item.open = false
2826
+ })
2827
+
2828
+ if (pendingVoice.value) {
2829
+ const voice = pendingVoice.value
2830
+ pendingVoice.value = null
2831
+ voiceMenuShow.value = false
2832
+ voiceChunks = []
2833
+ if (chat.show.temp) {
2834
+ sendMsgRaw(
2835
+ chat.show.id + '/' + chat.show.temp,
2836
+ chat.show.type,
2837
+ [voice],
2838
+ true,
2839
+ echo,
2840
+ )
2841
+ } else {
2842
+ sendMsgRaw(
2843
+ chat.show.id,
2844
+ chat.show.type,
2845
+ [voice],
2846
+ true,
2847
+ echo,
2848
+ )
2849
+ }
2850
+ msg.value = ''
2851
+ sendCache.value = []
2852
+ imgCache.value.clear()
2853
+ return
2854
+ }
2855
+
2856
+ for (const [key, base64data] of imgCache.value) {
2857
+ sendCache.value[key] = {
2858
+ type: 'image',
2859
+ file: 'base64://' + base64data.substring(
2860
+ base64data.indexOf('base64,') + 7,
2861
+ base64data.length
2862
+ )
2863
+ }
2864
+ }
2865
+ const parsedMsg = SendUtil.parseMsg(
2866
+ msg.value,
2867
+ sendCache.value,
2868
+ [],
2869
+ )
2870
+ if (chat.show.temp) {
2871
+ sendMsgRaw(
2872
+ chat.show.id + '/' + chat.show.temp,
2873
+ chat.show.type,
2874
+ parsedMsg,
2875
+ true,
2876
+ echo,
2877
+ )
2878
+ } else {
2879
+ sendMsgRaw(
2880
+ chat.show.id,
2881
+ chat.show.type,
2882
+ parsedMsg,
2883
+ true,
2884
+ echo,
2885
+ )
2886
+ }
2887
+ tags.value.checkNewLineFlag = true
2888
+ msg.value = ''
2889
+ sendCache.value = []
2890
+ imgCache.value.clear()
2891
+ scrollBottom()
2892
+ cancelReply()
2893
+ scheduleResizeMainInput(undefined, true)
2894
+ }
2895
+
2896
+ function updateList(newLength: number, oldLength: number) {
2897
+ if (oldLength == 0 && newLength > 0) {
2898
+ const name =
2899
+ authStore.jsonMap.set_message_read?.name ?? undefined
2900
+ if (!name) return
2901
+ let private_name =
2902
+ authStore.jsonMap.set_message_read?.private_name ??
2903
+ name
2904
+ if (!private_name) private_name = name
2905
+ if (chatStore.chatInfo.show.type == 'group') {
2906
+ Connector.send(
2907
+ name,
2908
+ {
2909
+ group_id: chat.show.id,
2910
+ message_id:
2911
+ list[list.length - 1].message_id,
2912
+ },
2913
+ 'setMessageRead',
2914
+ )
2915
+ } else {
2916
+ Connector.send(
2917
+ private_name,
2918
+ {
2919
+ user_id: chat.show.id,
2920
+ message_id:
2921
+ list[list.length - 1].message_id,
2922
+ },
2923
+ 'setMessageRead',
2924
+ )
2925
+ }
2926
+ if(shouldAutoFocus()) {
2927
+ toMainInput()
2928
+ }
2929
+ }
2930
+
2931
+ if (
2932
+ tags.value.showBottomButton &&
2933
+ !uiStore.nowGetHistory &&
2934
+ oldLength > 0
2935
+ ) {
2936
+ if (NewMsgNum.value !== 0) {
2937
+ NewMsgNum.value =
2938
+ NewMsgNum.value + Math.abs(newLength - oldLength)
2939
+ } else {
2940
+ NewMsgNum.value = Math.abs(newLength - oldLength)
2941
+ }
2942
+ }
2943
+ if (
2944
+ list.length > 200 &&
2945
+ !uiStore.nowGetHistory &&
2946
+ !tags.value.showBottomButton
2947
+ ) {
2948
+ chatStore.messageList = []
2949
+ const info = {
2950
+ type: chat.show.type,
2951
+ id: chat.show.id,
2952
+ name: chat.show.name,
2953
+ avatar: chat.show.avatar,
2954
+ jump: chat.show.jump,
2955
+ } as BaseChatInfoElem
2956
+ loadHistoryFirst(info)
2957
+ uiStore.nowGetHistory = true
2958
+ }
2959
+
2960
+ const pan = document.getElementById('msgPan')
2961
+ if (pan !== null) {
2962
+ const height = pan.scrollHeight
2963
+ nextTick(() => {
2964
+ const newPan = document.getElementById('msgPan')
2965
+ if (newPan !== null) {
2966
+ if (uiStore.nowGetHistory) {
2967
+ scrollTo(
2968
+ newPan.scrollHeight - height,
2969
+ false,
2970
+ )
2971
+ }
2972
+ if (!uiStore.nowGetHistory) {
2973
+ if (!tags.value.showBottomButton) {
2974
+ scrollTo(newPan.scrollHeight)
2975
+ }
2976
+ if (oldLength <= 0) {
2977
+ scrollTo(newPan.scrollHeight, false)
2978
+ scrollBottomAfterLayout()
2979
+ }
2980
+ updateBottomButtonVisibility()
2981
+ }
2982
+ }
2983
+
2984
+ const getImgList = () => {
2985
+ const getImgList = [] as string[]
2986
+ for(const item of list) {
2987
+ if (item.message !== undefined) {
2988
+ for(const msgItem of item.message) {
2989
+ if (
2990
+ msgItem.type === 'image' &&
2991
+ msgItem.file != 'marketface'
2992
+ ) {
2993
+ getImgList.push(msgItem.url)
2994
+ }
2995
+ }
2996
+ }
2997
+ }
2998
+ return getImgList
2999
+ }
3000
+ chatImg.value = Img.fromList(getImgList())
3001
+ if (
3002
+ chatStore.chatInfo.show &&
3003
+ chatStore.chatInfo.show.jump
3004
+ ) {
3005
+ new Logger().debug(
3006
+ '进入跳转至消息:' +
3007
+ chatStore.chatInfo.show.jump,
3008
+ )
3009
+ scrollToMsgLocal(
3010
+ 'chat-' + chatStore.chatInfo.show.jump,
3011
+ )
3012
+ chatStore.chatInfo.show.jump = undefined
3013
+ }
3014
+ })
3015
+ }
3016
+ }
3017
+
3018
+ function msgClick(_: Event, data: any) {
3019
+ const message_id = data.message_id
3020
+ if (multipleSelectList.value.length > 0) {
3021
+ if (multipleSelectList.value.indexOf(message_id) > -1) {
3022
+ multipleSelectList.value =
3023
+ multipleSelectList.value.filter((item) => {
3024
+ return item != message_id
3025
+ })
3026
+ } else {
3027
+ multipleSelectList.value.push(message_id)
3028
+ }
3029
+ }
3030
+ }
3031
+
3032
+ function delMsgs() {
3033
+ new PopInfo().add(
3034
+ PopType.INFO,
3035
+ $t('欸嘿,这个按钮只是用来占位置的'),
3036
+ )
3037
+ }
3038
+
3039
+ function copyMsgs() {
3040
+ const msgList = list.filter((item: any) => {
3041
+ return multipleSelectList.value.indexOf(item.message_id) > -1
3042
+ })
3043
+ let msgText = ''
3044
+ let lastDate = ''
3045
+ msgList.forEach((item: any) => {
3046
+ const time = new Date(getViewTime(item.time))
3047
+ const date =
3048
+ time.getFullYear() +
3049
+ '-' +
3050
+ (time.getMonth() + 1) +
3051
+ '-' +
3052
+ time.getDate()
3053
+ if (date != lastDate) {
3054
+ msgText += '\n—— ' + date + ' ——\n'
3055
+ lastDate = date
3056
+ }
3057
+ msgText +=
3058
+ item.sender.nickname +
3059
+ ' ' +
3060
+ time.getHours() +
3061
+ ':' +
3062
+ time.getMinutes() +
3063
+ ':' +
3064
+ time.getSeconds() +
3065
+ '\n' +
3066
+ getMsgRawTxt(item) +
3067
+ '\n\n'
3068
+ })
3069
+ const popInfo = new PopInfo()
3070
+ app.config.globalProperties.$copyText(msgText).then(
3071
+ () => {
3072
+ popInfo.add(PopType.INFO, $t('复制成功'), true)
3073
+ multipleSelectList.value = []
3074
+ },
3075
+ () => {
3076
+ popInfo.add(PopType.ERR, $t('复制失败'), true)
3077
+ },
3078
+ )
3079
+ }
3080
+
3081
+ async function recallMsgs() {
3082
+ const msgList = list.filter((item: any) => multipleSelectList.value.includes(item.message_id))
3083
+ const tasks: Promise<unknown>[] = []
3084
+ for (const msgItem of msgList) {
3085
+ const msgId = msgItem.message_id
3086
+ const channelId = String(
3087
+ msgItem.channel_id
3088
+ ?? msgItem.channelId
3089
+ ?? chatStore.chatInfo.show.channel_id
3090
+ ?? chat.show.id,
3091
+ )
3092
+ tasks.push(Connector.callApi('delete_msg', { channel_id: channelId, message_id: msgId }))
3093
+ }
3094
+ multipleSelectList.value = []
3095
+ await Promise.all(tasks)
3096
+ }
3097
+
3098
+ function showJin() {
3099
+ details.value[2].open = !details.value[2].open
3100
+ if (chatStore.chatInfo.info.jin_info.list.length == 0) {
3101
+ const name =
3102
+ authStore.jsonMap?.group_essence?.name ??
3103
+ 'get_essence_msg_list'
3104
+ Connector.send(
3105
+ name,
3106
+ {
3107
+ group_id: chat.show.id,
3108
+ pages: 0,
3109
+ },
3110
+ 'getJin',
3111
+ )
3112
+ }
3113
+ tags.value.showMoreDetail = !tags.value.showMoreDetail
3114
+ }
3115
+
3116
+ async function handleInput(event: Event) {
3117
+ const input = event.target as HTMLInputElement
3118
+ scheduleResizeMainInput(input)
3119
+
3120
+ const diff = getDifferencesWithRanges(msg.value, oldMsg.value)
3121
+ let { end, str } = { end: 0, str: '' }
3122
+ if(diff.length > 0) {
3123
+ ({ end, str } = diff[0])
3124
+ }
3125
+
3126
+ if(str.indexOf(']') >= 0) {
3127
+ const prefix = oldMsg.value.substring(0, end)
3128
+ const imageTokenIndex = prefix.lastIndexOf('[图片]')
3129
+ if (imageTokenIndex >= 0 && imageTokenIndex < end) {
3130
+ let imageOrdinal = 0
3131
+ let cursor = -1
3132
+ while ((cursor = prefix.indexOf('[图片]', cursor + 1)) >= 0) {
3133
+ imageOrdinal++
3134
+ }
3135
+ const imageIndices = sendCache.value
3136
+ .map((segment, index) => segment?.type === 'image' ? index : -1)
3137
+ .filter((index) => index >= 0)
3138
+ const imageIndex = imageIndices[imageOrdinal - 1]
3139
+ if (imageIndex !== undefined && imgCache.value.has(imageIndex)) {
3140
+ deleteImg(imageIndex)
3141
+ }
3142
+ }
3143
+ const sqIndex = oldMsg.value.substring(0, end).lastIndexOf('[SQ:')
3144
+ if(sqIndex >= 0 && sqIndex < end) {
3145
+ const msgHas = oldMsg.value.substring(sqIndex)
3146
+ const sq = oldMsg.value.slice(sqIndex, msgHas.indexOf(']') + sqIndex + 1)
3147
+ const numStr = sq.replace('[SQ:', '').replace(']', '')
3148
+ const num = Number(numStr)
3149
+ if(!isNaN(num) && imgCache.value.has(num)) {
3150
+ deleteImg(num)
3151
+ }
3152
+ }
3153
+ const atMatch = oldMsg.value.substring(0, end).match(/\[@[^\]]+\]$/)
3154
+ if (atMatch) {
3155
+ const atIndex = sendCache.value.findIndex((segment) => segment?.type === 'at')
3156
+ if (atIndex >= 0) sendCache.value.splice(atIndex, 1)
3157
+ }
3158
+ }
3159
+
3160
+ if (details.value[3].open) {
3161
+ if (searchDebounceTimer.value) {
3162
+ clearTimeout(searchDebounceTimer.value)
3163
+ searchDebounceTimer.value = null
3164
+ }
3165
+ const value = input.value
3166
+ if (value.length == 0) {
3167
+ searchRequestId.value++
3168
+ tags.value.search.list = reactive(list)
3169
+ } else if (settingsStore.sysConfig.enable_local_history) {
3170
+ const requestId = ++searchRequestId.value
3171
+ searchDebounceTimer.value = setTimeout(async () => {
3172
+ const results = await dbSearchMessages(
3173
+ authStore.loginInfo.uin,
3174
+ chatStore.chatInfo.show.id,
3175
+ value,
3176
+ )
3177
+ if (requestId !== searchRequestId.value || !details.value[3].open) return
3178
+ tags.value.search.list = results
3179
+ }, 180)
3180
+ } else {
3181
+ searchRequestId.value++
3182
+ tags.value.search.list = list.filter(
3183
+ (item: any) => {
3184
+ const rawMessage = getMsgRawTxt(item)
3185
+ return rawMessage.indexOf(value) !== -1
3186
+ },
3187
+ )
3188
+ }
3189
+ }
3190
+ }
3191
+
3192
+ function openSearch() {
3193
+ details.value[3].open = !details.value[3].open
3194
+ tags.value.showMoreDetail = !tags.value.showMoreDetail
3195
+ }
3196
+
3197
+ function closeSearch() {
3198
+ if (searchDebounceTimer.value) {
3199
+ clearTimeout(searchDebounceTimer.value)
3200
+ searchDebounceTimer.value = null
3201
+ }
3202
+ searchRequestId.value++
3203
+ details.value[3].open = !details.value[3].open
3204
+ msg.value = ''
3205
+ tags.value.search.list = reactive(list)
3206
+ scheduleResizeMainInput()
3207
+ }
3208
+
3209
+ function sendPoke(userId: number) {
3210
+ if (authStore.jsonMap.poke) {
3211
+ let name = authStore.jsonMap.poke?.name
3212
+ if (
3213
+ chat.show.type == 'user' &&
3214
+ authStore.jsonMap.poke.private_name
3215
+ ) {
3216
+ name = authStore.jsonMap.poke.private_name
3217
+ }
3218
+ Connector.send(
3219
+ name,
3220
+ {
3221
+ user_id: userId,
3222
+ group_id: chat.show.id,
3223
+ },
3224
+ 'sendPoke',
3225
+ )
3226
+ }
3227
+ tags.value.showMoreDetail = false
3228
+ tags.value.menuDisplay.poke = false
3229
+ }
3230
+
3231
+ function reedit(msgData: any) {
3232
+ msg.value = ''
3233
+ sendCache.value = []
3234
+ imgCache.value.clear()
3235
+ cancelReply()
3236
+ for (const seg of msgData.message) {
3237
+ if (seg.type === 'text') {
3238
+ msg.value += seg.text
3239
+ } else if (seg.type === 'reply') {
3240
+ const foundMsg = list.find((item: any) => item.message_id == seg.id)
3241
+ if (!foundMsg) continue
3242
+ replyMsg(foundMsg)
3243
+ } else if (seg.type === 'record' || seg.type === 'audio') {
3244
+ pendingVoice.value = {
3245
+ ...seg,
3246
+ name: seg.name || $t('语音'),
3247
+ } as MsgItemElem
3248
+ msg.value = ''
3249
+ sendCache.value = []
3250
+ imgCache.value.clear()
3251
+ } else {
3252
+ addSpecialMsg({
3253
+ addText: false,
3254
+ msgObj: seg,
3255
+ })
3256
+ msg.value += mediaToken(sendCache.value.length - 1)
3257
+ }
3258
+ }
3259
+ toMainInput()
3260
+ }
3261
+
3262
+ function jinScroll(event: Event) {
3263
+ const body = event.target as HTMLDivElement
3264
+ if (
3265
+ body.scrollTop + body.clientHeight === body.scrollHeight &&
3266
+ !tags.value.isJinLoading
3267
+ ) {
3268
+ if (chat.info.jin_info.is_end == false) {
3269
+ tags.value.isJinLoading = true
3270
+ const name =
3271
+ authStore.jsonMap?.group_essence?.name ??
3272
+ 'get_essence_msg_list'
3273
+ Connector.send(
3274
+ name,
3275
+ {
3276
+ group_id: chat.show.id,
3277
+ pages: chat.info.jin_info.pages + 1,
3278
+ },
3279
+ 'getJin',
3280
+ )
3281
+ }
3282
+ }
3283
+ }
3284
+
3285
+ function viewerEssImg(url: string) {
3286
+ if (!viewerRef?.value) return
3287
+ viewerRef.value.open(new Img(url))
3288
+ }
3289
+
3290
+ function moreFunClick(type = 'default') {
3291
+ let hasOpen = false
3292
+ details.value.forEach((item) => {
3293
+ if (item.open) hasOpen = true
3294
+ item.open = false
3295
+ })
3296
+ if (hasOpen) return
3297
+ if (tags.value.showMoreDetail) {
3298
+ tags.value.showMoreDetail = false
3299
+ return
3300
+ }
3301
+ switch(type) {
3302
+ case 'default': tags.value.showMoreDetail = true; break
3303
+ case 'img': runSelectImg(); break
3304
+ case 'file': runSelectFile(); break
3305
+ case 'face': details.value[1].open = !details.value[1].open; break
3306
+ }
3307
+ }
3308
+
3309
+ function getTargetWin(): HTMLDivElement | undefined {
3310
+ const chatPan = document.getElementById('chat-pan')
3311
+ if (!chatPan) return
3312
+ if(tags.value.openChatInfo) {
3313
+ return chatPan.getElementsByClassName('chat-info-pan')[0] as HTMLDivElement
3314
+ } else if(mergePan.value?.isMergeOpen()) {
3315
+ return chatPan.getElementsByClassName('merge-pan')[0] as HTMLDivElement
3316
+ } else {
3317
+ return chatPan as HTMLDivElement
3318
+ }
3319
+ }
3320
+
3321
+ function exitWin() {
3322
+ if(tags.value.openChatInfo) {
3323
+ openChatInfoPan()
3324
+ } else if(mergePan.value?.isMergeOpen()) {
3325
+ mergePan.value?.closeMergeMsg()
3326
+ setTimeout(() => {
3327
+ const chatPan = document.getElementById('chat-pan')
3328
+ const mergePanEl = chatPan!.getElementsByClassName('merge-pan')[0] as HTMLDivElement
3329
+ if(mergePanEl) {
3330
+ mergePanEl.style.transform = ''
3331
+ }
3332
+ }, 500)
3333
+ } else {
3334
+ chatStore.chatInfo.show.id = 0
3335
+ uiStore.openSideBar = true
3336
+ new Logger().add(LogType.UI, '右滑打开侧边栏触发完成')
3337
+ }
3338
+ }
3339
+ </script>
3340
+
3341
+ <style scoped>
3342
+ /* 消息动画 */
3343
+ .msglist-move {
3344
+ transition: all 0.3s;
3345
+ }
3346
+
3347
+ .msglist-enter-active {
3348
+ transition: all 0.4s;
3349
+ }
3350
+
3351
+ .msglist-leave-active {
3352
+ transition: all 0.2s;
3353
+ }
3354
+
3355
+ .msglist-enter-from {
3356
+ transform: translateX(-20px);
3357
+ opacity: 0;
3358
+ }
3359
+
3360
+ .msglist-leave-to {
3361
+ opacity: 0;
3362
+ }
3363
+
3364
+ /* 更多功能面板动画 */
3365
+ .pan-enter-active,
3366
+ .pan-leave-active {
3367
+ transition: opacity 0.3s;
3368
+ }
3369
+
3370
+ .pan-enter-from {
3371
+ transform: translateX(20px);
3372
+ opacity: 0;
3373
+ }
3374
+
3375
+ .pan-leave-to {
3376
+ opacity: 0;
3377
+ }
3378
+
3379
+ .more-detail {
3380
+ position: relative;
3381
+ }
3382
+
3383
+ .more-detail.voice-menu-open {
3384
+ overflow: visible;
3385
+ }
3386
+
3387
+ .voice-menu-wrap {
3388
+ position: relative;
3389
+ width: 50px;
3390
+ height: 50px;
3391
+ margin: 0 10px;
3392
+ cursor: pointer;
3393
+ border-radius: 7px;
3394
+ background: var(--color-card-1);
3395
+ }
3396
+
3397
+ .voice-menu-wrap > div:first-child {
3398
+ display: flex;
3399
+ align-items: center;
3400
+ justify-content: center;
3401
+ width: 100%;
3402
+ height: 100%;
3403
+ }
3404
+
3405
+ .voice-menu-wrap > div:first-child > svg {
3406
+ color: var(--color-font-1);
3407
+ margin: 15px;
3408
+ height: 20px;
3409
+ width: 20px;
3410
+ }
3411
+
3412
+ .voice-menu-wrap > div:first-child.active {
3413
+ background: rgba(244, 67, 54, 0.12);
3414
+ }
3415
+
3416
+ .voice-menu-wrap > div:first-child.active > svg {
3417
+ color: #f44336;
3418
+ }
3419
+
3420
+ .voice-menu {
3421
+ position: absolute;
3422
+ right: 0;
3423
+ bottom: 56px;
3424
+ z-index: 20;
3425
+ min-width: 120px;
3426
+ overflow: hidden;
3427
+ border-radius: 8px;
3428
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
3429
+ background: var(--color-card, #fff);
3430
+ }
3431
+
3432
+ .voice-menu > div {
3433
+ padding: 8px 14px;
3434
+ cursor: pointer;
3435
+ white-space: nowrap;
3436
+ }
3437
+
3438
+ .voice-menu > div:hover {
3439
+ background: var(--color-hover-bg, rgba(0, 0, 0, 0.06));
3440
+ }
3441
+
3442
+ .voice-pending {
3443
+ display: flex;
3444
+ align-items: center;
3445
+ justify-content: space-between;
3446
+ height: 36px;
3447
+ padding: 0 12px;
3448
+ cursor: pointer;
3449
+ border-radius: 6px;
3450
+ color: var(--color-main, #3b82f6);
3451
+ background: var(--color-active-bg, rgba(59, 130, 246, 0.08));
3452
+ }
3453
+ </style>