koishi-plugin-chat-patch 2.0.1 → 2.1.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.
package/client/index.scss CHANGED
@@ -10,6 +10,44 @@
10
10
  }
11
11
 
12
12
  .chat-patch-wrapper {
13
+ /* 完全隐藏滚动条 - 增强版 */
14
+ .mobile-scrollbar-fix {
15
+ /* 隐藏 Element Plus 滚动条组件 */
16
+ .el-scrollbar__bar {
17
+ display: none !important;
18
+ opacity: 0 !important;
19
+ width: 0 !important;
20
+ height: 0 !important;
21
+ }
22
+
23
+ .el-scrollbar__thumb {
24
+ display: none !important;
25
+ }
26
+
27
+ .el-scrollbar__wrap {
28
+ overflow-x: hidden !important;
29
+
30
+ /* 隐藏原生滚动条 - 所有浏览器 */
31
+ scrollbar-width: none !important; /* Firefox */
32
+ -ms-overflow-style: none !important; /* IE/Edge */
33
+
34
+ &::-webkit-scrollbar {
35
+ display: none !important; /* Chrome/Safari */
36
+ width: 0 !important;
37
+ height: 0 !important;
38
+ background: transparent !important;
39
+ }
40
+
41
+ &::-webkit-scrollbar-track {
42
+ display: none !important;
43
+ }
44
+
45
+ &::-webkit-scrollbar-thumb {
46
+ display: none !important;
47
+ }
48
+ }
49
+ }
50
+
13
51
  color: var(--chat-text);
14
52
  background-color: var(--chat-bg);
15
53
 
@@ -37,4 +75,16 @@
37
75
  width: 100%;
38
76
  height: 100%;
39
77
  border: none;
78
+ }
79
+
80
+ .message-highlight {
81
+ animation: highlight-fade 1.5s ease-in-out;
82
+ border-radius: 8px;
83
+ }
84
+
85
+ @keyframes highlight-fade {
86
+ 0% { background-color: transparent; }
87
+ 10% { background-color: rgba(64, 158, 255, 0.4); filter: brightness(1.2); }
88
+ 30% { background-color: rgba(64, 158, 255, 0.4); filter: brightness(1.2); }
89
+ 100% { background-color: transparent; }
40
90
  }
@@ -1,5 +1,5 @@
1
- import { ref, computed, onMounted, nextTick, onUnmounted, reactive } from 'vue'
2
- import { receive } from '@koishijs/client'
1
+ import { ref, computed, onMounted, nextTick, onUnmounted, reactive, watch } from 'vue'
2
+ import { receive, send } from '@koishijs/client'
3
3
  import { useChatData } from './composables/useChatData'
4
4
  import { useChatActions } from './composables/useChatActions'
5
5
  import { useImageCache } from './composables/useImageCache'
@@ -19,24 +19,45 @@ export function useChatLogic() {
19
19
  const { getCachedImageUrl, cacheImage } = useImageCache()
20
20
 
21
21
  // 状态管理
22
+ const menu = ref({ show: false, x: 0, y: 0, type: '', id: '', isPinned: false, hasMedia: false })
22
23
  const selectedBot = ref('')
23
24
  const selectedChannel = ref('')
24
25
  const inputText = ref('')
25
26
  const uploadedImages = ref<any[]>([])
26
27
  const scrollRef = ref<any>(null)
28
+ const inputRef = ref<any>(null)
27
29
  const isMobile = ref(false)
28
- const mobileView = ref<'bots' | 'channels' | 'messages' | 'forward' | 'image'>('bots')
30
+ const mobileView = ref<'bots' | 'channels' | 'messages' | 'forward' | 'image' | 'profile' | 'raw'>('bots')
29
31
  const isLoadingHistory = ref(false)
32
+ const keyboardHeight = ref(0) // 键盘高度
30
33
 
31
34
  // 合并转发详情状态
32
35
  const forwardData = reactive({
33
36
  messages: [] as any[]
34
37
  })
38
+ const forwardDialogVisible = ref(false)
35
39
 
36
40
  // 图片查看器状态
37
41
  const imageViewer = reactive({
38
42
  url: ''
39
43
  })
44
+ const imageViewerVisible = ref(false)
45
+ const imageZoom = ref(1)
46
+
47
+ // 原始消息查看状态
48
+ const rawMessage = reactive({
49
+ content: ''
50
+ })
51
+ const rawMessageVisible = ref(false)
52
+
53
+ // 引用/回复状态
54
+ const replyingTo = ref<any>(null)
55
+
56
+ // 用户资料状态
57
+ const userProfile = reactive({
58
+ data: null as any
59
+ })
60
+ const userProfileVisible = ref(false)
40
61
 
41
62
  // 计算属性
42
63
  const currentChannels = computed(() => getChannels(selectedBot.value))
@@ -45,6 +66,10 @@ export function useChatLogic() {
45
66
  const c = currentChannels.value.find(i => i.id === selectedChannel.value)
46
67
  return c ? c.name : ''
47
68
  })
