| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374 |
- import type { ChatSDK, Conversation } from '@tencentcloud/lite-chat'
- import { userApi } from '~/api/user'
- import type { UserInfoItemVO } from '~/types/api'
- import { imApi } from '~/api/im'
- import { IM_ADMIN } from '~/constants'
- type TencentCloudChatStatic = typeof import('@tencentcloud/lite-chat').default
- export type GetConversationListOptions = Parameters<ChatSDK['getConversationList']>[0]
- export type ChatLoginOptions = {
- userId: string
- /**
- * If not provided, `useChat()` will call `/im/userSign` to fetch it.
- */
- userSig?: string
- /**
- * Custom userSig provider. If both `userSig` and `getUserSig` are provided,
- * `userSig` takes precedence.
- */
- getUserSig?: (userId: string) => Promise<string>
- }
- let _initPromise: Promise<void> | null = null
- let _loginPromise: Promise<void> | null = null
- let _loginUserId: string | null = null
- let _sdk: TencentCloudChatStatic | null = null
- let _onReady: (() => void) | null = null
- let _onNotReady: (() => void) | null = null
- type ConversationListUpdatedEvent = { data: Conversation[] }
- let _onConversationListUpdated: ((event: ConversationListUpdatedEvent) => void) | null = null
- let _bizUserInfoFetchTimer: ReturnType<typeof setTimeout> | null = null
- function normalizeLoginUser(v: unknown) {
- if (v === null || v === undefined) return ''
- return String(v)
- }
- function chunkArray<T>(arr: T[], size: number) {
- if (size <= 0) return [arr]
- const out: T[][] = []
- for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size))
- return out
- }
- async function defaultGetUserSig() {
- const sig = await imApi.getUserSig()
- return sig ?? ''
- }
- export function useChat() {
- const chat = useState<ChatSDK | null>('im-chat-sdk', () => null)
- const ready = useState<boolean>('im-chat-ready', () => false)
- const conversationList = useState<Conversation[]>('im-conversation-list', () => [])
- const conversationLoading = useState<boolean>('im-conversation-loading', () => false)
- const conversationError = useState<string | null>('im-conversation-error', () => null)
- // Business user info cache: userNo(userID) -> info
- const bizUserInfoMap = useState<Record<string, UserInfoItemVO>>('im-biz-user-info-map', () => ({}))
- const bizUserInfoLoading = useState<boolean>('im-biz-user-info-loading', () => false)
- const bizUserInfoError = useState<string | null>('im-biz-user-info-error', () => null)
- // Business user online state cache: userNo -> online
- const bizUserOnlineStateMap = useState<Record<string, boolean>>('im-biz-user-online-state-map', () => ({}))
- const bizUserOnlineStateLoading = useState<boolean>('im-biz-user-online-state-loading', () => false)
- const bizUserOnlineStateError = useState<string | null>('im-biz-user-online-state-error', () => null)
- function getPeerUserNoFromConversation(conv: Conversation) {
- const rawUserId = conv?.userProfile?.userID ? String(conv.userProfile.userID) : ''
- if (rawUserId) {
- // Some SDK payloads may return "C2Cxxx" as userID; business uses plain userNo.
- if (rawUserId.toUpperCase().startsWith('C2C')) return rawUserId.slice(3)
- return rawUserId
- }
- const cid = conv?.conversationID ? String(conv.conversationID) : ''
- return cid.toUpperCase().startsWith('C2C') ? cid.slice(3) : ''
- }
- function extractUserNosFromConversations(list: Conversation[]) {
- const userNos: string[] = []
- for (const conv of list) {
- const userNo = getPeerUserNoFromConversation(conv)
- if (!userNo) continue
- if (userNo === IM_ADMIN) continue
- userNos.push(String(userNo))
- }
- return userNos
- }
- function hasOnlineState(userNo: string) {
- return Object.prototype.hasOwnProperty.call(bizUserOnlineStateMap.value, userNo)
- }
- async function fetchBizUserInfos(userNos: string[]) {
- const uniq = Array.from(new Set(userNos.filter(Boolean)))
- const missing = uniq.filter(id => !bizUserInfoMap.value[id])
- if (missing.length === 0) return
- bizUserInfoLoading.value = true
- bizUserInfoError.value = null
- try {
- const chunks = chunkArray(missing, 20)
- const results = await Promise.all(chunks.map(c => userApi.getUserInfos(c)))
- for (const res of results) {
- const list = res?.list
- if (!Array.isArray(list)) continue
- for (const item of list) {
- if (!item?.userNo) continue
- bizUserInfoMap.value[item.userNo] = item
- }
- }
- }
- catch (e: unknown) {
- const err = e as { message?: unknown }
- bizUserInfoError.value = err?.message ? String(err.message) : String(e)
- // Do not block IM list rendering if business api fails
- console.warn('[im] fetchBizUserInfos failed:', e)
- }
- finally {
- bizUserInfoLoading.value = false
- }
- }
- async function ensureBizUserInfos(userNos: string[]) {
- await fetchBizUserInfos(userNos)
- }
- async function fetchBizUserOnlineStates(userNos: string[]) {
- const uniq = Array.from(new Set(userNos.filter(Boolean)))
- const missing = uniq.filter(id => !hasOnlineState(id))
- if (missing.length === 0) return
- bizUserOnlineStateLoading.value = true
- bizUserOnlineStateError.value = null
- try {
- const chunks = chunkArray(missing, 20)
- const results = await Promise.all(chunks.map(c => userApi.getUsersOnlineState(c)))
- for (const res of results) {
- const list = res?.list
- if (!Array.isArray(list)) continue
- for (const item of list) {
- if (!item?.userNo) continue
- bizUserOnlineStateMap.value[item.userNo] = Boolean(item.online)
- }
- }
- }
- catch (e: unknown) {
- const err = e as { message?: unknown }
- bizUserOnlineStateError.value = err?.message ? String(err.message) : String(e)
- // Do not block IM list rendering if business api fails
- console.warn('[im] fetchBizUserOnlineStates failed:', e)
- }
- finally {
- bizUserOnlineStateLoading.value = false
- }
- }
- async function ensureBizUserOnlineStates(userNos: string[]) {
- await fetchBizUserOnlineStates(userNos)
- }
- function queueFetchBizUserInfosFromConversations(list: Conversation[]) {
- if (!import.meta.client) return
- if (_bizUserInfoFetchTimer) clearTimeout(_bizUserInfoFetchTimer)
- _bizUserInfoFetchTimer = setTimeout(() => {
- _bizUserInfoFetchTimer = null
- const userNos = extractUserNosFromConversations(list)
- void fetchBizUserInfos(userNos)
- void fetchBizUserOnlineStates(userNos)
- }, 80)
- }
- async function init() {
- if (!import.meta.client) return
- if (chat.value) return
- if (_initPromise) return _initPromise
- _initPromise = (async () => {
- const runtimeConfig = useRuntimeConfig()
- const sdkAppId = Number(runtimeConfig.public.imAppId)
- if (!sdkAppId) throw new Error('Missing runtimeConfig.public.imAppId')
- const { default: TencentCloudChat } = await import('@tencentcloud/lite-chat')
- _sdk = TencentCloudChat
- const instance: ChatSDK = TencentCloudChat.create({ SDKAppID: sdkAppId })
- instance.setLogLevel(3)
- _onReady = () => {
- ready.value = true
- }
- _onNotReady = () => {
- ready.value = false
- }
- instance.on(TencentCloudChat.EVENT.SDK_READY, _onReady)
- instance.on(TencentCloudChat.EVENT.SDK_NOT_READY, _onNotReady)
- _onConversationListUpdated = (event) => {
- const list = event?.data
- if (Array.isArray(list)) {
- conversationList.value = list
- queueFetchBizUserInfosFromConversations(conversationList.value)
- }
- }
- instance.on(TencentCloudChat.EVENT.CONVERSATION_LIST_UPDATED, _onConversationListUpdated)
- chat.value = instance
- })().finally(() => {
- _initPromise = null
- })
- return _initPromise
- }
- async function waitReady(timeoutMs = 15_000) {
- if (!import.meta.client) return
- await init()
- if (!chat.value) throw new Error('Chat SDK is not initialized')
- if (ready.value) return
- if (!_sdk?.EVENT?.SDK_READY) throw new Error('Chat SDK EVENT is not available')
- const instance = chat.value
- await new Promise<void>((resolve, reject) => {
- const handler = () => {
- cleanup()
- resolve()
- }
- const timer = setTimeout(() => {
- cleanup()
- reject(new Error('Chat SDK not ready (timeout)'))
- }, timeoutMs)
- const cleanup = () => {
- clearTimeout(timer)
- instance.off(_sdk!.EVENT.SDK_READY, handler)
- }
- instance.on(_sdk!.EVENT.SDK_READY, handler)
- })
- }
- async function login(options: ChatLoginOptions) {
- if (!import.meta.client) return
- const userId = options.userId?.trim()
- if (!userId) throw new Error('Missing userId')
- await init()
- if (!chat.value) throw new Error('Chat SDK is not initialized')
- const instance = chat.value
- const currentUser = normalizeLoginUser(instance.getLoginUser?.())
- // Already logged-in and ready: do nothing.
- if (ready.value && currentUser === userId) return
- // De-dup concurrent logins for the same user.
- if (_loginPromise && _loginUserId === userId) return _loginPromise
- _loginUserId = userId
- _loginPromise = (async () => {
- // If logged in as another user, logout first.
- if (currentUser && currentUser !== userId) {
- try {
- await instance.logout()
- }
- catch {
- // ignore
- }
- ready.value = false
- }
- const userSig
- = options.userSig
- ?? (options.getUserSig ? await options.getUserSig(userId) : await defaultGetUserSig())
- await instance.login({ userID: userId, userSig })
- await waitReady()
- })().finally(() => {
- _loginPromise = null
- _loginUserId = null
- })
- return _loginPromise
- }
- async function logout() {
- if (!import.meta.client) return
- await init()
- if (!chat.value) return
- await chat.value.logout()
- }
- async function getConversationList(options?: GetConversationListOptions) {
- if (!import.meta.client) return []
- await waitReady()
- if (!chat.value) throw new Error('Chat SDK is not initialized')
- conversationLoading.value = true
- conversationError.value = null
- try {
- const resolvedOptions
- = options ?? (_sdk ? ({ type: _sdk.TYPES.CONV_C2C } as GetConversationListOptions) : undefined)
- const res = await chat.value.getConversationList(resolvedOptions)
- const list = res?.data?.conversationList ?? res?.data
- conversationList.value = Array.isArray(list) ? list : []
- queueFetchBizUserInfosFromConversations(conversationList.value)
- return conversationList.value
- }
- catch (e: unknown) {
- const err = e as { message?: unknown }
- conversationError.value = err?.message ? String(err.message) : String(e)
- throw e
- }
- finally {
- conversationLoading.value = false
- }
- }
- async function destroy() {
- if (!import.meta.client) return
- if (!chat.value) return
- const instance = chat.value
- if (_sdk?.EVENT && _onReady) instance.off(_sdk.EVENT.SDK_READY, _onReady)
- if (_sdk?.EVENT && _onNotReady) instance.off(_sdk.EVENT.SDK_NOT_READY, _onNotReady)
- if (_sdk?.EVENT && _onConversationListUpdated) instance.off(_sdk.EVENT.CONVERSATION_LIST_UPDATED, _onConversationListUpdated)
- _onReady = null
- _onNotReady = null
- _onConversationListUpdated = null
- await instance.destroy()
- chat.value = null
- ready.value = false
- conversationList.value = []
- conversationLoading.value = false
- conversationError.value = null
- bizUserInfoMap.value = {}
- bizUserInfoLoading.value = false
- bizUserInfoError.value = null
- bizUserOnlineStateMap.value = {}
- bizUserOnlineStateLoading.value = false
- bizUserOnlineStateError.value = null
- if (_bizUserInfoFetchTimer) {
- clearTimeout(_bizUserInfoFetchTimer)
- _bizUserInfoFetchTimer = null
- }
- }
- return {
- chat,
- ready,
- conversationList,
- conversationLoading,
- conversationError,
- bizUserInfoMap,
- bizUserInfoLoading,
- bizUserInfoError,
- bizUserOnlineStateMap,
- bizUserOnlineStateLoading,
- bizUserOnlineStateError,
- init,
- login,
- logout,
- ensureBizUserInfos,
- ensureBizUserOnlineStates,
- getConversationList,
- destroy,
- }
- }
|