UNPKG

@sendbird/uikit-chat-hooks

Version:

A set of React hooks for integrating Sendbird chat functionality into your React app.

367 lines (333 loc) 13 kB
import { useRef } from 'react'; import { MessageListParams } from '@sendbird/chat/message'; import type { SendbirdBaseChannel, SendbirdBaseMessage, SendbirdOpenChannel, SendbirdPreviousMessageListQuery, } from '@sendbird/uikit-utils'; import { ASYNC_NOOP, NOOP, isDifferentChannel, isMyMessage, useAsyncEffect, useForceUpdate, useFreshCallback, useUniqHandlerId, } from '@sendbird/uikit-utils'; import { useChannelHandler } from '../../handler/useChannelHandler'; import { useConnectionHandler } from '../../handler/useConnectionHandler'; import type { UseOpenChannelMessages, UseOpenChannelMessagesOptions } from '../../types'; import { useChannelMessagesReducer } from '../useChannelMessagesReducer'; const createMessageQuery = (channel: SendbirdOpenChannel, creator?: UseOpenChannelMessagesOptions['queryCreator']) => { if (creator) return creator(); return channel.createPreviousMessageListQuery({ limit: 100, reverse: true, }); }; export const useOpenChannelMessagesWithQuery: UseOpenChannelMessages = (sdk, channel, userId, options) => { const queryRef = useRef<SendbirdPreviousMessageListQuery>(); const forceUpdate = useForceUpdate(); const handlerId = useUniqHandlerId('useOpenChannelMessagesWithQuery'); const { loading, refreshing, messages, newMessages, updateMessages, updateNewMessages, deleteNewMessages, deleteMessages, updateLoading, updateRefreshing, } = useChannelMessagesReducer(options?.sortComparator); const init = useFreshCallback(async (uid?: string) => { if (uid) { queryRef.current = createMessageQuery(channel, options?.queryCreator); if (queryRef.current?.hasNext) { const list = await queryRef.current?.load(); updateMessages(list, true, sdk.currentUser?.userId); } updateNewMessages([], true, sdk.currentUser?.userId); } }); const channelUpdater = (channel: SendbirdBaseChannel) => { if (channel.isOpenChannel() && !isDifferentChannel(channel, channel)) { forceUpdate(); } }; useConnectionHandler(sdk, handlerId, { async onReconnectSucceeded() { const lastMessage = messages[0]; if (!lastMessage) return; const messageContext = { updatedMessages: [] as SendbirdBaseMessage[], addedMessages: [] as SendbirdBaseMessage[], deletedMessageIds: [] as (number | string)[], }; const changeLogsContext = { hasMore: false, token: '', }; const messageQueryContext = { hasMore: false, timestamp: lastMessage.createdAt, }; // Updated & Deleted messages const changelogsParams = { replyType: queryRef.current?.replyType, includeMetaArray: queryRef.current?.includeMetaArray, includeReactions: queryRef.current?.includeReactions, includeThreadInfo: queryRef.current?.includeThreadInfo, includeParentMessageInfo: queryRef.current?.includeParentMessageInfo, }; const changeLogsByTS = await channel.getMessageChangeLogsSinceTimestamp(lastMessage.createdAt); changeLogsContext.token = changeLogsByTS.token; changeLogsContext.hasMore = changeLogsByTS.hasMore; messageContext.updatedMessages.push(...changeLogsByTS.updatedMessages); messageContext.deletedMessageIds.push(...changeLogsByTS.deletedMessageIds); while (changeLogsContext.hasMore) { const changeLogsByToken = await channel.getMessageChangeLogsSinceToken(changeLogsByTS.token, changelogsParams); changeLogsContext.token = changeLogsByToken.token; changeLogsContext.hasMore = changeLogsByToken.hasMore; messageContext.updatedMessages.push(...changeLogsByToken.updatedMessages); messageContext.deletedMessageIds.push(...changeLogsByToken.deletedMessageIds); } // Added messages const messageQueryParams: MessageListParams = { prevResultSize: 0, nextResultSize: queryRef.current?.limit ?? 100, reverse: queryRef.current?.reverse, includeParentMessageInfo: queryRef.current?.includeParentMessageInfo, includeThreadInfo: queryRef.current?.includeThreadInfo, includeReactions: queryRef.current?.includeReactions, includeMetaArray: queryRef.current?.includeMetaArray, replyType: queryRef.current?.replyType, customTypesFilter: queryRef.current?.customTypesFilter as never, messageTypeFilter: queryRef.current?.messageTypeFilter, senderUserIdsFilter: queryRef.current?.senderUserIdsFilter as never, showSubchannelMessagesOnly: queryRef.current?.showSubchannelMessagesOnly, }; const queriedMessages = await channel.getMessagesByTimestamp(lastMessage.createdAt, messageQueryParams); messageQueryContext.hasMore = queriedMessages.length > 0; if (messageQueryContext.hasMore) { messageQueryContext.timestamp = queriedMessages[0].createdAt; messageContext.addedMessages.unshift(...queriedMessages); } while (messageQueryContext.hasMore) { const queriedMessages = await channel.getMessagesByTimestamp(messageQueryContext.timestamp, messageQueryParams); messageQueryContext.hasMore = queriedMessages.length > 0; if (messageQueryContext.hasMore) { messageQueryContext.timestamp = queriedMessages[0].createdAt; messageContext.addedMessages.unshift(...queriedMessages); } } // Update to View updateMessages( [...messageContext.addedMessages, ...messageContext.updatedMessages], false, sdk.currentUser?.userId, ); deleteMessages(messageContext.deletedMessageIds, []); if (messageContext.addedMessages.length > 0) { if (options?.shouldCountNewMessages?.()) { updateNewMessages(messageContext.addedMessages, false, sdk.currentUser?.userId); } if (options?.onMessagesReceived) { options.onMessagesReceived(messageContext.addedMessages); } } }, }); useChannelHandler( sdk, handlerId, { // Messages onMessageReceived(eventChannel, message) { if (isDifferentChannel(channel, eventChannel)) return; if (isMyMessage(message, sdk.currentUser?.userId)) return; updateMessages([message], false, sdk.currentUser?.userId); if (options?.shouldCountNewMessages?.()) { updateNewMessages([message], false, sdk.currentUser?.userId); } if (options?.onMessagesReceived) { options.onMessagesReceived([message]); } }, onMessageUpdated(eventChannel, message) { if (isDifferentChannel(channel, eventChannel)) return; if (isMyMessage(message, sdk.currentUser?.userId)) return; updateMessages([message], false, sdk.currentUser?.userId); }, onMessageDeleted(eventChannel, messageId) { if (isDifferentChannel(channel, eventChannel)) return; deleteMessages([messageId], []); deleteNewMessages([messageId], []); }, // Channels onChannelChanged: channelUpdater, onChannelFrozen: channelUpdater, onChannelUnfrozen: channelUpdater, onChannelParticipantCountChanged(eventChannel) { if (isDifferentChannel(channel, eventChannel)) return; channelUpdater(eventChannel); }, onChannelDeleted(channelUrl, type) { if (channel.url === channelUrl && type === 'open') { options?.onChannelDeleted?.(); } }, // Users onOperatorUpdated: channelUpdater, onUserUnbanned: channelUpdater, onUserMuted: channelUpdater, onUserUnmuted: channelUpdater, onUserBanned(eventChannel, bannedUser) { if (isDifferentChannel(channel, eventChannel)) return; if (bannedUser.userId === sdk.currentUser?.userId) { options?.onChannelDeleted?.(); } else { channelUpdater(eventChannel); } }, }, 'open', ); useAsyncEffect(async () => { updateLoading(true); try { await channel.enter(); await init(userId); } catch (error) { options?.onError?.(error); options?.onChannelDeleted?.(); } finally { updateLoading(false); } return () => { channel.exit().catch(NOOP); }; }, [channel.url, userId]); const refresh: ReturnType<UseOpenChannelMessages>['refresh'] = useFreshCallback(async () => { updateRefreshing(true); await init(userId); updateRefreshing(false); }); const prev: ReturnType<UseOpenChannelMessages>['prev'] = useFreshCallback(async () => { if (queryRef.current && queryRef.current?.hasNext) { const list = await queryRef.current?.load(); updateMessages(list, false, sdk.currentUser?.userId); } }); const hasPrev: ReturnType<UseOpenChannelMessages>['hasPrev'] = useFreshCallback( () => queryRef.current?.hasNext ?? false, ); const next: ReturnType<UseOpenChannelMessages>['next'] = useFreshCallback(ASYNC_NOOP); const hasNext: ReturnType<UseOpenChannelMessages>['hasNext'] = useFreshCallback(() => false); const sendUserMessage: ReturnType<UseOpenChannelMessages>['sendUserMessage'] = useFreshCallback( (params, onPending) => { return new Promise((resolve, reject) => { channel .sendUserMessage(params) .onPending((pendingMessage) => { if (pendingMessage.isUserMessage()) { updateMessages([pendingMessage], false, sdk.currentUser?.userId); onPending?.(pendingMessage); } }) .onSucceeded((sentMessage) => { if (sentMessage.isUserMessage()) { updateMessages([sentMessage], false, sdk.currentUser?.userId); resolve(sentMessage); } }) .onFailed((err, failedMessage) => { if (failedMessage) { updateMessages([failedMessage], false, sdk.currentUser?.userId); } reject(err); }); }); }, ); const sendFileMessage: ReturnType<UseOpenChannelMessages>['sendFileMessage'] = useFreshCallback( (params, onPending) => { return new Promise((resolve, reject) => { channel .sendFileMessage(params) .onPending((pendingMessage) => { if (pendingMessage.isFileMessage()) { updateMessages([pendingMessage], false, sdk.currentUser?.userId); onPending?.(pendingMessage); } }) .onSucceeded((sentMessage) => { if (sentMessage.isFileMessage()) { updateMessages([sentMessage], false, sdk.currentUser?.userId); resolve(sentMessage); } }) .onFailed((err, failedMessage) => { if (failedMessage) { updateMessages([failedMessage], false, sdk.currentUser?.userId); } reject(err); }); }); }, ); const updateUserMessage: ReturnType<UseOpenChannelMessages>['updateUserMessage'] = useFreshCallback( async (messageId, params) => { const updatedMessage = await channel.updateUserMessage(messageId, params); updateMessages([updatedMessage], false, sdk.currentUser?.userId); return updatedMessage; }, ); const updateFileMessage: ReturnType<UseOpenChannelMessages>['updateFileMessage'] = useFreshCallback( async (messageId, params) => { const updatedMessage = await channel.updateFileMessage(messageId, params); updateMessages([updatedMessage], false, sdk.currentUser?.userId); return updatedMessage; }, ); const resendMessage: ReturnType<UseOpenChannelMessages>['resendMessage'] = useFreshCallback(async (failedMessage) => { const message = await (() => { if (failedMessage.isUserMessage()) return channel.resendUserMessage(failedMessage); if (failedMessage.isFileMessage()) return channel.resendFileMessage(failedMessage); return null; })(); if (message) updateMessages([message], false, sdk.currentUser?.userId); }); const deleteMessage: ReturnType<UseOpenChannelMessages>['deleteMessage'] = useFreshCallback(async (message) => { if (message.sendingStatus === 'succeeded') { if (message.isUserMessage()) await channel.deleteMessage(message); if (message.isFileMessage()) await channel.deleteMessage(message); } else { deleteMessages([message.messageId], [message.reqId]); } }); const resetNewMessages: ReturnType<UseOpenChannelMessages>['resetNewMessages'] = useFreshCallback(() => { updateNewMessages([], true, sdk.currentUser?.userId); }); return { loading, refreshing, refresh, messages, newMessages, next, hasNext, prev, hasPrev, sendUserMessage, sendFileMessage, updateUserMessage, updateFileMessage, resendMessage, deleteMessage, resetNewMessages, }; };