-import { computed } from 'vue'
+import type {
+ AudioMessage,
+ CardMessage,
+ FaceMessage,
+ FileMessage,
+ ImageMessage,
+ MaterialMessage,
+ QuoteMessage,
+ TextMessage,
+ VideoMessage,
+} from '#/views/im/utils/message';
-import { IconifyIcon as Icon } from '@vben/icons'
-import { formatFileSize } from '@vben/utils'
+import { computed } from 'vue';
-import { CardLineLabel } from '#/views/im/home/components/card'
-import { ImContentType } from '#/views/im/utils/constants'
-import { getClientConversationId } from '#/views/im/utils/db'
-import {
- type AudioMessage,
- type CardMessage,
- type FaceMessage,
- type FileMessage,
- getFileIconInfo,
- type ImageMessage,
- type MaterialMessage,
- parseMessage,
- type QuoteMessage,
- type TextMessage,
- type VideoMessage
-} from '#/views/im/utils/message'
-import { formatSeconds } from '#/views/im/utils/time'
-import { getSenderDisplayName } from '#/views/im/utils/user'
+import { IconifyIcon as Icon } from '@vben/icons';
+import { formatFileSize } from '@vben/utils';
-import { useConversationStore } from '../../../../store/conversationStore'
-import { useMessageStore } from '../../../../store/messageStore'
+import { CardLineLabel } from '#/views/im/home/components/card';
+import { ImContentType } from '#/views/im/utils/constants';
+import { getClientConversationId } from '#/views/im/utils/db';
+import { getFileIconInfo, parseMessage } from '#/views/im/utils/message';
+import { formatSeconds } from '#/views/im/utils/time';
+import { getSenderDisplayName } from '#/views/im/utils/user';
-defineOptions({ name: 'ImReplyPreview' })
+import { useConversationStore } from '../../../../store/conversationStore';
+import { useMessageStore } from '../../../../store/messageStore';
+
+defineOptions({ name: 'ImReplyPreview' });
const props = withDefaults(
defineProps<{
/** 气泡内为 true 支持点击跳转,输入条为 false */
- clickable?: boolean
+ clickable?: boolean;
/** 输入条为 true 显示 × 关闭按钮 */
- closable?: boolean
+ closable?: boolean;
/** 自己发送的气泡为 true,把竖线镜像到右侧,与气泡同侧 */
- mirrored?: boolean
- quote: QuoteMessage
+ mirrored?: boolean;
+ quote: QuoteMessage;
}>(),
{
clickable: false,
closable: false,
- mirrored: false
- }
-)
+ mirrored: false,
+ },
+);
const emit = defineEmits<{
- close: []
- locate: [messageId: number]
-}>()
+ close: [];
+ locate: [messageId: number];
+}>();
-const MAX_TEXT_PREVIEW_LEN = 60 // 文本摘要在引用块里展示的最大字符数
+const MAX_TEXT_PREVIEW_LEN = 60; // 文本摘要在引用块里展示的最大字符数
-const conversationStore = useConversationStore()
-const messageStore = useMessageStore()
+const conversationStore = useConversationStore();
+const messageStore = useMessageStore();
/** 在当前会话消息列表里查找原消息,仅用于实时判断是否已撤回;摘要 / 缩略图都从 quote.content 直接派生 */
const liveMessage = computed(() => {
- const conversation = conversationStore.activeConversation
+ const conversation = conversationStore.activeConversation;
if (!conversation || !props.quote.messageId) {
- return undefined
+ return undefined;
}
return messageStore
- .getMessages(getClientConversationId(conversation.type, conversation.targetId))
- .find((message) => message.id === props.quote.messageId)
-})
+ .getMessages(
+ getClientConversationId(conversation.type, conversation.targetId),
+ )
+ .find((message) => message.id === props.quote.messageId);
+});
/** 命中本地缓存且 type === RECALL 才判定为已撤回;不在缓存的当快照仍有效 */
-const isRecalled = computed(() => liveMessage.value?.type === ImContentType.RECALL)
+const isRecalled = computed(
+ () => liveMessage.value?.type === ImContentType.RECALL,
+);
/** 渲染时实时算,与气泡上方显示名走同一套规则,避免备注变更后引用块陈旧 */
const senderName = computed(() => {
- const conversation = conversationStore.activeConversation
+ const conversation = conversationStore.activeConversation;
if (!conversation) {
- return ''
+ return '';
}
- return getSenderDisplayName(props.quote.senderId, conversation.type, conversation.targetId)
-})
+ return getSenderDisplayName(
+ props.quote.senderId,
+ conversation.type,
+ conversation.targetId,
+ );
+});
/** quote.content 解析一次缓存,让多个 computed 复用,长会话每条引用气泡少一次 JSON.parse */
type AnyQuotePayload = Partial<
@@ -88,49 +96,53 @@ type AnyQuotePayload = Partial<
MaterialMessage &
TextMessage &
VideoMessage
->
-const parsedPayload = computed(() => parseMessage
(props.quote.content))
+>;
+const parsedPayload = computed(() =>
+ parseMessage(props.quote.content),
+);
-const isText = computed(() => props.quote.type === ImContentType.TEXT)
-const isFile = computed(() => props.quote.type === ImContentType.FILE)
-const isVoice = computed(() => props.quote.type === ImContentType.VOICE)
-const isCard = computed(() => props.quote.type === ImContentType.CARD)
-const isFace = computed(() => props.quote.type === ImContentType.FACE)
-const isMaterial = computed(() => props.quote.type === ImContentType.MATERIAL)
+const isText = computed(() => props.quote.type === ImContentType.TEXT);
+const isFile = computed(() => props.quote.type === ImContentType.FILE);
+const isVoice = computed(() => props.quote.type === ImContentType.VOICE);
+const isCard = computed(() => props.quote.type === ImContentType.CARD);
+const isFace = computed(() => props.quote.type === ImContentType.FACE);
+const isMaterial = computed(() => props.quote.type === ImContentType.MATERIAL);
/** 文本超过 MAX_TEXT_PREVIEW_LEN 截断,长内容不撑爆引用块 */
const textPreview = computed(() => {
- const text = parsedPayload.value?.content ?? ''
- return text.length <= MAX_TEXT_PREVIEW_LEN ? text : `${text.slice(0, Math.max(0, MAX_TEXT_PREVIEW_LEN))}…`
-})
+ const text = parsedPayload.value?.content ?? '';
+ return text.length <= MAX_TEXT_PREVIEW_LEN
+ ? text
+ : `${text.slice(0, Math.max(0, MAX_TEXT_PREVIEW_LEN))}…`;
+});
/** 文件 icon:按扩展名挑色,跟主气泡渲染同源 */
-const fileIcon = computed(() => getFileIconInfo(parsedPayload.value?.name))
+const fileIcon = computed(() => getFileIconInfo(parsedPayload.value?.name));
/** 缩略图 URL:图片 / 视频 / 表情贴图 / 频道素材封面从 quote.content 直接取,不依赖本地缓存 */
const thumbnailUrl = computed(() => {
if (isRecalled.value) {
- return undefined
+ return undefined;
}
- const { type } = props.quote
+ const { type } = props.quote;
if (type === ImContentType.IMAGE) {
- return parsedPayload.value?.thumbnailUrl || parsedPayload.value?.url
+ return parsedPayload.value?.thumbnailUrl || parsedPayload.value?.url;
}
if (type === ImContentType.VIDEO || type === ImContentType.MATERIAL) {
- return parsedPayload.value?.coverUrl
+ return parsedPayload.value?.coverUrl;
}
if (type === ImContentType.FACE) {
- return parsedPayload.value?.url
+ return parsedPayload.value?.url;
}
- return undefined
-})
+ return undefined;
+});
/** 仅 clickable 且未撤回时触发跳转 */
function onClick() {
if (!props.clickable || isRecalled.value) {
- return
+ return;
}
- emit('locate', props.quote.messageId)
+ emit('locate', props.quote.messageId);
}
@@ -154,9 +166,11 @@ function onClick() {
? 'pl-1 pr-2 border-r-2 border-r-solid border-r-[var(--ant-color-border)]'
: 'pl-2 pr-1 border-l-2 border-l-solid border-l-[var(--ant-color-border)]',
{
- 'cursor-pointer hover:text-[var(--ant-color-text)]': clickable && !isRecalled,
- 'hover:bg-[var(--ant-color-fill-secondary)]': (clickable && !isRecalled) || closable
- }
+ 'cursor-pointer hover:text-[var(--ant-color-text)]':
+ clickable && !isRecalled,
+ 'hover:bg-[var(--ant-color-fill-secondary)]':
+ (clickable && !isRecalled) || closable,
+ },
]"
@click="onClick"
>
@@ -166,11 +180,18 @@ function onClick() {
原消息已撤回
- {{ textPreview }}
+ {{
+ textPreview
+ }}
-
+
{{ parsedPayload.name }}
@@ -208,7 +229,10 @@ function onClick() {
[频道]
-
+
{{ parsedPayload.title }}
diff --git a/apps/web-antd/src/views/im/home/pages/conversation/components/message/tip-segments.vue b/apps/web-antd/src/views/im/home/pages/conversation/components/message/tip-segments.vue
index 19b7be2b1..c9cc9b4ca 100644
--- a/apps/web-antd/src/views/im/home/pages/conversation/components/message/tip-segments.vue
+++ b/apps/web-antd/src/views/im/home/pages/conversation/components/message/tip-segments.vue
@@ -5,31 +5,31 @@
- text 段原样输出
-->
@@ -40,7 +40,9 @@ function handleMentionClick(
class="text-[#576b95]"
:class="{ 'cursor-pointer hover:underline': isClickableMention(segment) }"
@click.stop="handleMentionClick(segment, $event)"
- >{{ segment.text }}
+ >
+ {{ segment.text }}
+
{{ segment.text }}
+ >
+ {{ segment.text }}
+
{{ segment.text }}
diff --git a/apps/web-antd/src/views/im/home/pages/conversation/index.vue b/apps/web-antd/src/views/im/home/pages/conversation/index.vue
index b18421510..a0a2415e8 100644
--- a/apps/web-antd/src/views/im/home/pages/conversation/index.vue
+++ b/apps/web-antd/src/views/im/home/pages/conversation/index.vue
@@ -1,64 +1,74 @@
-
+
-
+
@@ -248,7 +281,11 @@ watch(() => uiStore.nextUnreadJumpNonce, jumpToNextUnread)
}}
@@ -276,6 +313,9 @@ watch(() => uiStore.nextUnreadJumpNonce, jumpToNextUnread)
-
+
diff --git a/apps/web-antd/src/views/im/home/store/channelStore.ts b/apps/web-antd/src/views/im/home/store/channelStore.ts
index 3f059ca0c..70a3a6a18 100644
--- a/apps/web-antd/src/views/im/home/store/channelStore.ts
+++ b/apps/web-antd/src/views/im/home/store/channelStore.ts
@@ -1,12 +1,12 @@
-import type { ImManagerChannelApi } from '#/api/im/manager/channel'
+import type { ImManagerChannelApi } from '#/api/im/manager/channel';
-import { acceptHMRUpdate, defineStore } from 'pinia'
+import { acceptHMRUpdate, defineStore } from 'pinia';
-import { getSimpleChannelList } from '#/api/im/manager/channel'
+import { getSimpleChannelList } from '#/api/im/manager/channel';
-import { ImConversationType } from '../../utils/constants'
-import { getDb } from '../../utils/db'
-import { useConversationStore } from './conversationStore'
+import { ImConversationType } from '../../utils/constants';
+import { getDb } from '../../utils/db';
+import { useConversationStore } from './conversationStore';
/**
* IM 频道 Store
@@ -17,13 +17,13 @@ import { useConversationStore } from './conversationStore'
export const useChannelStore = defineStore('imChannelStore', {
state: () => ({
channels: [] as ImManagerChannelApi.Channel[],
- loaded: false
+ loaded: false,
}),
getters: {
getChannel(state): (id: number) => ImManagerChannelApi.Channel | undefined {
- return (id: number) => state.channels.find((c) => c.id === id)
- }
+ return (id: number) => state.channels.find((c) => c.id === id);
+ },
},
actions: {
@@ -32,15 +32,16 @@ export const useChannelStore = defineStore('imChannelStore', {
/** 从 IndexedDB 恢复频道列表 */
async loadChannelList(): Promise {
try {
- const cached = await getDb().getAll('channels')
+ const cached =
+ await getDb().getAll('channels');
if (!cached || cached.length === 0) {
- return false
+ return false;
}
- this.channels = cached
- return true
+ this.channels = cached;
+ return true;
} catch (error) {
- console.warn('[IM channelStore] 本地频道缓存读取失败', error)
- return false
+ console.warn('[IM channelStore] 本地频道缓存读取失败', error);
+ return false;
}
},
@@ -48,13 +49,15 @@ export const useChannelStore = defineStore('imChannelStore', {
saveChannelList(): void {
void getDb()
.transaction(['channels'], 'readwrite', async (tx) => {
- const db = getDb()
- await db.clearStore('channels', tx)
+ const db = getDb();
+ await db.clearStore('channels', tx);
for (const channel of this.channels) {
- await db.put('channels', channel, tx)
+ await db.put('channels', channel, tx);
}
})
- .catch((error) => console.warn('[IM channelStore] 本地频道缓存写入失败', error))
+ .catch((error) =>
+ console.warn('[IM channelStore] 本地频道缓存写入失败', error),
+ );
},
// ==================== 远端拉取 ====================
@@ -62,47 +65,51 @@ export const useChannelStore = defineStore('imChannelStore', {
/** 拉取启用的频道精简列表;成功后回填会话列表已有的频道 name / avatar,覆盖 IDB 旧占位 */
async fetchChannelList(force = false) {
if (this.loaded && !force) {
- return
+ return;
}
try {
- this.channels = (await getSimpleChannelList()) || []
- this.loaded = true
- this.syncChannelConversationMetadata()
- this.saveChannelList()
+ this.channels = (await getSimpleChannelList()) || [];
+ this.loaded = true;
+ this.syncChannelConversationMetadata();
+ this.saveChannelList();
} catch (error) {
- console.warn('[IM channelStore] fetchChannelList 失败', error)
+ console.warn('[IM channelStore] fetchChannelList 失败', error);
}
},
/** 用最新的频道信息覆盖已有 CHANNEL 会话的 name / avatar */
syncChannelConversationMetadata() {
- const conversationStore = useConversationStore()
- const indexed = new Map(this.channels.map((c) => [c.id, c]))
+ const conversationStore = useConversationStore();
+ const indexed = new Map(this.channels.map((c) => [c.id, c]));
conversationStore.conversations.forEach((conversation) => {
if (conversation.type !== ImConversationType.CHANNEL) {
- return
+ return;
}
- const channel = indexed.get(conversation.targetId)
+ const channel = indexed.get(conversation.targetId);
if (!channel) {
- return
+ return;
}
- conversationStore.updateConversation(ImConversationType.CHANNEL, conversation.targetId, {
- name: channel.name,
- avatar: channel.avatar
- })
- })
+ conversationStore.updateConversation(
+ ImConversationType.CHANNEL,
+ conversation.targetId,
+ {
+ name: channel.name,
+ avatar: channel.avatar,
+ },
+ );
+ });
},
/** 清空频道内存 */
clear() {
- this.channels = []
- this.loaded = false
- }
- }
-})
+ this.channels = [];
+ this.loaded = false;
+ },
+ },
+});
if (import.meta.hot) {
- import.meta.hot.accept(acceptHMRUpdate(useChannelStore, import.meta.hot))
+ import.meta.hot.accept(acceptHMRUpdate(useChannelStore, import.meta.hot));
}
-export const useChannelStoreWithOut = () => useChannelStore()
+export const useChannelStoreWithOut = () => useChannelStore();
diff --git a/apps/web-antd/src/views/im/home/store/conversationStore.ts b/apps/web-antd/src/views/im/home/store/conversationStore.ts
index a799ffc60..88a1930ab 100644
--- a/apps/web-antd/src/views/im/home/store/conversationStore.ts
+++ b/apps/web-antd/src/views/im/home/store/conversationStore.ts
@@ -1,76 +1,77 @@
+import type { DbTransaction } from '../../utils/db';
import type {
Conversation,
ConversationDO,
ConversationRead,
ConversationReadDO,
- MessageDO
-} from '../types'
+ MessageDO,
+} from '../types';
-import type { ImConversationReadApi } from '#/api/im/conversation/read'
+import type { ImConversationReadApi } from '#/api/im/conversation/read';
-import { acceptHMRUpdate, defineStore } from 'pinia'
+import { acceptHMRUpdate, defineStore } from 'pinia';
-import { pullMyConversationReadList as apiPullMyConversationReadList } from '#/api/im/conversation/read'
-import { getCurrentUserId } from '#/views/im/utils/auth'
+import { pullMyConversationReadList as apiPullMyConversationReadList } from '#/api/im/conversation/read';
+import { getCurrentUserId } from '#/views/im/utils/auth';
-import { CONVERSATION_RECENT_FORWARD_MAX } from '../../utils/config'
+import { CONVERSATION_RECENT_FORWARD_MAX } from '../../utils/config';
import {
ImConversationType,
ImMessageReceiptStatus,
ImMessageStatus,
- isNormalMessage
-} from '../../utils/constants'
-import { type DbTransaction, getClientConversationId, getDb, StorageKeys } from '../../utils/db'
-import { runIncrementalPull } from '../../utils/pull'
-import { useMessageStore } from './messageStore'
+ isNormalMessage,
+} from '../../utils/constants';
+import { getClientConversationId, getDb, StorageKeys } from '../../utils/db';
+import { runIncrementalPull } from '../../utils/pull';
+import { useMessageStore } from './messageStore';
-const PERSIST_DRAFT_DEBOUNCE_MS = 500
-const pendingDraftConversations = new Set()
+const PERSIST_DRAFT_DEBOUNCE_MS = 500;
+const pendingDraftConversations = new Set();
/** 创建会话读位置记录 */
function createConversationRead(
type: number,
targetId: number,
- messageId: number
+ messageId: number,
): ConversationRead {
return {
conversationType: type,
targetId,
messageId,
- updateTime: Date.now()
- }
+ updateTime: Date.now(),
+ };
}
/** 创建草稿保存防抖函数 */
function createDraftDebounce(fn: () => void, wait: number) {
- let timer: ReturnType | undefined
+ let timer: ReturnType | undefined;
const run = () => {
if (timer) {
- clearTimeout(timer)
- timer = undefined
+ clearTimeout(timer);
+ timer = undefined;
}
- fn()
- }
+ fn();
+ };
const debounced = () => {
if (timer) {
- clearTimeout(timer)
+ clearTimeout(timer);
}
- timer = setTimeout(run, wait)
- }
+ timer = setTimeout(run, wait);
+ };
debounced.cancel = () => {
if (timer) {
- clearTimeout(timer)
- timer = undefined
+ clearTimeout(timer);
+ timer = undefined;
}
- }
- debounced.flush = run
- return debounced
+ };
+ debounced.flush = run;
+ return debounced;
}
/** 会话转 IndexedDB 记录 */
function toConversationDO(conversation: Conversation): ConversationDO {
- const draft = conversation.draft
+ const draft = conversation.draft;
return {
targetId: conversation.targetId,
type: conversation.type,
@@ -93,18 +94,20 @@ function toConversationDO(conversation: Conversation): ConversationDO {
silent: conversation.silent,
atMe: conversation.atMe,
atAll: conversation.atAll,
- draft: draft ? { ...draft, reply: draft.reply ? { ...draft.reply } : undefined } : undefined,
- clientConversationId: getClientConversationId(conversation.type, conversation.targetId)
- }
+ draft: draft
+ ? { ...draft, reply: draft.reply ? { ...draft.reply } : undefined }
+ : undefined,
+ clientConversationId: getClientConversationId(
+ conversation.type,
+ conversation.targetId,
+ ),
+ };
}
/** IndexedDB 记录转会话 */
function fromConversationDO(conversation: ConversationDO): Conversation {
- const {
- clientConversationId: _clientConversationId,
- ...rest
- } = conversation
- return rest
+ const { clientConversationId: _clientConversationId, ...rest } = conversation;
+ return rest;
}
/** 会话读位置转 IndexedDB 记录 */
@@ -114,26 +117,31 @@ function toConversationReadDO(record: ConversationRead): ConversationReadDO {
targetId: record.targetId,
messageId: record.messageId,
updateTime: record.updateTime,
- clientConversationId: getClientConversationId(record.conversationType, record.targetId)
- }
+ clientConversationId: getClientConversationId(
+ record.conversationType,
+ record.targetId,
+ ),
+ };
}
/** IndexedDB 记录转会话读位置 */
function fromConversationReadDO(record: ConversationReadDO): ConversationRead {
- const { clientConversationId: _clientConversationId, ...rest } = record
- return rest
+ const { clientConversationId: _clientConversationId, ...rest } = record;
+ return rest;
}
/** 是否为有效会话读位置 */
-function isValidConversationReadRecord(record: ImConversationReadApi.ConversationReadRespVO): boolean {
- return !!record.conversationType && !!record.targetId && !!record.messageId
+function isValidConversationReadRecord(
+ record: ImConversationReadApi.ConversationReadRespVO,
+): boolean {
+ return !!record.conversationType && !!record.targetId && !!record.messageId;
}
/** 获取对方普通消息最大编号 */
function getMaxIncomingNormalMessageId(
- messages: Array>
+ messages: Array>,
): number {
- let maxMessageId = 0
+ let maxMessageId = 0;
for (const message of messages) {
if (
message.id &&
@@ -142,10 +150,10 @@ function getMaxIncomingNormalMessageId(
message.status !== ImMessageStatus.RECALL &&
message.id > maxMessageId
) {
- maxMessageId = message.id
+ maxMessageId = message.id;
}
}
- return maxMessageId
+ return maxMessageId;
}
export const useConversationStore = defineStore('imConversationStore', {
@@ -154,7 +162,7 @@ export const useConversationStore = defineStore('imConversationStore', {
conversationReads: {} as Record, // 会话读位置
activeConversation: null as Conversation | null, // 当前激活的会话
loading: false, // 是否正在批量加载
- recentForwardConversationKeys: [] as string[] // 最近转发会话 key 列表
+ recentForwardConversationKeys: [] as string[], // 最近转发会话 key 列表
}),
getters: {
@@ -163,20 +171,23 @@ export const useConversationStore = defineStore('imConversationStore', {
return state.conversations
.filter((conversation) => !conversation.deleted)
.toSorted((a, b) => {
- const aTop = a.top ? 1 : 0
- const bTop = b.top ? 1 : 0
+ const aTop = a.top ? 1 : 0;
+ const bTop = b.top ? 1 : 0;
if (aTop !== bTop) {
- return bTop - aTop
+ return bTop - aTop;
}
- return (b.lastSendTime || 0) - (a.lastSendTime || 0)
- })
+ return (b.lastSendTime || 0) - (a.lastSendTime || 0);
+ });
},
/** 未读总数 */
getTotalUnreadCount(state): number {
return state.conversations
.filter((conversation) => !conversation.deleted && !conversation.silent)
- .reduce((sum, conversation) => sum + (conversation.unreadCount || 0), 0)
+ .reduce(
+ (sum, conversation) => sum + (conversation.unreadCount || 0),
+ 0,
+ );
},
/** 查找会话 */
@@ -184,47 +195,60 @@ export const useConversationStore = defineStore('imConversationStore', {
(state) =>
(type: number, targetId: number): Conversation | undefined =>
state.conversations.find(
- (conversation) => conversation.type === type && conversation.targetId === targetId
+ (conversation) =>
+ conversation.type === type && conversation.targetId === targetId,
),
/** 查找会话读位置 */
getConversationRead:
(state) =>
(type: number, targetId: number): ConversationRead | undefined =>
- state.conversationReads[getClientConversationId(type, targetId)]
+ state.conversationReads[getClientConversationId(type, targetId)],
},
actions: {
/** 加载会话 */
async loadConversationList() {
// 1. 清理旧账号内存
- const userId = getCurrentUserId()
+ const userId = getCurrentUserId();
if (!userId) {
- this.clear()
- return
+ this.clear();
+ return;
}
const previousActiveKey = this.activeConversation
- ? getClientConversationId(this.activeConversation.type, this.activeConversation.targetId)
- : null
- this.clear()
+ ? getClientConversationId(
+ this.activeConversation.type,
+ this.activeConversation.targetId,
+ )
+ : null;
+ this.clear();
// 2. 从 IndexedDB 读取会话和轻量设置
- const db = getDb()
+ const db = getDb();
const [conversations, conversationReads, recent] = await Promise.all([
db.getAll('conversations'),
db.getAll('conversationReads'),
- db.getSetting(StorageKeys.settings.recentForwardConversationKeys)
- ])
- const nextConversationReads: Record = {}
+ db.getSetting(
+ StorageKeys.settings.recentForwardConversationKeys,
+ ),
+ ]);
+ const nextConversationReads: Record = {};
for (const record of conversationReads) {
- const item = fromConversationReadDO(record)
- nextConversationReads[getClientConversationId(item.conversationType, item.targetId)] = item
+ const item = fromConversationReadDO(record);
+ nextConversationReads[
+ getClientConversationId(item.conversationType, item.targetId)
+ ] = item;
}
- const nextConversations = conversations.map((conversation) => fromConversationDO(conversation))
- this.conversationReads = nextConversationReads
- await this.applyLocalConversationReads(nextConversations)
- this.conversations = nextConversations
+ const nextConversations = conversations.map((conversation) =>
+ fromConversationDO(conversation),
+ );
+ this.conversationReads = nextConversationReads;
+ await this.applyLocalConversationReads(nextConversations);
+ this.conversations = nextConversations;
if (Array.isArray(recent)) {
- this.recentForwardConversationKeys = recent.slice(0, CONVERSATION_RECENT_FORWARD_MAX)
+ this.recentForwardConversationKeys = recent.slice(
+ 0,
+ CONVERSATION_RECENT_FORWARD_MAX,
+ );
}
// 3. 恢复当前激活会话
if (previousActiveKey) {
@@ -232,206 +256,250 @@ export const useConversationStore = defineStore('imConversationStore', {
this.conversations.find(
(conversation) =>
!conversation.deleted &&
- getClientConversationId(conversation.type, conversation.targetId) ===
- previousActiveKey
- ) ?? null
+ getClientConversationId(
+ conversation.type,
+ conversation.targetId,
+ ) === previousActiveKey,
+ ) ?? null;
}
},
/** 清空会话内存 */
clear() {
- saveDraftConversationListDebounced.cancel()
- pendingDraftConversations.clear()
- this.conversations = []
- this.conversationReads = {}
- this.activeConversation = null
- this.recentForwardConversationKeys = []
+ saveDraftConversationListDebounced.cancel();
+ pendingDraftConversations.clear();
+ this.conversations = [];
+ this.conversationReads = {};
+ this.activeConversation = null;
+ this.recentForwardConversationKeys = [];
},
/** 持久化会话读位置 */
async saveConversationReadRecord(
target: ConversationRead | ConversationRead[] | null | undefined,
- tx?: DbTransaction
+ tx?: DbTransaction,
): Promise {
- let targets: ConversationRead[] = []
+ let targets: ConversationRead[] = [];
if (Array.isArray(target)) {
- targets = target
+ targets = target;
} else if (target) {
- targets = [target]
+ targets = [target];
}
- const records = targets.map((record) => toConversationReadDO(record))
+ const records = targets.map((record) => toConversationReadDO(record));
if (records.length === 0) {
- return
+ return;
}
- const db = getDb()
+ const db = getDb();
if (tx) {
for (const record of records) {
- await db.put('conversationReads', record, tx)
+ await db.put('conversationReads', record, tx);
}
- return
+ return;
}
await db.transaction(['conversationReads'], 'readwrite', async (tx) => {
for (const record of records) {
- await db.put('conversationReads', record, tx)
+ await db.put('conversationReads', record, tx);
}
- })
+ });
},
/** 应用本地会话读位置 */
async applyLocalConversationReads(conversations?: Conversation[]) {
- const targetConversations = conversations || this.conversations
- const changedConversations: Conversation[] = []
+ const targetConversations = conversations || this.conversations;
+ const changedConversations: Conversation[] = [];
for (const conversation of targetConversations) {
- const record = this.getConversationRead(conversation.type, conversation.targetId)
+ const record = this.getConversationRead(
+ conversation.type,
+ conversation.targetId,
+ );
if (!record) {
- continue
+ continue;
}
if (this.applyReadToConversation(conversation, record.messageId)) {
- changedConversations.push(conversation)
- continue
+ changedConversations.push(conversation);
+ continue;
}
- if (conversation.unreadCount === 0 && !conversation.atMe && !conversation.atAll) {
- continue
+ if (
+ conversation.unreadCount === 0 &&
+ !conversation.atMe &&
+ !conversation.atAll
+ ) {
+ continue;
}
const messages = await getDb().getAllByIndex(
'messages',
'clientConversationId',
- getClientConversationId(conversation.type, conversation.targetId)
- )
- const maxIncomingMessageId = getMaxIncomingNormalMessageId(messages)
- if (maxIncomingMessageId > 0 && maxIncomingMessageId <= record.messageId) {
- conversation.unreadCount = 0
- conversation.atMe = false
- conversation.atAll = false
- changedConversations.push(conversation)
+ getClientConversationId(conversation.type, conversation.targetId),
+ );
+ const maxIncomingMessageId = getMaxIncomingNormalMessageId(messages);
+ if (
+ maxIncomingMessageId > 0 &&
+ maxIncomingMessageId <= record.messageId
+ ) {
+ conversation.unreadCount = 0;
+ conversation.atMe = false;
+ conversation.atAll = false;
+ changedConversations.push(conversation);
}
}
if (changedConversations.length > 0) {
- await this.saveConversationRecord(changedConversations)
+ await this.saveConversationRecord(changedConversations);
}
},
/** 判断消息是否已被会话读位置覆盖 */
isMessageCoveredByReadPosition(
conversation: Pick,
- message?: null | { id?: number }
+ message?: null | { id?: number },
): boolean {
if (!message?.id) {
- return false
+ return false;
}
- const record = this.getConversationRead(conversation.type, conversation.targetId)
- return !!record && message.id <= record.messageId
+ const record = this.getConversationRead(
+ conversation.type,
+ conversation.targetId,
+ );
+ return !!record && message.id <= record.messageId;
},
/** 判断会话读位置是否覆盖消息编号 */
- isReadPositionCovered(type: number, targetId: number, messageId?: number): boolean {
+ isReadPositionCovered(
+ type: number,
+ targetId: number,
+ messageId?: number,
+ ): boolean {
if (!messageId) {
- return false
+ return false;
}
- const record = this.getConversationRead(type, targetId)
- return !!record && record.messageId >= messageId
+ const record = this.getConversationRead(type, targetId);
+ return !!record && record.messageId >= messageId;
},
/** 判断服务端已读位置是否覆盖消息编号 */
- isReportedReadPositionCovered(type: number, targetId: number, messageId?: number): boolean {
+ isReportedReadPositionCovered(
+ type: number,
+ targetId: number,
+ messageId?: number,
+ ): boolean {
if (!messageId) {
- return false
+ return false;
}
- const conversation = this.getConversation(type, targetId)
- return (conversation?.reportedReadMessageId || 0) >= messageId
+ const conversation = this.getConversation(type, targetId);
+ return (conversation?.reportedReadMessageId || 0) >= messageId;
},
/** 应用读位置到会话 */
- applyReadToConversation(conversation: Conversation, messageId: number): boolean {
- if (!conversation.lastMessageId || conversation.lastMessageId > messageId) {
- return false
+ applyReadToConversation(
+ conversation: Conversation,
+ messageId: number,
+ ): boolean {
+ if (
+ !conversation.lastMessageId ||
+ conversation.lastMessageId > messageId
+ ) {
+ return false;
}
- if (conversation.unreadCount === 0 && !conversation.atMe && !conversation.atAll) {
- return false
+ if (
+ conversation.unreadCount === 0 &&
+ !conversation.atMe &&
+ !conversation.atAll
+ ) {
+ return false;
}
- conversation.unreadCount = 0
- conversation.atMe = false
- conversation.atAll = false
- return true
+ conversation.unreadCount = 0;
+ conversation.atMe = false;
+ conversation.atAll = false;
+ return true;
},
/** 应用会话读位置 */
async applyConversationReadList(
records: ImConversationReadApi.ConversationReadRespVO[],
- isActive?: () => boolean
+ isActive?: () => boolean,
): Promise {
if (records.length === 0) {
- return
+ return;
}
- const changedReads = new Map()
- const changedConversations = new Map()
- const changedMessages = new Map()
- const db = getDb()
- const messageStore = useMessageStore()
+ const changedReads = new Map();
+ const changedConversations = new Map();
+ const changedMessages = new Map();
+ const db = getDb();
+ const messageStore = useMessageStore();
// 1. 按读位置更新会话未读和频道已读态
for (const record of records) {
if (isActive && !isActive()) {
- return
+ return;
}
if (!isValidConversationReadRecord(record)) {
- continue
+ continue;
}
const clientConversationId = getClientConversationId(
record.conversationType,
- record.targetId
- )
- let storedMessages: MessageDO[] | undefined
+ record.targetId,
+ );
+ let storedMessages: MessageDO[] | undefined;
const getStoredMessages = async () => {
if (!storedMessages) {
storedMessages = await db.getAllByIndex(
'messages',
'clientConversationId',
- clientConversationId
- )
+ clientConversationId,
+ );
}
- return storedMessages
- }
- const current = this.conversationReads[clientConversationId]
- const messageId = Math.max(record.messageId, current?.messageId || 0)
- const conversation = this.getConversation(record.conversationType, record.targetId)
- if (conversation && record.messageId > (conversation.reportedReadMessageId || 0)) {
- conversation.reportedReadMessageId = record.messageId
- changedConversations.set(clientConversationId, conversation)
+ return storedMessages;
+ };
+ const current = this.conversationReads[clientConversationId];
+ const messageId = Math.max(record.messageId, current?.messageId || 0);
+ const conversation = this.getConversation(
+ record.conversationType,
+ record.targetId,
+ );
+ if (
+ conversation &&
+ record.messageId > (conversation.reportedReadMessageId || 0)
+ ) {
+ conversation.reportedReadMessageId = record.messageId;
+ changedConversations.set(clientConversationId, conversation);
}
if (!current || messageId > current.messageId) {
const next = {
conversationType: record.conversationType,
targetId: record.targetId,
messageId,
- updateTime: record.updateTime
- }
- this.conversationReads[clientConversationId] = next
- changedReads.set(clientConversationId, next)
+ updateTime: record.updateTime,
+ };
+ this.conversationReads[clientConversationId] = next;
+ changedReads.set(clientConversationId, next);
}
- if (conversation && this.applyReadToConversation(conversation, messageId)) {
- changedConversations.set(clientConversationId, conversation)
+ if (
+ conversation &&
+ this.applyReadToConversation(conversation, messageId)
+ ) {
+ changedConversations.set(clientConversationId, conversation);
} else if (conversation) {
- const maxIncomingMessageId = getMaxIncomingNormalMessageId(await getStoredMessages())
+ const maxIncomingMessageId = getMaxIncomingNormalMessageId(
+ await getStoredMessages(),
+ );
if (maxIncomingMessageId > 0 && maxIncomingMessageId <= messageId) {
- conversation.unreadCount = 0
- conversation.atMe = false
- conversation.atAll = false
- changedConversations.set(clientConversationId, conversation)
+ conversation.unreadCount = 0;
+ conversation.atMe = false;
+ conversation.atAll = false;
+ changedConversations.set(clientConversationId, conversation);
}
}
if (record.conversationType !== ImConversationType.CHANNEL) {
- continue
+ continue;
}
- const memoryMessages = messageStore.getMessages(clientConversationId)
+ const memoryMessages = messageStore.getMessages(clientConversationId);
for (const message of memoryMessages) {
if (
message.id &&
message.id <= messageId &&
message.receiptStatus !== ImMessageReceiptStatus.DONE
) {
- message.receiptStatus = ImMessageReceiptStatus.DONE
+ message.receiptStatus = ImMessageReceiptStatus.DONE;
}
}
for (const message of await getStoredMessages()) {
@@ -440,8 +508,8 @@ export const useConversationStore = defineStore('imConversationStore', {
message.id <= messageId &&
message.receiptStatus !== ImMessageReceiptStatus.DONE
) {
- message.receiptStatus = ImMessageReceiptStatus.DONE
- changedMessages.set(message.messageKey, message)
+ message.receiptStatus = ImMessageReceiptStatus.DONE;
+ changedMessages.set(message.messageKey, message);
}
}
}
@@ -452,32 +520,36 @@ export const useConversationStore = defineStore('imConversationStore', {
changedConversations.size === 0 &&
changedMessages.size === 0
) {
- return
+ return;
}
if (isActive && !isActive()) {
- return
+ return;
}
- const stores: Array<'conversationReads' | 'conversations' | 'messages'> = []
+ const stores: Array<'conversationReads' | 'conversations' | 'messages'> =
+ [];
if (changedReads.size > 0) {
- stores.push('conversationReads')
+ stores.push('conversationReads');
}
if (changedConversations.size > 0) {
- stores.push('conversations')
+ stores.push('conversations');
}
if (changedMessages.size > 0) {
- stores.push('messages')
+ stores.push('messages');
}
await db.transaction(stores, 'readwrite', async (tx) => {
if (changedReads.size > 0) {
- await this.saveConversationReadRecord([...changedReads.values()], tx)
+ await this.saveConversationReadRecord([...changedReads.values()], tx);
}
if (changedConversations.size > 0) {
- await this.saveConversationRecord([...changedConversations.values()], tx)
+ await this.saveConversationRecord(
+ [...changedConversations.values()],
+ tx,
+ );
}
for (const message of changedMessages.values()) {
- await db.put('messages', message, tx)
+ await db.put('messages', message, tx);
}
- })
+ });
},
/** 增量拉取会话读位置 */
@@ -487,103 +559,114 @@ export const useConversationStore = defineStore('imConversationStore', {
apiPullMyConversationReadList,
async (records) => {
if (isActive && !isActive()) {
- return false
+ return false;
}
- await this.applyConversationReadList(records, isActive)
+ await this.applyConversationReadList(records, isActive);
if (isActive && !isActive()) {
- return false
+ return false;
}
- return true
+ return true;
},
- isActive
- )
+ isActive,
+ );
},
/** 执行会话记录持久化 */
async saveConversationRecord(
target: Conversation | Conversation[] | null | undefined,
- tx?: DbTransaction
+ tx?: DbTransaction,
): Promise {
- const db = getDb()
- const conversations = (Array.isArray(target) ? target : (target ? [target] : [])).map(
- (conversation) => toConversationDO(conversation)
- )
+ const db = getDb();
+ const conversations =
+ // oxlint-disable-next-line unicorn/no-nested-ternary
+ (Array.isArray(target) ? target : target ? [target] : []).map(
+ (conversation) => toConversationDO(conversation),
+ );
if (conversations.length === 0) {
- return
+ return;
}
if (tx) {
for (const conversation of conversations) {
- await db.put('conversations', conversation, tx)
+ await db.put('conversations', conversation, tx);
}
- return
+ return;
}
await db.transaction(['conversations'], 'readwrite', async (tx) => {
for (const conversation of conversations) {
- await db.put('conversations', conversation, tx)
+ await db.put('conversations', conversation, tx);
}
- })
+ });
},
/** 持久化单个会话 */
- saveConversation(conversation: Conversation | null | undefined, tx?: DbTransaction): void {
+ saveConversation(
+ conversation: Conversation | null | undefined,
+ tx?: DbTransaction,
+ ): void {
if (!conversation) {
- return
+ return;
}
void this.saveConversationRecord(conversation, tx).catch((error) =>
- console.warn('[IM conversationStore] 会话写入失败', error)
- )
+ console.warn('[IM conversationStore] 会话写入失败', error),
+ );
},
/** 持久化会话列表 */
- saveConversationList(conversations?: Conversation[] | null, tx?: DbTransaction): void {
+ saveConversationList(
+ conversations?: Conversation[] | null,
+ tx?: DbTransaction,
+ ): void {
if (this.loading && !tx) {
- return
+ return;
}
- void this.saveConversationRecord(conversations || this.conversations, tx).catch((error) =>
- console.warn('[IM conversationStore] 会话写入失败', error)
- )
+ void this.saveConversationRecord(
+ conversations || this.conversations,
+ tx,
+ ).catch((error) =>
+ console.warn('[IM conversationStore] 会话写入失败', error),
+ );
},
/** 确保会话存在 */
ensureConversation(info: {
- avatar: string
- name: string
- silent?: boolean
- targetId: number
- type: number
+ avatar: string;
+ name: string;
+ silent?: boolean;
+ targetId: number;
+ type: number;
}): Conversation {
// 1. 创建不存在的会话
- let conversation = this.getConversation(info.type, info.targetId)
+ let conversation = this.getConversation(info.type, info.targetId);
if (!conversation) {
conversation = this.createEmptyConversation(
info.type,
info.targetId,
info.name,
info.avatar,
- info.silent
- )
- this.conversations.unshift(conversation)
+ info.silent,
+ );
+ this.conversations.unshift(conversation);
} else if (conversation.deleted) {
// 2. 恢复软删除会话
- conversation.deleted = false
- conversation.name = info.name || conversation.name
- conversation.avatar = info.avatar || conversation.avatar
+ conversation.deleted = false;
+ conversation.name = info.name || conversation.name;
+ conversation.avatar = info.avatar || conversation.avatar;
if (info.silent !== undefined) {
- conversation.silent = info.silent
+ conversation.silent = info.silent;
}
} else {
// 3. 同步会话展示元数据
if (info.name) {
- conversation.name = info.name
+ conversation.name = info.name;
}
if (info.avatar) {
- conversation.avatar = info.avatar
+ conversation.avatar = info.avatar;
}
if (info.silent !== undefined) {
- conversation.silent = info.silent
+ conversation.silent = info.silent;
}
}
- return conversation
+ return conversation;
},
/** 打开或创建会话 */
@@ -592,7 +675,7 @@ export const useConversationStore = defineStore('imConversationStore', {
type: number,
name: string,
avatar: string,
- options?: { silent?: boolean }
+ options?: { silent?: boolean },
): Conversation {
// 1. 确保会话在列表中
const conversation = this.ensureConversation({
@@ -600,23 +683,23 @@ export const useConversationStore = defineStore('imConversationStore', {
targetId,
name,
avatar,
- silent: options?.silent
- })
+ silent: options?.silent,
+ });
// 2. 激活会话并保存
- this.setActiveConversation(conversation)
- this.saveConversation(conversation)
- return conversation
+ this.setActiveConversation(conversation);
+ this.saveConversation(conversation);
+ return conversation;
},
/** 设置当前会话 */
setActiveConversation(conversation: Conversation | null) {
- this.activeConversation = conversation
+ this.activeConversation = conversation;
if (!conversation) {
- return
+ return;
}
// 懒加载消息并保存会话摘要
- void useMessageStore().ensureConversationMessageListLoaded(conversation)
- this.saveConversation(conversation)
+ void useMessageStore().ensureConversationMessageListLoaded(conversation);
+ this.saveConversation(conversation);
},
/** 创建空会话 */
@@ -625,7 +708,7 @@ export const useConversationStore = defineStore('imConversationStore', {
targetId: number,
name: string,
avatar: string,
- silent = false
+ silent = false,
): Conversation {
return {
targetId,
@@ -639,85 +722,94 @@ export const useConversationStore = defineStore('imConversationStore', {
top: false,
silent,
atMe: false,
- atAll: false
- }
+ atAll: false,
+ };
},
/** 设置置顶 */
setConversationTop(type: number, targetId: number, top: boolean) {
- const conversation = this.getConversation(type, targetId)
+ const conversation = this.getConversation(type, targetId);
if (!conversation) {
- return
+ return;
}
- conversation.top = top
- this.saveConversation(conversation)
+ conversation.top = top;
+ this.saveConversation(conversation);
},
/** 设置免打扰 */
setConversationSilent(type: number, targetId: number, silent: boolean) {
- const conversation = this.getConversation(type, targetId)
+ const conversation = this.getConversation(type, targetId);
if (!conversation) {
- return
+ return;
}
- conversation.silent = silent
- this.saveConversation(conversation)
+ conversation.silent = silent;
+ this.saveConversation(conversation);
},
/** 删除会话 */
removeConversation(type: number, targetId: number) {
// 1. 标记会话删除
- const conversation = this.getConversation(type, targetId)
+ const conversation = this.getConversation(type, targetId);
if (!conversation) {
- return
+ return;
}
if (this.activeConversation === conversation) {
- this.activeConversation = null
+ this.activeConversation = null;
}
- conversation.deleted = true
+ conversation.deleted = true;
// 2. 删除会话关联的消息和草稿
- useMessageStore().deleteConversationMessageList(type, targetId)
- this.clearConversationDraft(conversation)
- this.saveConversation(conversation)
+ useMessageStore().deleteConversationMessageList(type, targetId);
+ this.clearConversationDraft(conversation);
+ this.saveConversation(conversation);
},
/** 删除私聊会话 */
removePrivateConversation(friendId: number) {
- this.removeConversation(ImConversationType.PRIVATE, friendId)
+ this.removeConversation(ImConversationType.PRIVATE, friendId);
},
/** 删除群聊会话 */
removeGroupConversation(groupId: number) {
- this.removeConversation(ImConversationType.GROUP, groupId)
+ this.removeConversation(ImConversationType.GROUP, groupId);
},
/** 标记会话已读 */
- markConversationRead(type: number, targetId: number, messageId?: number): void {
- const conversation = this.getConversation(type, targetId)
+ markConversationRead(
+ type: number,
+ targetId: number,
+ messageId?: number,
+ ): void {
+ const conversation = this.getConversation(type, targetId);
if (!conversation) {
- return
+ return;
}
- const key = getClientConversationId(type, targetId)
- const current = this.conversationReads[key]
- const readMessageIdAdvanced = !!messageId && messageId > (current?.messageId || 0)
+ const key = getClientConversationId(type, targetId);
+ const current = this.conversationReads[key];
+ const readMessageIdAdvanced =
+ !!messageId && messageId > (current?.messageId || 0);
if (
conversation.unreadCount === 0 &&
!conversation.atMe &&
!conversation.atAll &&
!readMessageIdAdvanced
) {
- return
+ return;
}
- conversation.unreadCount = 0
- conversation.atMe = false
- conversation.atAll = false
+ conversation.unreadCount = 0;
+ conversation.atMe = false;
+ conversation.atAll = false;
if (readMessageIdAdvanced) {
- const record = createConversationRead(type, targetId, messageId)
- this.conversationReads[key] = record
+ const record = createConversationRead(type, targetId, messageId);
+ this.conversationReads[key] = record;
void getDb()
- .transaction(['conversations', 'conversationReads'], 'readwrite', async (tx) => {
- await this.saveConversationRecord(conversation, tx)
- await this.saveConversationReadRecord(record, tx)
- })
+ .transaction(
+ ['conversations', 'conversationReads'],
+ 'readwrite',
+ async (tx) => {
+ await this.saveConversationRecord(conversation, tx);
+ await this.saveConversationReadRecord(record, tx);
+ },
+ )
.catch((error) =>
console.warn(
'[IM conversationStore] 会话已读写入失败',
@@ -725,27 +817,34 @@ export const useConversationStore = defineStore('imConversationStore', {
conversationType: type,
targetId,
messageId,
- conversationKey: key
+ conversationKey: key,
},
- error
- )
- )
- return
+ error,
+ ),
+ );
+ return;
}
- this.saveConversation(conversation)
+ this.saveConversation(conversation);
},
/** 标记会话已上报服务端读位置 */
- markConversationReadReported(type: number, targetId: number, messageId?: number): void {
+ markConversationReadReported(
+ type: number,
+ targetId: number,
+ messageId?: number,
+ ): void {
if (!messageId) {
- return
+ return;
}
- const conversation = this.getConversation(type, targetId)
- if (!conversation || messageId <= (conversation.reportedReadMessageId || 0)) {
- return
+ const conversation = this.getConversation(type, targetId);
+ if (
+ !conversation ||
+ messageId <= (conversation.reportedReadMessageId || 0)
+ ) {
+ return;
}
- conversation.reportedReadMessageId = messageId
- this.saveConversation(conversation)
+ conversation.reportedReadMessageId = messageId;
+ this.saveConversation(conversation);
},
// ==================== 最近转发 ====================
@@ -753,24 +852,24 @@ export const useConversationStore = defineStore('imConversationStore', {
/** 推送最近转发会话 */
pushRecentForwardConversationKeyList(keys: string[]) {
if (!keys || keys.length === 0) {
- return
+ return;
}
- const merged = [...keys, ...this.recentForwardConversationKeys]
+ const merged = [...keys, ...this.recentForwardConversationKeys];
this.recentForwardConversationKeys = [...new Set(merged)].slice(
0,
- CONVERSATION_RECENT_FORWARD_MAX
- )
- this.saveRecentForwardConversationKeyList()
+ CONVERSATION_RECENT_FORWARD_MAX,
+ );
+ this.saveRecentForwardConversationKeyList();
},
/** 移除最近转发会话 */
removeRecentForwardConversationKey(key: string) {
- const index = this.recentForwardConversationKeys.indexOf(key)
+ const index = this.recentForwardConversationKeys.indexOf(key);
if (index === -1) {
- return
+ return;
}
- this.recentForwardConversationKeys.splice(index, 1)
- this.saveRecentForwardConversationKeyList()
+ this.recentForwardConversationKeys.splice(index, 1);
+ this.saveRecentForwardConversationKeyList();
},
/** 保存最近转发会话 */
@@ -778,133 +877,163 @@ export const useConversationStore = defineStore('imConversationStore', {
void getDb()
.setSetting(
StorageKeys.settings.recentForwardConversationKeys,
- this.recentForwardConversationKeys.slice(0, CONVERSATION_RECENT_FORWARD_MAX)
+ this.recentForwardConversationKeys.slice(
+ 0,
+ CONVERSATION_RECENT_FORWARD_MAX,
+ ),
)
- .catch((error) => console.warn('[IM conversationStore] 最近转发列表写入失败', error))
+ .catch((error) =>
+ console.warn('[IM conversationStore] 最近转发列表写入失败', error),
+ );
},
// ==================== 会话维护 ====================
/** 重排会话 */
sortConversationList() {
- this.conversations.sort((a, b) => (b.lastSendTime || 0) - (a.lastSendTime || 0))
- this.saveConversationList(this.conversations)
+ this.conversations.sort(
+ (a, b) => (b.lastSendTime || 0) - (a.lastSendTime || 0),
+ );
+ this.saveConversationList(this.conversations);
},
/** 同步会话展示元数据 */
updateConversation(
type: number,
targetId: number,
- info: { avatar?: string; name?: string; silent?: boolean }
+ info: { avatar?: string; name?: string; silent?: boolean },
) {
- const conversation = this.getConversation(type, targetId)
+ const conversation = this.getConversation(type, targetId);
if (!conversation) {
- return
+ return;
}
- let changed = false
+ let changed = false;
if (info.name && conversation.name !== info.name) {
- conversation.name = info.name
- changed = true
+ conversation.name = info.name;
+ changed = true;
}
if (info.avatar !== undefined && conversation.avatar !== info.avatar) {
- conversation.avatar = info.avatar || ''
- changed = true
+ conversation.avatar = info.avatar || '';
+ changed = true;
}
if (info.silent !== undefined && conversation.silent !== info.silent) {
- conversation.silent = info.silent
- changed = true
+ conversation.silent = info.silent;
+ changed = true;
}
if (changed) {
- this.saveConversation(conversation)
+ this.saveConversation(conversation);
}
},
// ==================== 草稿 ====================
/** 获取草稿 */
- getConversationDraft(conversation: { targetId: number; type: number; }): Conversation['draft'] | undefined {
- return this.getConversation(conversation.type, conversation.targetId)?.draft
+ getConversationDraft(conversation: {
+ targetId: number;
+ type: number;
+ }): Conversation['draft'] | undefined {
+ return this.getConversation(conversation.type, conversation.targetId)
+ ?.draft;
},
/** 设置草稿 */
setConversationDraft(
- conversation: { targetId: number; type: number; },
- snapshot: NonNullable
+ conversation: { targetId: number; type: number },
+ snapshot: NonNullable,
): void {
if (!snapshot.plain.trim() && !snapshot.reply) {
- this.clearConversationDraft(conversation)
- return
+ this.clearConversationDraft(conversation);
+ return;
}
- const target = this.getConversation(conversation.type, conversation.targetId)
+ const target = this.getConversation(
+ conversation.type,
+ conversation.targetId,
+ );
if (!target) {
- return
+ return;
}
- target.draft = snapshot
- this.scheduleConversationDraftSave(target)
+ target.draft = snapshot;
+ this.scheduleConversationDraftSave(target);
},
/** 清除草稿 */
- clearConversationDraft(conversation: { targetId: number; type: number; }): void {
- const target = this.getConversation(conversation.type, conversation.targetId)
+ clearConversationDraft(conversation: {
+ targetId: number;
+ type: number;
+ }): void {
+ const target = this.getConversation(
+ conversation.type,
+ conversation.targetId,
+ );
if (!target?.draft) {
- return
+ return;
}
- target.draft = undefined
- this.scheduleConversationDraftSave(target)
+ target.draft = undefined;
+ this.scheduleConversationDraftSave(target);
},
/** 设置回复草稿 */
setConversationReplyDraft(
- conversation: { targetId: number; type: number; },
- quote: NonNullable['reply']
+ conversation: { targetId: number; type: number },
+ quote: NonNullable['reply'],
) {
if (!quote) {
- return
+ return;
}
- const existing = this.getConversationDraft(conversation)
+ const existing = this.getConversationDraft(conversation);
this.setConversationDraft(conversation, {
html: existing?.html ?? '',
plain: existing?.plain ?? '',
- reply: quote
- })
+ reply: quote,
+ });
},
/** 清除回复草稿 */
- clearConversationReplyDraft(conversation: { targetId: number; type: number; }): void {
- const existing = this.getConversationDraft(conversation)
+ clearConversationReplyDraft(conversation: {
+ targetId: number;
+ type: number;
+ }): void {
+ const existing = this.getConversationDraft(conversation);
if (!existing?.reply) {
- return
+ return;
}
- this.setConversationDraft(conversation, { ...existing, reply: undefined })
+ this.setConversationDraft(conversation, {
+ ...existing,
+ reply: undefined,
+ });
},
/** 调度草稿保存 */
scheduleConversationDraftSave(conversation: Conversation): void {
- pendingDraftConversations.add(conversation)
- saveDraftConversationListDebounced()
+ pendingDraftConversations.add(conversation);
+ saveDraftConversationListDebounced();
},
/** 立即保存草稿 */
flushConversationDraftSave(): void {
- saveDraftConversationListDebounced.flush()
- }
- }
-})
+ saveDraftConversationListDebounced.flush();
+ },
+ },
+});
-export const useConversationStoreWithOut = () => useConversationStore()
+export const useConversationStoreWithOut = () => useConversationStore();
/** 合并草稿写入 */
const saveDraftConversationListDebounced = createDraftDebounce(() => {
- const conversations = [...pendingDraftConversations]
- pendingDraftConversations.clear()
+ const conversations = [...pendingDraftConversations];
+ pendingDraftConversations.clear();
if (conversations.length === 0) {
- return
+ return;
}
void useConversationStoreWithOut()
.saveConversationRecord(conversations)
- .catch((error) => console.warn('[IM conversationStore] 草稿写入失败', error))
-}, PERSIST_DRAFT_DEBOUNCE_MS)
+ .catch((error) =>
+ console.warn('[IM conversationStore] 草稿写入失败', error),
+ );
+}, PERSIST_DRAFT_DEBOUNCE_MS);
if (import.meta.hot) {
- import.meta.hot.accept(acceptHMRUpdate(useConversationStore, import.meta.hot))
+ import.meta.hot.accept(
+ acceptHMRUpdate(useConversationStore, import.meta.hot),
+ );
}
diff --git a/apps/web-antd/src/views/im/home/store/faceStore.ts b/apps/web-antd/src/views/im/home/store/faceStore.ts
index 81224436a..b27040a56 100644
--- a/apps/web-antd/src/views/im/home/store/faceStore.ts
+++ b/apps/web-antd/src/views/im/home/store/faceStore.ts
@@ -1,12 +1,16 @@
-import type { ImFacePackApi } from '#/api/im/face/pack'
-import type { ImFaceUserItemApi } from '#/api/im/face/userItem'
+import type { ImFacePackApi } from '#/api/im/face/pack';
+import type { ImFaceUserItemApi } from '#/api/im/face/userItem';
-import { ref } from 'vue'
+import { ref } from 'vue';
-import { acceptHMRUpdate, defineStore } from 'pinia'
+import { acceptHMRUpdate, defineStore } from 'pinia';
-import { getFacePackList as apiGetFacePackList } from '#/api/im/face/pack'
-import { createFaceUserItem as apiCreateFaceUserItem, deleteFaceUserItem as apiDeleteFaceUserItem, getFaceUserItemList as apiGetFaceUserItemList } from '#/api/im/face/userItem'
+import { getFacePackList as apiGetFacePackList } from '#/api/im/face/pack';
+import {
+ createFaceUserItem as apiCreateFaceUserItem,
+ deleteFaceUserItem as apiDeleteFaceUserItem,
+ getFaceUserItemList as apiGetFaceUserItemList,
+} from '#/api/im/face/userItem';
/**
* IM 表情面板数据 store(系统表情包 + 个人表情)
@@ -16,14 +20,13 @@ import { createFaceUserItem as apiCreateFaceUserItem, deleteFaceUserItem as apiD
* - 个人表情:切到「收藏」tab / 长按消息「添加到表情」时按需拉
*/
export const useFaceStore = defineStore('imFace', () => {
-
/** 系统表情包列表(含每个包的 items);运营管理后台维护 */
- const facePacks = ref([])
+ const facePacks = ref([]);
/** 个人表情包列表(用户长按「添加到表情」/ 上传产生) */
- const faceUserItems = ref([])
+ const faceUserItems = ref([]);
/** clear() 时递增;旧账号请求返回后不写入新账号内存 */
- let storeEpoch = 0
+ let storeEpoch = 0;
/**
* 系统表情包拉取 promise;ensureFacePackList 内 cache:
@@ -31,51 +34,51 @@ export const useFaceStore = defineStore('imFace', () => {
* - resolve 后保留对象 = 后续调用 await 立即返回,不再发请求
* - reject 后置回 null,让调用方下次重试
*/
- let facePacksPromise: null | Promise = null
+ let facePacksPromise: null | Promise = null;
/** 按需拉取系统表情包(已拉过则直接复用 cached promise) */
async function ensureFacePackList(): Promise {
if (!facePacksPromise) {
- const requestEpoch = storeEpoch
+ const requestEpoch = storeEpoch;
facePacksPromise = apiGetFacePackList()
.then((data) => {
if (requestEpoch !== storeEpoch) {
- return
+ return;
}
- facePacks.value = data || []
+ facePacks.value = data || [];
})
.catch((error) => {
- console.warn('[IM] 拉取表情包失败', error)
+ console.warn('[IM] 拉取表情包失败', error);
if (requestEpoch === storeEpoch) {
- facePacksPromise = null
+ facePacksPromise = null;
}
- throw error
- })
+ throw error;
+ });
}
- return facePacksPromise
+ return facePacksPromise;
}
/** 个人表情拉取 promise;语义同上 */
- let faceUserItemsPromise: null | Promise = null
+ let faceUserItemsPromise: null | Promise = null;
/** 按需拉取个人表情(已拉过则直接复用 cached promise) */
async function ensureFaceUserItemList(): Promise {
if (!faceUserItemsPromise) {
- const requestEpoch = storeEpoch
+ const requestEpoch = storeEpoch;
faceUserItemsPromise = apiGetFaceUserItemList()
.then((data) => {
if (requestEpoch !== storeEpoch) {
- return
+ return;
}
- faceUserItems.value = data || []
+ faceUserItems.value = data || [];
})
.catch((error) => {
- console.warn('[IM] 拉取个人表情失败', error)
+ console.warn('[IM] 拉取个人表情失败', error);
if (requestEpoch === storeEpoch) {
- faceUserItemsPromise = null
+ faceUserItemsPromise = null;
}
- throw error
- })
+ throw error;
+ });
}
- return faceUserItemsPromise
+ return faceUserItemsPromise;
}
/**
@@ -83,15 +86,17 @@ export const useFaceStore = defineStore('imFace', () => {
*
* 来源:1. 用户在表情面板「+」上传图片 2. 长按消息「添加到表情」
*/
- async function addFaceUserItem(reqVO: ImFaceUserItemApi.FaceUserItemSaveReqVO): Promise {
- const requestEpoch = storeEpoch
- const id = await apiCreateFaceUserItem(reqVO)
+ async function addFaceUserItem(
+ reqVO: ImFaceUserItemApi.FaceUserItemSaveReqVO,
+ ): Promise {
+ const requestEpoch = storeEpoch;
+ const id = await apiCreateFaceUserItem(reqVO);
if (!id) {
- return false
+ return false;
}
// 已切账号时跳过旧请求结果
if (requestEpoch !== storeEpoch) {
- return false
+ return false;
}
// id 不在缓存里才插入;服务端唯一约束兜底了 race,本地理论上不会拿到重复 id
if (!faceUserItems.value.some((item) => item.id === id)) {
@@ -100,36 +105,38 @@ export const useFaceStore = defineStore('imFace', () => {
url: reqVO.url,
name: reqVO.name,
width: reqVO.width,
- height: reqVO.height
- })
+ height: reqVO.height,
+ });
}
- return true
+ return true;
}
/** 删除个人表情;本地立即移除 */
async function removeFaceUserItem(id: number): Promise {
- const requestEpoch = storeEpoch
+ const requestEpoch = storeEpoch;
try {
- await apiDeleteFaceUserItem(id)
+ await apiDeleteFaceUserItem(id);
// 已切账号时跳过旧请求结果
if (requestEpoch !== storeEpoch) {
- return false
+ return false;
}
- faceUserItems.value = faceUserItems.value.filter((item) => item.id !== id)
- return true
+ faceUserItems.value = faceUserItems.value.filter(
+ (item) => item.id !== id,
+ );
+ return true;
} catch (error) {
- console.warn('[IM] 删除个人表情失败', { id }, error)
- return false
+ console.warn('[IM] 删除个人表情失败', { id }, error);
+ return false;
}
}
/** 清空表情缓存 */
function clear(): void {
- facePacks.value = []
- faceUserItems.value = []
- facePacksPromise = null
- faceUserItemsPromise = null
- storeEpoch++
+ facePacks.value = [];
+ faceUserItems.value = [];
+ facePacksPromise = null;
+ faceUserItemsPromise = null;
+ storeEpoch++;
}
return {
@@ -139,13 +146,13 @@ export const useFaceStore = defineStore('imFace', () => {
ensureFaceUserItemList,
addFaceUserItem,
removeFaceUserItem,
- clear
- }
-})
+ clear,
+ };
+});
/** 在 setup 外(路由守卫等)取 store 实例的工具方法 */
-export const useFaceStoreWithOut = () => useFaceStore()
+export const useFaceStoreWithOut = () => useFaceStore();
if (import.meta.hot) {
- import.meta.hot.accept(acceptHMRUpdate(useFaceStore, import.meta.hot))
+ import.meta.hot.accept(acceptHMRUpdate(useFaceStore, import.meta.hot));
}
diff --git a/apps/web-antd/src/views/im/home/store/friendStore.ts b/apps/web-antd/src/views/im/home/store/friendStore.ts
index bf959b836..dd02b2158 100644
--- a/apps/web-antd/src/views/im/home/store/friendStore.ts
+++ b/apps/web-antd/src/views/im/home/store/friendStore.ts
@@ -1,60 +1,78 @@
-import type { Friend, FriendLite, FriendRequest } from '../types'
+import type { Friend, FriendLite, FriendRequest } from '../types';
-import type { ImFriendApi } from '#/api/im/friend'
-import type { ImFriendRequestApi } from '#/api/im/friend/request'
+import type { ImFriendApi } from '#/api/im/friend';
+import type { ImFriendRequestApi } from '#/api/im/friend/request';
-import { CommonStatusEnum } from '@vben/constants'
+import { CommonStatusEnum } from '@vben/constants';
-import { acceptHMRUpdate, defineStore } from 'pinia'
+import { acceptHMRUpdate, defineStore } from 'pinia';
-import { blockFriend as apiBlockFriend, deleteFriend as apiDeleteFriend, getFriend as apiGetFriend, getMyFriendList as apiGetMyFriendList, pullMyFriendList as apiPullMyFriendList, unblockFriend as apiUnblockFriend, updateFriend as apiUpdateFriend } from '#/api/im/friend'
-import { agreeFriendRequest as apiAgreeFriendRequest, applyFriendRequest as apiApplyFriendRequest, getMyFriendRequest as apiGetMyFriendRequest, getMyFriendRequestList as apiGetMyFriendRequestList, pullMyFriendRequestList as apiPullMyFriendRequestList, refuseFriendRequest as apiRefuseFriendRequest } from '#/api/im/friend/request'
-import { getCurrentUserId } from '#/views/im/utils/auth'
+import {
+ blockFriend as apiBlockFriend,
+ deleteFriend as apiDeleteFriend,
+ getFriend as apiGetFriend,
+ getMyFriendList as apiGetMyFriendList,
+ pullMyFriendList as apiPullMyFriendList,
+ unblockFriend as apiUnblockFriend,
+ updateFriend as apiUpdateFriend,
+} from '#/api/im/friend';
+import {
+ agreeFriendRequest as apiAgreeFriendRequest,
+ applyFriendRequest as apiApplyFriendRequest,
+ getMyFriendRequest as apiGetMyFriendRequest,
+ getMyFriendRequestList as apiGetMyFriendRequestList,
+ pullMyFriendRequestList as apiPullMyFriendRequestList,
+ refuseFriendRequest as apiRefuseFriendRequest,
+} from '#/api/im/friend/request';
+import { getCurrentUserId } from '#/views/im/utils/auth';
-import { FRIEND_REQUEST_PAGE_SIZE } from '../../utils/config'
-import { ImConversationType, ImFriendRequestHandleResult } from '../../utils/constants'
-import { getDb, StorageKeys } from '../../utils/db'
-import { runIncrementalPull } from '../../utils/pull'
-import { getFriendDisplayName } from '../../utils/user'
-import { useConversationStore } from './conversationStore'
+import { FRIEND_REQUEST_PAGE_SIZE } from '../../utils/config';
+import {
+ ImConversationType,
+ ImFriendRequestHandleResult,
+} from '../../utils/constants';
+import { getDb, StorageKeys } from '../../utils/db';
+import { runIncrementalPull } from '../../utils/pull';
+import { getFriendDisplayName } from '../../utils/user';
+import { useConversationStore } from './conversationStore';
-type PendingRequest = { epoch: number; promise: Promise; userId: number; }
+type PendingRequest = { epoch: number; promise: Promise; userId: number };
/** 当前正在进行的好友列表拉取;多 dispatcher 同时触发时复用同一 Promise,避免雪崩重拉 */
-let pendingFetchFriends: null | PendingRequest = null
+let pendingFetchFriends: null | PendingRequest = null;
/** 当前正在进行的好友申请列表拉取;多端连续多条申请到达时复用同一 Promise,避免雪崩重拉 */
-let pendingFetchRequests: null | PendingRequest = null
+let pendingFetchRequests: null | PendingRequest = null;
/** 当前正在进行的「加载更多申请」请求 */
-let pendingLoadMoreRequests: null | PendingRequest = null
+let pendingLoadMoreRequests: null | PendingRequest = null;
/** 当前正在进行的好友详情请求 */
-const pendingFetchFriendInfos = new Map>()
+const pendingFetchFriendInfos = new Map>();
/** clear() 时递增;旧账号那次还没返回的请求 resolve 后比对一致才写 store,防跨账号数据泄漏 */
-let storeEpoch = 0
+let storeEpoch = 0;
/** 构建好友详情请求去重 key */
function getPendingFriendInfoKey(userId: number, friendUserId: number): string {
- return `${userId}:${friendUserId}`
+ return `${userId}:${friendUserId}`;
}
/** 好友通知 payload(对齐后端 BaseFriendNotification + 子类裁减后的字段) */
export interface FriendNotificationPayload {
- operatorUserId: number
- friendUserId: number
+ operatorUserId: number;
+ friendUserId: number;
// FRIEND_REQUEST_* 系列:申请记录的核心字段(避免 payload 携带完整 DO)
- requestId?: number
- applyContent?: string
- handleContent?: string
- addSource?: number
+ requestId?: number;
+ applyContent?: string;
+ handleContent?: string;
+ addSource?: number;
// FRIEND_REQUEST_RECEIVED:申请方聚合字段,供前端直推 push 进列表,无需回拉
- fromNickname?: string
- fromAvatar?: string
+ fromNickname?: string;
+ fromAvatar?: string;
// FRIEND_UPDATE:单边属性变更
- displayName?: string
- silent?: boolean
- pinned?: boolean
+ displayName?: string;
+ silent?: boolean;
+ pinned?: boolean;
// FRIEND_DELETE:是否级联清理本端相关数据(如私聊会话)
- clear?: boolean
+ clear?: boolean;
}
/**
@@ -73,7 +91,7 @@ export const useFriendStore = defineStore('imFriendStore', {
/** 我相关的好友申请列表(含我发起的 + 别人加我的;后端按 id 倒序游标分页) */
friendRequests: [] as FriendRequest[],
/** 是否还有更早的申请记录可加载;返回不满 page size 即置 false */
- hasMoreFriendRequests: true
+ hasMoreFriendRequests: true,
}),
getters: {
@@ -84,19 +102,21 @@ export const useFriendStore = defineStore('imFriendStore', {
* 直接 find 时 N 条消息 × M 好友 = O(N×M);建索引后单次读 O(1),重建只在写好友(fetchFriendList / upsertFriend 等)时发生
*/
getFriendMap: (state): Map => {
- const map = new Map()
+ const map = new Map();
for (const friend of state.friends) {
- map.set(friend.friendUserId, friend)
+ map.set(friend.friendUserId, friend);
}
- return map
+ return map;
},
/** 按 friendUserId 找好友(含已软删的 DISABLE 记录,调用方自行判定) */
getFriend(): (friendUserId: number) => Friend | undefined {
- return (friendUserId: number) => this.getFriendMap.get(friendUserId)
+ return (friendUserId: number) => this.getFriendMap.get(friendUserId);
},
/** 当前生效的好友列表(过滤掉 DISABLE 软删记录) */
getActiveFriendList: (state): Friend[] => {
- return state.friends.filter((friend) => friend.status !== CommonStatusEnum.DISABLE)
+ return state.friends.filter(
+ (friend) => friend.status !== CommonStatusEnum.DISABLE,
+ );
},
/** 当前生效好友的 Lite 视图(PickerPanel / 选人弹窗共用,自带拼音字段供分桶 / 搜索) */
getActiveFriendLiteList(): FriendLite[] {
@@ -106,31 +126,32 @@ export const useFriendStore = defineStore('imFriendStore', {
nicknamePinyin: friend.nicknamePinyin,
avatar: friend.avatar,
displayName: friend.displayName,
- displayNamePinyin: friend.displayNamePinyin
- }))
+ displayNamePinyin: friend.displayNamePinyin,
+ }));
},
/** 判断对方是否是当前用户的有效好友(存在 + 非 DISABLE) */
isActiveFriend() {
return (friendUserId: number): boolean => {
- const entry = this.getFriend(friendUserId)
- return !!entry && entry.status !== CommonStatusEnum.DISABLE
- }
+ const entry = this.getFriend(friendUserId);
+ return !!entry && entry.status !== CommonStatusEnum.DISABLE;
+ };
},
/** 我的黑名单(blocked=true 且 ENABLE) */
getBlockedFriendList: (state): Friend[] => {
return state.friends.filter(
- (friend) => friend.status !== CommonStatusEnum.DISABLE && friend.blocked === true
- )
+ (friend) =>
+ friend.status !== CommonStatusEnum.DISABLE && friend.blocked === true,
+ );
},
/** 未处理申请数(接收方=我)—— 实时派生,「新的朋友」红点用 */
getUnhandledRequestCount: (state): number => {
- const currentUserId = getCurrentUserId()
+ const currentUserId = getCurrentUserId();
return state.friendRequests.filter(
(request) =>
request.handleResult === ImFriendRequestHandleResult.UNHANDLED &&
- request.toUserId === currentUserId
- ).length
- }
+ request.toUserId === currentUserId,
+ ).length;
+ },
},
actions: {
@@ -141,21 +162,22 @@ export const useFriendStore = defineStore('imFriendStore', {
try {
const [friends, friendRequests] = await Promise.all([
getDb().getAll('friends'),
- getDb().getAll('friendRequests')
- ])
+ getDb().getAll('friendRequests'),
+ ]);
if (friends.length > 0) {
- this.friends = friends
+ this.friends = friends;
}
if (friendRequests.length > 0) {
this.friendRequests = friendRequests.toSorted(
- (requestA, requestB) => requestB.id - requestA.id
- )
- this.hasMoreFriendRequests = friendRequests.length >= FRIEND_REQUEST_PAGE_SIZE
+ (requestA, requestB) => requestB.id - requestA.id,
+ );
+ this.hasMoreFriendRequests =
+ friendRequests.length >= FRIEND_REQUEST_PAGE_SIZE;
}
- return friends.length > 0
+ return friends.length > 0;
} catch (error) {
- console.warn('[IM friendStore] 本地好友缓存读取失败', error)
- return false
+ console.warn('[IM friendStore] 本地好友缓存读取失败', error);
+ return false;
}
},
@@ -163,58 +185,64 @@ export const useFriendStore = defineStore('imFriendStore', {
saveFriendList(): void {
void getDb()
.transaction(['friends'], 'readwrite', async (tx) => {
- const db = getDb()
- await db.clearStore('friends', tx)
+ const db = getDb();
+ await db.clearStore('friends', tx);
for (const friend of this.friends) {
if (friend.id) {
- await db.put('friends', friend, tx)
+ await db.put('friends', friend, tx);
}
}
})
- .catch((error) => console.warn('[IM friendStore] 本地好友缓存写入失败', error))
+ .catch((error) =>
+ console.warn('[IM friendStore] 本地好友缓存写入失败', error),
+ );
},
/** 保存单个好友 */
async saveFriendRecord(friend: Friend | undefined): Promise {
if (!friend?.id) {
- return
+ return;
}
- await getDb().put('friends', friend)
+ await getDb().put('friends', friend);
},
/** 保存单个好友 */
saveFriend(friend: Friend | undefined): void {
void this.saveFriendRecord(friend).catch((error) =>
- console.warn('[IM friendStore] 本地好友写入失败', error)
- )
+ console.warn('[IM friendStore] 本地好友写入失败', error),
+ );
},
/** 保存好友申请列表 */
saveFriendRequestList(): void {
void getDb()
.transaction(['friendRequests'], 'readwrite', async (tx) => {
- const db = getDb()
- await db.clearStore('friendRequests', tx)
+ const db = getDb();
+ await db.clearStore('friendRequests', tx);
for (const request of this.friendRequests) {
- await db.put('friendRequests', request, tx)
+ await db.put('friendRequests', request, tx);
}
})
- .catch((error) => console.warn('[IM friendStore] 本地好友申请缓存写入失败', error))
+ .catch((error) =>
+ console.warn('[IM friendStore] 本地好友申请缓存写入失败', error),
+ );
},
/** 保存单条好友申请 */
- async saveFriendRequestRecord(request: FriendRequest | undefined): Promise {
+ async saveFriendRequestRecord(
+ request: FriendRequest | undefined,
+ ): Promise {
if (!request) {
- return
+ return;
}
- await getDb().put('friendRequests', request)
+ await getDb().put('friendRequests', request);
},
/** 保存单条好友申请 */
saveFriendRequest(request: FriendRequest | undefined): void {
void this.saveFriendRequestRecord(request).catch((error) =>
- console.warn('[IM friendStore] 本地好友申请写入失败', error)
- )
+ console.warn('[IM friendStore] 本地好友申请写入失败', error),
+ );
},
// ==================== 远端拉取 ====================
@@ -222,47 +250,58 @@ export const useFriendStore = defineStore('imFriendStore', {
/** 从后端拉取并覆盖本地列表(含 DISABLE 历史好友给已删对话兜底);只同步 ENABLE 的会话信息,DISABLE 的不动 —— cascade 清会话由 WS dispatcher 按 payload.clear 处理,避免 fetchFriendList 覆盖用户「不清空聊天记录」的选择 */
async fetchFriendList(force = false) {
if (this.loaded && !force) {
- return
+ return;
}
// 快照 epoch;clear() 之后到 .then 之间触发的 epoch++ 表示账号已切,旧结果不能写入新 store
- const requestEpoch = storeEpoch
- const requestUserId = getCurrentUserId()
+ const requestEpoch = storeEpoch;
+ const requestUserId = getCurrentUserId();
if (
pendingFetchFriends?.epoch === requestEpoch &&
pendingFetchFriends.userId === requestUserId
) {
- return pendingFetchFriends.promise
+ return pendingFetchFriends.promise;
}
const promise = apiGetMyFriendList()
.then((list) => {
- if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) {
- return
+ if (
+ requestEpoch !== storeEpoch ||
+ getCurrentUserId() !== requestUserId
+ ) {
+ return;
}
- this.friends = (list || []).map((friend) => convertFriend(friend))
- this.loaded = true
- const conversationStore = useConversationStore()
+ this.friends = (list || []).map((friend) => convertFriend(friend));
+ this.loaded = true;
+ const conversationStore = useConversationStore();
for (const friend of this.friends) {
if (friend.status === CommonStatusEnum.DISABLE) {
- continue
+ continue;
}
- conversationStore.updateConversation(ImConversationType.PRIVATE, friend.friendUserId, {
- name: getFriendDisplayName(friend),
- avatar: friend.avatar,
- silent: friend.silent
- })
+ conversationStore.updateConversation(
+ ImConversationType.PRIVATE,
+ friend.friendUserId,
+ {
+ name: getFriendDisplayName(friend),
+ avatar: friend.avatar,
+ silent: friend.silent,
+ },
+ );
}
- this.saveFriendList()
+ this.saveFriendList();
})
.finally(() => {
if (
pendingFetchFriends?.epoch === requestEpoch &&
pendingFetchFriends.userId === requestUserId
) {
- pendingFetchFriends = null
+ pendingFetchFriends = null;
}
- })
- pendingFetchFriends = { epoch: requestEpoch, userId: requestUserId, promise }
- return promise
+ });
+ pendingFetchFriends = {
+ epoch: requestEpoch,
+ userId: requestUserId,
+ promise,
+ };
+ return promise;
},
/**
@@ -272,391 +311,445 @@ export const useFriendStore = defineStore('imFriendStore', {
*/
async pullFriends() {
// 快照 epoch;账号在拉取途中切换(clear() → epoch++)时丢弃旧账号那几页结果,防跨账号数据泄漏
- const requestEpoch = storeEpoch
- const requestUserId = getCurrentUserId()
- const isActive = () => requestEpoch === storeEpoch && getCurrentUserId() === requestUserId
+ const requestEpoch = storeEpoch;
+ const requestUserId = getCurrentUserId();
+ const isActive = () =>
+ requestEpoch === storeEpoch && getCurrentUserId() === requestUserId;
await runIncrementalPull(
StorageKeys.settings.friendPullCursor,
apiPullMyFriendList,
async (records) => {
if (!isActive()) {
- return false
+ return false;
}
- await Promise.all(records.map((vo) => this.upsertFriendForPull(convertFriend(vo))))
- return true
+ await Promise.all(
+ records.map((vo) => this.upsertFriendForPull(convertFriend(vo))),
+ );
+ return true;
},
- isActive
- )
+ isActive,
+ );
// 置 loaded,供通讯录页 fetchFriendList(force=false) 复用缓存而非重复全量拉
if (isActive()) {
- this.loaded = true
+ this.loaded = true;
}
},
/** 按 friendUserId 获取详情并合并到本地(保证 nickname / avatar 最新) */
async fetchFriendInfo(friendUserId: number) {
- const requestEpoch = storeEpoch
- const requestUserId = getCurrentUserId()
+ const requestEpoch = storeEpoch;
+ const requestUserId = getCurrentUserId();
if (!requestUserId) {
- return
+ return;
}
- const key = getPendingFriendInfoKey(requestUserId, friendUserId)
- const inflight = pendingFetchFriendInfos.get(key)
+ const key = getPendingFriendInfoKey(requestUserId, friendUserId);
+ const inflight = pendingFetchFriendInfos.get(key);
if (inflight) {
- return inflight
+ return inflight;
}
const promise = (async () => {
try {
- const data = await apiGetFriend(friendUserId)
+ const data = await apiGetFriend(friendUserId);
if (!data) {
- return
+ return;
}
// clear() 已切账号:旧请求的好友详情不能再 upsert 进新账号的 friends
- if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) {
- return
+ if (
+ requestEpoch !== storeEpoch ||
+ getCurrentUserId() !== requestUserId
+ ) {
+ return;
}
- this.upsertFriend(convertFriend(data))
+ this.upsertFriend(convertFriend(data));
} catch (error) {
- console.warn('[IM friendStore] fetchFriendInfo 失败', error)
+ console.warn('[IM friendStore] fetchFriendInfo 失败', error);
}
})().finally(() => {
if (pendingFetchFriendInfos.get(key) === promise) {
- pendingFetchFriendInfos.delete(key)
+ pendingFetchFriendInfos.delete(key);
}
- })
- pendingFetchFriendInfos.set(key, promise)
- return promise
+ });
+ pendingFetchFriendInfos.set(key, promise);
+ return promise;
},
// ==================== 申请-审批 ====================
/** 发起好友申请:成功后等待对方同意(不直接落地为好友) */
- async applyFriendRequest(reqVO: ImFriendRequestApi.FriendRequestApplyReqVO): Promise {
- return await apiApplyFriendRequest(reqVO)
+ async applyFriendRequest(
+ reqVO: ImFriendRequestApi.FriendRequestApplyReqVO,
+ ): Promise {
+ return await apiApplyFriendRequest(reqVO);
},
/** 同意一条好友申请;后端会双向落库 + 推 FRIEND_ADD,本端等通知到达再 upsertFriend */
async agreeFriendRequest(requestId: number) {
- await apiAgreeFriendRequest(requestId)
- await this.applyHandleResult(requestId, ImFriendRequestHandleResult.AGREED)
+ await apiAgreeFriendRequest(requestId);
+ await this.applyHandleResult(
+ requestId,
+ ImFriendRequestHandleResult.AGREED,
+ );
},
/** 拒绝一条好友申请 */
async refuseFriendRequest(requestId: number, handleContent?: string) {
- await apiRefuseFriendRequest(requestId, handleContent)
- await this.applyHandleResult(requestId, ImFriendRequestHandleResult.REFUSED, handleContent)
+ await apiRefuseFriendRequest(requestId, handleContent);
+ await this.applyHandleResult(
+ requestId,
+ ImFriendRequestHandleResult.REFUSED,
+ handleContent,
+ );
},
/** 把 handleResult 应用到本地申请记录;找不到就按 id 单查兜底 upsert,避免破坏 id 倒序 */
async applyHandleResult(
requestId: number,
result: number,
- handleContent?: string
+ handleContent?: string,
): Promise {
- const request = this.getFriendRequest(requestId)
+ const request = this.getFriendRequest(requestId);
if (request) {
- request.handleResult = result
+ request.handleResult = result;
if (handleContent !== undefined) {
- request.handleContent = handleContent
+ request.handleContent = handleContent;
}
- request.handleTime = Date.now()
- this.saveFriendRequest(request)
- return
+ request.handleTime = Date.now();
+ this.saveFriendRequest(request);
+ return;
}
- await this.fetchFriendRequest(requestId)
+ await this.fetchFriendRequest(requestId);
},
/** 拉取「我相关」的好友申请列表首页(页面打开 / 收到 FRIEND_REQUEST_RECEIVED 时刷新);pending 期间复用同一 Promise */
async fetchFriendRequestList() {
if (pendingFetchRequests) {
- const currentUserId = getCurrentUserId()
+ const currentUserId = getCurrentUserId();
if (
pendingFetchRequests.epoch === storeEpoch &&
pendingFetchRequests.userId === currentUserId
) {
- return pendingFetchRequests.promise
+ return pendingFetchRequests.promise;
}
}
- const requestEpoch = storeEpoch
- const requestUserId = getCurrentUserId()
+ const requestEpoch = storeEpoch;
+ const requestUserId = getCurrentUserId();
const promise = apiGetMyFriendRequestList(FRIEND_REQUEST_PAGE_SIZE)
.then((list) => {
- if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) {
- return
+ if (
+ requestEpoch !== storeEpoch ||
+ getCurrentUserId() !== requestUserId
+ ) {
+ return;
}
- const items = (list || []).map((request) => convertFriendRequest(request))
- this.friendRequests = items
+ const items = (list || []).map((request) =>
+ convertFriendRequest(request),
+ );
+ this.friendRequests = items;
// 不足一页即没有更多;满页可能还有,等 loadMore 拉到 0 条再确定
- this.hasMoreFriendRequests = items.length >= FRIEND_REQUEST_PAGE_SIZE
- this.saveFriendRequestList()
+ this.hasMoreFriendRequests = items.length >= FRIEND_REQUEST_PAGE_SIZE;
+ this.saveFriendRequestList();
})
.finally(() => {
if (
pendingFetchRequests?.epoch === requestEpoch &&
pendingFetchRequests.userId === requestUserId
) {
- pendingFetchRequests = null
+ pendingFetchRequests = null;
}
- })
- pendingFetchRequests = { epoch: requestEpoch, userId: requestUserId, promise }
- return promise
+ });
+ pendingFetchRequests = {
+ epoch: requestEpoch,
+ userId: requestUserId,
+ promise,
+ };
+ return promise;
},
/** 加载更多申请(按本地最旧 requestId 游标分页);无更多 / pending 中直接返回 */
async loadMoreFriendRequestList() {
- const requestUserId = getCurrentUserId()
+ const requestUserId = getCurrentUserId();
const hasSameFetchPending =
- pendingFetchRequests?.epoch === storeEpoch && pendingFetchRequests.userId === requestUserId
+ pendingFetchRequests?.epoch === storeEpoch &&
+ pendingFetchRequests.userId === requestUserId;
if (!this.hasMoreFriendRequests || hasSameFetchPending) {
- return
+ return;
}
if (
pendingLoadMoreRequests?.epoch === storeEpoch &&
pendingLoadMoreRequests.userId === requestUserId
) {
- return pendingLoadMoreRequests.promise
+ return pendingLoadMoreRequests.promise;
}
- const oldest = this.friendRequests[this.friendRequests.length - 1]
+ const oldest = this.friendRequests[this.friendRequests.length - 1];
if (!oldest) {
- return this.fetchFriendRequestList()
+ return this.fetchFriendRequestList();
}
- const requestEpoch = storeEpoch
- const promise = apiGetMyFriendRequestList(FRIEND_REQUEST_PAGE_SIZE, oldest.id)
+ const requestEpoch = storeEpoch;
+ const promise = apiGetMyFriendRequestList(
+ FRIEND_REQUEST_PAGE_SIZE,
+ oldest.id,
+ )
.then((list) => {
- if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) {
- return
+ if (
+ requestEpoch !== storeEpoch ||
+ getCurrentUserId() !== requestUserId
+ ) {
+ return;
}
- const items = (list || []).map((request) => convertFriendRequest(request))
- this.friendRequests.push(...items)
- this.hasMoreFriendRequests = items.length >= FRIEND_REQUEST_PAGE_SIZE
- this.saveFriendRequestList()
+ const items = (list || []).map((request) =>
+ convertFriendRequest(request),
+ );
+ this.friendRequests.push(...items);
+ this.hasMoreFriendRequests = items.length >= FRIEND_REQUEST_PAGE_SIZE;
+ this.saveFriendRequestList();
})
.finally(() => {
if (
pendingLoadMoreRequests?.epoch === requestEpoch &&
pendingLoadMoreRequests.userId === requestUserId
) {
- pendingLoadMoreRequests = null
+ pendingLoadMoreRequests = null;
}
- })
- pendingLoadMoreRequests = { epoch: requestEpoch, userId: requestUserId, promise }
- return promise
+ });
+ pendingLoadMoreRequests = {
+ epoch: requestEpoch,
+ userId: requestUserId,
+ promise,
+ };
+ return promise;
},
/** 按 id 查申请记录;列表是按 id 倒序的小列表,O(n) find 即可,不再维护 Map 索引 */
getFriendRequest(requestId: number): FriendRequest | undefined {
- return this.friendRequests.find((request) => request.id === requestId)
+ return this.friendRequests.find((request) => request.id === requestId);
},
/** 按 id 从后端单查并 upsert 到本地(dispatcher 兜底用,避免全量重拉);后端带越权过滤 */
async fetchFriendRequest(requestId: number) {
- const requestEpoch = storeEpoch
- const requestUserId = getCurrentUserId()
- const data = await apiGetMyFriendRequest(requestId)
+ const requestEpoch = storeEpoch;
+ const requestUserId = getCurrentUserId();
+ const data = await apiGetMyFriendRequest(requestId);
if (!data) {
- return
+ return;
}
// clear() 已切账号:旧请求的申请记录不能再写进新账号的 friendRequests
if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) {
- return
+ return;
}
- this.upsertFriendRequest(convertFriendRequest(data))
+ this.upsertFriendRequest(convertFriendRequest(data));
},
/** 合并单条好友申请:已有则按 id 覆盖;新记录按 id 倒序插入(比本地最旧还老则跳过,留给 loadMore 带回) */
upsertFriendRequest(next: FriendRequest) {
void this.upsertFriendRequestForPull(next).catch((error) =>
- console.warn('[IM friendStore] 本地好友申请写入失败', error)
- )
+ console.warn('[IM friendStore] 本地好友申请写入失败', error),
+ );
},
/** 合并单条好友申请 */
async upsertFriendRequestForPull(next: FriendRequest): Promise {
- const existing = this.getFriendRequest(next.id)
+ const existing = this.getFriendRequest(next.id);
if (existing) {
- Object.assign(existing, next)
- await this.saveFriendRequestRecord(existing)
- return
+ Object.assign(existing, next);
+ await this.saveFriendRequestRecord(existing);
+ return;
}
// 比本地最旧 id 还老:不入列表,让 loadMore 自然带回,避免破坏 id 倒序 / 后续 loadMore 重复 push
- const oldest = this.friendRequests[this.friendRequests.length - 1]
+ const oldest = this.friendRequests[this.friendRequests.length - 1];
if (oldest && next.id < oldest.id) {
- return
+ return;
}
// 按 id 倒序找首个比自己小的位置插入;找不到则追加末尾
- const insertIndex = this.friendRequests.findIndex((request) => request.id < next.id)
+ const insertIndex = this.friendRequests.findIndex(
+ (request) => request.id < next.id,
+ );
if (insertIndex === -1) {
- this.friendRequests.push(next)
+ this.friendRequests.push(next);
} else {
- this.friendRequests.splice(insertIndex, 0, next)
+ this.friendRequests.splice(insertIndex, 0, next);
}
- await this.saveFriendRequestRecord(next)
+ await this.saveFriendRequestRecord(next);
},
/** 增量拉取好友申请变更并合并(重连 / 离线补偿);按 update_time + id 游标,已处理的按 handleResult 覆盖 */
async pullFriendRequests() {
- const requestEpoch = storeEpoch
- const requestUserId = getCurrentUserId()
- const isActive = () => requestEpoch === storeEpoch && getCurrentUserId() === requestUserId
+ const requestEpoch = storeEpoch;
+ const requestUserId = getCurrentUserId();
+ const isActive = () =>
+ requestEpoch === storeEpoch && getCurrentUserId() === requestUserId;
await runIncrementalPull(
StorageKeys.settings.friendRequestPullCursor,
apiPullMyFriendRequestList,
async (records) => {
if (!isActive()) {
- return false
+ return false;
}
await Promise.all(
- records.map((vo) => this.upsertFriendRequestForPull(convertFriendRequest(vo)))
- )
- return true
+ records.map((vo) =>
+ this.upsertFriendRequestForPull(convertFriendRequest(vo)),
+ ),
+ );
+ return true;
},
- isActive
- )
+ isActive,
+ );
},
// ==================== 好友关系操作 ====================
/** 删除好友(单向软删,本端置 DISABLE);clear=true 时级联清理本地相关数据(如私聊会话),并透传后端给多端同步 */
async deleteFriend(friendUserId: number, clear: boolean = true) {
- const requestEpoch = storeEpoch
- const requestUserId = getCurrentUserId()
- await apiDeleteFriend(friendUserId, clear)
+ const requestEpoch = storeEpoch;
+ const requestUserId = getCurrentUserId();
+ await apiDeleteFriend(friendUserId, clear);
if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) {
- return
+ return;
}
- this.removeFriend(friendUserId, clear)
+ this.removeFriend(friendUserId, clear);
},
/** 切换免打扰:同步会话的 silent 字段,避免会话列表 silent 图标等 1210 推到才更新 */
async setFriendSilent(friendUserId: number, silent: boolean) {
- const requestEpoch = storeEpoch
- const requestUserId = getCurrentUserId()
- await apiUpdateFriend({ friendUserId, silent })
+ const requestEpoch = storeEpoch;
+ const requestUserId = getCurrentUserId();
+ await apiUpdateFriend({ friendUserId, silent });
if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) {
- return
+ return;
}
- const friend = this.getFriend(friendUserId)
+ const friend = this.getFriend(friendUserId);
if (friend) {
- friend.silent = silent
- const conversationStore = useConversationStore()
- conversationStore.updateConversation(ImConversationType.PRIVATE, friendUserId, { silent })
- this.saveFriend(friend)
+ friend.silent = silent;
+ const conversationStore = useConversationStore();
+ conversationStore.updateConversation(
+ ImConversationType.PRIVATE,
+ friendUserId,
+ { silent },
+ );
+ this.saveFriend(friend);
}
},
/** 切换联系人置顶 */
async setFriendPinned(friendUserId: number, pinned: boolean) {
- const requestEpoch = storeEpoch
- const requestUserId = getCurrentUserId()
- await apiUpdateFriend({ friendUserId, pinned })
+ const requestEpoch = storeEpoch;
+ const requestUserId = getCurrentUserId();
+ await apiUpdateFriend({ friendUserId, pinned });
if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) {
- return
+ return;
}
- const friend = this.getFriend(friendUserId)
+ const friend = this.getFriend(friendUserId);
if (friend) {
- friend.pinned = pinned
- this.saveFriend(friend)
+ friend.pinned = pinned;
+ this.saveFriend(friend);
}
},
/** 拉黑好友:本端乐观更新 + 调接口;后端 FRIEND_BLOCK 推到时由 dispatcher 兜底同步多端 */
async blockFriend(friendUserId: number) {
- const requestEpoch = storeEpoch
- const requestUserId = getCurrentUserId()
- await apiBlockFriend(friendUserId)
+ const requestEpoch = storeEpoch;
+ const requestUserId = getCurrentUserId();
+ await apiBlockFriend(friendUserId);
if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) {
- return
+ return;
}
- const friend = this.getFriend(friendUserId)
+ const friend = this.getFriend(friendUserId);
if (friend) {
- friend.blocked = true
- this.saveFriend(friend)
+ friend.blocked = true;
+ this.saveFriend(friend);
}
},
/** 移出黑名单:本端乐观更新 + 调接口;后端 FRIEND_UNBLOCK 推到时由 dispatcher 兜底同步多端 */
async unblockFriend(friendUserId: number) {
- const requestEpoch = storeEpoch
- const requestUserId = getCurrentUserId()
- await apiUnblockFriend(friendUserId)
+ const requestEpoch = storeEpoch;
+ const requestUserId = getCurrentUserId();
+ await apiUnblockFriend(friendUserId);
if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) {
- return
+ return;
}
- const friend = this.getFriend(friendUserId)
+ const friend = this.getFriend(friendUserId);
if (friend) {
- friend.blocked = false
- this.saveFriend(friend)
+ friend.blocked = false;
+ this.saveFriend(friend);
}
},
/** 修改好友展示备注(仅自己可见) */
async setFriendDisplayName(friendUserId: number, displayName: string) {
- const requestEpoch = storeEpoch
- const requestUserId = getCurrentUserId()
- const value = displayName.trim()
+ const requestEpoch = storeEpoch;
+ const requestUserId = getCurrentUserId();
+ const value = displayName.trim();
// 后端 displayName 语义:null/undefined = 不改,"" = 清空,所以这里直接传 value(可能是空串)
- await apiUpdateFriend({ friendUserId, displayName: value })
+ await apiUpdateFriend({ friendUserId, displayName: value });
if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) {
- return
+ return;
}
- const friend = this.getFriend(friendUserId)
+ const friend = this.getFriend(friendUserId);
if (friend) {
- friend.displayName = value
- const conversationStore = useConversationStore()
- conversationStore.updateConversation(ImConversationType.PRIVATE, friendUserId, {
- name: getFriendDisplayName(friend)
- })
- this.saveFriend(friend)
+ friend.displayName = value;
+ const conversationStore = useConversationStore();
+ conversationStore.updateConversation(
+ ImConversationType.PRIVATE,
+ friendUserId,
+ {
+ name: getFriendDisplayName(friend),
+ },
+ );
+ this.saveFriend(friend);
}
},
/** 本地合并 / 新增某个好友(WebSocket 事件 & 手动刷新都用) */
upsertFriend(friend: Friend) {
void this.upsertFriendForPull(friend).catch((error) =>
- console.warn('[IM friendStore] 本地好友写入失败', error)
- )
+ console.warn('[IM friendStore] 本地好友写入失败', error),
+ );
},
/** 本地合并 / 新增某个好友 */
async upsertFriendForPull(friend: Friend): Promise {
const index = this.friends.findIndex(
- (existing) => existing.friendUserId === friend.friendUserId
- )
+ (existing) => existing.friendUserId === friend.friendUserId,
+ );
if (index === -1) {
this.friends.push({
...friend,
- status: friend.status ?? CommonStatusEnum.ENABLE
- })
+ status: friend.status ?? CommonStatusEnum.ENABLE,
+ });
} else {
this.friends[index] = {
...this.friends[index],
...friend,
- status: friend.status ?? CommonStatusEnum.ENABLE
- }
+ status: friend.status ?? CommonStatusEnum.ENABLE,
+ };
}
- const conversationStore = useConversationStore()
- const merged = this.getFriend(friend.friendUserId)
- conversationStore.updateConversation(ImConversationType.PRIVATE, friend.friendUserId, {
- name: merged ? getFriendDisplayName(merged) : friend.nickname,
- avatar: friend.avatar,
- silent: friend.silent
- })
- await this.saveFriendRecord(merged)
+ const conversationStore = useConversationStore();
+ const merged = this.getFriend(friend.friendUserId);
+ conversationStore.updateConversation(
+ ImConversationType.PRIVATE,
+ friend.friendUserId,
+ {
+ name: merged ? getFriendDisplayName(merged) : friend.nickname,
+ avatar: friend.avatar,
+ silent: friend.silent,
+ },
+ );
+ await this.saveFriendRecord(merged);
},
/** 本地标记删除(WebSocket FRIEND_DELETE 事件触发;clear=true 时级联清相关数据如私聊会话) */
removeFriend(friendUserId: number, clear: boolean = true) {
- const friend = this.getFriend(friendUserId)
+ const friend = this.getFriend(friendUserId);
if (friend) {
// blocked 不动,跟后端 deleteFriend0「删好友期间保留拉黑状态」对齐
- friend.status = CommonStatusEnum.DISABLE
- friend.deleteTime = Date.now()
+ friend.status = CommonStatusEnum.DISABLE;
+ friend.deleteTime = Date.now();
}
if (clear) {
- const conversationStore = useConversationStore()
- conversationStore.removePrivateConversation(friendUserId)
+ const conversationStore = useConversationStore();
+ conversationStore.removePrivateConversation(friendUserId);
}
- this.saveFriend(friend)
+ this.saveFriend(friend);
},
// ==================== WebSocket 事件 dispatcher(1201-1210 段) ====================
@@ -664,14 +757,16 @@ export const useFriendStore = defineStore('imFriendStore', {
/** FRIEND_REQUEST_RECEIVED(1203):收到新申请;payload 已带申请方昵称 / 头像,按 requestId 直推 push 进列表 */
applyFriendRequestReceivedNotification(payload: FriendNotificationPayload) {
if (!payload.requestId) {
- return
+ return;
}
- const currentUserId = getCurrentUserId()
- const existingIndex = this.friendRequests.findIndex((item) => item.id === payload.requestId)
+ const currentUserId = getCurrentUserId();
+ const existingIndex = this.friendRequests.findIndex(
+ (item) => item.id === payload.requestId,
+ );
if (existingIndex !== -1) {
- const existing = this.friendRequests.splice(existingIndex, 1)[0]
+ const existing = this.friendRequests.splice(existingIndex, 1)[0];
if (!existing) {
- return
+ return;
}
const next = {
...existing,
@@ -682,11 +777,11 @@ export const useFriendStore = defineStore('imFriendStore', {
addSource: payload.addSource,
createTime: Date.now(),
fromNickname: payload.fromNickname,
- fromAvatar: payload.fromAvatar
- }
- this.friendRequests.unshift(next)
- this.saveFriendRequest(next)
- return
+ fromAvatar: payload.fromAvatar,
+ };
+ this.friendRequests.unshift(next);
+ this.saveFriendRequest(next);
+ return;
}
const next = {
id: payload.requestId,
@@ -697,30 +792,33 @@ export const useFriendStore = defineStore('imFriendStore', {
addSource: payload.addSource,
createTime: Date.now(),
fromNickname: payload.fromNickname,
- fromAvatar: payload.fromAvatar
- }
- this.friendRequests.unshift(next)
- this.saveFriendRequest(next)
+ fromAvatar: payload.fromAvatar,
+ };
+ this.friendRequests.unshift(next);
+ this.saveFriendRequest(next);
},
/** FRIEND_REQUEST_APPROVED(1201):我的申请被同意;按 requestId 更新状态(FRIEND_ADD 会另外推) */
applyFriendRequestApprovedNotification(payload: FriendNotificationPayload) {
if (!payload.requestId) {
- return
+ return;
}
- void this.applyHandleResult(payload.requestId, ImFriendRequestHandleResult.AGREED)
+ void this.applyHandleResult(
+ payload.requestId,
+ ImFriendRequestHandleResult.AGREED,
+ );
},
/** FRIEND_REQUEST_REJECTED(1202):我的申请被拒绝;按 requestId 更新状态 */
applyFriendRequestRejectedNotification(payload: FriendNotificationPayload) {
if (!payload.requestId) {
- return
+ return;
}
void this.applyHandleResult(
payload.requestId,
ImFriendRequestHandleResult.REFUSED,
- payload.handleContent
- )
+ payload.handleContent,
+ );
},
/**
@@ -728,81 +826,91 @@ export const useFriendStore = defineStore('imFriendStore', {
* peerUserId 由 websocketStore 按帧 sender / receiver 算好传入:becomeFriends 单条入库后双方收到同一份 payload,
* 本端真正的「对端」是帧上的另一个用户,不是 payload.friendUserId(payload 里固定是 toUserId)。
*/
- applyFriendAddNotification(_payload: FriendNotificationPayload, peerUserId: number) {
+ applyFriendAddNotification(
+ _payload: FriendNotificationPayload,
+ peerUserId: number,
+ ) {
if (this.isActiveFriend(peerUserId)) {
- return
+ return;
}
- void this.fetchFriendInfo(peerUserId)
+ void this.fetchFriendInfo(peerUserId);
},
/**
* FRIEND_DELETE(1205):好友被删除;本端清理 + 按 payload.clear 决定是否级联清会话(多端跟主操作端一致)
* peerUserId 由 websocketStore 按帧 sender / receiver 算好传入;与 FRIEND_ADD 保持一致的 peer 推断
*/
- applyFriendDeleteNotification(payload: FriendNotificationPayload, peerUserId: number) {
- this.removeFriend(peerUserId, payload.clear !== false)
+ applyFriendDeleteNotification(
+ payload: FriendNotificationPayload,
+ peerUserId: number,
+ ) {
+ this.removeFriend(peerUserId, payload.clear !== false);
},
/** FRIEND_BLOCK(1207):拉黑;多端同步 */
applyFriendBlockNotification(payload: FriendNotificationPayload) {
- const friend = this.getFriend(payload.friendUserId)
+ const friend = this.getFriend(payload.friendUserId);
if (friend) {
- friend.blocked = true
- this.saveFriend(friend)
+ friend.blocked = true;
+ this.saveFriend(friend);
}
},
/** FRIEND_UNBLOCK(1208):移出黑名单;多端同步 */
applyFriendUnblockNotification(payload: FriendNotificationPayload) {
- const friend = this.getFriend(payload.friendUserId)
+ const friend = this.getFriend(payload.friendUserId);
if (friend) {
- friend.blocked = false
- this.saveFriend(friend)
+ friend.blocked = false;
+ this.saveFriend(friend);
}
},
/** FRIEND_INFO_UPDATED(1209):好友资料变更(昵称 / 头像);重拉详情 */
applyFriendInfoUpdatedNotification(payload: FriendNotificationPayload) {
- void this.fetchFriendInfo(payload.friendUserId)
+ void this.fetchFriendInfo(payload.friendUserId);
},
/** FRIEND_UPDATE(1210):批量更新(备注 / 免打扰 / 联系人置顶);多端同步 */
applyFriendUpdateNotification(payload: FriendNotificationPayload) {
- const friend = this.getFriend(payload.friendUserId)
+ const friend = this.getFriend(payload.friendUserId);
if (!friend) {
- return
+ return;
}
- if (payload.displayName != null) {
- friend.displayName = payload.displayName
+ if (payload.displayName !== null) {
+ friend.displayName = payload.displayName;
}
- if (payload.silent != null) {
- friend.silent = payload.silent
+ if (payload.silent !== null) {
+ friend.silent = payload.silent;
}
- if (payload.pinned != null) {
- friend.pinned = payload.pinned
+ if (payload.pinned !== null) {
+ friend.pinned = payload.pinned;
}
- const conversationStore = useConversationStore()
- conversationStore.updateConversation(ImConversationType.PRIVATE, payload.friendUserId, {
- name: getFriendDisplayName(friend),
- silent: friend.silent
- })
- this.saveFriend(friend)
+ const conversationStore = useConversationStore();
+ conversationStore.updateConversation(
+ ImConversationType.PRIVATE,
+ payload.friendUserId,
+ {
+ name: getFriendDisplayName(friend),
+ silent: friend.silent,
+ },
+ );
+ this.saveFriend(friend);
},
/** 清空好友内存状态,并废弃未返回请求(pending Promise 置空 + storeEpoch++) */
clear() {
- this.friends = []
- this.friendRequests = []
- this.loaded = false
- this.hasMoreFriendRequests = true
- pendingFetchFriends = null
- pendingFetchRequests = null
- pendingLoadMoreRequests = null
- pendingFetchFriendInfos.clear()
- storeEpoch++
- }
- }
-})
+ this.friends = [];
+ this.friendRequests = [];
+ this.loaded = false;
+ this.hasMoreFriendRequests = true;
+ pendingFetchFriends = null;
+ pendingFetchRequests = null;
+ pendingLoadMoreRequests = null;
+ pendingFetchFriendInfos.clear();
+ storeEpoch++;
+ },
+ },
+});
function convertFriend(vo: ImFriendApi.FriendRespVO): Friend {
return {
@@ -819,11 +927,13 @@ function convertFriend(vo: ImFriendApi.FriendRespVO): Friend {
blocked: !!vo.blocked,
status: vo.status,
addTime: vo.addTime ? new Date(vo.addTime).getTime() : undefined,
- deleteTime: vo.deleteTime ? new Date(vo.deleteTime).getTime() : undefined
- }
+ deleteTime: vo.deleteTime ? new Date(vo.deleteTime).getTime() : undefined,
+ };
}
-function convertFriendRequest(vo: ImFriendRequestApi.FriendRequestRespVO): FriendRequest {
+function convertFriendRequest(
+ vo: ImFriendRequestApi.FriendRequestRespVO,
+): FriendRequest {
return {
id: vo.id,
fromUserId: vo.fromUserId,
@@ -837,13 +947,13 @@ function convertFriendRequest(vo: ImFriendRequestApi.FriendRequestRespVO): Frien
fromNickname: vo.fromNickname,
fromAvatar: vo.fromAvatar,
toNickname: vo.toNickname,
- toAvatar: vo.toAvatar
- }
+ toAvatar: vo.toAvatar,
+ };
}
-export const useFriendStoreWithOut = () => useFriendStore()
+export const useFriendStoreWithOut = () => useFriendStore();
// dev: 让 Pinia 的 actions / state 改动支持 HMR,避免每次改 store 都得硬刷
if (import.meta.hot) {
- import.meta.hot.accept(acceptHMRUpdate(useFriendStore, import.meta.hot))
+ import.meta.hot.accept(acceptHMRUpdate(useFriendStore, import.meta.hot));
}
diff --git a/apps/web-antd/src/views/im/home/store/groupRequestStore.ts b/apps/web-antd/src/views/im/home/store/groupRequestStore.ts
index 24ea5dae3..6c4f1fbb8 100644
--- a/apps/web-antd/src/views/im/home/store/groupRequestStore.ts
+++ b/apps/web-antd/src/views/im/home/store/groupRequestStore.ts
@@ -1,19 +1,25 @@
-import type { ImGroupRequestApi } from '#/api/im/group/request'
+import type { ImGroupRequestApi } from '#/api/im/group/request';
-import { acceptHMRUpdate, defineStore } from 'pinia'
+import { acceptHMRUpdate, defineStore } from 'pinia';
-import { agreeGroupRequest as apiAgreeGroupRequest, getMyGroupRequest as apiGetMyGroupRequest, getUnhandledRequestList as apiGetUnhandledRequestList, pullMyGroupRequestList as apiPullMyGroupRequestList, refuseGroupRequest as apiRefuseGroupRequest } from '#/api/im/group/request'
-import { getCurrentUserId } from '#/views/im/utils/auth'
-import { ImGroupRequestHandleResult } from '#/views/im/utils/constants'
+import {
+ agreeGroupRequest as apiAgreeGroupRequest,
+ getMyGroupRequest as apiGetMyGroupRequest,
+ getUnhandledRequestList as apiGetUnhandledRequestList,
+ pullMyGroupRequestList as apiPullMyGroupRequestList,
+ refuseGroupRequest as apiRefuseGroupRequest,
+} from '#/api/im/group/request';
+import { getCurrentUserId } from '#/views/im/utils/auth';
+import { ImGroupRequestHandleResult } from '#/views/im/utils/constants';
-import { getDb, StorageKeys } from '../../utils/db'
-import { runIncrementalPull } from '../../utils/pull'
+import { getDb, StorageKeys } from '../../utils/db';
+import { runIncrementalPull } from '../../utils/pull';
-type PendingRequest = { epoch: number; promise: Promise; userId: number; }
+type PendingRequest = { epoch: number; promise: Promise; userId: number };
/** clear() 时递增;旧账号 in-flight 的 pullGroupRequests 结果 resolve 后比对一致才写 store,防跨账号红点污染(与 friendStore 同口径) */
-let storeEpoch = 0
-let pendingUnhandledFetch: null | PendingRequest = null
+let storeEpoch = 0;
+let pendingUnhandledFetch: null | PendingRequest = null;
/**
* IM 加群申请 Store
@@ -33,7 +39,7 @@ export const useGroupRequestStore = defineStore('imGroupRequestStore', {
/** 我管理的所有群下未处理申请列表(按 id 倒序) */
unhandledList: [] as ImGroupRequestApi.GroupRequestRespVO[],
/** fetchUnhandledGroupRequestList 是否成功执行过;避免横幅显示 0 然后跳数字的闪烁 */
- loaded: false
+ loaded: false,
}),
getters: {
@@ -41,38 +47,45 @@ export const useGroupRequestStore = defineStore('imGroupRequestStore', {
* 各群下未处理申请数的 Map;O(N) 扫一次缓存供 ConversationItem 等 N 处复用,避免 N×M 重复 filter
*/
getUnhandledGroupRequestCountMap(state): Map {
- const map = new Map()
+ const map = new Map();
for (const request of state.unhandledList) {
- map.set(request.groupId, (map.get(request.groupId) ?? 0) + 1)
+ map.set(request.groupId, (map.get(request.groupId) ?? 0) + 1);
}
- return map
+ return map;
},
/** 指定群下的未处理申请数 */
getUnhandledGroupRequestCount(): (groupId: number) => number {
- return (groupId: number) => this.getUnhandledGroupRequestCountMap.get(groupId) ?? 0
+ return (groupId: number) =>
+ this.getUnhandledGroupRequestCountMap.get(groupId) ?? 0;
},
/** 指定群下的未处理申请列表 */
getUnhandledGroupRequestListByGroupId:
(state) =>
(groupId: number): ImGroupRequestApi.GroupRequestRespVO[] =>
- state.unhandledList.filter((r) => r.groupId === groupId)
+ state.unhandledList.filter((r) => r.groupId === groupId),
},
actions: {
/** 从 IndexedDB 恢复加群申请 */
async loadGroupRequestList(): Promise {
try {
- const cached = await getDb().getAll('groupRequests')
+ const cached =
+ await getDb().getAll(
+ 'groupRequests',
+ );
if (!cached || cached.length === 0) {
- return false
+ return false;
}
this.unhandledList = cached
- .filter((request) => request.handleResult === ImGroupRequestHandleResult.UNHANDLED)
- .toSorted((requestA, requestB) => requestB.id - requestA.id)
- return true
+ .filter(
+ (request) =>
+ request.handleResult === ImGroupRequestHandleResult.UNHANDLED,
+ )
+ .toSorted((requestA, requestB) => requestB.id - requestA.id);
+ return true;
} catch (error) {
- console.warn('[IM groupRequestStore] 本地加群申请缓存读取失败', error)
- return false
+ console.warn('[IM groupRequestStore] 本地加群申请缓存读取失败', error);
+ return false;
}
},
@@ -80,55 +93,69 @@ export const useGroupRequestStore = defineStore('imGroupRequestStore', {
saveGroupRequestList(): void {
void getDb()
.transaction(['groupRequests'], 'readwrite', async (tx) => {
- const db = getDb()
- await db.clearStore('groupRequests', tx)
+ const db = getDb();
+ await db.clearStore('groupRequests', tx);
for (const request of this.unhandledList) {
- await db.put('groupRequests', request, tx)
+ await db.put('groupRequests', request, tx);
}
})
- .catch((error) => console.warn('[IM groupRequestStore] 本地加群申请缓存写入失败', error))
+ .catch((error) =>
+ console.warn(
+ '[IM groupRequestStore] 本地加群申请缓存写入失败',
+ error,
+ ),
+ );
},
/** 保存单条加群申请 */
- async saveGroupRequestRecord(request: ImGroupRequestApi.GroupRequestRespVO): Promise {
- await getDb().put('groupRequests', request)
+ async saveGroupRequestRecord(
+ request: ImGroupRequestApi.GroupRequestRespVO,
+ ): Promise {
+ await getDb().put('groupRequests', request);
},
/** 保存单条加群申请 */
saveGroupRequest(request: ImGroupRequestApi.GroupRequestRespVO): void {
void this.saveGroupRequestRecord(request).catch((error) =>
- console.warn('[IM groupRequestStore] 本地加群申请写入失败', error)
- )
+ console.warn('[IM groupRequestStore] 本地加群申请写入失败', error),
+ );
},
/** 拉取我管理的所有群下未处理申请;进 IM 后 / 升级 admin 后 / WS 推送有冲突时调用 */
async fetchUnhandledGroupRequestList() {
- const requestEpoch = storeEpoch
- const requestUserId = getCurrentUserId()
+ const requestEpoch = storeEpoch;
+ const requestUserId = getCurrentUserId();
if (
pendingUnhandledFetch?.epoch === requestEpoch &&
pendingUnhandledFetch.userId === requestUserId
) {
- return pendingUnhandledFetch.promise
+ return pendingUnhandledFetch.promise;
}
const promise = (async () => {
- const list = await apiGetUnhandledRequestList()
- if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) {
- return
+ const list = await apiGetUnhandledRequestList();
+ if (
+ requestEpoch !== storeEpoch ||
+ getCurrentUserId() !== requestUserId
+ ) {
+ return;
}
- this.unhandledList = list || []
- this.loaded = true
- this.saveGroupRequestList()
+ this.unhandledList = list || [];
+ this.loaded = true;
+ this.saveGroupRequestList();
})().finally(() => {
if (
pendingUnhandledFetch?.epoch === requestEpoch &&
pendingUnhandledFetch.userId === requestUserId
) {
- pendingUnhandledFetch = null
+ pendingUnhandledFetch = null;
}
- })
- pendingUnhandledFetch = { epoch: requestEpoch, userId: requestUserId, promise }
- return promise
+ });
+ pendingUnhandledFetch = {
+ epoch: requestEpoch,
+ userId: requestUserId,
+ promise,
+ };
+ return promise;
},
/**
@@ -137,16 +164,16 @@ export const useGroupRequestStore = defineStore('imGroupRequestStore', {
* 同一对 group_id, user_id 复用记录时 requestId 不变但 applyContent / inviterUserId 会刷新,所以无条件 fetch + 排到头部
*/
async addGroupRequestById(requestId: number) {
- const requestEpoch = storeEpoch
- const requestUserId = getCurrentUserId()
- const request = await apiGetMyGroupRequest(requestId)
+ const requestEpoch = storeEpoch;
+ const requestUserId = getCurrentUserId();
+ const request = await apiGetMyGroupRequest(requestId);
if (!request) {
- return
+ return;
}
if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) {
- return
+ return;
}
- this.upsertGroupRequest(request)
+ this.upsertGroupRequest(request);
},
/**
@@ -156,18 +183,23 @@ export const useGroupRequestStore = defineStore('imGroupRequestStore', {
*/
upsertGroupRequest(request: ImGroupRequestApi.GroupRequestRespVO) {
void this.upsertGroupRequestForPull(request).catch((error) =>
- console.warn('[IM groupRequestStore] 本地加群申请写入失败', error)
- )
+ console.warn('[IM groupRequestStore] 本地加群申请写入失败', error),
+ );
},
/** 本地合并 / 新增单条加群申请 */
- async upsertGroupRequestForPull(request: ImGroupRequestApi.GroupRequestRespVO): Promise {
+ async upsertGroupRequestForPull(
+ request: ImGroupRequestApi.GroupRequestRespVO,
+ ): Promise {
if (request.handleResult !== ImGroupRequestHandleResult.UNHANDLED) {
- await this.removeGroupRequestByIdForPull(request.id)
- return
+ await this.removeGroupRequestByIdForPull(request.id);
+ return;
}
- this.unhandledList = [request, ...this.unhandledList.filter((r) => r.id !== request.id)]
- await this.saveGroupRequestRecord(request)
+ this.unhandledList = [
+ request,
+ ...this.unhandledList.filter((r) => r.id !== request.id),
+ ];
+ await this.saveGroupRequestRecord(request);
},
/**
@@ -178,65 +210,72 @@ export const useGroupRequestStore = defineStore('imGroupRequestStore', {
*/
async pullGroupRequests() {
// 快照 epoch;账号在拉取途中切换(clear() → epoch++)时丢弃旧账号那几页结果,防跨账号红点污染
- const requestEpoch = storeEpoch
- const requestUserId = getCurrentUserId()
- const isActive = () => requestEpoch === storeEpoch && getCurrentUserId() === requestUserId
+ const requestEpoch = storeEpoch;
+ const requestUserId = getCurrentUserId();
+ const isActive = () =>
+ requestEpoch === storeEpoch && getCurrentUserId() === requestUserId;
await runIncrementalPull(
StorageKeys.settings.groupRequestPullCursor,
apiPullMyGroupRequestList,
async (records) => {
if (!isActive()) {
- return false
+ return false;
}
- await Promise.all(records.map((vo) => this.upsertGroupRequestForPull(vo)))
- return true
+ await Promise.all(
+ records.map((vo) => this.upsertGroupRequestForPull(vo)),
+ );
+ return true;
},
- isActive
- )
+ isActive,
+ );
if (isActive()) {
- this.loaded = true
+ this.loaded = true;
}
},
/** WS 收到 1505 / 1506 或本端处理完一条:按 requestId 从列表移除 */
removeGroupRequestById(requestId: number) {
- this.unhandledList = this.unhandledList.filter((r) => r.id !== requestId)
+ this.unhandledList = this.unhandledList.filter((r) => r.id !== requestId);
void getDb()
.delete('groupRequests', requestId)
- .catch((error) => console.warn('[IM groupRequestStore] 本地加群申请删除失败', error))
+ .catch((error) =>
+ console.warn('[IM groupRequestStore] 本地加群申请删除失败', error),
+ );
},
/** 删除单条加群申请 */
async removeGroupRequestByIdForPull(requestId: number): Promise {
- this.unhandledList = this.unhandledList.filter((r) => r.id !== requestId)
- await getDb().delete('groupRequests', requestId)
+ this.unhandledList = this.unhandledList.filter((r) => r.id !== requestId);
+ await getDb().delete('groupRequests', requestId);
},
/** 同意申请;本端处理后立即从列表移除,避免被反复点击 */
async agreeGroupRequest(requestId: number) {
- await apiAgreeGroupRequest(requestId)
- this.removeGroupRequestById(requestId)
+ await apiAgreeGroupRequest(requestId);
+ this.removeGroupRequestById(requestId);
},
/** 拒绝申请 */
async refuseGroupRequest(requestId: number, handleContent?: string) {
- await apiRefuseGroupRequest(requestId, handleContent)
- this.removeGroupRequestById(requestId)
+ await apiRefuseGroupRequest(requestId, handleContent);
+ this.removeGroupRequestById(requestId);
},
/** 清空加群申请内存 */
clear() {
- this.unhandledList = []
- this.loaded = false
+ this.unhandledList = [];
+ this.loaded = false;
// 账号切换:递增 epoch 废弃旧账号 in-flight 的 pullGroupRequests 结果,避免写进新账号红点列表
- storeEpoch++
- pendingUnhandledFetch = null
- }
- }
-})
+ storeEpoch++;
+ pendingUnhandledFetch = null;
+ },
+ },
+});
-export const useGroupRequestStoreWithOut = () => useGroupRequestStore()
+export const useGroupRequestStoreWithOut = () => useGroupRequestStore();
if (import.meta.hot) {
- import.meta.hot.accept(acceptHMRUpdate(useGroupRequestStore, import.meta.hot))
+ import.meta.hot.accept(
+ acceptHMRUpdate(useGroupRequestStore, import.meta.hot),
+ );
}
diff --git a/apps/web-antd/src/views/im/home/store/groupStore.ts b/apps/web-antd/src/views/im/home/store/groupStore.ts
index 8db5512fd..cc0af8ec3 100644
--- a/apps/web-antd/src/views/im/home/store/groupStore.ts
+++ b/apps/web-antd/src/views/im/home/store/groupStore.ts
@@ -1,48 +1,62 @@
-import type { Group, GroupDO, GroupMember, Message } from '../types'
+import type { GroupNotificationPayload } from '../../utils/message';
+import type { Group, GroupDO, GroupMember, Message } from '../types';
-import type { ImGroupApi } from '#/api/im/group'
-import type { ImGroupMemberApi } from '#/api/im/group/member'
+import type { ImGroupApi } from '#/api/im/group';
+import type { ImGroupMemberApi } from '#/api/im/group/member';
-import { CommonStatusEnum } from '@vben/constants'
+import { CommonStatusEnum } from '@vben/constants';
-import { acceptHMRUpdate, defineStore } from 'pinia'
+import { acceptHMRUpdate, defineStore } from 'pinia';
-import { getGroup as apiGetGroup, getMyGroupList as apiGetMyGroupList } from '#/api/im/group'
-import { getGroupMember as apiGetGroupMember, getGroupMemberList as apiGetGroupMemberList, updateGroupMember as apiUpdateGroupMember } from '#/api/im/group/member'
-import { getCurrentUserId } from '#/views/im/utils/auth'
+import {
+ getGroup as apiGetGroup,
+ getMyGroupList as apiGetMyGroupList,
+} from '#/api/im/group';
+import {
+ getGroupMember as apiGetGroupMember,
+ getGroupMemberList as apiGetGroupMemberList,
+ updateGroupMember as apiUpdateGroupMember,
+} from '#/api/im/group/member';
+import { getCurrentUserId } from '#/views/im/utils/auth';
import {
ImContentType,
ImConversationType,
ImGroupMemberRole,
- ImMessageStatus
-} from '../../utils/constants'
-import { getDb } from '../../utils/db'
-import { type GroupNotificationPayload } from '../../utils/message'
-import { getGroupDisplayName } from '../../utils/user'
-import { useConversationStore } from './conversationStore'
-import { useGroupRequestStore } from './groupRequestStore'
+ ImMessageStatus,
+} from '../../utils/constants';
+import { getDb } from '../../utils/db';
+import { getGroupDisplayName } from '../../utils/user';
+import { useConversationStore } from './conversationStore';
+import { useGroupRequestStore } from './groupRequestStore';
/** clear() 时递增;旧账号 in-flight 的成员请求返回后比对一致才写 store */
-let storeEpoch = 0
+let storeEpoch = 0;
/**
* fetchGroupMemberList 并发去重表:同 groupId 同时进的请求共用一个 Promise
*
* key 必须带 userId——账号切换时 A 的请求不能被 B 复用,否则 IIFE 内部的 saveGroupMemberList 会把 A 的成员数据写进 B 的 IDB 桶
*/
-const pendingMemberFetches = new Map>()
-const pendingMemberKey = (userId: number, groupId: number) => `${userId}:${groupId}`
+const pendingMemberFetches = new Map>();
+const pendingMemberKey = (userId: number, groupId: number) =>
+ `${userId}:${groupId}`;
/**
* fetchGroupMember 单成员并发去重表:同 (groupId, memberUserId) 同时进的请求共用一个 Promise
*
* 跟整群表分开:单成员 fetch 跟整群 fetch 语义不同(单成员不回填 me 的 silent),不能互相代替
*/
-const pendingSingleMemberFetches = new Map>()
+const pendingSingleMemberFetches = new Map<
+ string,
+ Promise
+>();
-const pendingSingleMemberKey = (userId: number, groupId: number, memberUserId: number) =>
- `${userId}:${groupId}:${memberUserId}`
+const pendingSingleMemberKey = (
+ userId: number,
+ groupId: number,
+ memberUserId: number,
+) => `${userId}:${groupId}:${memberUserId}`;
/** 构建群 IndexedDB 记录 */
function buildGroupDO(group: Group): GroupDO {
@@ -54,21 +68,21 @@ function buildGroupDO(group: Group): GroupDO {
membersLoaded: _membersLoaded,
membersExpired: _membersExpired,
...record
- } = group
- return record
+ } = group;
+ return record;
}
/** 判断当前用户是否在 payload.memberUserIds 里(GROUP_CREATE / INVITE / KICK 自判用) */
function isSelfInPayloadMembers(payload: GroupNotificationPayload): boolean {
- const selfUserId = getCurrentUserId()
- return !!selfUserId && (payload.memberUserIds || []).includes(selfUserId)
+ const selfUserId = getCurrentUserId();
+ return !!selfUserId && (payload.memberUserIds || []).includes(selfUserId);
}
/** 刷新我管理的群申请红点 */
function refreshUnhandledGroupRequests(): void {
useGroupRequestStore()
.fetchUnhandledGroupRequestList()
- .catch(() => undefined)
+ .catch(() => undefined);
}
/**
@@ -83,22 +97,22 @@ export const useGroupStore = defineStore('imGroupStore', {
state: () => ({
groups: [] as Group[],
loaded: false, // 仅 fetchGroupList 成功后置位;loadGroupList(IDB)不置位,否则后台 SWR 刷新会被缓存命中跳过
- groupMembersExpired: false // 进入 IM / 重连后置位;IDB 里的成员桶延迟加载到内存时,也要按过期处理
+ groupMembersExpired: false, // 进入 IM / 重连后置位;IDB 里的成员桶延迟加载到内存时,也要按过期处理
}),
getters: {
getGroup:
(state) =>
(id: number): Group | undefined => {
- return state.groups.find((g) => g.id === id)
+ return state.groups.find((g) => g.id === id);
},
/** 群成员 userId → GroupMember 索引;调用方按 userId 反查昵称 / 头像等元信息 */
getGroupMemberMap:
(state) =>
(id: number): Map => {
- const group = state.groups.find((g) => g.id === id)
- return new Map((group?.members || []).map((m) => [m.userId, m]))
- }
+ const group = state.groups.find((g) => g.id === id);
+ return new Map((group?.members || []).map((m) => [m.userId, m]));
+ },
},
actions: {
@@ -107,15 +121,15 @@ export const useGroupStore = defineStore('imGroupStore', {
/** 从 IndexedDB 恢复群列表 */
async loadGroupList(): Promise {
try {
- const cached = await getDb().getAll('groups')
+ const cached = await getDb().getAll('groups');
if (!cached || cached.length === 0) {
- return false
+ return false;
}
- this.groups = cached
- return true
+ this.groups = cached;
+ return true;
} catch (error) {
- console.warn('[IM groupStore] 本地群缓存读取失败', error)
- return false
+ console.warn('[IM groupStore] 本地群缓存读取失败', error);
+ return false;
}
},
@@ -123,54 +137,56 @@ export const useGroupStore = defineStore('imGroupStore', {
saveGroupList(): void {
void getDb()
.transaction(['groups'], 'readwrite', async (tx) => {
- const db = getDb()
- await db.clearStore('groups', tx)
+ const db = getDb();
+ await db.clearStore('groups', tx);
for (const group of this.groups) {
- await db.put('groups', buildGroupDO(group), tx)
+ await db.put('groups', buildGroupDO(group), tx);
}
})
- .catch((error) => console.warn('[IM groupStore] 本地群缓存写入失败', error))
+ .catch((error) =>
+ console.warn('[IM groupStore] 本地群缓存写入失败', error),
+ );
},
/** 保存单个群 */
async saveGroupRecord(group: Group | undefined): Promise {
if (!group) {
- return
+ return;
}
- await getDb().put('groups', buildGroupDO(group))
+ await getDb().put('groups', buildGroupDO(group));
},
/** 保存单个群 */
saveGroup(group: Group | undefined): void {
void this.saveGroupRecord(group).catch((error) =>
- console.warn('[IM groupStore] 本地群写入失败', error)
- )
+ console.warn('[IM groupStore] 本地群写入失败', error),
+ );
},
/** 从 IndexedDB 恢复指定群成员 */
async loadGroupMemberList(groupId: number): Promise {
// in-memory 已"完整"加载(fetchGroupMemberList 跑过或上次冷启动从 IDB 整桶恢复过):直接复用;
// 单成员补齐(fetchGroupMember)写进的 partial members 不在此返回缓存——其 membersLoaded=false
- const cachedGroup = this.getGroup(groupId)
+ const cachedGroup = this.getGroup(groupId);
if (cachedGroup?.members && cachedGroup.membersLoaded) {
- return cachedGroup.members
+ return cachedGroup.members;
}
try {
const cached = await getDb().getAllByIndex(
'groupMembers',
'groupId',
- groupId
- )
+ groupId,
+ );
if (!cached || cached.length === 0) {
- return null
+ return null;
}
// 把 IDB 拿到的成员落到对应 group
- const group = this.getGroup(groupId)
+ const group = this.getGroup(groupId);
if (group) {
- group.members = cached
- group.memberCount = cached.length
- group.membersLoaded = true
- group.membersExpired = this.groupMembersExpired
+ group.members = cached;
+ group.memberCount = cached.length;
+ group.membersLoaded = true;
+ group.membersExpired = this.groupMembersExpired;
} else {
// group 还没就位:仅 in-memory 占位(name='' 表示未知),不调 upsertGroup —— 避免把假名灌进 conversation.name + groups IDB 桶;
// 后续,等 fetchGroupList 浅合并时,被真名覆盖
@@ -180,35 +196,42 @@ export const useGroupStore = defineStore('imGroupStore', {
members: cached,
memberCount: cached.length,
membersLoaded: true,
- membersExpired: this.groupMembersExpired
- })
+ membersExpired: this.groupMembersExpired,
+ });
}
- return cached
+ return cached;
} catch (error) {
- console.warn('[IM groupStore] 本地群成员缓存读取失败', { groupId }, error)
- return null
+ console.warn(
+ '[IM groupStore] 本地群成员缓存读取失败',
+ { groupId },
+ error,
+ );
+ return null;
}
},
/** 保存指定群成员 */
saveGroupMemberList(groupId: number): void {
- const members = this.getGroup(groupId)?.members
+ const members = this.getGroup(groupId)?.members;
if (!members) {
- return
+ return;
}
void getDb()
.transaction(['groupMembers'], 'readwrite', async (tx) => {
- const db = getDb()
- await db.deleteByIndex('groupMembers', 'groupId', groupId, tx)
+ const db = getDb();
+ await db.deleteByIndex('groupMembers', 'groupId', groupId, tx);
for (const member of members) {
if (member.id) {
- await db.put('groupMembers', member, tx)
+ await db.put('groupMembers', member, tx);
}
}
})
.catch((error) =>
- console.warn(`[IM groupStore] 本地群成员缓存写入失败 (groupId=${groupId})`, error)
- )
+ console.warn(
+ `[IM groupStore] 本地群成员缓存写入失败 (groupId=${groupId})`,
+ error,
+ ),
+ );
},
// ==================== 远端拉取 ====================
@@ -216,22 +239,22 @@ export const useGroupStore = defineStore('imGroupStore', {
/** 拉取群列表;同步刷新对应群聊会话的展示名 / 头像 + 落 IDB */
async fetchGroupList(force = false) {
if (this.loaded && !force) {
- return
+ return;
}
- const requestEpoch = storeEpoch
- const requestUserId = getCurrentUserId()
+ const requestEpoch = storeEpoch;
+ const requestUserId = getCurrentUserId();
// 拉取当前登录用户加入的所有群(不带成员;成员按需再走 fetchGroupMemberList)
- const list = await apiGetMyGroupList()
+ const list = await apiGetMyGroupList();
if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) {
- return
+ return;
}
- const fresh = (list || []).map((group) => convertGroup(group))
+ const fresh = (list || []).map((group) => convertGroup(group));
// 合并而非全量替换:成员缓存只在成员列表接口维护,群个人设置以群列表接口为准
- const groupMap = new Map(this.groups.map((group) => [group.id, group]))
+ const groupMap = new Map(this.groups.map((group) => [group.id, group]));
this.groups = fresh.map((group) => {
- const existing = groupMap.get(group.id)
+ const existing = groupMap.get(group.id);
if (!existing) {
- return { ...group, activeCallExpired: true, infoLoaded: true }
+ return { ...group, activeCallExpired: true, infoLoaded: true };
}
return {
...group,
@@ -241,26 +264,30 @@ export const useGroupStore = defineStore('imGroupStore', {
members: existing.members,
memberCount: existing.memberCount ?? group.memberCount,
membersLoaded: existing.membersLoaded,
- membersExpired: existing.membersExpired
- }
- })
- this.loaded = true
- const conversationStore = useConversationStore()
+ membersExpired: existing.membersExpired,
+ };
+ });
+ this.loaded = true;
+ const conversationStore = useConversationStore();
for (const group of this.groups) {
- conversationStore.updateConversation(ImConversationType.GROUP, group.id, {
- name: getGroupDisplayName(group),
- avatar: group.avatar,
- silent: group.silent
- })
+ conversationStore.updateConversation(
+ ImConversationType.GROUP,
+ group.id,
+ {
+ name: getGroupDisplayName(group),
+ avatar: group.avatar,
+ silent: group.silent,
+ },
+ );
}
- this.saveGroupList()
- this.preloadMembersForEmptyAvatarGroups()
+ this.saveGroupList();
+ this.preloadMembersForEmptyAvatarGroups();
},
/** 失效全部群详情缓存 */
markAllGroupInfoExpired() {
for (const group of this.groups) {
- group.infoLoaded = false
+ group.infoLoaded = false;
}
},
@@ -270,23 +297,32 @@ export const useGroupStore = defineStore('imGroupStore', {
if (
group.avatar ||
group.joinStatus === CommonStatusEnum.DISABLE ||
- (group.membersLoaded && !group.membersExpired && group.members?.length)
+ (group.membersLoaded &&
+ !group.membersExpired &&
+ group.members?.length)
) {
- continue
+ continue;
}
- const force = !!group.membersLoaded && !group.membersExpired && !group.members?.length
+ const force =
+ !!group.membersLoaded &&
+ !group.membersExpired &&
+ !group.members?.length;
this.fetchGroupMemberList(group.id, force).catch((error) => {
- console.warn('[IM groupStore] 预加载群头像成员失败', { groupId: group.id }, error)
- })
+ console.warn(
+ '[IM groupStore] 预加载群头像成员失败',
+ { groupId: group.id },
+ error,
+ );
+ });
}
},
/** 失效全部群成员缓存 */
markAllGroupMembersExpired() {
- this.groupMembersExpired = true
+ this.groupMembersExpired = true;
for (const group of this.groups) {
if (group.membersLoaded) {
- group.membersExpired = true
+ group.membersExpired = true;
}
}
},
@@ -294,105 +330,121 @@ export const useGroupStore = defineStore('imGroupStore', {
/** 失效全部群通话探测缓存 */
markAllGroupActiveCallsExpired() {
for (const group of this.groups) {
- group.activeCallExpired = true
+ group.activeCallExpired = true;
}
},
/** 标记群通话探测已加载 */
markGroupActiveCallLoaded(groupId: number) {
- const group = this.getGroup(groupId)
+ const group = this.getGroup(groupId);
if (!group) {
- return
+ return;
}
- group.activeCallLoaded = true
- group.activeCallExpired = false
+ group.activeCallLoaded = true;
+ group.activeCallExpired = false;
},
/** 判断群通话是否需要重新探测 */
isGroupActiveCallExpired(groupId: number): boolean {
- const group = this.getGroup(groupId)
- return !group?.activeCallLoaded || !!group.activeCallExpired
+ const group = this.getGroup(groupId);
+ return !group?.activeCallLoaded || !!group.activeCallExpired;
},
/** 失效指定群成员缓存 */
markGroupMembersExpired(groupId: number) {
- const group = this.getGroup(groupId)
+ const group = this.getGroup(groupId);
if (group?.membersLoaded) {
- group.membersExpired = true
+ group.membersExpired = true;
}
},
/** 单群刷新:用 /im/group/get 拉一份最新元数据再 upsert,常用于 GROUP_UPDATE 推送后或手动 reload */
async fetchGroupInfo(groupId: number, force = false) {
- const cached = this.getGroup(groupId)
+ const cached = this.getGroup(groupId);
if (cached?.infoLoaded && !force) {
- return
+ return;
}
try {
- const data = await apiGetGroup(groupId)
+ const data = await apiGetGroup(groupId);
if (!data) {
- return
+ return;
}
- this.upsertGroup({ ...convertGroup(data), infoLoaded: true })
+ this.upsertGroup({ ...convertGroup(data), infoLoaded: true });
} catch (error) {
- console.warn('[IM groupStore] fetchGroupInfo 失败', error)
+ console.warn('[IM groupStore] fetchGroupInfo 失败', error);
}
},
/** 按群拉取成员(in-memory 缓存 + 并发去重,force=true 强刷)+ 落 IDB */
- fetchGroupMemberList(groupId: number, force = false): Promise {
+ fetchGroupMemberList(
+ groupId: number,
+ force = false,
+ ): Promise {
// in-memory "完整"加载过才命中——单成员补齐写入的 partial members 不在此返回(membersLoaded=false)
- const cached = this.getGroup(groupId)
- if (cached && cached.members && cached.membersLoaded && !cached.membersExpired && !force) {
- return Promise.resolve(cached.members)
+ const cached = this.getGroup(groupId);
+ if (
+ cached &&
+ cached.members &&
+ cached.membersLoaded &&
+ !cached.membersExpired &&
+ !force
+ ) {
+ return Promise.resolve(cached.members);
}
// 未登录:不发起请求也不登记 in-flight,避免污染单飞表
- const requestUserId = getCurrentUserId()
+ const requestUserId = getCurrentUserId();
if (!requestUserId) {
- return Promise.resolve([])
+ return Promise.resolve([]);
}
- const requestEpoch = storeEpoch
+ const requestEpoch = storeEpoch;
// 同 (userId, groupId) 已经有正在飞的请求:直接复用,避免重复打接口
- const key = pendingMemberKey(requestUserId, groupId)
- const inflight = pendingMemberFetches.get(key)
+ const key = pendingMemberKey(requestUserId, groupId);
+ const inflight = pendingMemberFetches.get(key);
if (inflight) {
- return inflight
+ return inflight;
}
const promise = (async () => {
// 拉接口 + 单 pass 转换:同时捕获 me 的原始 VO,给下面回填 user-per-group 字段(silent / groupRemark)用
- const list = await apiGetGroupMemberList(groupId)
- if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) {
- return []
+ const list = await apiGetGroupMemberList(groupId);
+ if (
+ requestEpoch !== storeEpoch ||
+ getCurrentUserId() !== requestUserId
+ ) {
+ return [];
}
- let meRaw: ImGroupMemberApi.GroupMemberRespVO | undefined
+ let meRaw: ImGroupMemberApi.GroupMemberRespVO | undefined;
const members = (list || []).map((member) => {
if (member.userId === requestUserId) {
- meRaw = member
+ meRaw = member;
}
- return convertGroupMember(member, groupId)
- })
- const silent = !!meRaw?.silent
- const groupRemark = meRaw?.groupRemark || ''
+ return convertGroupMember(member, groupId);
+ });
+ const silent = !!meRaw?.silent;
+ const groupRemark = meRaw?.groupRemark || '';
// 必须 await 之后重新 getGroup,避免 fetchGroupList 已并发写入真实 group 的 race
- const group = this.getGroup(groupId)
- const isPlaceholder = !group
- let groupFieldsChanged = false
+ const group = this.getGroup(groupId);
+ const isPlaceholder = !group;
+ let groupFieldsChanged = false;
if (group) {
- group.members = members
- group.memberCount = members.length
- group.membersLoaded = true
- group.membersExpired = false
+ group.members = members;
+ group.memberCount = members.length;
+ group.membersLoaded = true;
+ group.membersExpired = false;
// silent / groupRemark 任一变化才同步到 conversation 和 IDB;groupRemark 变化要顺带刷会话名
if (group.silent !== silent || group.groupRemark !== groupRemark) {
- group.silent = silent
- group.groupRemark = groupRemark
- groupFieldsChanged = true
- const conversationStore = useConversationStore()
- conversationStore.updateConversation(ImConversationType.GROUP, groupId, {
- name: getGroupDisplayName(group),
- silent
- })
+ group.silent = silent;
+ group.groupRemark = groupRemark;
+ groupFieldsChanged = true;
+ const conversationStore = useConversationStore();
+ conversationStore.updateConversation(
+ ImConversationType.GROUP,
+ groupId,
+ {
+ name: getGroupDisplayName(group),
+ silent,
+ },
+ );
}
} else {
// group 还没就位:仅 in-memory push 占位(name='' 表示未知),不调 upsertGroup——避免把假名灌进 conversation.name + groups IDB 桶;
@@ -405,22 +457,22 @@ export const useGroupStore = defineStore('imGroupStore', {
silent,
groupRemark,
membersLoaded: true,
- membersExpired: false
- })
+ membersExpired: false,
+ });
}
// groups 桶仅在 user-per-group 字段实际变化时写——避免一次批量进群引发多次整桶重写
- this.saveGroupMemberList(groupId)
+ this.saveGroupMemberList(groupId);
if (!isPlaceholder && groupFieldsChanged) {
- this.saveGroup(group)
+ this.saveGroup(group);
}
- return members
+ return members;
// 无论成功 / 失败都要从单飞表清掉,否则后续同 group 请求永远拿到这个 stale Promise
- })().finally(() => pendingMemberFetches.delete(key))
+ })().finally(() => pendingMemberFetches.delete(key));
// 把 Promise 登记进单飞表,让此后短时间内的同 (userId, groupId) 请求复用
- pendingMemberFetches.set(key, promise)
- return promise
+ pendingMemberFetches.set(key, promise);
+ return promise;
},
/**
@@ -429,208 +481,242 @@ export const useGroupStore = defineStore('imGroupStore', {
* 跟 fetchGroupMemberList 区别:只拉这一个成员,不动 me 的 silent / groupRemark(不是 me 的话拿不到);
* 命中时把成员 upsert 进 group.members 数组并落 IDB,让后续渲染能用 displayUserName
*/
- fetchGroupMember(groupId: number, memberUserId: number): Promise {
+ fetchGroupMember(
+ groupId: number,
+ memberUserId: number,
+ ): Promise {
// in-memory 命中直接返回,不打接口
- const cached = this.getGroup(groupId)?.members?.find((m) => m.userId === memberUserId)
+ const cached = this.getGroup(groupId)?.members?.find(
+ (m) => m.userId === memberUserId,
+ );
if (cached) {
- return Promise.resolve(cached)
+ return Promise.resolve(cached);
}
// 未登录:不发起请求也不登记 in-flight,避免污染单飞表
- const requestUserId = getCurrentUserId()
+ const requestUserId = getCurrentUserId();
if (!requestUserId) {
- return Promise.resolve(null)
+ return Promise.resolve(null);
}
- const requestEpoch = storeEpoch
+ const requestEpoch = storeEpoch;
// 同 (userId, groupId, memberUserId) 已经有正在飞的请求:直接复用
- const key = pendingSingleMemberKey(requestUserId, groupId, memberUserId)
- const inflight = pendingSingleMemberFetches.get(key)
+ const key = pendingSingleMemberKey(requestUserId, groupId, memberUserId);
+ const inflight = pendingSingleMemberFetches.get(key);
if (inflight) {
- return inflight
+ return inflight;
}
const promise = (async () => {
- const data = await apiGetGroupMember(groupId, memberUserId)
+ const data = await apiGetGroupMember(groupId, memberUserId);
if (!data) {
- return null
+ return null;
}
- if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) {
- return null
+ if (
+ requestEpoch !== storeEpoch ||
+ getCurrentUserId() !== requestUserId
+ ) {
+ return null;
}
- const member = convertGroupMember(data, groupId)
+ const member = convertGroupMember(data, groupId);
// 把这一条 upsert 进 group.members 仅供 in-memory 渲染兜底;group 还没就位则用 placeholder
// 注意:不写 IDB——成员桶语义是"全量",存"1 人桶"会污染下次冷启动的 loadGroupMemberList
- const group = this.getGroup(groupId)
+ const group = this.getGroup(groupId);
if (group) {
- const memberList = group.members ?? []
- const index = memberList.findIndex((m) => m.userId === memberUserId)
+ const memberList = group.members ?? [];
+ const index = memberList.findIndex((m) => m.userId === memberUserId);
if (index === -1) {
- memberList.push(member)
+ memberList.push(member);
} else {
- memberList[index] = member
+ memberList[index] = member;
}
- group.members = memberList
+ group.members = memberList;
} else {
// memberCount 不设:后续 fetchGroupList 合并 `existing.memberCount ?? fresh.memberCount` 时,
// 占位值会顶替真实值(fresh 不带 memberCount),等 fetchGroupMemberList 跑过才能拿到真实数
this.groups.push({
id: groupId,
name: '',
- members: [member]
- })
+ members: [member],
+ });
}
- return member
- })().finally(() => pendingSingleMemberFetches.delete(key))
- pendingSingleMemberFetches.set(key, promise)
- return promise
+ return member;
+ })().finally(() => pendingSingleMemberFetches.delete(key));
+ pendingSingleMemberFetches.set(key, promise);
+ return promise;
},
/** 按 id 插入或合并群(命中则浅合并保留旧字段,未命中则追加),同步把展示名 / 头像 / 免打扰推到对应会话 */
upsertGroup(group: Group) {
void this.upsertGroupAndSave(group).catch((error) =>
- console.warn('[IM groupStore] 本地群写入失败', error)
- )
+ console.warn('[IM groupStore] 本地群写入失败', error),
+ );
},
/** 按 id 插入或合并群 */
async upsertGroupAndSave(group: Group): Promise {
- const index = this.groups.findIndex((g) => g.id === group.id)
+ const index = this.groups.findIndex((g) => g.id === group.id);
if (index === -1) {
- this.groups.push(group)
+ this.groups.push(group);
} else {
- this.groups[index] = { ...this.groups[index], ...group }
+ this.groups[index] = { ...this.groups[index], ...group };
}
// 同步推到 conversation:群名 / 头像 / 免打扰是会话列表展示用的,必须紧随 group 变更
- const merged = this.getGroup(group.id) ?? group
- const conversationStore = useConversationStore()
+ const merged = this.getGroup(group.id) ?? group;
+ const conversationStore = useConversationStore();
conversationStore.updateConversation(ImConversationType.GROUP, group.id, {
name: getGroupDisplayName(merged),
avatar: merged.avatar,
- silent: merged.silent
- })
+ silent: merged.silent,
+ });
// 持久化到 IDB(fire-and-forget)
- await this.saveGroupRecord(merged)
+ await this.saveGroupRecord(merged);
},
/** 本地移除群缓存和群会话;群解散(GROUP_DEL)、退群、被踢都复用 */
removeGroup(id: number) {
// 本地硬删(区别于好友删除的软删保留记录);级联清群聊会话避免列表里留死群
- this.groups = this.groups.filter((g) => g.id !== id)
- const conversationStore = useConversationStore()
- conversationStore.removeGroupConversation(id)
+ this.groups = this.groups.filter((g) => g.id !== id);
+ const conversationStore = useConversationStore();
+ conversationStore.removeGroupConversation(id);
void getDb()
.transaction(['groups', 'groupMembers'], 'readwrite', async (tx) => {
- const db = getDb()
- await db.delete('groups', id, tx)
- await db.deleteByIndex('groupMembers', 'groupId', id, tx)
+ const db = getDb();
+ await db.delete('groups', id, tx);
+ await db.deleteByIndex('groupMembers', 'groupId', id, tx);
})
- .catch((error) => console.warn(`[IM groupStore] 群缓存删除失败 (groupId=${id})`, error))
+ .catch((error) =>
+ console.warn(`[IM groupStore] 群缓存删除失败 (groupId=${id})`, error),
+ );
},
/** 切换免打扰:推后端 + 落本地 + 同步会话列表的 silent,避免 silent 图标 / 总未读 / 提示音判断与设置漂移;和 friendStore.setFriendSilent 对齐 */
async setGroupSilent(id: number, silent: boolean) {
- await apiUpdateGroupMember({ groupId: id, silent })
- const group = this.getGroup(id)
+ await apiUpdateGroupMember({ groupId: id, silent });
+ const group = this.getGroup(id);
if (!group) {
- return
+ return;
}
- group.silent = silent
- const conversationStore = useConversationStore()
- conversationStore.updateConversation(ImConversationType.GROUP, id, { silent })
- this.saveGroup(group)
+ group.silent = silent;
+ const conversationStore = useConversationStore();
+ conversationStore.updateConversation(ImConversationType.GROUP, id, {
+ silent,
+ });
+ this.saveGroup(group);
},
/** 批量更新群成员角色;本地不命中则忽略,等 fetchGroupMemberList 兜底 */
- updateGroupMemberRoleList(groupId: number, userIds: number[], role: number) {
- const group = this.getGroup(groupId)
+ updateGroupMemberRoleList(
+ groupId: number,
+ userIds: number[],
+ role: number,
+ ) {
+ const group = this.getGroup(groupId);
if (!group?.members?.length) {
- return
+ return;
}
// 命中目标且角色已变化才标记 changed,避免无变化时整数组重建触发响应式
- const idSet = new Set(userIds)
- let changed = false
+ const idSet = new Set(userIds);
+ let changed = false;
const newMembers = group.members.map((member) => {
if (!idSet.has(member.userId) || member.role === role) {
- return member
+ return member;
}
- changed = true
- return { ...member, role }
- })
+ changed = true;
+ return { ...member, role };
+ });
// 有变化才整组替换,让响应式只在真有更新时通知下游
if (changed) {
- group.members = newMembers
- this.saveGroupMemberList(groupId)
+ group.members = newMembers;
+ this.saveGroupMemberList(groupId);
}
},
/** 群主转让:群表 ownerUserId 改为新值;旧群主 role → NORMAL;新群主 role → OWNER */
- transferGroupOwner(groupId: number, oldOwnerId: number, newOwnerId: number) {
- const group = this.getGroup(groupId)
+ transferGroupOwner(
+ groupId: number,
+ oldOwnerId: number,
+ newOwnerId: number,
+ ) {
+ const group = this.getGroup(groupId);
if (!group) {
- return
+ return;
}
if (group.ownerUserId !== newOwnerId) {
- group.ownerUserId = newOwnerId
+ group.ownerUserId = newOwnerId;
}
- this.updateGroupMemberRoleList(groupId, [oldOwnerId], ImGroupMemberRole.NORMAL)
- this.updateGroupMemberRoleList(groupId, [newOwnerId], ImGroupMemberRole.OWNER)
- this.saveGroup(group)
+ this.updateGroupMemberRoleList(
+ groupId,
+ [oldOwnerId],
+ ImGroupMemberRole.NORMAL,
+ );
+ this.updateGroupMemberRoleList(
+ groupId,
+ [newOwnerId],
+ ImGroupMemberRole.OWNER,
+ );
+ this.saveGroup(group);
},
/** 本地剔除群成员(GROUP_MEMBER_QUIT / KICK 事件);不命中则等 fetchGroupMemberList 兜底 */
removeLocalGroupMemberList(groupId: number, userIds: number[]) {
- const group = this.getGroup(groupId)
+ const group = this.getGroup(groupId);
if (!group?.members?.length || userIds.length === 0) {
- return
+ return;
}
- const idSet = new Set(userIds)
- const next = group.members.filter((member) => !idSet.has(member.userId))
+ const idSet = new Set(userIds);
+ const next = group.members.filter((member) => !idSet.has(member.userId));
if (next.length === group.members.length) {
- return
+ return;
}
- group.members = next
- group.memberCount = next.length
- this.saveGroupMemberList(groupId)
+ group.members = next;
+ group.memberCount = next.length;
+ this.saveGroupMemberList(groupId);
},
/** 本地更新群成员的 status(自己退群 / 被踢的本地预置;让 isMember 立即收敛到 stranger,不依赖 removeGroup 的整群移除) */
updateGroupMemberStatus(groupId: number, userId: number, status: number) {
- const group = this.getGroup(groupId)
- const member = group?.members?.find((m) => m.userId === userId)
+ const group = this.getGroup(groupId);
+ const member = group?.members?.find((m) => m.userId === userId);
if (!member || member.status === status) {
- return
+ return;
}
- member.status = status
- this.saveGroupMemberList(groupId)
+ member.status = status;
+ this.saveGroupMemberList(groupId);
},
/** 本地更新群成员的 displayUserName(GROUP_MEMBER_NICKNAME_UPDATE 事件);不命中则等 fetchGroupMemberList 兜底 */
- updateGroupMemberDisplayUserName(groupId: number, userId: number, displayUserName: string) {
- const group = this.getGroup(groupId)
- const member = group?.members?.find((m) => m.userId === userId)
+ updateGroupMemberDisplayUserName(
+ groupId: number,
+ userId: number,
+ displayUserName: string,
+ ) {
+ const group = this.getGroup(groupId);
+ const member = group?.members?.find((m) => m.userId === userId);
if (!member || member.displayUserName === displayUserName) {
- return
+ return;
}
- member.displayUserName = displayUserName
- this.saveGroupMemberList(groupId)
+ member.displayUserName = displayUserName;
+ this.saveGroupMemberList(groupId);
},
/** 局部更新群字段(name / notice / avatar 等);未命中本地缓存时静默忽略,等 fetchGroupList 兜底;新值跟旧值都相同时跳过响应式 + IDB 写 */
updateGroupFields(groupId: number, fields: Partial) {
- const group = this.getGroup(groupId)
+ const group = this.getGroup(groupId);
if (!group) {
- return
+ return;
}
- const changed = (Object.keys(fields) as (keyof Group)[]).some((k) => group[k] !== fields[k])
+ const changed = (Object.keys(fields) as (keyof Group)[]).some(
+ (k) => group[k] !== fields[k],
+ );
if (!changed) {
- return
+ return;
}
- Object.assign(group, fields)
- const conversationStore = useConversationStore()
+ Object.assign(group, fields);
+ const conversationStore = useConversationStore();
conversationStore.updateConversation(ImConversationType.GROUP, groupId, {
name: getGroupDisplayName(group),
avatar: group.avatar,
- silent: group.silent
- })
- this.saveGroup(group)
+ silent: group.silent,
+ });
+ this.saveGroup(group);
},
/**
@@ -641,248 +727,298 @@ export const useGroupStore = defineStore('imGroupStore', {
*/
applyGroupNotification(groupId: number, type: number, content?: string) {
if (!groupId) {
- return
+ return;
}
- let payload: GroupNotificationPayload
+ let payload: GroupNotificationPayload;
try {
- payload = content ? JSON.parse(content) : {}
+ payload = content ? JSON.parse(content) : {};
} catch (error) {
console.warn(
'[IM groupStore] applyGroupNotification 解析 content 失败',
{ groupId, type, contentLength: content?.length ?? 0 },
- error
- )
- return
+ error,
+ );
+ return;
}
switch (type) {
case ImContentType.GROUP_ADMIN_ADD: {
this.updateGroupMemberRoleList(
groupId,
payload.memberUserIds || [],
- ImGroupMemberRole.ADMIN
- )
- this.markGroupMembersExpired(groupId)
+ ImGroupMemberRole.ADMIN,
+ );
+ this.markGroupMembersExpired(groupId);
// 自己被加为管理员,原本看不到的群下未处理申请现在变可见,重新拉一次 unhandledList
if (isSelfInPayloadMembers(payload)) {
- refreshUnhandledGroupRequests()
+ refreshUnhandledGroupRequests();
}
- break
+ break;
}
case ImContentType.GROUP_ADMIN_REMOVE: {
this.updateGroupMemberRoleList(
groupId,
payload.memberUserIds || [],
- ImGroupMemberRole.NORMAL
- )
- this.markGroupMembersExpired(groupId)
+ ImGroupMemberRole.NORMAL,
+ );
+ this.markGroupMembersExpired(groupId);
if (isSelfInPayloadMembers(payload)) {
- refreshUnhandledGroupRequests()
+ refreshUnhandledGroupRequests();
}
- break
+ break;
}
case ImContentType.GROUP_BANNED: {
- this.updateGroupFields(groupId, { banned: !!payload.banned })
- break
+ this.updateGroupFields(groupId, { banned: !!payload.banned });
+ break;
}
case ImContentType.GROUP_CANCEL_MUTED: {
- this.updateGroupFields(groupId, { mutedAll: false })
- break
+ this.updateGroupFields(groupId, { mutedAll: false });
+ break;
}
case ImContentType.GROUP_CREATE: {
- this.applyGroupCreateNotification(groupId, payload)
- break
+ this.applyGroupCreateNotification(groupId, payload);
+ break;
}
case ImContentType.GROUP_DISSOLVE: {
- this.removeGroup(groupId)
- break
+ this.removeGroup(groupId);
+ break;
}
case ImContentType.GROUP_INFO_UPDATE: {
- this.applyGroupInfoUpdateNotification(groupId, payload)
- break
+ this.applyGroupInfoUpdateNotification(groupId, payload);
+ break;
}
case ImContentType.GROUP_MEMBER_CANCEL_MUTED: {
- this.applyGroupMemberCancelMutedNotification(groupId, payload)
- break
+ this.applyGroupMemberCancelMutedNotification(groupId, payload);
+ break;
}
case ImContentType.GROUP_MEMBER_ENTER: {
- this.applyGroupMemberEnterNotification(groupId, payload)
- break
+ this.applyGroupMemberEnterNotification(groupId, payload);
+ break;
}
case ImContentType.GROUP_MEMBER_INVITE: {
- this.applyGroupMemberInviteNotification(groupId, payload)
- break
+ this.applyGroupMemberInviteNotification(groupId, payload);
+ break;
}
case ImContentType.GROUP_MEMBER_KICK: {
- this.applyGroupMemberKickNotification(groupId, payload)
- break
+ this.applyGroupMemberKickNotification(groupId, payload);
+ break;
}
case ImContentType.GROUP_MEMBER_MUTED: {
- this.applyGroupMemberMutedNotification(groupId, payload)
- break
+ this.applyGroupMemberMutedNotification(groupId, payload);
+ break;
}
case ImContentType.GROUP_MEMBER_NICKNAME_UPDATE: {
- this.applyGroupMemberNicknameUpdateNotification(groupId, payload)
- break
+ this.applyGroupMemberNicknameUpdateNotification(groupId, payload);
+ break;
}
case ImContentType.GROUP_MEMBER_QUIT: {
- this.applyGroupMemberQuitNotification(groupId, payload)
- break
+ this.applyGroupMemberQuitNotification(groupId, payload);
+ break;
}
case ImContentType.GROUP_MESSAGE_PIN: {
- this.applyGroupMessagePinNotification(groupId, payload)
- break
+ this.applyGroupMessagePinNotification(groupId, payload);
+ break;
}
case ImContentType.GROUP_MESSAGE_UNPIN: {
- this.applyGroupMessageUnpinNotification(groupId, payload)
- break
+ this.applyGroupMessageUnpinNotification(groupId, payload);
+ break;
}
case ImContentType.GROUP_MUTED: {
- this.updateGroupFields(groupId, { mutedAll: true })
- break
+ this.updateGroupFields(groupId, { mutedAll: true });
+ break;
}
case ImContentType.GROUP_NAME_UPDATE: {
- this.applyGroupNameUpdateNotification(groupId, payload)
- break
+ this.applyGroupNameUpdateNotification(groupId, payload);
+ break;
}
case ImContentType.GROUP_NOTICE_UPDATE: {
- this.applyGroupNoticeUpdateNotification(groupId, payload)
- break
+ this.applyGroupNoticeUpdateNotification(groupId, payload);
+ break;
}
case ImContentType.GROUP_OWNER_TRANSFER: {
- this.applyGroupOwnerTransferNotification(groupId, payload)
- break
+ this.applyGroupOwnerTransferNotification(groupId, payload);
+ break;
}
}
},
/** 创建群广播:群未就位时拉群详情 */
- async applyGroupCreateNotification(groupId: number, payload: GroupNotificationPayload) {
+ async applyGroupCreateNotification(
+ groupId: number,
+ payload: GroupNotificationPayload,
+ ) {
if (!isSelfInPayloadMembers(payload)) {
- return
+ return;
}
- const selfUserId = getCurrentUserId()
- const selfIsOperator = !!selfUserId && payload.operatorUserId === selfUserId
+ const selfUserId = getCurrentUserId();
+ const selfIsOperator =
+ !!selfUserId && payload.operatorUserId === selfUserId;
if (selfIsOperator && this.getGroup(groupId)) {
- return
+ return;
}
- await this.fetchGroupInfo(groupId, true)
+ await this.fetchGroupInfo(groupId, true);
},
/** 群名变更:按 newName 局部更新本地群名 */
- applyGroupNameUpdateNotification(groupId: number, payload: GroupNotificationPayload) {
+ applyGroupNameUpdateNotification(
+ groupId: number,
+ payload: GroupNotificationPayload,
+ ) {
if (payload.newName) {
- this.updateGroupFields(groupId, { name: payload.newName })
+ this.updateGroupFields(groupId, { name: payload.newName });
}
},
/** 群公告变更:按 newNotice 局部更新(允许空串作为「清空公告」) */
- applyGroupNoticeUpdateNotification(groupId: number, payload: GroupNotificationPayload) {
- this.updateGroupFields(groupId, { notice: payload.newNotice ?? '' })
+ applyGroupNoticeUpdateNotification(
+ groupId: number,
+ payload: GroupNotificationPayload,
+ ) {
+ this.updateGroupFields(groupId, { notice: payload.newNotice ?? '' });
},
/** 群信息变更:同步头像、进群审批 */
- applyGroupInfoUpdateNotification(groupId: number, payload: GroupNotificationPayload) {
- const fields: Partial = {}
+ applyGroupInfoUpdateNotification(
+ groupId: number,
+ payload: GroupNotificationPayload,
+ ) {
+ const fields: Partial = {};
if (payload.newAvatar) {
- fields.avatar = payload.newAvatar
+ fields.avatar = payload.newAvatar;
}
- if (payload.newJoinApproval != null) {
- fields.joinApproval = payload.newJoinApproval
+ if (payload.newJoinApproval !== null) {
+ fields.joinApproval = payload.newJoinApproval;
}
if (Object.keys(fields).length > 0) {
- this.updateGroupFields(groupId, fields)
+ this.updateGroupFields(groupId, fields);
}
},
/** 成员加入:被邀请者本端 group 未就位先 fetchGroupInfo 初次拉取;所有人都刷成员列表(新成员 nickname / avatar 不在 payload) */
- async applyGroupMemberInviteNotification(groupId: number, payload: GroupNotificationPayload) {
+ async applyGroupMemberInviteNotification(
+ groupId: number,
+ payload: GroupNotificationPayload,
+ ) {
// 自己刚被拉进来:必须 await fetchGroupInfo 让群入 state.groups,否则 fetchGroupMemberList 的 guard 会兜空
if (isSelfInPayloadMembers(payload) && !this.getGroup(groupId)) {
- await this.fetchGroupInfo(groupId, true)
+ await this.fetchGroupInfo(groupId, true);
}
- this.markGroupMembersExpired(groupId)
- this.fetchGroupMemberList(groupId, true).catch(() => undefined)
+ this.markGroupMembersExpired(groupId);
+ this.fetchGroupMemberList(groupId, true).catch(() => undefined);
},
/** 自由进群:进群者本端 group 未就位先 fetchGroupInfo 初次拉取;所有人都刷成员列表 */
- async applyGroupMemberEnterNotification(groupId: number, payload: GroupNotificationPayload) {
- const selfUserId = getCurrentUserId()
+ async applyGroupMemberEnterNotification(
+ groupId: number,
+ payload: GroupNotificationPayload,
+ ) {
+ const selfUserId = getCurrentUserId();
// 自己自由进群:必须 await fetchGroupInfo 让群入 state.groups,否则 fetchGroupMemberList 的 guard 会兜空
- if (selfUserId && payload.entrantUserId === selfUserId && !this.getGroup(groupId)) {
- await this.fetchGroupInfo(groupId, true)
+ if (
+ selfUserId &&
+ payload.entrantUserId === selfUserId &&
+ !this.getGroup(groupId)
+ ) {
+ await this.fetchGroupInfo(groupId, true);
}
- this.markGroupMembersExpired(groupId)
- this.fetchGroupMemberList(groupId, true).catch(() => undefined)
+ this.markGroupMembersExpired(groupId);
+ this.fetchGroupMemberList(groupId, true).catch(() => undefined);
},
/** 成员退群:退群者本人先把 self.status 置 DISABLE 再 removeGroup(保留状态语义 + 维持 groups 列表干净);其他成员从本地列表移除 quitter */
- applyGroupMemberQuitNotification(groupId: number, payload: GroupNotificationPayload) {
- const selfUserId = getCurrentUserId()
+ applyGroupMemberQuitNotification(
+ groupId: number,
+ payload: GroupNotificationPayload,
+ ) {
+ const selfUserId = getCurrentUserId();
if (selfUserId && payload.operatorUserId === selfUserId) {
- this.updateGroupMemberStatus(groupId, selfUserId, CommonStatusEnum.DISABLE)
- this.removeGroup(groupId)
+ this.updateGroupMemberStatus(
+ groupId,
+ selfUserId,
+ CommonStatusEnum.DISABLE,
+ );
+ this.removeGroup(groupId);
} else if (payload.operatorUserId) {
- this.removeLocalGroupMemberList(groupId, [payload.operatorUserId])
- this.markGroupMembersExpired(groupId)
+ this.removeLocalGroupMemberList(groupId, [payload.operatorUserId]);
+ this.markGroupMembersExpired(groupId);
}
},
/** 成员被移出:被踢者本人先把 self.status 置 DISABLE 再 removeGroup;其他成员从本地列表移除被踢者 */
- applyGroupMemberKickNotification(groupId: number, payload: GroupNotificationPayload) {
- const memberIds = payload.memberUserIds || []
- const selfUserId = getCurrentUserId()
+ applyGroupMemberKickNotification(
+ groupId: number,
+ payload: GroupNotificationPayload,
+ ) {
+ const memberIds = payload.memberUserIds || [];
+ const selfUserId = getCurrentUserId();
if (isSelfInPayloadMembers(payload)) {
if (selfUserId) {
- this.updateGroupMemberStatus(groupId, selfUserId, CommonStatusEnum.DISABLE)
+ this.updateGroupMemberStatus(
+ groupId,
+ selfUserId,
+ CommonStatusEnum.DISABLE,
+ );
}
- this.removeGroup(groupId)
+ this.removeGroup(groupId);
} else if (memberIds.length > 0) {
- this.removeLocalGroupMemberList(groupId, memberIds)
- this.markGroupMembersExpired(groupId)
+ this.removeLocalGroupMemberList(groupId, memberIds);
+ this.markGroupMembersExpired(groupId);
}
},
/** 成员昵称变更:按 operatorUserId 局部更新对应 member.displayUserName */
- applyGroupMemberNicknameUpdateNotification(groupId: number, payload: GroupNotificationPayload) {
+ applyGroupMemberNicknameUpdateNotification(
+ groupId: number,
+ payload: GroupNotificationPayload,
+ ) {
if (payload.operatorUserId) {
this.updateGroupMemberDisplayUserName(
groupId,
payload.operatorUserId,
- payload.displayUserName ?? ''
- )
- this.markGroupMembersExpired(groupId)
+ payload.displayUserName ?? '',
+ );
+ this.markGroupMembersExpired(groupId);
}
},
/** 群主转让:旧群主 → NORMAL,新群主 → OWNER;新群主自己侧重新拉申请列表 */
- applyGroupOwnerTransferNotification(groupId: number, payload: GroupNotificationPayload) {
+ applyGroupOwnerTransferNotification(
+ groupId: number,
+ payload: GroupNotificationPayload,
+ ) {
if (payload.operatorUserId && payload.newOwnerUserId) {
- this.transferGroupOwner(groupId, payload.operatorUserId, payload.newOwnerUserId)
- this.markGroupMembersExpired(groupId)
+ this.transferGroupOwner(
+ groupId,
+ payload.operatorUserId,
+ payload.newOwnerUserId,
+ );
+ this.markGroupMembersExpired(groupId);
}
// 自己接管群主:原本看不到的群下未处理申请现在变可见,重新拉一次 unhandledList
- const selfUserId = getCurrentUserId()
+ const selfUserId = getCurrentUserId();
if (selfUserId && payload.newOwnerUserId === selfUserId) {
- refreshUnhandledGroupRequests()
+ refreshUnhandledGroupRequests();
} else if (selfUserId && payload.operatorUserId === selfUserId) {
- refreshUnhandledGroupRequests()
+ refreshUnhandledGroupRequests();
}
},
/** 群消息置顶:从 payload 取消息展示数据加入置顶列表 */
- applyGroupMessagePinNotification(groupId: number, payload: GroupNotificationPayload) {
- const message = payload.message
+ applyGroupMessagePinNotification(
+ groupId: number,
+ payload: GroupNotificationPayload,
+ ) {
+ const message = payload.message;
if (!message) {
- return
+ return;
}
- const group = this.getGroup(groupId)
+ const group = this.getGroup(groupId);
if (!group) {
- return
+ return;
}
// 幂等:已存在同 messageId 不重复 push
- const existing = group.pinnedMessages || []
+ const existing = group.pinnedMessages || [];
if (existing.some((msg) => msg.id === message.id)) {
- return
+ return;
}
group.pinnedMessages = [
...existing,
@@ -897,64 +1033,81 @@ export const useGroupStore = defineStore('imGroupStore', {
targetId: message.groupId || groupId,
selfSend: message.senderId === getCurrentUserId(),
atUserIds: message.atUserIds ? [...message.atUserIds] : [],
- receiverUserIds: message.receiverUserIds ? [...message.receiverUserIds] : []
- }
- ]
- this.saveGroup(group)
+ receiverUserIds: message.receiverUserIds
+ ? [...message.receiverUserIds]
+ : [],
+ },
+ ];
+ this.saveGroup(group);
},
/** 群消息取消置顶:按 messageId 从本地置顶列表中移除 */
- applyGroupMessageUnpinNotification(groupId: number, payload: GroupNotificationPayload) {
+ applyGroupMessageUnpinNotification(
+ groupId: number,
+ payload: GroupNotificationPayload,
+ ) {
if (!payload.messageId) {
- return
+ return;
}
- const group = this.getGroup(groupId)
+ const group = this.getGroup(groupId);
if (!group?.pinnedMessages?.length) {
- return
+ return;
}
- const newPinnedMessages = group.pinnedMessages.filter((m) => m.id !== payload.messageId)
+ const newPinnedMessages = group.pinnedMessages.filter(
+ (m) => m.id !== payload.messageId,
+ );
if (newPinnedMessages.length === group.pinnedMessages.length) {
- return
+ return;
}
- group.pinnedMessages = newPinnedMessages
- this.saveGroup(group)
+ group.pinnedMessages = newPinnedMessages;
+ this.saveGroup(group);
},
/** 单成员禁言:更新目标成员的 muteEndTime */
- applyGroupMemberMutedNotification(groupId: number, payload: GroupNotificationPayload) {
- const group = this.getGroup(groupId)
- const member = group?.members?.find((m) => m.userId === payload.mutedUserId)
+ applyGroupMemberMutedNotification(
+ groupId: number,
+ payload: GroupNotificationPayload,
+ ) {
+ const group = this.getGroup(groupId);
+ const member = group?.members?.find(
+ (m) => m.userId === payload.mutedUserId,
+ );
if (member && payload.muteEndTime) {
- member.muteEndTime = payload.muteEndTime
- this.saveGroupMemberList(groupId)
- this.markGroupMembersExpired(groupId)
+ member.muteEndTime = payload.muteEndTime;
+ this.saveGroupMemberList(groupId);
+ this.markGroupMembersExpired(groupId);
}
},
/** 单成员取消禁言:清空目标成员的 muteEndTime */
- applyGroupMemberCancelMutedNotification(groupId: number, payload: GroupNotificationPayload) {
- const group = this.getGroup(groupId)
- const member = group?.members?.find((m) => m.userId === payload.mutedUserId)
+ applyGroupMemberCancelMutedNotification(
+ groupId: number,
+ payload: GroupNotificationPayload,
+ ) {
+ const group = this.getGroup(groupId);
+ const member = group?.members?.find(
+ (m) => m.userId === payload.mutedUserId,
+ );
if (member) {
- member.muteEndTime = undefined
- this.saveGroupMemberList(groupId)
- this.markGroupMembersExpired(groupId)
+ member.muteEndTime = undefined;
+ this.saveGroupMemberList(groupId);
+ this.markGroupMembersExpired(groupId);
}
},
/** 切账号时仅清 in-memory,IDB 按 userId 分桶天然隔离,回切秒开 */
clear() {
- this.groups = []
- this.loaded = false
- this.groupMembersExpired = false
+ this.groups = [];
+ this.loaded = false;
+ this.groupMembersExpired = false;
// 账号切换:递增 epoch 废弃旧账号 in-flight 的成员请求
- storeEpoch++
+ storeEpoch++;
// 单飞表跟 in-memory state 一起重置;旧账号 in-flight 的请求 finally 也会自己 delete key,提前清空只是更干脆
- pendingMemberFetches.clear()
- pendingSingleMemberFetches.clear()
- }
- }
-})
+ pendingMemberFetches.clear();
+ pendingSingleMemberFetches.clear();
+ },
+ },
+});
function convertGroup(group: ImGroupApi.GroupRespVO): Group {
return {
@@ -963,21 +1116,23 @@ function convertGroup(group: ImGroupApi.GroupRespVO): Group {
avatar: group.avatar,
notice: group.notice,
ownerUserId: group.ownerUserId,
- pinnedMessages: group.pinnedMessages?.map((message) => convertGroupMessageVO(message)),
+ pinnedMessages: group.pinnedMessages?.map((message) =>
+ convertGroupMessageVO(message),
+ ),
mutedAll: group.mutedAll,
banned: group.banned,
joinApproval: group.joinApproval,
joinStatus: group.joinStatus,
groupRemark: group.groupRemark,
- silent: group.silent
- }
+ silent: group.silent,
+ };
}
/** 后端 ImGroupMessageApi.GroupMessageRespVO -> 前端 Message:补 targetId / selfSend / sendTime 等派生字段 */
function convertGroupMessageVO(
- message: NonNullable[number]
+ message: NonNullable[number],
): Message {
- const currentUserId = getCurrentUserId()
+ const currentUserId = getCurrentUserId();
return {
id: message.id,
clientMessageId: message.clientMessageId || '',
@@ -991,11 +1146,14 @@ function convertGroupMessageVO(
atUserIds: message.atUserIds || [],
receiverUserIds: message.receiverUserIds || [],
receiptStatus: message.receiptStatus,
- readCount: message.readCount
- }
+ readCount: message.readCount,
+ };
}
-function convertGroupMember(member: ImGroupMemberApi.GroupMemberRespVO, groupId: number): GroupMember {
+function convertGroupMember(
+ member: ImGroupMemberApi.GroupMemberRespVO,
+ groupId: number,
+): GroupMember {
return {
id: member.id,
userId: member.userId,
@@ -1005,13 +1163,13 @@ function convertGroupMember(member: ImGroupMemberApi.GroupMemberRespVO, groupId:
displayUserName: member.displayUserName,
status: member.status,
role: member.role,
- muteEndTime: member.muteEndTime
- }
+ muteEndTime: member.muteEndTime,
+ };
}
-export const useGroupStoreWithOut = () => useGroupStore()
+export const useGroupStoreWithOut = () => useGroupStore();
// dev: 让 Pinia 的 actions 改动支持 HMR,免去每次改 store 都要硬刷
if (import.meta.hot) {
- import.meta.hot.accept(acceptHMRUpdate(useGroupStore, import.meta.hot))
+ import.meta.hot.accept(acceptHMRUpdate(useGroupStore, import.meta.hot));
}
diff --git a/apps/web-antd/src/views/im/home/store/messageStore.ts b/apps/web-antd/src/views/im/home/store/messageStore.ts
index fa17b6740..b91d23f54 100644
--- a/apps/web-antd/src/views/im/home/store/messageStore.ts
+++ b/apps/web-antd/src/views/im/home/store/messageStore.ts
@@ -1,8 +1,9 @@
-import type { Conversation, Message, MessageDO } from '../types'
+import type { DbTransaction } from '../../utils/db';
+import type { Conversation, Message, MessageDO } from '../types';
-import { acceptHMRUpdate, defineStore } from 'pinia'
+import { acceptHMRUpdate, defineStore } from 'pinia';
-import { getCurrentUserId } from '#/views/im/utils/auth'
+import { getCurrentUserId } from '#/views/im/utils/auth';
import {
IM_AT_ALL_USER_ID,
@@ -11,82 +12,82 @@ import {
ImMessageReceiptStatus,
ImMessageStatus,
isGroupNotification,
- isNormalMessage
-} from '../../utils/constants'
-import { resolveConversationLastContent } from '../../utils/conversation'
+ isNormalMessage,
+} from '../../utils/constants';
+import { resolveConversationLastContent } from '../../utils/conversation';
import {
- type DbTransaction,
getClientConversationId,
getClientMessageKey,
getDb,
getServerMessageKey,
parseClientConversationId,
setMessageMaxId,
- StorageKeys
-} from '../../utils/db'
+ StorageKeys,
+} from '../../utils/db';
import {
generateClientMessageId,
parseRecallMessageId,
- revokeBlobUrlsInContent
-} from '../../utils/message'
-import { isGroupQuit, tryGetSenderDisplayName } from '../../utils/user'
-import { useConversationStore } from './conversationStore'
-import { useGroupStore } from './groupStore'
+ revokeBlobUrlsInContent,
+} from '../../utils/message';
+import { isGroupQuit, tryGetSenderDisplayName } from '../../utils/user';
+import { useConversationStore } from './conversationStore';
+import { useGroupStore } from './groupStore';
-const MESSAGE_CACHE_RECENT_CONVERSATION_LIMIT = 5
-const MESSAGE_CACHE_RETAIN_CONVERSATION_LIMIT = MESSAGE_CACHE_RECENT_CONVERSATION_LIMIT + 1
-const ackMergingPromises = new Map>()
+const MESSAGE_CACHE_RECENT_CONVERSATION_LIMIT = 5;
+const MESSAGE_CACHE_RETAIN_CONVERSATION_LIMIT =
+ MESSAGE_CACHE_RECENT_CONVERSATION_LIMIT + 1;
+const ackMergingPromises = new Map>();
interface MessageConversationInfo {
- type: number
- targetId: number
- name: string
- avatar: string
- silent?: boolean
+ type: number;
+ targetId: number;
+ name: string;
+ avatar: string;
+ silent?: boolean;
}
interface PersistMessageRecordOptions {
- mergeClientRecord?: boolean
+ mergeClientRecord?: boolean;
}
/** 拉取消息批量处理项 */
export type PulledMessage =
| {
- conversationInfo: MessageConversationInfo
- kind: 'insert'
- message: Message
+ conversationInfo: MessageConversationInfo;
+ kind: 'insert';
+ message: Message;
}
| {
- conversationType: number
- kind: 'recall'
- recallSignalContent: string
- targetId: number
- }
+ conversationType: number;
+ kind: 'recall';
+ recallSignalContent: string;
+ targetId: number;
+ };
/** 获取会话的消息缓存 key */
function getMessageCacheKey(type: number, targetId: number): string {
- return getClientConversationId(type, targetId)
+ return getClientConversationId(type, targetId);
}
/** 生成消息本地主键 */
function getMessageKey(
message: Pick,
- conversationType: number
+ conversationType: number,
): string {
return message.id
? getServerMessageKey(conversationType, message.id)
- : getClientMessageKey(message.clientMessageId)
+ : getClientMessageKey(message.clientMessageId);
}
/** 补齐客户端消息编号 */
function ensureClientMessageId(message: Message): Message {
if (!message.clientMessageId) {
- message.clientMessageId = generateClientMessageId()
+ message.clientMessageId = generateClientMessageId();
}
if (!message.id) {
- message.id = undefined
+ message.id = undefined;
}
- return message
+ return message;
}
/** 转换为 IndexedDB 消息记录 */
@@ -100,7 +101,9 @@ function buildMessageDO(message: Message, conversationType: number): MessageDO {
sendTime: message.sendTime,
senderId: message.senderId,
atUserIds: message.atUserIds ? [...message.atUserIds] : undefined,
- receiverUserIds: message.receiverUserIds ? [...message.receiverUserIds] : undefined,
+ receiverUserIds: message.receiverUserIds
+ ? [...message.receiverUserIds]
+ : undefined,
receiptStatus: message.receiptStatus,
readCount: message.readCount,
materialId: message.materialId,
@@ -108,8 +111,11 @@ function buildMessageDO(message: Message, conversationType: number): MessageDO {
selfSend: message.selfSend,
messageKey: getMessageKey(message, conversationType),
conversationType,
- clientConversationId: getClientConversationId(conversationType, message.targetId)
- }
+ clientConversationId: getClientConversationId(
+ conversationType,
+ message.targetId,
+ ),
+ };
}
/** IndexedDB 消息记录转前端消息 */
@@ -119,113 +125,143 @@ function buildMessageFromDO(message: MessageDO): Message {
conversationType: _conversationType,
clientConversationId: _clientConversationId,
...rest
- } = message
- return rest
+ } = message;
+ return rest;
}
/** 算出末条消息的发送人快照 */
function deriveLastSenderDisplayName(
conversation: Conversation,
- senderId: number
+ senderId: number,
): string | undefined {
// 1. 优先使用当前内存中的好友 / 群成员信息
- const liveSenderName = tryGetSenderDisplayName(senderId, conversation.type, conversation.targetId)
+ const liveSenderName = tryGetSenderDisplayName(
+ senderId,
+ conversation.type,
+ conversation.targetId,
+ );
if (liveSenderName) {
- return liveSenderName
+ return liveSenderName;
}
// 2. 群成员缓存缺失时异步补齐
if (conversation.type === ImConversationType.GROUP) {
- const groupStore = useGroupStore()
- const group = groupStore.getGroup(conversation.targetId)
+ const groupStore = useGroupStore();
+ const group = groupStore.getGroup(conversation.targetId);
if (!group || isGroupQuit(group)) {
- return conversation.lastSenderId === senderId ? conversation.lastSenderDisplayName : undefined
+ return conversation.lastSenderId === senderId
+ ? conversation.lastSenderDisplayName
+ : undefined;
}
const fetchPromise =
group?.membersLoaded && !group.membersExpired
? groupStore.fetchGroupMember(conversation.targetId, senderId)
- : groupStore.fetchGroupMemberList(conversation.targetId)
+ : groupStore.fetchGroupMemberList(conversation.targetId);
fetchPromise.catch((error) =>
console.warn(
'[IM messageStore] 兜底拉群成员失败',
- { groupId: conversation.targetId, senderId, fullFetch: !group?.membersLoaded },
- error
- )
- )
+ {
+ groupId: conversation.targetId,
+ senderId,
+ fullFetch: !group?.membersLoaded,
+ },
+ error,
+ ),
+ );
}
- return conversation.lastSenderId === senderId ? conversation.lastSenderDisplayName : undefined
+ return conversation.lastSenderId === senderId
+ ? conversation.lastSenderDisplayName
+ : undefined;
}
/** 按消息更新会话摘要 */
-function applyConversationSummary(conversation: Conversation, message: Message): void {
- const senderDisplayName = deriveLastSenderDisplayName(conversation, message.senderId)
+function applyConversationSummary(
+ conversation: Conversation,
+ message: Message,
+): void {
+ const senderDisplayName = deriveLastSenderDisplayName(
+ conversation,
+ message.senderId,
+ );
conversation.lastContent = resolveConversationLastContent(
message,
conversation.type,
conversation.targetId,
- senderDisplayName
- )
- conversation.lastSendTime = message.sendTime || Date.now()
- conversation.lastSenderId = message.senderId
- conversation.lastMessageType = message.type
- conversation.lastMessageId = message.id
- conversation.lastClientMessageId = message.clientMessageId
- conversation.lastMessageStatus = message.status
- conversation.lastReceiptStatus = message.receiptStatus
- conversation.lastSelfSend = message.selfSend
- conversation.lastSenderDisplayName = senderDisplayName
+ senderDisplayName,
+ );
+ conversation.lastSendTime = message.sendTime || Date.now();
+ conversation.lastSenderId = message.senderId;
+ conversation.lastMessageType = message.type;
+ conversation.lastMessageId = message.id;
+ conversation.lastClientMessageId = message.clientMessageId;
+ conversation.lastMessageStatus = message.status;
+ conversation.lastReceiptStatus = message.receiptStatus;
+ conversation.lastSelfSend = message.selfSend;
+ conversation.lastSenderDisplayName = senderDisplayName;
}
/** 按末条消息重算会话摘要 */
-function recomputeConversationLast(conversation: Conversation, messages: Message[]): void {
- const last = messages[messages.length - 1]
+function recomputeConversationLast(
+ conversation: Conversation,
+ messages: Message[],
+): void {
+ const last = messages[messages.length - 1];
if (last) {
- applyConversationSummary(conversation, last)
- return
+ applyConversationSummary(conversation, last);
+ return;
}
- conversation.lastContent = ''
- conversation.lastSendTime = 0
- conversation.lastSenderId = undefined
- conversation.lastMessageType = undefined
- conversation.lastMessageId = undefined
- conversation.lastClientMessageId = undefined
- conversation.lastMessageStatus = undefined
- conversation.lastReceiptStatus = undefined
- conversation.lastSelfSend = undefined
- conversation.lastSenderDisplayName = undefined
+ conversation.lastContent = '';
+ conversation.lastSendTime = 0;
+ conversation.lastSenderId = undefined;
+ conversation.lastMessageType = undefined;
+ conversation.lastMessageId = undefined;
+ conversation.lastClientMessageId = undefined;
+ conversation.lastMessageStatus = undefined;
+ conversation.lastReceiptStatus = undefined;
+ conversation.lastSelfSend = undefined;
+ conversation.lastSenderDisplayName = undefined;
}
/** 同步群 @ 状态 */
-function syncConversationAtFlags(conversation: Conversation, message: Message): void {
+function syncConversationAtFlags(
+ conversation: Conversation,
+ message: Message,
+): void {
if (
message.selfSend ||
conversation.type !== ImConversationType.GROUP ||
!message.atUserIds ||
message.atUserIds.length === 0
) {
- return
+ return;
}
- const currentUserId = getCurrentUserId()
+ const currentUserId = getCurrentUserId();
if (currentUserId && message.atUserIds.includes(currentUserId)) {
- conversation.atMe = true
+ conversation.atMe = true;
}
if (message.atUserIds.includes(IM_AT_ALL_USER_ID)) {
- conversation.atAll = true
+ conversation.atAll = true;
}
}
/** 应用服务端消息更新 */
-function applyServerMessageUpdate(message: Message, updates: Partial): void {
+function applyServerMessageUpdate(
+ message: Message,
+ updates: Partial,
+): void {
if (updates.content && updates.content !== message.content) {
- revokeBlobUrlsInContent(message.content)
+ revokeBlobUrlsInContent(message.content);
}
- Object.assign(message, updates)
+ Object.assign(message, updates);
if (updates.id === 0) {
- message.id = undefined
+ message.id = undefined;
}
- if (updates.status !== undefined && updates.status !== ImMessageStatus.SENDING) {
- message.uploadProgress = undefined
+ if (
+ updates.status !== undefined &&
+ updates.status !== ImMessageStatus.SENDING
+ ) {
+ message.uploadProgress = undefined;
if (updates.status !== ImMessageStatus.FAILED) {
- message._localFile = undefined
+ message._localFile = undefined;
}
}
}
@@ -233,9 +269,11 @@ function applyServerMessageUpdate(message: Message, updates: Partial):
/** 判断是否为同一条消息 */
function isSameMessage(left: Message, right: Message): boolean {
if (left.id && right.id && left.id === right.id) {
- return true
+ return true;
}
- return !!left.clientMessageId && left.clientMessageId === right.clientMessageId
+ return (
+ !!left.clientMessageId && left.clientMessageId === right.clientMessageId
+ );
}
export const useMessageStore = defineStore('imMessageStore', {
@@ -245,7 +283,7 @@ export const useMessageStore = defineStore('imMessageStore', {
privateReadMaxIds: {} as Partial>,
privateMessageMaxId: 0,
groupMessageMaxId: 0,
- channelMessageMaxId: 0
+ channelMessageMaxId: 0,
}),
getters: {
@@ -253,7 +291,7 @@ export const useMessageStore = defineStore('imMessageStore', {
getMessages:
(state) =>
(clientConversationId: string): Message[] =>
- state.messagesByConversation[clientConversationId] || []
+ state.messagesByConversation[clientConversationId] || [],
},
actions: {
@@ -261,138 +299,163 @@ export const useMessageStore = defineStore('imMessageStore', {
clear() {
Object.values(this.messagesByConversation).forEach((messages) => {
messages.forEach((message) => {
- revokeBlobUrlsInContent(message.content)
- message._localFile = undefined
- })
- })
- this.messagesByConversation = {}
- this.loadedConversationKeys = []
- this.privateReadMaxIds = {}
- this.privateMessageMaxId = 0
- this.groupMessageMaxId = 0
- this.channelMessageMaxId = 0
- ackMergingPromises.clear()
+ revokeBlobUrlsInContent(message.content);
+ message._localFile = undefined;
+ });
+ });
+ this.messagesByConversation = {};
+ this.loadedConversationKeys = [];
+ this.privateReadMaxIds = {};
+ this.privateMessageMaxId = 0;
+ this.groupMessageMaxId = 0;
+ this.channelMessageMaxId = 0;
+ ackMergingPromises.clear();
},
/** 从 settings 加载消息游标 */
async loadMessageCursorList() {
- const db = getDb()
+ const db = getDb();
const [privateMaxId, groupMaxId, channelMaxId] = await Promise.all([
db.getSetting(StorageKeys.settings.privateMessageMaxId),
db.getSetting(StorageKeys.settings.groupMessageMaxId),
- db.getSetting(StorageKeys.settings.channelMessageMaxId)
- ])
- this.privateMessageMaxId = privateMaxId || 0
- this.groupMessageMaxId = groupMaxId || 0
- this.channelMessageMaxId = channelMaxId || 0
+ db.getSetting(StorageKeys.settings.channelMessageMaxId),
+ ]);
+ this.privateMessageMaxId = privateMaxId || 0;
+ this.groupMessageMaxId = groupMaxId || 0;
+ this.channelMessageMaxId = channelMaxId || 0;
},
/** 更新内存游标 */
updateMessageCursor(conversationType: number, messageId?: number) {
if (!messageId) {
- return
+ return;
}
- if (conversationType === ImConversationType.PRIVATE && messageId > this.privateMessageMaxId) {
- this.privateMessageMaxId = messageId
+ if (
+ conversationType === ImConversationType.PRIVATE &&
+ messageId > this.privateMessageMaxId
+ ) {
+ this.privateMessageMaxId = messageId;
} else if (
conversationType === ImConversationType.GROUP &&
messageId > this.groupMessageMaxId
) {
- this.groupMessageMaxId = messageId
+ this.groupMessageMaxId = messageId;
} else if (
conversationType === ImConversationType.CHANNEL &&
messageId > this.channelMessageMaxId
) {
- this.channelMessageMaxId = messageId
+ this.channelMessageMaxId = messageId;
}
},
/** 获取私聊对方已读位置缓存 */
getPrivateReadMaxId(peerId: number): number | undefined {
- return this.privateReadMaxIds[peerId]
+ return this.privateReadMaxIds[peerId];
},
/** 更新私聊对方已读位置缓存 */
- updatePrivateReadMaxId(peerId: number, maxReadId: null | number = 0): number {
+ updatePrivateReadMaxId(
+ peerId: number,
+ maxReadId: null | number = 0,
+ ): number {
if (!peerId) {
- return 0
+ return 0;
}
- const nextMaxReadId = maxReadId || 0
- const current = this.getPrivateReadMaxId(peerId)
+ const nextMaxReadId = maxReadId || 0;
+ const current = this.getPrivateReadMaxId(peerId);
if (current !== undefined && nextMaxReadId <= current) {
- return current
+ return current;
}
- this.privateReadMaxIds = { ...this.privateReadMaxIds, [peerId]: nextMaxReadId }
- return nextMaxReadId
+ this.privateReadMaxIds = {
+ ...this.privateReadMaxIds,
+ [peerId]: nextMaxReadId,
+ };
+ return nextMaxReadId;
},
/** 清空私聊对方已读位置缓存 */
clearPrivateReadMaxIdCache(): void {
- this.privateReadMaxIds = {}
+ this.privateReadMaxIds = {};
},
/** 标记会话近期使用 */
touchConversationMessageCache(clientConversationId: string) {
this.loadedConversationKeys = [
clientConversationId,
- ...this.loadedConversationKeys.filter((key) => key !== clientConversationId)
- ]
+ ...this.loadedConversationKeys.filter(
+ (key) => key !== clientConversationId,
+ ),
+ ];
// 保留当前活跃会话 + 最近打开过的会话
- const retained = this.loadedConversationKeys.slice(0, MESSAGE_CACHE_RETAIN_CONVERSATION_LIMIT)
- const removed = this.loadedConversationKeys.slice(MESSAGE_CACHE_RETAIN_CONVERSATION_LIMIT)
- this.loadedConversationKeys = retained
+ const retained = this.loadedConversationKeys.slice(
+ 0,
+ MESSAGE_CACHE_RETAIN_CONVERSATION_LIMIT,
+ );
+ const removed = this.loadedConversationKeys.slice(
+ MESSAGE_CACHE_RETAIN_CONVERSATION_LIMIT,
+ );
+ this.loadedConversationKeys = retained;
removed.forEach((key) => {
- Reflect.deleteProperty(this.messagesByConversation, key)
- })
+ Reflect.deleteProperty(this.messagesByConversation, key);
+ });
},
/** 加载当前会话最近消息 */
async loadMoreMessageList(
clientConversationId: string,
beforeSendTime?: number,
- limit = 50
+ limit = 50,
): Promise {
// 1. 从 IndexedDB 倒序读取一页,返回前已按时间升序排列
- const list = await getDb().getMessageListByConversation(clientConversationId, {
- beforeSendTime,
- limit
- })
+ const list = await getDb().getMessageListByConversation(
+ clientConversationId,
+ {
+ beforeSendTime,
+ limit,
+ },
+ );
// 2. 合并到内存缓存,过滤已存在的消息
- const parsed = parseClientConversationId(clientConversationId)
+ const parsed = parseClientConversationId(clientConversationId);
if (!parsed) {
- return []
+ return [];
}
- const messages = list.map((message) => buildMessageFromDO(message))
- const existing = this.messagesByConversation[clientConversationId] || []
- const existingKeys = new Set(existing.map((message) => getMessageKey(message, parsed.type)))
+ const messages = list.map((message) => buildMessageFromDO(message));
+ const existing = this.messagesByConversation[clientConversationId] || [];
+ const existingKeys = new Set(
+ existing.map((message) => getMessageKey(message, parsed.type)),
+ );
const fresh = messages.filter(
- (message) => !existingKeys.has(getMessageKey(message, parsed.type))
- )
- this.messagesByConversation[clientConversationId] = [...fresh, ...existing].toSorted(
- (messageA, messageB) => (messageA.sendTime || 0) - (messageB.sendTime || 0)
- )
- this.touchConversationMessageCache(clientConversationId)
- return fresh
+ (message) => !existingKeys.has(getMessageKey(message, parsed.type)),
+ );
+ this.messagesByConversation[clientConversationId] = [
+ ...fresh,
+ ...existing,
+ ].toSorted(
+ (messageA, messageB) =>
+ (messageA.sendTime || 0) - (messageB.sendTime || 0),
+ );
+ this.touchConversationMessageCache(clientConversationId);
+ return fresh;
},
/** 确保会话消息已加载 */
async ensureConversationMessageListLoaded(conversation: Conversation) {
- const key = getMessageCacheKey(conversation.type, conversation.targetId)
+ const key = getMessageCacheKey(conversation.type, conversation.targetId);
if (this.messagesByConversation[key]) {
- this.touchConversationMessageCache(key)
- return
+ this.touchConversationMessageCache(key);
+ return;
}
- await this.loadMoreMessageList(key)
+ await this.loadMoreMessageList(key);
},
/** 获取内存消息数组 */
getMessageList(conversationType: number, targetId: number): Message[] {
- const key = getMessageCacheKey(conversationType, targetId)
+ const key = getMessageCacheKey(conversationType, targetId);
if (!this.messagesByConversation[key]) {
- this.messagesByConversation[key] = []
+ this.messagesByConversation[key] = [];
}
- this.touchConversationMessageCache(key)
- return this.messagesByConversation[key]
+ this.touchConversationMessageCache(key);
+ return this.messagesByConversation[key];
},
/** 持久化消息记录 */
@@ -400,96 +463,107 @@ export const useMessageStore = defineStore('imMessageStore', {
message: Message,
conversationType: number,
tx?: DbTransaction,
- options?: PersistMessageRecordOptions
+ options?: PersistMessageRecordOptions,
) {
- const db = getDb()
- const next = buildMessageDO(message, conversationType)
+ const db = getDb();
+ const next = buildMessageDO(message, conversationType);
// 服务端 key 替换 client key
if (options?.mergeClientRecord && message.id && message.clientMessageId) {
const existing = await db.getByIndex(
'messages',
'clientMessageId',
message.clientMessageId,
- tx
- )
+ tx,
+ );
if (existing && existing.messageKey !== next.messageKey) {
- await db.delete('messages', existing.messageKey, tx)
+ await db.delete('messages', existing.messageKey, tx);
}
}
- await db.put('messages', next, tx)
+ await db.put('messages', next, tx);
},
/** 保存消息游标 */
- async saveMessageCursor(conversationType: number, messageId?: number, tx?: DbTransaction) {
- await setMessageMaxId(conversationType, messageId, tx)
- this.updateMessageCursor(conversationType, messageId)
+ async saveMessageCursor(
+ conversationType: number,
+ messageId?: number,
+ tx?: DbTransaction,
+ ) {
+ await setMessageMaxId(conversationType, messageId, tx);
+ this.updateMessageCursor(conversationType, messageId);
},
/** 应用撤回到内存 */
applyRecallMessageInMemory(
conversationType: number,
targetId: number,
- recallSignalContent: string
+ recallSignalContent: string,
) {
// 1. 定位被撤回的原消息
- const messageId = parseRecallMessageId(recallSignalContent)
+ const messageId = parseRecallMessageId(recallSignalContent);
if (!messageId) {
- return null
+ return null;
}
- const conversationStore = useConversationStore()
- const conversation = conversationStore.getConversation(conversationType, targetId)
+ const conversationStore = useConversationStore();
+ const conversation = conversationStore.getConversation(
+ conversationType,
+ targetId,
+ );
if (!conversation) {
- return null
+ return null;
}
- const messages = this.getMessageList(conversationType, targetId)
- const message = messages.find((item) => item.id === messageId)
+ const messages = this.getMessageList(conversationType, targetId);
+ const message = messages.find((item) => item.id === messageId);
if (!message) {
- return null
+ return null;
}
// 2. 更新消息和会话摘要
- message.type = ImContentType.RECALL
- message.status = ImMessageStatus.RECALL
- message.content = ''
+ message.type = ImContentType.RECALL;
+ message.status = ImMessageStatus.RECALL;
+ message.content = '';
if (messages[messages.length - 1]?.id === messageId) {
- recomputeConversationLast(conversation, messages)
+ recomputeConversationLast(conversation, messages);
}
- return { conversation, message }
+ return { conversation, message };
},
/** 批量写入拉取消息 */
async applyPulledMessageList(
pulledMessages: PulledMessage[],
conversationType: number,
- maxMessageId?: number
+ maxMessageId?: number,
) {
if (pulledMessages.length === 0) {
// 1. 空批次只推进游标
- await this.saveMessageCursor(conversationType, maxMessageId)
- return
+ await this.saveMessageCursor(conversationType, maxMessageId);
+ return;
}
- const conversationStore = useConversationStore()
+ const conversationStore = useConversationStore();
const persistedMessages = new Map<
string,
- { conversationType: number; mergeClientRecord?: boolean; message: Message; }
- >()
- const changedConversations = new Map()
+ {
+ conversationType: number;
+ mergeClientRecord?: boolean;
+ message: Message;
+ }
+ >();
+ const changedConversations = new Map();
const addChanged = (
conversation: Conversation,
message: Message,
- options?: PersistMessageRecordOptions
+ options?: PersistMessageRecordOptions,
) => {
const clientConversationId = getClientConversationId(
conversation.type,
- conversation.targetId
- )
- changedConversations.set(clientConversationId, conversation)
+ conversation.targetId,
+ );
+ changedConversations.set(clientConversationId, conversation);
persistedMessages.set(getMessageKey(message, conversation.type), {
message,
conversationType: conversation.type,
- mergeClientRecord: options?.mergeClientRecord
- })
- }
+ mergeClientRecord: options?.mergeClientRecord,
+ });
+ };
// 1. 先更新内存,收集需要持久化的消息和会话
for (const pulledMessage of pulledMessages) {
@@ -498,68 +572,80 @@ export const useMessageStore = defineStore('imMessageStore', {
const changed = this.applyRecallMessageInMemory(
pulledMessage.conversationType,
pulledMessage.targetId,
- pulledMessage.recallSignalContent
- )
+ pulledMessage.recallSignalContent,
+ );
if (changed) {
- addChanged(changed.conversation, changed.message)
+ addChanged(changed.conversation, changed.message);
}
- continue
+ continue;
}
- const { conversationInfo } = pulledMessage
- const hasServerClientMessageId = !!pulledMessage.message.clientMessageId
- const message = ensureClientMessageId(pulledMessage.message)
+ const { conversationInfo } = pulledMessage;
+ const hasServerClientMessageId =
+ !!pulledMessage.message.clientMessageId;
+ const message = ensureClientMessageId(pulledMessage.message);
// 1.2 确保会话和消息缓存存在
- const conversation = conversationStore.ensureConversation(conversationInfo)
- const messages = this.getMessageList(conversationInfo.type, conversationInfo.targetId)
- const existingIndex = messages.findIndex((existing) => isSameMessage(existing, message))
+ const conversation =
+ conversationStore.ensureConversation(conversationInfo);
+ const messages = this.getMessageList(
+ conversationInfo.type,
+ conversationInfo.targetId,
+ );
+ const existingIndex = messages.findIndex((existing) =>
+ isSameMessage(existing, message),
+ );
if (existingIndex !== -1) {
- const existing = messages[existingIndex]
+ const existing = messages[existingIndex];
if (!existing) {
- continue
+ continue;
}
// 1.3 已存在消息合并服务端状态
- applyServerMessageUpdate(existing, message)
+ applyServerMessageUpdate(existing, message);
if (existingIndex === messages.length - 1) {
- recomputeConversationLast(conversation, messages)
- syncConversationAtFlags(conversation, message)
+ recomputeConversationLast(conversation, messages);
+ syncConversationAtFlags(conversation, message);
}
addChanged(conversation, existing, {
- mergeClientRecord: hasServerClientMessageId
- })
- continue
+ mergeClientRecord: hasServerClientMessageId,
+ });
+ continue;
}
// 1.4 新消息更新会话摘要和未读状态
- applyConversationSummary(conversation, message)
- syncConversationAtFlags(conversation, message)
+ applyConversationSummary(conversation, message);
+ syncConversationAtFlags(conversation, message);
const isActive =
- conversationStore.activeConversation?.type === conversationInfo.type &&
- conversationStore.activeConversation?.targetId === conversationInfo.targetId
+ conversationStore.activeConversation?.type ===
+ conversationInfo.type &&
+ conversationStore.activeConversation?.targetId ===
+ conversationInfo.targetId;
if (
!message.selfSend &&
!isActive &&
- !conversationStore.isMessageCoveredByReadPosition(conversation, message) &&
+ !conversationStore.isMessageCoveredByReadPosition(
+ conversation,
+ message,
+ ) &&
isNormalMessage(message.type) &&
message.status !== ImMessageStatus.RECALL
) {
- conversation.unreadCount++
+ conversation.unreadCount++;
}
// 1.5 新消息按服务端 id 插入内存列表
- let insertIndex = messages.length
+ let insertIndex = messages.length;
if (message.id) {
for (const [index, existing] of messages.entries()) {
if (existing.id && message.id < existing.id) {
- insertIndex = index
- break
+ insertIndex = index;
+ break;
}
}
}
- messages.splice(insertIndex, 0, message)
+ messages.splice(insertIndex, 0, message);
addChanged(conversation, message, {
- mergeClientRecord: hasServerClientMessageId && !!message.id
- })
+ mergeClientRecord: hasServerClientMessageId && !!message.id,
+ });
}
// 2. 单事务写入消息、会话摘要和游标
@@ -569,20 +655,28 @@ export const useMessageStore = defineStore('imMessageStore', {
async (tx) => {
// 2.1 写入本批变更消息
for (const item of persistedMessages.values()) {
- await this.saveMessageRecord(item.message, item.conversationType, tx, {
- mergeClientRecord: item.mergeClientRecord
- })
+ await this.saveMessageRecord(
+ item.message,
+ item.conversationType,
+ tx,
+ {
+ mergeClientRecord: item.mergeClientRecord,
+ },
+ );
}
// 2.2 写入本批变更会话
- await conversationStore.saveConversationRecord([...changedConversations.values()], tx)
+ await conversationStore.saveConversationRecord(
+ [...changedConversations.values()],
+ tx,
+ );
// 2.3 写入本批游标
- await setMessageMaxId(conversationType, maxMessageId, tx)
- }
- )
+ await setMessageMaxId(conversationType, maxMessageId, tx);
+ },
+ );
// 3. 持久化成功后推进内存游标
- this.updateMessageCursor(conversationType, maxMessageId)
+ this.updateMessageCursor(conversationType, maxMessageId);
for (const item of persistedMessages.values()) {
- this.updateMessageCursor(item.conversationType, item.message.id)
+ this.updateMessageCursor(item.conversationType, item.message.id);
}
},
@@ -590,100 +684,126 @@ export const useMessageStore = defineStore('imMessageStore', {
insertMessage(
conversationInfo: MessageConversationInfo,
messageInfo: Message,
- options?: { saveMaxId?: boolean }
+ options?: { saveMaxId?: boolean },
): Promise {
- const conversationStore = useConversationStore()
- const hasIncomingClientMessageId = !!messageInfo.clientMessageId
- const message = ensureClientMessageId(messageInfo)
+ const conversationStore = useConversationStore();
+ const hasIncomingClientMessageId = !!messageInfo.clientMessageId;
+ const message = ensureClientMessageId(messageInfo);
// 1. 先处理消息带来的群资料变更
- if (conversationInfo.type === ImConversationType.GROUP && isGroupNotification(message.type)) {
+ if (
+ conversationInfo.type === ImConversationType.GROUP &&
+ isGroupNotification(message.type)
+ ) {
useGroupStore().applyGroupNotification(
conversationInfo.targetId,
message.type,
- message.content
- )
+ message.content,
+ );
}
// 2. 确保会话和消息缓存存在
- const conversation = conversationStore.ensureConversation(conversationInfo)
- const messages = this.getMessageList(conversationInfo.type, conversationInfo.targetId)
- const existingIndex = messages.findIndex((item) => isSameMessage(item, message))
+ const conversation =
+ conversationStore.ensureConversation(conversationInfo);
+ const messages = this.getMessageList(
+ conversationInfo.type,
+ conversationInfo.targetId,
+ );
+ const existingIndex = messages.findIndex((item) =>
+ isSameMessage(item, message),
+ );
// 3. 已存在消息走覆盖更新
if (existingIndex !== -1) {
- const existing = messages[existingIndex]
+ const existing = messages[existingIndex];
if (!existing) {
- return Promise.resolve()
+ return Promise.resolve();
}
- applyServerMessageUpdate(existing, message)
+ applyServerMessageUpdate(existing, message);
if (existingIndex === messages.length - 1) {
- recomputeConversationLast(conversation, messages)
- syncConversationAtFlags(conversation, message)
+ recomputeConversationLast(conversation, messages);
+ syncConversationAtFlags(conversation, message);
}
return getDb()
- .transaction(['messages', 'conversations', 'settings'], 'readwrite', async (tx) => {
- await this.saveMessageRecord(existing, conversationInfo.type, tx, {
- mergeClientRecord: hasIncomingClientMessageId
- })
- await conversationStore.saveConversationRecord(conversation, tx)
- if (options?.saveMaxId !== false) {
- await setMessageMaxId(conversationInfo.type, message.id, tx)
- }
- })
+ .transaction(
+ ['messages', 'conversations', 'settings'],
+ 'readwrite',
+ async (tx) => {
+ await this.saveMessageRecord(
+ existing,
+ conversationInfo.type,
+ tx,
+ {
+ mergeClientRecord: hasIncomingClientMessageId,
+ },
+ );
+ await conversationStore.saveConversationRecord(conversation, tx);
+ if (options?.saveMaxId !== false) {
+ await setMessageMaxId(conversationInfo.type, message.id, tx);
+ }
+ },
+ )
.catch((error) => {
- console.error('[IM messageStore] 消息写入失败', error)
- throw error
+ console.error('[IM messageStore] 消息写入失败', error);
+ throw error;
})
.then(() => {
- this.updateMessageCursor(conversationInfo.type, message.id)
- })
+ this.updateMessageCursor(conversationInfo.type, message.id);
+ });
}
// 4. 新消息更新会话摘要和未读状态
- applyConversationSummary(conversation, message)
- syncConversationAtFlags(conversation, message)
+ applyConversationSummary(conversation, message);
+ syncConversationAtFlags(conversation, message);
const isActive =
conversationStore.activeConversation?.type === conversationInfo.type &&
- conversationStore.activeConversation?.targetId === conversationInfo.targetId
+ conversationStore.activeConversation?.targetId ===
+ conversationInfo.targetId;
if (
!message.selfSend &&
!isActive &&
- !conversationStore.isMessageCoveredByReadPosition(conversation, message) &&
+ !conversationStore.isMessageCoveredByReadPosition(
+ conversation,
+ message,
+ ) &&
isNormalMessage(message.type) &&
message.status !== ImMessageStatus.RECALL
) {
- conversation.unreadCount++
+ conversation.unreadCount++;
}
// 5. 新消息按 id 插入到内存数组
- let insertIndex = messages.length
+ let insertIndex = messages.length;
if (message.id) {
for (const [index, existing] of messages.entries()) {
if (existing.id && message.id < existing.id) {
- insertIndex = index
- break
+ insertIndex = index;
+ break;
}
}
}
- messages.splice(insertIndex, 0, message)
+ messages.splice(insertIndex, 0, message);
// 6. 单事务写入消息、会话摘要和游标
return getDb()
- .transaction(['messages', 'conversations', 'settings'], 'readwrite', async (tx) => {
- await this.saveMessageRecord(message, conversationInfo.type, tx, {
- mergeClientRecord: hasIncomingClientMessageId && !!message.id
- })
- await conversationStore.saveConversationRecord(conversation, tx)
- if (options?.saveMaxId !== false) {
- await setMessageMaxId(conversationInfo.type, message.id, tx)
- }
- })
+ .transaction(
+ ['messages', 'conversations', 'settings'],
+ 'readwrite',
+ async (tx) => {
+ await this.saveMessageRecord(message, conversationInfo.type, tx, {
+ mergeClientRecord: hasIncomingClientMessageId && !!message.id,
+ });
+ await conversationStore.saveConversationRecord(conversation, tx);
+ if (options?.saveMaxId !== false) {
+ await setMessageMaxId(conversationInfo.type, message.id, tx);
+ }
+ },
+ )
.catch((error) => {
- console.error('[IM messageStore] 消息写入失败', error)
- throw error
+ console.error('[IM messageStore] 消息写入失败', error);
+ throw error;
})
.then(() => {
- this.updateMessageCursor(conversationInfo.type, message.id)
- })
+ this.updateMessageCursor(conversationInfo.type, message.id);
+ });
},
/** ack 合并 */
@@ -691,23 +811,23 @@ export const useMessageStore = defineStore('imMessageStore', {
conversationType: number,
targetId: number,
clientMessageId: string,
- updates: Partial
+ updates: Partial,
) {
- const mergeKey = `${conversationType}:${targetId}:${clientMessageId}`
- const existingPromise = ackMergingPromises.get(mergeKey)
+ const mergeKey = `${conversationType}:${targetId}:${clientMessageId}`;
+ const existingPromise = ackMergingPromises.get(mergeKey);
if (existingPromise) {
- return existingPromise
+ return existingPromise;
}
const promise = this.doAckMessage(
conversationType,
targetId,
clientMessageId,
- updates
+ updates,
).finally(() => {
- ackMergingPromises.delete(mergeKey)
- })
- ackMergingPromises.set(mergeKey, promise)
- return promise
+ ackMergingPromises.delete(mergeKey);
+ });
+ ackMergingPromises.set(mergeKey, promise);
+ return promise;
},
/** 执行 ack 合并 */
@@ -715,43 +835,52 @@ export const useMessageStore = defineStore('imMessageStore', {
conversationType: number,
targetId: number,
clientMessageId: string,
- updates: Partial
+ updates: Partial,
) {
// 1. 定位待合并消息
- const conversationStore = useConversationStore()
- const conversation = conversationStore.getConversation(conversationType, targetId)
+ const conversationStore = useConversationStore();
+ const conversation = conversationStore.getConversation(
+ conversationType,
+ targetId,
+ );
if (!conversation) {
- return
+ return;
}
- const messages = this.getMessageList(conversationType, targetId)
- const message = messages.find((item) => item.clientMessageId === clientMessageId)
+ const messages = this.getMessageList(conversationType, targetId);
+ const message = messages.find(
+ (item) => item.clientMessageId === clientMessageId,
+ );
if (!message) {
- return
+ return;
}
- message._ackMerging = true
+ message._ackMerging = true;
try {
// 2. 合并服务端 ack 到内存
- applyServerMessageUpdate(message, updates)
+ applyServerMessageUpdate(message, updates);
if (messages[messages.length - 1] === message) {
- recomputeConversationLast(conversation, messages)
+ recomputeConversationLast(conversation, messages);
}
// 3. 单事务写入消息、会话摘要和游标
await getDb()
- .transaction(['messages', 'conversations', 'settings'], 'readwrite', async (tx) => {
- await this.saveMessageRecord(message, conversationType, tx, {
- mergeClientRecord: true
- })
- await conversationStore.saveConversationRecord(conversation, tx)
- await setMessageMaxId(conversationType, message.id, tx)
- })
+ .transaction(
+ ['messages', 'conversations', 'settings'],
+ 'readwrite',
+ async (tx) => {
+ await this.saveMessageRecord(message, conversationType, tx, {
+ mergeClientRecord: true,
+ });
+ await conversationStore.saveConversationRecord(conversation, tx);
+ await setMessageMaxId(conversationType, message.id, tx);
+ },
+ )
.catch((error) => {
- console.error('[IM messageStore] ack 写入失败', error)
- throw error
- })
- this.updateMessageCursor(conversationType, message.id)
+ console.error('[IM messageStore] ack 写入失败', error);
+ throw error;
+ });
+ this.updateMessageCursor(conversationType, message.id);
} finally {
// 4. 清理合并标记
- message._ackMerging = false
+ message._ackMerging = false;
}
},
@@ -760,27 +889,27 @@ export const useMessageStore = defineStore('imMessageStore', {
conversationType: number,
targetId: number,
clientMessageId: string,
- patch: Partial
+ patch: Partial,
) {
const message = this.getMessageList(conversationType, targetId).find(
- (item) => item.clientMessageId === clientMessageId
- )
+ (item) => item.clientMessageId === clientMessageId,
+ );
if (!message) {
- return
+ return;
}
- let changed = false
+ let changed = false;
for (const key in patch) {
if (
Object.prototype.hasOwnProperty.call(patch, key) &&
(patch as Record)[key] !==
(message as unknown as Record)[key]
) {
- changed = true
- break
+ changed = true;
+ break;
}
}
if (changed) {
- applyServerMessageUpdate(message, patch)
+ applyServerMessageUpdate(message, patch);
}
},
@@ -788,43 +917,52 @@ export const useMessageStore = defineStore('imMessageStore', {
async recallMessage(
conversationType: number,
targetId: number,
- recallSignalContent: string
+ recallSignalContent: string,
): Promise {
- const conversationStore = useConversationStore()
+ const conversationStore = useConversationStore();
const changed = this.applyRecallMessageInMemory(
conversationType,
targetId,
- recallSignalContent
- )
+ recallSignalContent,
+ );
if (!changed) {
- return
+ return;
}
await getDb()
.transaction(['messages', 'conversations'], 'readwrite', async (tx) => {
- await this.saveMessageRecord(changed.message, conversationType, tx)
- await conversationStore.saveConversationRecord(changed.conversation, tx)
+ await this.saveMessageRecord(changed.message, conversationType, tx);
+ await conversationStore.saveConversationRecord(
+ changed.conversation,
+ tx,
+ );
})
.catch((error) => {
- console.error('[IM messageStore] 撤回消息写入失败', error)
- throw error
- })
+ console.error('[IM messageStore] 撤回消息写入失败', error);
+ throw error;
+ });
},
/** 应用已读回执 */
applyMessageReadReceipt(options: {
- conversationType: number
- groupMessageId?: number
- privateReadMaxId?: number
- readCount?: number
- receiptStatus?: number
- targetId: number
+ conversationType: number;
+ groupMessageId?: number;
+ privateReadMaxId?: number;
+ readCount?: number;
+ receiptStatus?: number;
+ targetId: number;
}) {
- const messages = this.getMessageList(options.conversationType, options.targetId)
- const changed: Message[] = []
+ const messages = this.getMessageList(
+ options.conversationType,
+ options.targetId,
+ );
+ const changed: Message[] = [];
// 1. 私聊回执批量更新自己发送的消息
- if (options.conversationType === ImConversationType.PRIVATE && options.privateReadMaxId) {
- this.updatePrivateReadMaxId(options.targetId, options.privateReadMaxId)
- const privateReadMaxId = options.privateReadMaxId
+ if (
+ options.conversationType === ImConversationType.PRIVATE &&
+ options.privateReadMaxId
+ ) {
+ this.updatePrivateReadMaxId(options.targetId, options.privateReadMaxId);
+ const privateReadMaxId = options.privateReadMaxId;
messages.forEach((message) => {
if (
message.selfSend &&
@@ -832,122 +970,152 @@ export const useMessageStore = defineStore('imMessageStore', {
message.id <= privateReadMaxId &&
message.receiptStatus === ImMessageReceiptStatus.PENDING
) {
- message.receiptStatus = ImMessageReceiptStatus.DONE
- changed.push(message)
+ message.receiptStatus = ImMessageReceiptStatus.DONE;
+ changed.push(message);
}
- })
- } else if (options.conversationType === ImConversationType.GROUP && options.groupMessageId) {
+ });
+ } else if (
+ options.conversationType === ImConversationType.GROUP &&
+ options.groupMessageId
+ ) {
// 2. 群聊回执更新单条消息
- const message = messages.find((item) => item.id === options.groupMessageId)
+ const message = messages.find(
+ (item) => item.id === options.groupMessageId,
+ );
if (message) {
if (options.readCount !== undefined) {
- message.readCount = options.readCount
+ message.readCount = options.readCount;
}
if (options.receiptStatus !== undefined) {
- message.receiptStatus = options.receiptStatus
+ message.receiptStatus = options.receiptStatus;
}
- changed.push(message)
+ changed.push(message);
}
}
if (changed.length === 0) {
- return
+ return;
}
// 3. 单事务写入变更消息
void getDb()
.transaction(['messages'], 'readwrite', async (tx) => {
for (const message of changed) {
- await this.saveMessageRecord(message, options.conversationType, tx)
+ await this.saveMessageRecord(message, options.conversationType, tx);
}
})
- .catch((error) => console.warn('[IM messageStore] 回执写入失败', error))
+ .catch((error) =>
+ console.warn('[IM messageStore] 回执写入失败', error),
+ );
},
/** 前置历史消息 */
- prependMessageList(conversationType: number, targetId: number, earlierMessages: Message[]) {
+ prependMessageList(
+ conversationType: number,
+ targetId: number,
+ earlierMessages: Message[],
+ ) {
if (earlierMessages.length === 0) {
- return
+ return;
}
- const messages = this.getMessageList(conversationType, targetId)
- const existingIds = new Set(messages.map((message) => message.id).filter(Boolean))
+ const messages = this.getMessageList(conversationType, targetId);
+ const existingIds = new Set(
+ messages.map((message) => message.id).filter(Boolean),
+ );
const fresh = earlierMessages
.map((message) => ensureClientMessageId(message))
.filter((message) => message.id && !existingIds.has(message.id))
- .toSorted((messageA, messageB) => (messageA.id || 0) - (messageB.id || 0))
+ .toSorted(
+ (messageA, messageB) => (messageA.id || 0) - (messageB.id || 0),
+ );
if (fresh.length === 0) {
- return
+ return;
}
- const key = getMessageCacheKey(conversationType, targetId)
- this.messagesByConversation[key] = [...fresh, ...messages]
+ const key = getMessageCacheKey(conversationType, targetId);
+ this.messagesByConversation[key] = [...fresh, ...messages];
void getDb()
.transaction(['messages'], 'readwrite', async (tx) => {
for (const message of fresh) {
- await this.saveMessageRecord(message, conversationType, tx)
+ await this.saveMessageRecord(message, conversationType, tx);
}
})
- .catch((error) => console.warn('[IM messageStore] 历史消息写入失败', error))
+ .catch((error) =>
+ console.warn('[IM messageStore] 历史消息写入失败', error),
+ );
},
/** 删除单条消息 */
removeMessage(
conversationType: number,
targetId: number,
- key: { clientMessageId?: string; id?: number; }
+ key: { clientMessageId?: string; id?: number },
) {
// 1. 定位会话和消息
- const conversationStore = useConversationStore()
- const conversation = conversationStore.getConversation(conversationType, targetId)
+ const conversationStore = useConversationStore();
+ const conversation = conversationStore.getConversation(
+ conversationType,
+ targetId,
+ );
if (!conversation) {
- return
+ return;
}
- const messages = this.getMessageList(conversationType, targetId)
+ const messages = this.getMessageList(conversationType, targetId);
const index = messages.findIndex((message) => {
if (key.id && message.id && message.id === key.id) {
- return true
+ return true;
}
- return !!key.clientMessageId && message.clientMessageId === key.clientMessageId
- })
+ return (
+ !!key.clientMessageId &&
+ message.clientMessageId === key.clientMessageId
+ );
+ });
if (index === -1) {
- return
+ return;
}
// 2. 从内存移除消息
- const [removed] = messages.splice(index, 1)
+ const [removed] = messages.splice(index, 1);
if (!removed) {
- return
+ return;
}
- revokeBlobUrlsInContent(removed.content)
+ revokeBlobUrlsInContent(removed.content);
if (index === messages.length) {
- recomputeConversationLast(conversation, messages)
+ recomputeConversationLast(conversation, messages);
}
// 3. 删除本地记录并保存会话摘要
getDb()
.delete('messages', getMessageKey(removed, conversationType))
- .catch((error) => console.warn('[IM messageStore] 消息删除失败', error))
- conversationStore.saveConversation(conversation)
+ .catch((error) =>
+ console.warn('[IM messageStore] 消息删除失败', error),
+ );
+ conversationStore.saveConversation(conversation);
},
/** 删除会话全部消息 */
deleteConversationMessageList(conversationType: number, targetId: number) {
// 1. 清理内存消息和媒体资源
- const clientConversationId = getClientConversationId(conversationType, targetId)
- const messages = this.messagesByConversation[clientConversationId] || []
+ const clientConversationId = getClientConversationId(
+ conversationType,
+ targetId,
+ );
+ const messages = this.messagesByConversation[clientConversationId] || [];
messages.forEach((message) => {
- revokeBlobUrlsInContent(message.content)
- message._localFile = undefined
- })
- Reflect.deleteProperty(this.messagesByConversation, clientConversationId)
+ revokeBlobUrlsInContent(message.content);
+ message._localFile = undefined;
+ });
+ Reflect.deleteProperty(this.messagesByConversation, clientConversationId);
this.loadedConversationKeys = this.loadedConversationKeys.filter(
- (key) => key !== clientConversationId
- )
+ (key) => key !== clientConversationId,
+ );
// 2. 删除 IndexedDB 消息
getDb()
.deleteByIndex('messages', 'clientConversationId', clientConversationId)
- .catch((error) => console.warn('[IM messageStore] 会话消息删除失败', error))
- }
- }
-})
+ .catch((error) =>
+ console.warn('[IM messageStore] 会话消息删除失败', error),
+ );
+ },
+ },
+});
-export const useMessageStoreWithOut = () => useMessageStore()
+export const useMessageStoreWithOut = () => useMessageStore();
if (import.meta.hot) {
- import.meta.hot.accept(acceptHMRUpdate(useMessageStore, import.meta.hot))
+ import.meta.hot.accept(acceptHMRUpdate(useMessageStore, import.meta.hot));
}
diff --git a/apps/web-antd/src/views/im/home/store/rtcStore.ts b/apps/web-antd/src/views/im/home/store/rtcStore.ts
index 46a98b982..1137cf906 100644
--- a/apps/web-antd/src/views/im/home/store/rtcStore.ts
+++ b/apps/web-antd/src/views/im/home/store/rtcStore.ts
@@ -1,91 +1,94 @@
-import type { ImRtcApi } from '#/api/im/rtc'
+import type {
+ ImRtcCallEndReasonValue,
+ ImRtcCallStageValue,
+ ImRtcParticipantStatusValue,
+} from '../../utils/constants';
-import { computed, ref } from 'vue'
+import type { ImRtcApi } from '#/api/im/rtc';
-import { defineStore } from 'pinia'
+import { computed, ref } from 'vue';
-import { getCurrentUserId } from '#/views/im/utils/auth'
+import { defineStore } from 'pinia';
+
+import { getCurrentUserId } from '#/views/im/utils/auth';
import {
ImConversationType,
- type ImRtcCallEndReasonValue,
ImRtcCallStage,
- type ImRtcCallStageValue,
ImRtcCallStatus,
- type ImRtcParticipantStatusValue
-} from '../../utils/constants'
-import { useFriendStore } from './friendStore'
-import { useGroupStore } from './groupStore'
+} from '../../utils/constants';
+import { useFriendStore } from './friendStore';
+import { useGroupStore } from './groupStore';
type GroupActiveCallCache = {
- participantsLoaded?: boolean // 是否已拉取完整参与者列表
-} & ImRtcApi.RtcGroupCallRespVO
+ participantsLoaded?: boolean; // 是否已拉取完整参与者列表
+} & ImRtcApi.RtcGroupCallRespVO;
// RTC_CALL 通话信令载荷;按 status 区分子类型语义
export interface ImRtcCallNotification {
- status: ImRtcParticipantStatusValue
- room: string
- conversationType: number
- mediaType: number
- groupId?: number
+ status: ImRtcParticipantStatusValue;
+ room: string;
+ conversationType: number;
+ mediaType: number;
+ groupId?: number;
// INVITE 专属:被叫接通需要的 LiveKit 连接参数 + 主叫展示信息
- livekitUrl?: string
- token?: string
- inviterUserId?: number
- inviterNickname?: string
- inviterAvatar?: string
+ livekitUrl?: string;
+ token?: string;
+ inviterUserId?: number;
+ inviterNickname?: string;
+ inviterAvatar?: string;
// INVITE 专属:本次被邀请人列表;包含收件人自身,前端来电小条按需过滤展示「邀请的其他人」
- inviteeIds?: number[]
+ inviteeIds?: number[];
// REJECT 专属:操作者展示信息(其它子类型走 RTC_CALL_END)
- operatorUserId?: number
- operatorNickname?: string
- operatorAvatar?: string
+ operatorUserId?: number;
+ operatorNickname?: string;
+ operatorAvatar?: string;
}
// RTC_PARTICIPANT_CONNECTED 通话参与者加入载荷(LiveKit webhook 转推)
export interface ImRtcParticipantConnectedNotification {
- room: string
- userId: number
- conversationType: number
- groupId?: number
+ room: string;
+ userId: number;
+ conversationType: number;
+ groupId?: number;
// 群聊场景非邀请成员首次填充胶囊条用
- mediaType?: number
- inviterUserId?: number
+ mediaType?: number;
+ inviterUserId?: number;
}
// RTC_PARTICIPANT_DISCONNECTED 通话参与者离开载荷(LiveKit webhook 转推)
export interface ImRtcParticipantDisconnectedNotification {
- room: string
- userId: number
- conversationType: number
- groupId?: number
+ room: string;
+ userId: number;
+ conversationType: number;
+ groupId?: number;
}
// RTC_CALL_END 通话结束载荷(入消息流;私聊渲染消息气泡,群聊渲染系统提示行)
export interface ImRtcCallEndNotification {
- room: string
- conversationType: number
- mediaType: number
- endReason: ImRtcCallEndReasonValue
- durationSeconds?: number
+ room: string;
+ conversationType: number;
+ mediaType: number;
+ endReason: ImRtcCallEndReasonValue;
+ durationSeconds?: number;
// 操作者聚合字段:HANGUP/CANCEL/REJECT 触发人;webhook 兜底为 null
- operatorUserId?: number
- operatorNickname?: string
- operatorAvatar?: string
+ operatorUserId?: number;
+ operatorNickname?: string;
+ operatorAvatar?: string;
}
export const useRtcStore = defineStore('imRtc', () => {
/** 当前阶段 */
- const stage = ref(ImRtcCallStage.IDLE)
+ const stage = ref(ImRtcCallStage.IDLE);
/** 当前通话;invite / accept / refreshToken 拿到的完整信息 */
- const call = ref(null)
+ const call = ref(null);
/** 来电载荷;仅 INCOMING 阶段使用;status 固定 INVITING,其它字段 INVITE 专属 */
- const incomingPayload = ref(null)
+ const incomingPayload = ref(null);
/** 进入 RUNNING 的时间戳;用于通话时长展示;reset 时清零 */
- const startedAt = ref(0)
+ const startedAt = ref(0);
/** 是否处于通话相关阶段 */
- const isActive = computed(() => stage.value !== ImRtcCallStage.IDLE)
+ const isActive = computed(() => stage.value !== ImRtcCallStage.IDLE);
/**
* 对端展示名;按阶段 + 会话类型分支:
@@ -93,57 +96,61 @@ export const useRtcStore = defineStore('imRtc', () => {
*/
const peerNickname = computed(() => {
if (stage.value === ImRtcCallStage.INCOMING) {
- return incomingPayload.value?.inviterNickname || ''
+ return incomingPayload.value?.inviterNickname || '';
}
- const c = call.value
- if (!c) return ''
+ const c = call.value;
+ if (!c) return '';
if (c.conversationType === ImConversationType.GROUP) {
- return useGroupStore().getGroup(c.groupId ?? 0)?.name || ''
+ return useGroupStore().getGroup(c.groupId ?? 0)?.name || '';
}
- const peerUserId = resolvePrivatePeerUserId(c)
- return (peerUserId && useFriendStore().getFriend(peerUserId)?.nickname) || ''
- })
+ const peerUserId = resolvePrivatePeerUserId(c);
+ return (
+ (peerUserId && useFriendStore().getFriend(peerUserId)?.nickname) || ''
+ );
+ });
/** 对端头像;策略同 peerNickname */
const peerAvatar = computed(() => {
if (stage.value === ImRtcCallStage.INCOMING) {
- return incomingPayload.value?.inviterAvatar || ''
+ return incomingPayload.value?.inviterAvatar || '';
}
- const c = call.value
- if (!c) return ''
+ const c = call.value;
+ if (!c) return '';
if (c.conversationType === ImConversationType.GROUP) {
- return useGroupStore().getGroup(c.groupId ?? 0)?.avatar || ''
+ return useGroupStore().getGroup(c.groupId ?? 0)?.avatar || '';
}
- const peerUserId = resolvePrivatePeerUserId(c)
- return (peerUserId && useFriendStore().getFriend(peerUserId)?.avatar) || ''
- })
+ const peerUserId = resolvePrivatePeerUserId(c);
+ return (peerUserId && useFriendStore().getFriend(peerUserId)?.avatar) || '';
+ });
/** 私聊场景对端 userId:自己是主叫则取首个 invitee,否则取 inviter */
- function resolvePrivatePeerUserId(c: ImRtcApi.RtcCallRespVO): number | undefined {
- const myId = getCurrentUserId()
- return c.inviterId === myId ? c.inviteeIds?.[0] : c.inviterId
+ function resolvePrivatePeerUserId(
+ c: ImRtcApi.RtcCallRespVO,
+ ): number | undefined {
+ const myId = getCurrentUserId();
+ return c.inviterId === myId ? c.inviteeIds?.[0] : c.inviterId;
}
/** 群活跃通话索引;groupId -> 群通话摘要;用于群聊顶部胶囊条 */
- const groupActiveCalls = ref