69
+ const selectedBotPlatform = computed(() => {
70
+ const bot = bots.value.find(b => b.selfId === selectedBot.value)
71
+ return bot?.platform || 'unknown'
72
+ })
48
73
 
49
74
  // 方法
50
75
  const selectBot = (id: string) => {
@@ -64,23 +89,74 @@ export function useChatLogic() {
64
89
 
65
90
  await nextTick()
66
91
  scrollToBottom()
92
+
93
+ // 针对图片加载导致的滚动偏移,在 300ms 和 800ms 后再次校准底部
94
+ setTimeout(scrollToBottom, 300)
95
+ setTimeout(scrollToBottom, 800)
67
96
  }
68
97
 
69
98
  const goBack = () => {
70
- if (mobileView.value === 'image') mobileView.value = 'messages'
71
- else if (mobileView.value === 'forward') mobileView.value = 'messages'
72
- else if (mobileView.value === 'messages') mobileView.value = 'channels'
73
- else if (mobileView.value === 'channels') mobileView.value = 'bots'
99
+ if (isMobile.value && mobileView.value !== 'bots') {
100
+ // 手机端使用 history.back(),由 popstate 监听器处理视图切换
101
+ window.history.back()
102
+ } else {
103
+ // PC 端或初始页面的逻辑
104
+ if (mobileView.value === 'image') {
105
+ mobileView.value = 'messages'
106
+ imageZoom.value = 1
107
+ }
108
+ else if (mobileView.value === 'forward') mobileView.value = 'messages'
109
+ else if (mobileView.value === 'profile') mobileView.value = 'messages'
110
+ else if (mobileView.value === 'raw') mobileView.value = 'messages'
111
+ else if (mobileView.value === 'messages') mobileView.value = 'channels'
112
+ else if (mobileView.value === 'channels') mobileView.value = 'bots'
113
+ }
74
114
  }
75
115
 
116
+ // 处理手机物理返回键
117
+ const handlePopState = (e: PopStateEvent) => {
118
+ if (!isMobile.value) return
119
+ if (e.state && e.state.view) {
120
+ mobileView.value = e.state.view
121
+ } else {
122
+ mobileView.value = 'bots'
123
+ }
124
+ }
125
+
126
+ // 监听视图变化,同步到 history state
127
+ watch(mobileView, (newView, oldView) => {
128
+ if (!isMobile.value) return
129
+ // 只有在非 popstate 导致的改变时才 pushState (简单判断:如果当前 state 不匹配则 push)
130
+ if (window.history.state?.view !== newView) {
131
+ window.history.pushState({ view: newView }, '')
132
+ }
133
+ })
134
+
76
135
  const showForward = (elements: any[]) => {
77
136
  forwardData.messages = elements.filter(e => e.type === 'message')
78
- mobileView.value = 'forward'
137
+ if (isMobile.value) {
138
+ mobileView.value = 'forward'
139
+ } else {
140
+ forwardDialogVisible.value = true
141
+ }
79
142
  }
80
143
 
81
144
  const openImageViewer = (url: string) => {
82
145
  imageViewer.url = url
83
- mobileView.value = 'image'
146
+ imageZoom.value = 1
147
+ if (isMobile.value) {
148
+ mobileView.value = 'image'
149
+ } else {
150
+ imageViewerVisible.value = true
151
+ }
152
+ }
153
+
154
+ const handleImageWheel = (e: WheelEvent) => {
155
+ if (e.deltaY < 0) {
156
+ imageZoom.value = Math.min(imageZoom.value + 0.1, 3)
157
+ } else {
158
+ imageZoom.value = Math.max(imageZoom.value - 0.1, 0.5)
159
+ }
84
160
  }
85
161
 
86
162
  const downloadImage = (url: string) => {
@@ -121,26 +197,368 @@ export function useChatLogic() {
121
197
  if (!selectedBot.value || !selectedChannel.value) return
122
198
  if (!inputText.value.trim() && !uploadedImages.value.length) return
123
199
 
124
- const res = await sendMessage(selectedBot.value, selectedChannel.value, inputText.value, uploadedImages.value)
200
+ let content = inputText.value
201
+ if (replyingTo.value) {
202
+ // 优先使用真实 ID 进行引用
203
+ const quoteId = replyingTo.value.realId || replyingTo.value.id
204
+ content = `<quote id="${quoteId}"/>${content}`
205
+ }
206
+
207
+ const res = await sendMessage(selectedBot.value, selectedChannel.value, content, uploadedImages.value)
125
208
  if (res?.success) {
126
209
  inputText.value = ''
127
210
  uploadedImages.value = []
211
+ replyingTo.value = null
128
212
  scrollToBottom()
129
213
  } else {
130
214
  ElMessage.error(res?.error || '发送失败')
131
215
  }
132
216
  }
133
217
 
218
+ // 复读消息 (+1)
219
+ const repeatMessage = async (msg: any) => {
220
+ if (!selectedBot.value || !selectedChannel.value) return
221
+
222
+ // 复读消息内容
223
+ let content = msg.content
224
+
225
+ // 处理引用逻辑
226
+ if (msg.quote) {
227
+ // 如果原消息有引用,保留引用
228
+ const quoteId = msg.quote.id
229
+ // 检查引用ID是否为临时ID
230
+ if (quoteId && !quoteId.startsWith('bot-msg-')) {
231
+ content = `<quote id="${quoteId}"/>${content}`
232
+ } else {
233
+ // 引用ID是临时ID,不添加引用,只发送内容
234
+ content = msg.content
235
+ }
236
+ } else if (msg.isBot || msg.userId === selectedBot.value) {
237
+ // 如果是复读机器人自己的消息
238
+ const messageId = msg.realId || msg.id
239
+
240
+ // 检查消息ID是否为临时ID(以bot-msg-开头)
241
+ if (messageId && messageId.startsWith('bot-msg-')) {
242
+ // 临时ID,不使用引用,直接发送内容
243
+ content = msg.content
244
+ } else if (messageId) {
245
+ // 真实ID,可以使用引用
246
+ content = `<quote id="${messageId}"/>${content}`
247
+ } else {
248
+ // 没有ID,只发送内容
249
+ content = msg.content
250
+ }
251
+ }
252
+
253
+ const res = await sendMessage(selectedBot.value, selectedChannel.value, content, [])
254
+ if (res?.success) {
255
+ scrollToBottom()
256
+ } else {
257
+ ElMessage.error(res?.error || '复读失败')
258
+ }
259
+ }
260
+
261
+ // 机器人右键菜单
262
+ const onBotMenu = (e: MouseEvent, bot: any) => {
263
+ e.preventDefault()
264
+ e.stopPropagation()
265
+ menu.value = {
266
+ show: true,
267
+ x: e.clientX,
268
+ y: e.clientY,
269
+ type: 'bot',
270
+ id: bot.selfId,
271
+ isPinned: pinnedBots.value.has(bot.selfId),
272
+ hasMedia: false
273
+ }
274
+ }
275
+
276
+ // 频道右键菜单
277
+ const onChannelMenu = (e: MouseEvent, channel: any) => {
278
+ e.preventDefault()
279
+ e.stopPropagation()
280
+ menu.value = {
281
+ show: true,
282
+ x: e.clientX,
283
+ y: e.clientY,
284
+ type: 'channel',
285
+ id: channel.id,
286
+ isPinned: pinnedChannels.value.has(`${selectedBot.value}:${channel.id}`),
287
+ hasMedia: false
288
+ }
289
+ }
290
+
291
+ // 消息右键菜单
292
+ const onMessageMenu = (e: MouseEvent, msg: any) => {
293
+ // 如果菜单已经显示,则关闭它并允许原生菜单弹出(第二次右键逻辑)
294
+ if (menu.value.show && menu.value.type === 'message' && menu.value.id === msg.id) {
295
+ menu.value.show = false
296
+ return
297
+ }
298
+
299
+ e.preventDefault()
300
+ e.stopPropagation()
301
+
302
+ const hasMedia = msg.elements?.some((el: any) => ['image', 'img', 'mface', 'audio', 'video'].includes(el.type))
303
+ menu.value = {
304
+ show: true,
305
+ x: e.clientX,
306
+ y: e.clientY,
307
+ type: 'message',
308
+ id: msg.id,
309
+ isPinned: false,
310
+ data: msg,
311
+ hasMedia
312
+ } as any
313
+ }
314
+
315
+ // 统一处理菜单动作
316
+ const handleMenuAction = async (action: string) => {
317
+ const type = menu.value.type
318
+ const id = menu.value.id
319
+ const isPinned = menu.value.isPinned
320
+ menu.value.show = false
321
+
322
+ if (action === 'pin') {
323
+ if (type === 'bot') await togglePinBot(id, isPinned, pinnedBots.value)
324
+ else await togglePinChannel(selectedBot.value, id, isPinned, pinnedChannels.value)
325
+ } else if (action === 'delete') {
326
+ // 删除逻辑保持在 index.vue 中通过 ElMessageBox 确认,或者这里直接处理
327
+ if (type === 'bot') await deleteBotData(id)
328
+ else await deleteChannelData(selectedBot.value, id)
329
+ location.reload()
330
+ }
331
+ }
332
+
333
+ // 兼容手机端的复制函数
334
+ const copyToClipboard = (text: string) => {
335
+ if (!text) {
336
+ ElMessage.warning('未复制任何内容')
337
+ return Promise.resolve()
338
+ }
339
+ if (navigator.clipboard && window.isSecureContext) {
340
+ return navigator.clipboard.writeText(text)
341
+ } else {
342
+ // 回退方案
343
+ const textArea = document.createElement("textarea")
344
+ textArea.value = text
345
+ textArea.style.position = "fixed"
346
+ textArea.style.left = "-999999px"
347
+ textArea.style.top = "-999999px"
348
+ document.body.appendChild(textArea)
349
+ textArea.focus()
350
+ textArea.select()
351
+ return new Promise<void>((res, rej) => {
352
+ document.execCommand('copy') ? res() : rej()
353
+ textArea.remove()
354
+ })
355
+ }
356
+ }
357
+
358
+ const handleMessageAction = async (action: string) => {
359
+ const msg = (menu.value as any).data
360
+ menu.value.show = false
361
+ if (!msg) return
362
+
363
+ if (action === 'copy') {
364
+ // 移除 HTML 标签
365
+ const text = (msg.content || '').replace(/<[^>]+>/g, '')
366
+ if (!text) {
367
+ ElMessage.warning('未复制任何内容')
368
+ } else {
369
+ copyToClipboard(text).then(() => ElMessage.success('已复制到剪贴板'))
370
+ }
371
+ } else if (action === 'copy-raw') {
372
+ // 查看原始消息,包含引用标签
373
+ let raw = msg.content || ''
374
+ if (msg.quote) {
375
+ raw = `<quote id="${msg.quote.id}"/>${raw}`
376
+ }
377
+ rawMessage.content = raw
378
+ if (isMobile.value) {
379
+ mobileView.value = 'raw'
380
+ } else {
381
+ rawMessageVisible.value = true
382
+ }
383
+ } else if (action === 'plus1') {
384
+ await repeatMessage(msg)
385
+ } else if (action === 'reply') {
386
+ replyingTo.value = msg
387
+ // 自动聚焦输入框
388
+ nextTick(() => {
389
+ // Element Plus 的 el-input 需要访问其内部的 textarea
390
+ const inputEl = inputRef.value?.$el?.querySelector('textarea') || inputRef.value?.ref
391
+ if (inputEl) {
392
+ inputEl.focus()
393
+ } else {
394
+ inputRef.value?.focus?.()
395
+ }
396
+ })
397
+ } else if (action === 'download') {
398
+ const media = msg.elements?.find((el: any) => ['image', 'img', 'mface', 'audio', 'video'].includes(el.type))
399
+ const url = media?.attrs?.src || media?.attrs?.url || media?.attrs?.file
400
+ if (url) downloadImage(url)
401
+ }
402
+ }
403
+
404
+ // 显示用户资料
405
+ const showUserProfile = async (msg: any) => {
406
+ if (!selectedBot.value) return
407
+
408
+ const res = await (send as any)('get-user-info', {
409
+ selfId: selectedBot.value,
410
+ userId: msg.userId,
411
+ guildId: msg.guildId
412
+ })
413
+
414
+ if (res.success) {
415
+ userProfile.data = res.data
416
+ if (isMobile.value) {
417
+ mobileView.value = 'profile'
418
+ } else {
419
+ userProfileVisible.value = true
420
+ }
421
+ } else {
422
+ ElMessage.error(res.error || '获取用户信息失败')
423
+ }
424
+ }
425
+
426
+ // 定位消息并高亮
427
+ const scrollToMessage = async (id: string) => {
428
+ // 尝试在当前列表中查找,优先匹配 data-id
429
+ let el = document.querySelector(`[data-id="${id}"]`) || document.getElementById(id)
430
+
431
+ if (!el) {
432
+ // 如果没找到,尝试向上加载历史记录
433
+ if (isLoadingHistory.value) return
434
+
435
+ ElMessage.info('正在向上查找历史消息...')
436
+
437
+ // 最多尝试向上查找 3 次
438
+ for (let i = 0; i < 3; i++) {
439
+ await loadHistory(selectedBot.value, selectedChannel.value)
440
+ // 等待 DOM 更新
441
+ await new Promise(resolve => setTimeout(resolve, 150))
442
+ el = document.querySelector(`[data-id="${id}"]`) || document.getElementById(id)
443
+ if (el) break
444
+ }
445
+ }
446
+
447
+ if (el) {
448
+ el.scrollIntoView({ behavior: 'smooth', block: 'center' })
449
+ // 找到消息内容容器进行高亮
450
+ const contentEl = el.querySelector('.cursor-context-menu') || el
451
+ contentEl.classList.add('message-highlight')
452
+ setTimeout(() => contentEl.classList.remove('message-highlight'), 1500)
453
+ } else {
454
+ ElMessage.warning('消息太久远,已不在当前列表中')
455
+ }
456
+ }
457
+
458
+ // 处理粘贴图片
459
+ const handlePaste = async (event: ClipboardEvent) => {
460
+ const items = event.clipboardData?.items
461
+ if (!items) return
462
+
463
+ for (let i = 0; i < items.length; i++) {
464
+ if (items[i].type.indexOf('image') !== -1) {
465
+ const file = items[i].getAsFile()
466
+ if (file) {
467
+ // 模拟文件上传逻辑
468
+ const reader = new FileReader()
469
+ reader.onload = async (e) => {
470
+ const base64 = e.target?.result as string
471
+ const res = await (send as any)('upload-image', {
472
+ file: base64,
473
+ filename: `pasted_image_${Date.now()}.png`,
474
+ mimeType: file.type
475
+ })
476
+ if (res.success) {
477
+ uploadedImages.value.push({
478
+ tempId: res.tempId,
479
+ preview: URL.createObjectURL(file),
480
+ filename: `pasted_image_${Date.now()}.png`
481
+ })
482
+ }
483
+ }
484
+ reader.readAsDataURL(file)
485
+ }
486
+ }
487
+ }
488
+ }
489
+
134
490
  const checkMobile = () => {
135
491
  isMobile.value = window.innerWidth <= 768
136
492
  }
137
493
 
494
+ // 监听键盘弹出(通过 visualViewport 或 window resize)
495
+ const updateKeyboardHeight = () => {
496
+ if (!isMobile.value) {
497
+ keyboardHeight.value = 0
498
+ return
499
+ }
500
+
501
+ // 使用 visualViewport API(现代浏览器)
502
+ if (window.visualViewport) {
503
+ const viewportHeight = window.visualViewport.height
504
+ const windowHeight = window.innerHeight
505
+ const calculatedHeight = Math.max(0, windowHeight - viewportHeight)
506
+
507
+ // 只有当键盘高度变化超过50px时才更新(避免小幅抖动)
508
+ if (Math.abs(calculatedHeight - keyboardHeight.value) > 50) {
509
+ keyboardHeight.value = calculatedHeight
510
+ }
511
+ } else {
512
+ keyboardHeight.value = 0
513
+ }
514
+ }
515
+
138
516
  // 生命周期
139
517
  let dispose: any[] = []
140
518
 
141
519
  onMounted(async () => {
142
520
  checkMobile()
143
521
  window.addEventListener('resize', checkMobile)
522
+ window.addEventListener('click', () => menu.value.show = false)
523
+ window.addEventListener('popstate', handlePopState)
524
+
525
+ // 监听键盘弹出
526
+ if (window.visualViewport) {
527
+ window.visualViewport.addEventListener('resize', updateKeyboardHeight)
528
+ window.visualViewport.addEventListener('scroll', updateKeyboardHeight)
529
+ }
530
+ window.addEventListener('resize', updateKeyboardHeight)
531
+
532
+ // 监听输入框聚焦和失焦
533
+ const handleFocus = () => {
534
+ // 延迟更新,等待键盘完全弹出
535
+ setTimeout(updateKeyboardHeight, 300)
536
+ }
537
+ const handleBlur = () => {
538
+ // 延迟更新,等待键盘完全收起
539
+ setTimeout(() => {
540
+ keyboardHeight.value = 0
541
+ }, 100)
542
+ }
543
+
544
+ // 为输入框添加事件监听
545
+ nextTick(() => {
546
+ const textarea = inputRef.value?.$el?.querySelector('textarea')
547
+ if (textarea) {
548
+ textarea.addEventListener('focus', handleFocus)
549
+ textarea.addEventListener('blur', handleBlur)
550
+ dispose.push(() => {
551
+ textarea.removeEventListener('focus', handleFocus)
552
+ textarea.removeEventListener('blur', handleBlur)
553
+ })
554
+ }
555
+ })
556
+
557
+ // 初始化 history state
558
+ if (isMobile.value) {
559
+ window.history.replaceState({ view: mobileView.value }, '')
560
+ }
561
+
144
562
  await loadConfig()
145
563
  await loadInitialData()
146
564
 
@@ -171,10 +589,29 @@ export function useChatLogic() {
171
589
  }
172
590
  })
173
591
  if (typeof d3 === 'function') dispose.push(d3)
592
+
593
+ // 监听机器人消息更新(发送成功后从虚拟 ID 转为真实 ID)
594
+ const d4 = receive('bot-message-updated', (data: any) => {
595
+ const channelKey = `${selectedBot.value}:${selectedChannel.value}`
596
+ if (data.channelKey !== channelKey) return
597
+
598
+ const msg = currentMessages.value.find(m => m.id === data.tempId)
599
+ if (msg) {
600
+ msg.sending = false
601
+ msg.realId = data.realId
602
+ }
603
+ })
604
+ if (typeof d4 === 'function') dispose.push(d4)
174
605
  })
