| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { Friend, FriendLite, FriendRequest } from '../types' |
| | | |
| | | import type { ImFriendApi } from '#/api/im/friend' |
| | | import type { ImFriendRequestApi } from '#/api/im/friend/request' |
| | | |
| | | import { CommonStatusEnum } from '#/packages/constants/src' |
| | | |
| | | 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 { 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<void>; userId: number; } |
| | | |
| | | /** å½åæ£å¨è¿è¡ç好åå表æåï¼å¤ dispatcher åæ¶è§¦åæ¶å¤ç¨åä¸ Promiseï¼é¿å
éªå´©éæ */ |
| | | let pendingFetchFriends: null | PendingRequest = null |
| | | /** å½åæ£å¨è¿è¡ç好åç³è¯·å表æåï¼å¤ç«¯è¿ç»å¤æ¡ç³è¯·å°è¾¾æ¶å¤ç¨åä¸ Promiseï¼é¿å
éªå´©éæ */ |
| | | let pendingFetchRequests: null | PendingRequest = null |
| | | /** å½åæ£å¨è¿è¡çãå è½½æ´å¤ç³è¯·ãè¯·æ± */ |
| | | let pendingLoadMoreRequests: null | PendingRequest = null |
| | | /** å½åæ£å¨è¿è¡ç好å详æ
è¯·æ± */ |
| | | const pendingFetchFriendInfos = new Map<string, Promise<void>>() |
| | | |
| | | /** clear() æ¶éå¢ï¼æ§è´¦å·é£æ¬¡è¿æ²¡è¿åçè¯·æ± resolve 忝坹ä¸è´æå storeï¼é²è·¨è´¦å·æ°æ®æ³æ¼ */ |
| | | let storeEpoch = 0 |
| | | |
| | | /** æå»ºå¥½å详æ
请æ±å»é key */ |
| | | function getPendingFriendInfoKey(userId: number, friendUserId: number): string { |
| | | return `${userId}:${friendUserId}` |
| | | } |
| | | |
| | | /** 好åéç¥ payloadï¼å¯¹é½å端 BaseFriendNotification + åç±»è£ååçåæ®µï¼ */ |
| | | export interface FriendNotificationPayload { |
| | | operatorUserId: number |
| | | friendUserId: number |
| | | // FRIEND_REQUEST_* ç³»åï¼ç³è¯·è®°å½çæ ¸å¿å段ï¼é¿å
payload æºå¸¦å®æ´ DOï¼ |
| | | requestId?: number |
| | | applyContent?: string |
| | | handleContent?: string |
| | | addSource?: number |
| | | // FRIEND_REQUEST_RECEIVEDï¼ç³è¯·æ¹èååæ®µï¼ä¾åç«¯ç´æ¨ push è¿åè¡¨ï¼æ éåæ |
| | | fromNickname?: string |
| | | fromAvatar?: string |
| | | // FRIEND_UPDATEï¼åè¾¹å±æ§åæ´ |
| | | displayName?: string |
| | | silent?: boolean |
| | | pinned?: boolean |
| | | // FRIEND_DELETEï¼æ¯å¦çº§èæ¸
çæ¬ç«¯ç¸å
³æ°æ®ï¼å¦ç§èä¼è¯ï¼ |
| | | clear?: boolean |
| | | } |
| | | |
| | | /** |
| | | * IM 好å Store |
| | | * |
| | | * è´è´£ï¼ |
| | | * - æå / ç¼åå½åç»å½ç¨æ·ç好åå表 + ç³è¯·å表 |
| | | * - ç³è¯·-å®¡æ¹æµç¨ï¼apply / agree / refuseï¼+ 夿³¨ / å
ææ° / è系人置顶 / æé» |
| | | * - æ¥æ¶ WebSocket 1201-1210 段ä½éç¥ï¼æäºä»¶ååå° friendStore å
é¨å dispatcher |
| | | */ |
| | | export const useFriendStore = defineStore('imFriendStore', { |
| | | state: () => ({ |
| | | friends: [] as Friend[], |
| | | // ä»
fetchFriendList æååç½®ä½ï¼loadFriendDataï¼IDBï¼ä¸ç½®ä½ï¼å¦ååå° SWR å·æ°ä¼è¢«ç¼åå½ä¸è·³è¿ |
| | | loaded: false, |
| | | /** æç¸å
³ç好åç³è¯·å表ï¼å«æåèµ·ç + å«äººå æçï¼å端æ id ååºæ¸¸æ åé¡µï¼ */ |
| | | friendRequests: [] as FriendRequest[], |
| | | /** æ¯å¦è¿ææ´æ©çç³è¯·è®°å½å¯å è½½ï¼è¿å䏿»¡ page size å³ç½® false */ |
| | | hasMoreFriendRequests: true |
| | | }), |
| | | |
| | | getters: { |
| | | /** |
| | | * friendUserId â Friend ç O(1) ç´¢å¼ï¼ä» friends æ°ç»æ´¾çï¼Pinia å¨ friends ååæ¶èªå¨éç® |
| | | * |
| | | * æ¶æ¯æ¸²æéè¦æ senderId 忥åé人头å / 夿³¨ï¼æ¯æ¡æ¶æ¯æ¸²æé½ä¼ getFriendï¼ |
| | | * ç´æ¥ find æ¶ N æ¡æ¶æ¯ à M 好å = O(NÃM)ï¼å»ºç´¢å¼å忬¡è¯» O(1)ï¼é建åªå¨å好åï¼fetchFriendList / upsertFriend çï¼æ¶åç |
| | | */ |
| | | getFriendMap: (state): Map<number, Friend> => { |
| | | const map = new Map<number, Friend>() |
| | | for (const friend of state.friends) { |
| | | map.set(friend.friendUserId, friend) |
| | | } |
| | | return map |
| | | }, |
| | | /** æ friendUserId æ¾å¥½åï¼å«å·²è½¯å ç DISABLE è®°å½ï¼è°ç¨æ¹èªè¡å¤å®ï¼ */ |
| | | getFriend(): (friendUserId: number) => Friend | undefined { |
| | | return (friendUserId: number) => this.getFriendMap.get(friendUserId) |
| | | }, |
| | | /** å½åçæç好åå表ï¼è¿æ»¤æ DISABLE 软å è®°å½ï¼ */ |
| | | getActiveFriendList: (state): Friend[] => { |
| | | return state.friends.filter((friend) => friend.status !== CommonStatusEnum.DISABLE) |
| | | }, |
| | | /** å½åçæå¥½åç Lite è§å¾ï¼PickerPanel / é人弹çªå
±ç¨ï¼èªå¸¦æ¼é³å段ä¾åæ¡¶ / æç´¢ï¼ */ |
| | | getActiveFriendLiteList(): FriendLite[] { |
| | | return this.getActiveFriendList.map((friend: Friend) => ({ |
| | | id: friend.friendUserId, |
| | | nickname: friend.nickname, |
| | | nicknamePinyin: friend.nicknamePinyin, |
| | | avatar: friend.avatar, |
| | | displayName: friend.displayName, |
| | | displayNamePinyin: friend.displayNamePinyin |
| | | })) |
| | | }, |
| | | /** å¤æå¯¹æ¹æ¯å¦æ¯å½åç¨æ·çææå¥½åï¼åå¨ + é DISABLEï¼ */ |
| | | isActiveFriend() { |
| | | return (friendUserId: number): boolean => { |
| | | 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 |
| | | ) |
| | | }, |
| | | /** æªå¤çç³è¯·æ°ï¼æ¥æ¶æ¹=æï¼ââ 宿¶æ´¾çï¼ãæ°çæåã红ç¹ç¨ */ |
| | | getUnhandledRequestCount: (state): number => { |
| | | const currentUserId = getCurrentUserId() |
| | | return state.friendRequests.filter( |
| | | (request) => |
| | | request.handleResult === ImFriendRequestHandleResult.UNHANDLED && |
| | | request.toUserId === currentUserId |
| | | ).length |
| | | } |
| | | }, |
| | | |
| | | actions: { |
| | | // ==================== æ¬å°ç¼å ==================== |
| | | |
| | | /** ä» IndexedDB æ¢å¤å¥½åå好åç³è¯· */ |
| | | async loadFriendData(): Promise<boolean> { |
| | | try { |
| | | const [friends, friendRequests] = await Promise.all([ |
| | | getDb().getAll<Friend>('friends'), |
| | | getDb().getAll<FriendRequest>('friendRequests') |
| | | ]) |
| | | if (friends.length > 0) { |
| | | 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 |
| | | } |
| | | return friends.length > 0 |
| | | } catch (error) { |
| | | console.warn('[IM friendStore] æ¬å°å¥½åç¼å读å失败', error) |
| | | return false |
| | | } |
| | | }, |
| | | |
| | | /** ä¿å好åå表 */ |
| | | saveFriendList(): void { |
| | | void getDb() |
| | | .transaction(['friends'], 'readwrite', async (tx) => { |
| | | const db = getDb() |
| | | await db.clearStore('friends', tx) |
| | | for (const friend of this.friends) { |
| | | if (friend.id) { |
| | | await db.put('friends', friend, tx) |
| | | } |
| | | } |
| | | }) |
| | | .catch((error) => console.warn('[IM friendStore] æ¬å°å¥½åç¼ååå
¥å¤±è´¥', error)) |
| | | }, |
| | | |
| | | /** ä¿åå个好å */ |
| | | async saveFriendRecord(friend: Friend | undefined): Promise<void> { |
| | | if (!friend?.id) { |
| | | return |
| | | } |
| | | await getDb().put('friends', friend) |
| | | }, |
| | | |
| | | /** ä¿åå个好å */ |
| | | saveFriend(friend: Friend | undefined): void { |
| | | void this.saveFriendRecord(friend).catch((error) => |
| | | console.warn('[IM friendStore] æ¬å°å¥½ååå
¥å¤±è´¥', error) |
| | | ) |
| | | }, |
| | | |
| | | /** ä¿å好åç³è¯·å表 */ |
| | | saveFriendRequestList(): void { |
| | | void getDb() |
| | | .transaction(['friendRequests'], 'readwrite', async (tx) => { |
| | | const db = getDb() |
| | | await db.clearStore('friendRequests', tx) |
| | | for (const request of this.friendRequests) { |
| | | await db.put('friendRequests', request, tx) |
| | | } |
| | | }) |
| | | .catch((error) => console.warn('[IM friendStore] æ¬å°å¥½åç³è¯·ç¼ååå
¥å¤±è´¥', error)) |
| | | }, |
| | | |
| | | /** ä¿å忡好åç³è¯· */ |
| | | async saveFriendRequestRecord(request: FriendRequest | undefined): Promise<void> { |
| | | if (!request) { |
| | | return |
| | | } |
| | | await getDb().put('friendRequests', request) |
| | | }, |
| | | |
| | | /** ä¿å忡好åç³è¯· */ |
| | | saveFriendRequest(request: FriendRequest | undefined): void { |
| | | void this.saveFriendRequestRecord(request).catch((error) => |
| | | console.warn('[IM friendStore] æ¬å°å¥½åç³è¯·åå
¥å¤±è´¥', error) |
| | | ) |
| | | }, |
| | | |
| | | // ==================== è¿ç«¯æå ==================== |
| | | |
| | | /** ä»å端æåå¹¶è¦çæ¬å°å表ï¼å« DISABLE åå²å¥½åç»å·²å 对è¯å
åºï¼ï¼åªåæ¥ ENABLE çä¼è¯ä¿¡æ¯ï¼DISABLE çä¸å¨ ââ cascade æ¸
ä¼è¯ç± WS dispatcher æ payload.clear å¤çï¼é¿å
fetchFriendList è¦çç¨æ·ã䏿¸
空è天记å½ãçéæ© */ |
| | | async fetchFriendList(force = false) { |
| | | if (this.loaded && !force) { |
| | | return |
| | | } |
| | | // å¿«ç
§ epochï¼clear() ä¹åå° .then ä¹é´è§¦åç epoch++ 表示账å·å·²åï¼æ§ç»æä¸è½åå
¥æ° store |
| | | const requestEpoch = storeEpoch |
| | | const requestUserId = getCurrentUserId() |
| | | if ( |
| | | pendingFetchFriends?.epoch === requestEpoch && |
| | | pendingFetchFriends.userId === requestUserId |
| | | ) { |
| | | return pendingFetchFriends.promise |
| | | } |
| | | const promise = apiGetMyFriendList() |
| | | .then((list) => { |
| | | if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) { |
| | | return |
| | | } |
| | | 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 |
| | | } |
| | | conversationStore.updateConversation(ImConversationType.PRIVATE, friend.friendUserId, { |
| | | name: getFriendDisplayName(friend), |
| | | avatar: friend.avatar, |
| | | silent: friend.silent |
| | | }) |
| | | } |
| | | this.saveFriendList() |
| | | }) |
| | | .finally(() => { |
| | | if ( |
| | | pendingFetchFriends?.epoch === requestEpoch && |
| | | pendingFetchFriends.userId === requestUserId |
| | | ) { |
| | | pendingFetchFriends = null |
| | | } |
| | | }) |
| | | pendingFetchFriends = { epoch: requestEpoch, userId: requestUserId, promise } |
| | | return promise |
| | | }, |
| | | |
| | | /** |
| | | * å¢éæå好ååæ´å¹¶åå¹¶ï¼è¿å
¥ IM é¦å±ä¸»æ°æ®æ¢å¤ï¼ç©ºæ¸¸æ = 馿¬¡å
¨éå¢éæï¼+ éè¿ / ç¦»çº¿è¡¥å¿ |
| | | * |
| | | * å«å·²å é¤å¥½åï¼æ status èµ° upsert |
| | | */ |
| | | async pullFriends() { |
| | | // å¿«ç
§ epochï¼è´¦å·å¨æåéä¸åæ¢ï¼clear() â epoch++ï¼æ¶ä¸¢å¼æ§è´¦å·é£å é¡µç»æï¼é²è·¨è´¦å·æ°æ®æ³æ¼ |
| | | const requestEpoch = storeEpoch |
| | | const requestUserId = getCurrentUserId() |
| | | const isActive = () => requestEpoch === storeEpoch && getCurrentUserId() === requestUserId |
| | | await runIncrementalPull( |
| | | StorageKeys.settings.friendPullCursor, |
| | | apiPullMyFriendList, |
| | | async (records) => { |
| | | if (!isActive()) { |
| | | return false |
| | | } |
| | | await Promise.all(records.map((vo) => this.upsertFriendForPull(convertFriend(vo)))) |
| | | return true |
| | | }, |
| | | isActive |
| | | ) |
| | | // ç½® loadedï¼ä¾é讯å½é¡µ fetchFriendList(force=false) å¤ç¨ç¼åèééå¤å
¨éæ |
| | | if (isActive()) { |
| | | this.loaded = true |
| | | } |
| | | }, |
| | | |
| | | /** æ friendUserId è·å详æ
å¹¶åå¹¶å°æ¬å°ï¼ä¿è¯ nickname / avatar ææ°ï¼ */ |
| | | async fetchFriendInfo(friendUserId: number) { |
| | | const requestEpoch = storeEpoch |
| | | const requestUserId = getCurrentUserId() |
| | | if (!requestUserId) { |
| | | return |
| | | } |
| | | const key = getPendingFriendInfoKey(requestUserId, friendUserId) |
| | | const inflight = pendingFetchFriendInfos.get(key) |
| | | if (inflight) { |
| | | return inflight |
| | | } |
| | | const promise = (async () => { |
| | | try { |
| | | const data = await apiGetFriend(friendUserId) |
| | | if (!data) { |
| | | return |
| | | } |
| | | // clear() å·²åè´¦å·ï¼æ§è¯·æ±ç好å详æ
ä¸è½å upsert è¿æ°è´¦å·ç friends |
| | | if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) { |
| | | return |
| | | } |
| | | this.upsertFriend(convertFriend(data)) |
| | | } catch (error) { |
| | | console.warn('[IM friendStore] fetchFriendInfo 失败', error) |
| | | } |
| | | })().finally(() => { |
| | | if (pendingFetchFriendInfos.get(key) === promise) { |
| | | pendingFetchFriendInfos.delete(key) |
| | | } |
| | | }) |
| | | pendingFetchFriendInfos.set(key, promise) |
| | | return promise |
| | | }, |
| | | |
| | | // ==================== ç³è¯·-å®¡æ¹ ==================== |
| | | |
| | | /** å起好åç³è¯·ï¼æååçå¾
对æ¹åæï¼ä¸ç´æ¥è½å°ä¸ºå¥½åï¼ */ |
| | | async applyFriendRequest(reqVO: ImFriendRequestApi.FriendRequestApplyReqVO): Promise<null | number> { |
| | | return await apiApplyFriendRequest(reqVO) |
| | | }, |
| | | |
| | | /** åæä¸æ¡å¥½åç³è¯·ï¼å端ä¼ååè½åº + æ¨ FRIEND_ADDï¼æ¬ç«¯çéç¥å°è¾¾å upsertFriend */ |
| | | async agreeFriendRequest(requestId: number) { |
| | | 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) |
| | | }, |
| | | |
| | | /** æ handleResult åºç¨å°æ¬å°ç³è¯·è®°å½ï¼æ¾ä¸å°å°±æ id 忥å
åº upsertï¼é¿å
ç ´å id ååº */ |
| | | async applyHandleResult( |
| | | requestId: number, |
| | | result: number, |
| | | handleContent?: string |
| | | ): Promise<void> { |
| | | const request = this.getFriendRequest(requestId) |
| | | if (request) { |
| | | request.handleResult = result |
| | | if (handleContent !== undefined) { |
| | | request.handleContent = handleContent |
| | | } |
| | | request.handleTime = Date.now() |
| | | this.saveFriendRequest(request) |
| | | return |
| | | } |
| | | await this.fetchFriendRequest(requestId) |
| | | }, |
| | | |
| | | /** æåãæç¸å
³ãç好åç³è¯·å表é¦é¡µï¼é¡µé¢æå¼ / æ¶å° FRIEND_REQUEST_RECEIVED æ¶å·æ°ï¼ï¼pending æé´å¤ç¨åä¸ Promise */ |
| | | async fetchFriendRequestList() { |
| | | if (pendingFetchRequests) { |
| | | const currentUserId = getCurrentUserId() |
| | | if ( |
| | | pendingFetchRequests.epoch === storeEpoch && |
| | | pendingFetchRequests.userId === currentUserId |
| | | ) { |
| | | return pendingFetchRequests.promise |
| | | } |
| | | } |
| | | const requestEpoch = storeEpoch |
| | | const requestUserId = getCurrentUserId() |
| | | const promise = apiGetMyFriendRequestList(FRIEND_REQUEST_PAGE_SIZE) |
| | | .then((list) => { |
| | | if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) { |
| | | return |
| | | } |
| | | const items = (list || []).map((request) => convertFriendRequest(request)) |
| | | this.friendRequests = items |
| | | // ä¸è¶³ä¸é¡µå³æ²¡ææ´å¤ï¼æ»¡é¡µå¯è½è¿æï¼ç loadMore æå° 0 æ¡åç¡®å® |
| | | this.hasMoreFriendRequests = items.length >= FRIEND_REQUEST_PAGE_SIZE |
| | | this.saveFriendRequestList() |
| | | }) |
| | | .finally(() => { |
| | | if ( |
| | | pendingFetchRequests?.epoch === requestEpoch && |
| | | pendingFetchRequests.userId === requestUserId |
| | | ) { |
| | | pendingFetchRequests = null |
| | | } |
| | | }) |
| | | pendingFetchRequests = { epoch: requestEpoch, userId: requestUserId, promise } |
| | | return promise |
| | | }, |
| | | |
| | | /** å è½½æ´å¤ç³è¯·ï¼ææ¬å°ææ§ requestId 游æ å页ï¼ï¼æ æ´å¤ / pending ä¸ç´æ¥è¿å */ |
| | | async loadMoreFriendRequestList() { |
| | | const requestUserId = getCurrentUserId() |
| | | const hasSameFetchPending = |
| | | pendingFetchRequests?.epoch === storeEpoch && pendingFetchRequests.userId === requestUserId |
| | | if (!this.hasMoreFriendRequests || hasSameFetchPending) { |
| | | return |
| | | } |
| | | if ( |
| | | pendingLoadMoreRequests?.epoch === storeEpoch && |
| | | pendingLoadMoreRequests.userId === requestUserId |
| | | ) { |
| | | return pendingLoadMoreRequests.promise |
| | | } |
| | | const oldest = this.friendRequests[this.friendRequests.length - 1] |
| | | if (!oldest) { |
| | | return this.fetchFriendRequestList() |
| | | } |
| | | const requestEpoch = storeEpoch |
| | | const promise = apiGetMyFriendRequestList(FRIEND_REQUEST_PAGE_SIZE, oldest.id) |
| | | .then((list) => { |
| | | 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() |
| | | }) |
| | | .finally(() => { |
| | | if ( |
| | | pendingLoadMoreRequests?.epoch === requestEpoch && |
| | | pendingLoadMoreRequests.userId === requestUserId |
| | | ) { |
| | | pendingLoadMoreRequests = null |
| | | } |
| | | }) |
| | | 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) |
| | | }, |
| | | |
| | | /** æ id ä»åç«¯åæ¥å¹¶ upsert å°æ¬å°ï¼dispatcher å
åºç¨ï¼é¿å
å
¨ééæï¼ï¼åç«¯å¸¦è¶æè¿æ»¤ */ |
| | | async fetchFriendRequest(requestId: number) { |
| | | const requestEpoch = storeEpoch |
| | | const requestUserId = getCurrentUserId() |
| | | const data = await apiGetMyFriendRequest(requestId) |
| | | if (!data) { |
| | | return |
| | | } |
| | | // clear() å·²åè´¦å·ï¼æ§è¯·æ±çç³è¯·è®°å½ä¸è½ååè¿æ°è´¦å·ç friendRequests |
| | | if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) { |
| | | return |
| | | } |
| | | this.upsertFriendRequest(convertFriendRequest(data)) |
| | | }, |
| | | |
| | | /** å并忡好åç³è¯·ï¼å·²æåæ id è¦çï¼æ°è®°å½æ id ååºæå
¥ï¼æ¯æ¬å°ææ§è¿èåè·³è¿ï¼çç» loadMore 带åï¼ */ |
| | | upsertFriendRequest(next: FriendRequest) { |
| | | void this.upsertFriendRequestForPull(next).catch((error) => |
| | | console.warn('[IM friendStore] æ¬å°å¥½åç³è¯·åå
¥å¤±è´¥', error) |
| | | ) |
| | | }, |
| | | |
| | | /** å并忡好åç³è¯· */ |
| | | async upsertFriendRequestForPull(next: FriendRequest): Promise<void> { |
| | | const existing = this.getFriendRequest(next.id) |
| | | if (existing) { |
| | | Object.assign(existing, next) |
| | | await this.saveFriendRequestRecord(existing) |
| | | return |
| | | } |
| | | // æ¯æ¬å°ææ§ id è¿èï¼ä¸å
¥å表ï¼è®© loadMore èªç¶å¸¦åï¼é¿å
ç ´å id ååº / åç» loadMore éå¤ push |
| | | const oldest = this.friendRequests[this.friendRequests.length - 1] |
| | | if (oldest && next.id < oldest.id) { |
| | | return |
| | | } |
| | | // æ id ååºæ¾é¦ä¸ªæ¯èªå·±å°çä½ç½®æå
¥ï¼æ¾ä¸å°åè¿½å æ«å°¾ |
| | | const insertIndex = this.friendRequests.findIndex((request) => request.id < next.id) |
| | | if (insertIndex === -1) { |
| | | this.friendRequests.push(next) |
| | | } else { |
| | | this.friendRequests.splice(insertIndex, 0, next) |
| | | } |
| | | await this.saveFriendRequestRecord(next) |
| | | }, |
| | | |
| | | /** å¢éæå好åç³è¯·åæ´å¹¶åå¹¶ï¼éè¿ / 离线补å¿ï¼ï¼æ update_time + id 游æ ï¼å·²å¤ççæ handleResult è¦ç */ |
| | | async pullFriendRequests() { |
| | | const requestEpoch = storeEpoch |
| | | const requestUserId = getCurrentUserId() |
| | | const isActive = () => requestEpoch === storeEpoch && getCurrentUserId() === requestUserId |
| | | await runIncrementalPull( |
| | | StorageKeys.settings.friendRequestPullCursor, |
| | | apiPullMyFriendRequestList, |
| | | async (records) => { |
| | | if (!isActive()) { |
| | | return false |
| | | } |
| | | await Promise.all( |
| | | records.map((vo) => this.upsertFriendRequestForPull(convertFriendRequest(vo))) |
| | | ) |
| | | return true |
| | | }, |
| | | isActive |
| | | ) |
| | | }, |
| | | |
| | | // ==================== 好åå
³ç³»æä½ ==================== |
| | | |
| | | /** å é¤å¥½åï¼åå软å ï¼æ¬ç«¯ç½® DISABLEï¼ï¼clear=true æ¶çº§èæ¸
çæ¬å°ç¸å
³æ°æ®ï¼å¦ç§èä¼è¯ï¼ï¼å¹¶éä¼ å端ç»å¤ç«¯åæ¥ */ |
| | | async deleteFriend(friendUserId: number, clear: boolean = true) { |
| | | const requestEpoch = storeEpoch |
| | | const requestUserId = getCurrentUserId() |
| | | await apiDeleteFriend(friendUserId, clear) |
| | | if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) { |
| | | return |
| | | } |
| | | this.removeFriend(friendUserId, clear) |
| | | }, |
| | | |
| | | /** 忢å
ææ°ï¼åæ¥ä¼è¯ç silent åæ®µï¼é¿å
ä¼è¯å表 silent 徿 ç 1210 æ¨å°ææ´æ° */ |
| | | async setFriendSilent(friendUserId: number, silent: boolean) { |
| | | const requestEpoch = storeEpoch |
| | | const requestUserId = getCurrentUserId() |
| | | await apiUpdateFriend({ friendUserId, silent }) |
| | | if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) { |
| | | return |
| | | } |
| | | const friend = this.getFriend(friendUserId) |
| | | if (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 }) |
| | | if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) { |
| | | return |
| | | } |
| | | const friend = this.getFriend(friendUserId) |
| | | if (friend) { |
| | | friend.pinned = pinned |
| | | this.saveFriend(friend) |
| | | } |
| | | }, |
| | | |
| | | /** æé»å¥½åï¼æ¬ç«¯ä¹è§æ´æ° + è°æ¥å£ï¼å端 FRIEND_BLOCK æ¨å°æ¶ç± dispatcher å
åºåæ¥å¤ç«¯ */ |
| | | async blockFriend(friendUserId: number) { |
| | | const requestEpoch = storeEpoch |
| | | const requestUserId = getCurrentUserId() |
| | | await apiBlockFriend(friendUserId) |
| | | if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) { |
| | | return |
| | | } |
| | | const friend = this.getFriend(friendUserId) |
| | | if (friend) { |
| | | friend.blocked = true |
| | | this.saveFriend(friend) |
| | | } |
| | | }, |
| | | |
| | | /** ç§»åºé»ååï¼æ¬ç«¯ä¹è§æ´æ° + è°æ¥å£ï¼å端 FRIEND_UNBLOCK æ¨å°æ¶ç± dispatcher å
åºåæ¥å¤ç«¯ */ |
| | | async unblockFriend(friendUserId: number) { |
| | | const requestEpoch = storeEpoch |
| | | const requestUserId = getCurrentUserId() |
| | | await apiUnblockFriend(friendUserId) |
| | | if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) { |
| | | return |
| | | } |
| | | const friend = this.getFriend(friendUserId) |
| | | if (friend) { |
| | | friend.blocked = false |
| | | this.saveFriend(friend) |
| | | } |
| | | }, |
| | | |
| | | /** ä¿®æ¹å¥½åå±ç¤ºå¤æ³¨ï¼ä»
èªå·±å¯è§ï¼ */ |
| | | async setFriendDisplayName(friendUserId: number, displayName: string) { |
| | | const requestEpoch = storeEpoch |
| | | const requestUserId = getCurrentUserId() |
| | | const value = displayName.trim() |
| | | // å端 displayName è¯ä¹ï¼null/undefined = 䏿¹ï¼"" = æ¸
ç©ºï¼æä»¥è¿éç´æ¥ä¼ valueï¼å¯è½æ¯ç©ºä¸²ï¼ |
| | | await apiUpdateFriend({ friendUserId, displayName: value }) |
| | | if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) { |
| | | return |
| | | } |
| | | const friend = this.getFriend(friendUserId) |
| | | if (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) |
| | | ) |
| | | }, |
| | | |
| | | /** æ¬å°åå¹¶ / æ°å¢æä¸ªå¥½å */ |
| | | async upsertFriendForPull(friend: Friend): Promise<void> { |
| | | const index = this.friends.findIndex( |
| | | (existing) => existing.friendUserId === friend.friendUserId |
| | | ) |
| | | if (index === -1) { |
| | | this.friends.push({ |
| | | ...friend, |
| | | status: friend.status ?? CommonStatusEnum.ENABLE |
| | | }) |
| | | } else { |
| | | this.friends[index] = { |
| | | ...this.friends[index], |
| | | ...friend, |
| | | 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) |
| | | }, |
| | | |
| | | /** æ¬å°æ è®°å é¤ï¼WebSocket FRIEND_DELETE äºä»¶è§¦åï¼clear=true æ¶çº§èæ¸
ç¸å
³æ°æ®å¦ç§èä¼è¯ï¼ */ |
| | | removeFriend(friendUserId: number, clear: boolean = true) { |
| | | const friend = this.getFriend(friendUserId) |
| | | if (friend) { |
| | | // blocked ä¸å¨ï¼è·å端 deleteFriend0ãå 好åæé´ä¿çæé»ç¶æãå¯¹é½ |
| | | friend.status = CommonStatusEnum.DISABLE |
| | | friend.deleteTime = Date.now() |
| | | } |
| | | if (clear) { |
| | | const conversationStore = useConversationStore() |
| | | conversationStore.removePrivateConversation(friendUserId) |
| | | } |
| | | this.saveFriend(friend) |
| | | }, |
| | | |
| | | // ==================== WebSocket äºä»¶ dispatcherï¼1201-1210 æ®µï¼ ==================== |
| | | |
| | | /** FRIEND_REQUEST_RECEIVED(1203)ï¼æ¶å°æ°ç³è¯·ï¼payload 已带ç³è¯·æ¹æµç§° / 头åï¼æ requestId ç´æ¨ push è¿å表 */ |
| | | applyFriendRequestReceivedNotification(payload: FriendNotificationPayload) { |
| | | if (!payload.requestId) { |
| | | return |
| | | } |
| | | const currentUserId = getCurrentUserId() |
| | | const existingIndex = this.friendRequests.findIndex((item) => item.id === payload.requestId) |
| | | if (existingIndex !== -1) { |
| | | const existing = this.friendRequests.splice(existingIndex, 1)[0] |
| | | if (!existing) { |
| | | return |
| | | } |
| | | const next = { |
| | | ...existing, |
| | | fromUserId: payload.operatorUserId, |
| | | toUserId: currentUserId, |
| | | handleResult: ImFriendRequestHandleResult.UNHANDLED, |
| | | applyContent: payload.applyContent, |
| | | addSource: payload.addSource, |
| | | createTime: Date.now(), |
| | | fromNickname: payload.fromNickname, |
| | | fromAvatar: payload.fromAvatar |
| | | } |
| | | this.friendRequests.unshift(next) |
| | | this.saveFriendRequest(next) |
| | | return |
| | | } |
| | | const next = { |
| | | id: payload.requestId, |
| | | fromUserId: payload.operatorUserId, |
| | | toUserId: currentUserId, |
| | | handleResult: ImFriendRequestHandleResult.UNHANDLED, |
| | | applyContent: payload.applyContent, |
| | | addSource: payload.addSource, |
| | | createTime: Date.now(), |
| | | fromNickname: payload.fromNickname, |
| | | fromAvatar: payload.fromAvatar |
| | | } |
| | | this.friendRequests.unshift(next) |
| | | this.saveFriendRequest(next) |
| | | }, |
| | | |
| | | /** FRIEND_REQUEST_APPROVED(1201)ï¼æçç³è¯·è¢«åæï¼æ requestId æ´æ°ç¶æï¼FRIEND_ADD ä¼å¦å¤æ¨ï¼ */ |
| | | applyFriendRequestApprovedNotification(payload: FriendNotificationPayload) { |
| | | if (!payload.requestId) { |
| | | return |
| | | } |
| | | void this.applyHandleResult(payload.requestId, ImFriendRequestHandleResult.AGREED) |
| | | }, |
| | | |
| | | /** FRIEND_REQUEST_REJECTED(1202)ï¼æçç³è¯·è¢«æç»ï¼æ requestId æ´æ°ç¶æ */ |
| | | applyFriendRequestRejectedNotification(payload: FriendNotificationPayload) { |
| | | if (!payload.requestId) { |
| | | return |
| | | } |
| | | void this.applyHandleResult( |
| | | payload.requestId, |
| | | ImFriendRequestHandleResult.REFUSED, |
| | | payload.handleContent |
| | | ) |
| | | }, |
| | | |
| | | /** |
| | | * FRIEND_ADD(1204)ï¼æ°å¢å¥½åï¼æ¬ç«¯æå好å详æ
å¹¶å
¥åº |
| | | * peerUserId ç± websocketStore æå¸§ sender / receiver ç®å¥½ä¼ å
¥ï¼becomeFriends åæ¡å
¥åºååæ¹æ¶å°åä¸ä»½ payloadï¼ |
| | | * æ¬ç«¯çæ£çãå¯¹ç«¯ãæ¯å¸§ä¸çå¦ä¸ä¸ªç¨æ·ï¼ä¸æ¯ payload.friendUserIdï¼payload éåºå®æ¯ toUserIdï¼ã |
| | | */ |
| | | applyFriendAddNotification(_payload: FriendNotificationPayload, peerUserId: number) { |
| | | if (this.isActiveFriend(peerUserId)) { |
| | | return |
| | | } |
| | | 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) |
| | | }, |
| | | |
| | | /** FRIEND_BLOCK(1207)ï¼æé»ï¼å¤ç«¯åæ¥ */ |
| | | applyFriendBlockNotification(payload: FriendNotificationPayload) { |
| | | const friend = this.getFriend(payload.friendUserId) |
| | | if (friend) { |
| | | friend.blocked = true |
| | | this.saveFriend(friend) |
| | | } |
| | | }, |
| | | |
| | | /** FRIEND_UNBLOCK(1208)ï¼ç§»åºé»ååï¼å¤ç«¯åæ¥ */ |
| | | applyFriendUnblockNotification(payload: FriendNotificationPayload) { |
| | | const friend = this.getFriend(payload.friendUserId) |
| | | if (friend) { |
| | | friend.blocked = false |
| | | this.saveFriend(friend) |
| | | } |
| | | }, |
| | | |
| | | /** FRIEND_INFO_UPDATED(1209)ï¼å¥½åèµæåæ´ï¼æµç§° / 头åï¼ï¼éæè¯¦æ
*/ |
| | | applyFriendInfoUpdatedNotification(payload: FriendNotificationPayload) { |
| | | void this.fetchFriendInfo(payload.friendUserId) |
| | | }, |
| | | |
| | | /** FRIEND_UPDATE(1210)ï¼æ¹éæ´æ°ï¼å¤æ³¨ / å
ææ° / è系人置顶ï¼ï¼å¤ç«¯åæ¥ */ |
| | | applyFriendUpdateNotification(payload: FriendNotificationPayload) { |
| | | const friend = this.getFriend(payload.friendUserId) |
| | | if (!friend) { |
| | | return |
| | | } |
| | | if (payload.displayName != null) { |
| | | friend.displayName = payload.displayName |
| | | } |
| | | if (payload.silent != null) { |
| | | friend.silent = payload.silent |
| | | } |
| | | 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) |
| | | }, |
| | | |
| | | /** æ¸
空好åå
åç¶æï¼å¹¶åºå¼æªè¿å请æ±ï¼pending Promise 置空 + storeEpoch++ï¼ */ |
| | | clear() { |
| | | 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 { |
| | | id: vo.id, |
| | | friendUserId: vo.friendUserId, |
| | | nickname: vo.nickname || String(vo.friendUserId), |
| | | nicknamePinyin: vo.nicknamePinyin, |
| | | avatar: vo.avatar, |
| | | silent: !!vo.silent, |
| | | displayName: vo.displayName || '', |
| | | displayNamePinyin: vo.displayNamePinyin, |
| | | addSource: vo.addSource, |
| | | pinned: !!vo.pinned, |
| | | blocked: !!vo.blocked, |
| | | status: vo.status, |
| | | addTime: vo.addTime ? new Date(vo.addTime).getTime() : undefined, |
| | | deleteTime: vo.deleteTime ? new Date(vo.deleteTime).getTime() : undefined |
| | | } |
| | | } |
| | | |
| | | function convertFriendRequest(vo: ImFriendRequestApi.FriendRequestRespVO): FriendRequest { |
| | | return { |
| | | id: vo.id, |
| | | fromUserId: vo.fromUserId, |
| | | toUserId: vo.toUserId, |
| | | handleResult: vo.handleResult, |
| | | applyContent: vo.applyContent, |
| | | handleContent: vo.handleContent, |
| | | addSource: vo.addSource, |
| | | handleTime: vo.handleTime ? new Date(vo.handleTime).getTime() : undefined, |
| | | createTime: vo.createTime ? new Date(vo.createTime).getTime() : 0, |
| | | fromNickname: vo.fromNickname, |
| | | fromAvatar: vo.fromAvatar, |
| | | toNickname: vo.toNickname, |
| | | toAvatar: vo.toAvatar |
| | | } |
| | | } |
| | | |
| | | export const useFriendStoreWithOut = () => useFriendStore() |
| | | |
| | | // dev: 让 Pinia ç actions / state æ¹å¨æ¯æ HMRï¼é¿å
æ¯æ¬¡æ¹ store é½å¾ç¡¬å· |
| | | if (import.meta.hot) { |
| | | import.meta.hot.accept(acceptHMRUpdate(useFriendStore, import.meta.hot)) |
| | | } |