175
606
 
176
607
  onUnmounted(() => {
177
608
  window.removeEventListener('resize', checkMobile)
609
+ window.removeEventListener('popstate', handlePopState)
610
+ window.removeEventListener('resize', updateKeyboardHeight)
611
+ if (window.visualViewport) {
612
+ window.visualViewport.removeEventListener('resize', updateKeyboardHeight)
613
+ window.visualViewport.removeEventListener('scroll', updateKeyboardHeight)
614
+ }
178
615
  dispose.forEach(d => d?.())
179
616
  })
180
617
 
@@ -196,7 +633,18 @@ export function useChatLogic() {
196
633
  mobileView,
197
634
  forwardData,
198
635
  imageViewer,
636
+ imageZoom,
637
+ rawMessage,
199
638
  isLoadingHistory,
639
+ forwardDialogVisible,
640
+ imageViewerVisible,
641
+ rawMessageVisible,
642
+ replyingTo,
643
+ userProfile,
644
+ userProfileVisible,
645
+ selectedBotPlatform,
646
+ menu,
647
+ keyboardHeight,
200
648
 
201
649
  // 方法
202
650
  selectBot,
@@ -212,7 +660,19 @@ export function useChatLogic() {
212
660
  goBack,
213
661
  showForward,
214
662
  openImageViewer,
663
+ handleImageWheel,
215
664
  downloadImage,
216
- handleScroll
665
+ handleScroll,
666
+ repeatMessage,
667
+ handlePaste,
668
+ copyToClipboard,
669
+ onBotMenu,
670
+ onChannelMenu,
671
+ onMessageMenu,
672
+ handleMenuAction,
673
+ handleMessageAction,
674
+ showUserProfile,
675
+ inputRef,
676
+ scrollToMessage
217
677
  }
218
678
  }
@@ -95,10 +95,12 @@ export function useChatData() {
95
95
  chatData.value.channels[selfId] = {}
96
96
  }
97
97
  if (!chatData.value.channels[selfId][channelId]) {
98
+ const isDirect = msg.isDirect || channelId.includes('private')
98
99
  chatData.value.channels[selfId][channelId] = {
99
100
  id: channelId,
100
- name: msg.guildName || msg.username || channelId,
101
- type: msg.channelType || 0
101
+ name: isDirect ? `私聊(${msg.username || channelId})` : (msg.guildName || channelId),
102
+ type: msg.channelType || 0,
103
+ isDirect
102
104
  }
103
105
  }
104
106