diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx index 115e523546..d44f60ff96 100644 --- a/package/src/components/Channel/Channel.tsx +++ b/package/src/components/Channel/Channel.tsx @@ -5,6 +5,7 @@ import debounce from 'lodash/debounce'; import throttle from 'lodash/throttle'; import { + Channel as ChannelClass, ChannelState, Channel as ChannelType, ConnectionChangeEvent, @@ -74,6 +75,7 @@ import { WutReaction, } from '../../icons'; import { FlatList as FlatListDefault } from '../../native'; +import * as dbApi from '../../store/apis'; import type { DefaultStreamChatGenerics } from '../../types/types'; import { addReactionToLocalState } from '../../utils/addReactionToLocalState'; import { queueTask } from '../../utils/pendingTaskUtils'; @@ -201,7 +203,7 @@ export type ChannelPropsWithContext< | 'StickyHeader' > > & - Pick, 'client' | 'isOnline'> & + Pick, 'client' | 'enableOfflineSupport'> & Partial< Omit< InputMessageInputContextValue, @@ -427,6 +429,7 @@ const ChannelWithContext = < doUpdateMessageRequest, EmptyStateIndicator = EmptyStateIndicatorDefault, enableMessageGroupingByUser = true, + enableOfflineSupport, enforceUniqueReaction = false, FileAttachment = FileAttachmentDefault, FileAttachmentGroup = FileAttachmentGroupDefault, @@ -467,7 +470,6 @@ const ChannelWithContext = < InputGiphySearch = InputGiphyCommandInputDefault, InputReplyStateHeader = InputReplyStateHeaderDefault, isAttachmentEqual, - isOnline, keyboardBehavior, KeyboardCompatibleView = KeyboardCompatibleViewDefault, keyboardVerticalOffset, @@ -1169,6 +1171,7 @@ const ChannelWithContext = < extraState = {}, ) => { if (channel) { + console.log(updatedMessage); channel.state.addMessageSorted(updatedMessage, true); if (thread && updatedMessage.parent_id) { extraState.threadMessages = channel.state.threads[updatedMessage.parent_id] || []; @@ -1268,6 +1271,8 @@ const ChannelWithContext = < ...extraFields } = message; + if (!channel.id) return; + const messageData = { attachments, id: retrying ? undefined : id, @@ -1279,12 +1284,25 @@ const ChannelWithContext = < try { let messageResponse = {} as SendMessageAPIResponse; - if (doSendMessageRequest) { messageResponse = await doSendMessageRequest(channel?.cid || '', messageData); - } else if (channel) { + } else if (!enableOfflineSupport || retrying) { messageResponse = await channel.sendMessage(messageData); + } else if (channel) { + dbApi.upsertMessages({ + messages: [{ ...message, cid: channel.cid }], + }); + messageResponse = (await queueTask({ + client, + task: { + channelId: channel.id, + channelType: channel.type, + payload: [messageData], + type: 'send-message', + }, + })) as SendMessageAPIResponse; } + if (messageResponse.message) { messageResponse.message.status = MessageStatusTypes.RECEIVED; if (retrying) { @@ -1296,6 +1314,9 @@ const ChannelWithContext = < } catch (err) { console.log(err); message.status = MessageStatusTypes.FAILED; + dbApi.upsertMessages({ + messages: [{ ...message, cid: channel.cid }], + }); updateMessage(message); } }; @@ -1477,67 +1498,105 @@ const ChannelWithContext = < } }; - const addReaction: MessagesContextValue['addReaction'] = useCallback( - (type, messageId) => { - if (!channel?.id || !client.user) return; + const sendReaction: MessagesContextValue['sendReaction'] = ( + type, + messageId, + ) => { + if (!channel?.id || !client.user) return; + + const payload: Parameters['sendReaction']> = [ + messageId, + { + type, + } as Reaction, + { enforce_unique: enforceUniqueReaction }, + ]; + + if (!enableOfflineSupport) { + return channel.sendReaction(...payload); + } - addReactionToLocalState({ - channel, - enforceUniqueReaction, - messageId, - reactionType: type, - user: client.user, - }); + addReactionToLocalState({ + channel, + enforceUniqueReaction, + messageId, + reactionType: type, + user: client.user, + }); - setMessages(channel.state.messages); + setMessages(channel.state.messages); - const payload = [ - messageId, - { - type, - } as Reaction, - { enforce_unique: enforceUniqueReaction }, - ]; + queueTask({ + client, + task: { + channelId: channel.id, + channelType: channel.type, + payload, + type: 'send-reaction', + }, + }); + }; - queueTask({ - client, - task: { - channelId: channel.id, - channelType: channel.type, - payload, - type: 'send-reaction', - }, - }); - }, - [isOnline], - ); + const deleteMessage: MessagesContextValue['deleteMessage'] = async ( + message, + ) => { + if (!channel.id) return; + + dbApi.updateMessage({ + message: { + ...message, + cid: channel.cid, + deleted_at: new Date().toISOString(), + type: 'deleted', + }, + }); + updateMessage({ ...message, deleted_at: new Date().toISOString(), type: 'deleted' }); + const data = await queueTask({ + client, + task: { + channelId: channel.id, + channelType: channel.type, + payload: [message.id], + type: 'delete-message', + }, + }); - const removeReaction: MessagesContextValue['removeReaction'] = useCallback( - (type: string, messageId: string) => { - if (!channel?.id || !client.user) return; + if (data?.message) { + updateMessage({ ...data.message }); + } + }; - removeReactionFromLocalState({ - channel, - messageId, - reactionType: type, - user: client.user, - }); + const deleteReaction: MessagesContextValue['deleteReaction'] = ( + type: string, + messageId: string, + ) => { + if (!channel?.id || !client.user) return; - setMessages(channel.state.messages); + const payload: Parameters = [messageId, type]; - const payload = [messageId, type]; - queueTask({ - client, - task: { - channelId: channel.id, - channelType: channel.type, - payload, - type: 'delete-reaction', - }, - }); - }, - [isOnline], - ); + if (!enableOfflineSupport) { + return channel.deleteReaction(...payload); + } + + removeReactionFromLocalState({ + channel, + messageId, + reactionType: type, + user: client.user, + }); + + setMessages(channel.state.messages); + + return queueTask({ + client, + task: { + channelId: channel.id, + channelType: channel.type, + payload, + type: 'delete-reaction', + }, + }); + }; /** * THREAD METHODS @@ -1712,7 +1771,6 @@ const ChannelWithContext = < const messagesContext = useCreateMessagesContext({ additionalTouchableProps, - addReaction, Attachment, AttachmentActions, AudioAttachment, @@ -1723,6 +1781,8 @@ const ChannelWithContext = < channelId, DateHeader, deletedMessagesVisibilityType, + deleteMessage, + deleteReaction, disableTypingIndicator, dismissKeyboardOnMessageTouch, enableMessageGroupingByUser, @@ -1778,11 +1838,11 @@ const ChannelWithContext = < OverlayReactionList, ReactionList, removeMessage, - removeReaction, Reply, retrySendMessage, ScrollToBottomButton, selectReaction, + sendReaction, setEditingState, setQuotedMessageState, supportedReactions, @@ -1878,7 +1938,7 @@ export const Channel = < >( props: PropsWithChildren>, ) => { - const { client, isOnline } = useChatContext(); + const { client, enableOfflineSupport } = useChatContext(); const { t } = useTranslationContext(); const shouldSyncChannel = props.thread?.id ? !!props.threadList : true; @@ -1907,7 +1967,7 @@ export const Channel = < {...{ client, - isOnline, + enableOfflineSupport, t, }} {...props} diff --git a/package/src/components/Channel/hooks/useCreateMessagesContext.ts b/package/src/components/Channel/hooks/useCreateMessagesContext.ts index dc8a2a761c..a746746f0c 100644 --- a/package/src/components/Channel/hooks/useCreateMessagesContext.ts +++ b/package/src/components/Channel/hooks/useCreateMessagesContext.ts @@ -7,7 +7,6 @@ export const useCreateMessagesContext = < StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >({ additionalTouchableProps, - addReaction, Attachment, AttachmentActions, AudioAttachment, @@ -18,6 +17,8 @@ export const useCreateMessagesContext = < channelId, DateHeader, deletedMessagesVisibilityType, + deleteMessage, + deleteReaction, disableTypingIndicator, dismissKeyboardOnMessageTouch, enableMessageGroupingByUser, @@ -73,11 +74,11 @@ export const useCreateMessagesContext = < OverlayReactionList, ReactionList, removeMessage, - removeReaction, Reply, retrySendMessage, ScrollToBottomButton, selectReaction, + sendReaction, setEditingState, setQuotedMessageState, supportedReactions, @@ -101,7 +102,7 @@ export const useCreateMessagesContext = < const messagesContext: MessagesContextValue = useMemo( () => ({ additionalTouchableProps, - addReaction, + sendReaction, Attachment, AttachmentActions, AudioAttachment, @@ -110,6 +111,7 @@ export const useCreateMessagesContext = < CardFooter, CardHeader, DateHeader, + deleteMessage, deletedMessagesVisibilityType, disableTypingIndicator, dismissKeyboardOnMessageTouch, @@ -166,7 +168,7 @@ export const useCreateMessagesContext = < OverlayReactionList, ReactionList, removeMessage, - removeReaction, + deleteReaction, Reply, retrySendMessage, ScrollToBottomButton, @@ -188,8 +190,6 @@ export const useCreateMessagesContext = < dismissKeyboardOnMessageTouch, initialScrollToFirstUnreadMessage, markdownRulesLength, - addReaction, - removeReaction, messageContentOrderValue, supportedReactionsLength, targetedMessage, diff --git a/package/src/components/Message/Message.tsx b/package/src/components/Message/Message.tsx index b8fd8a25f4..d5ed160344 100644 --- a/package/src/components/Message/Message.tsx +++ b/package/src/components/Message/Message.tsx @@ -123,7 +123,8 @@ export type MessagePropsWithContext< Pick, 'groupStyles' | 'message'> & Pick< MessagesContextValue, - | 'addReaction' + | 'sendReaction' + | 'deleteMessage' | 'dismissKeyboardOnMessageTouch' | 'forceAlignMessages' | 'handleBlock' @@ -146,7 +147,7 @@ export type MessagePropsWithContext< | 'onPressMessage' | 'OverlayReactionList' | 'removeMessage' - | 'removeReaction' + | 'deleteReaction' | 'retrySendMessage' | 'selectReaction' | 'setEditingState' @@ -219,8 +220,9 @@ const MessageWithContext = < const isMessageTypeDeleted = props.message.type === 'deleted'; const { - addReaction, + sendReaction, channel, + deleteMessage: deleteMessageFromContext, disabled, dismissKeyboard, dismissKeyboardOnMessageTouch, @@ -259,7 +261,7 @@ const MessageWithContext = < OverlayReactionList, preventPress, removeMessage, - removeReaction, + deleteReaction, retrySendMessage, selectReaction, setData, @@ -462,14 +464,15 @@ const MessageWithContext = < handleTogglePinMessage, handleToggleReaction, } = useMessageActionHandlers({ - addReaction, channel, client, + deleteMessage: deleteMessageFromContext, + deleteReaction, enforceUniqueReaction, message, removeMessage, - removeReaction, retrySendMessage, + sendReaction, setEditingState, setQuotedMessageState, supportedReactions, @@ -490,9 +493,10 @@ const MessageWithContext = < threadReply, unpinMessage, } = useMessageActions({ - addReaction, channel, client, + deleteMessage: deleteMessageFromContext, + deleteReaction, enforceUniqueReaction, handleBlock, handleCopy, @@ -509,9 +513,9 @@ const MessageWithContext = < onThreadSelect, openThread, removeMessage, - removeReaction, retrySendMessage, selectReaction, + sendReaction, setEditingState, setOverlay, setQuotedMessageState, @@ -730,6 +734,7 @@ const areEqual = ({ - addReaction, channel, client, + deleteMessage, + deleteReaction, message, - removeMessage, - removeReaction, retrySendMessage, + sendReaction, setEditingState, setQuotedMessageState, supportedReactions, - updateMessage, }: Pick< MessagesContextValue, - | 'addReaction' - | 'removeMessage' - | 'removeReaction' + | 'sendReaction' + | 'deleteMessage' + | 'deleteReaction' | 'retrySendMessage' | 'setEditingState' | 'setQuotedMessageState' | 'supportedReactions' - | 'updateMessage' > & Pick, 'channel' | 'enforceUniqueReaction'> & Pick, 'client'> & @@ -50,14 +47,7 @@ export const useMessageActionHandlers = < (mute) => mute.user.id === client.userID && mute.target.id === message.user?.id, ); - const handleDeleteMessage = async () => { - if (message.status === MessageStatusTypes.FAILED) { - removeMessage(message); - } else { - const data = await client.deleteMessage(message.id); - updateMessage(data.message); - } - }; + const handleDeleteMessage = () => deleteMessage(message as MessageResponse); const handleToggleMuteUser = async () => { if (!message.user?.id) { @@ -131,9 +121,9 @@ export const useMessageActionHandlers = < try { if (channel && messageId) { if (ownReaction) { - await removeReaction(reactionType, messageId); + await deleteReaction(reactionType, messageId); } else { - await addReaction(reactionType, messageId); + await sendReaction(reactionType, messageId); } } } catch (err) { diff --git a/package/src/components/Message/hooks/useMessageActions.tsx b/package/src/components/Message/hooks/useMessageActions.tsx index 4de8fd614d..003c2d2e5a 100644 --- a/package/src/components/Message/hooks/useMessageActions.tsx +++ b/package/src/components/Message/hooks/useMessageActions.tsx @@ -34,9 +34,10 @@ import { removeReservedFields } from '../utils/removeReservedFields'; export const useMessageActions = < StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >({ - addReaction, channel, client, + deleteMessage: deleteMessageFromContext, + deleteReaction, enforceUniqueReaction, handleBlock, handleCopy, @@ -53,9 +54,9 @@ export const useMessageActions = < onThreadSelect, openThread, removeMessage, - removeReaction, retrySendMessage, selectReaction, + sendReaction, setEditingState, setOverlay, setQuotedMessageState, @@ -64,7 +65,8 @@ export const useMessageActions = < updateMessage, }: Pick< MessagesContextValue, - | 'addReaction' + | 'deleteMessage' + | 'sendReaction' | 'handleBlock' | 'handleCopy' | 'handleDelete' @@ -77,7 +79,7 @@ export const useMessageActions = < | 'handleReaction' | 'handleThreadReply' | 'removeMessage' - | 'removeReaction' + | 'deleteReaction' | 'retrySendMessage' | 'setEditingState' | 'setQuotedMessageState' @@ -108,14 +110,15 @@ export const useMessageActions = < handleTogglePinMessage, handleToggleReaction, } = useMessageActionHandlers({ - addReaction, channel, client, + deleteMessage: deleteMessageFromContext, + deleteReaction, enforceUniqueReaction, message, removeMessage, - removeReaction, retrySendMessage, + sendReaction, setEditingState, setQuotedMessageState, supportedReactions, diff --git a/package/src/contexts/messagesContext/MessagesContext.tsx b/package/src/contexts/messagesContext/MessagesContext.tsx index 907a988d4e..06ab4d279e 100644 --- a/package/src/contexts/messagesContext/MessagesContext.tsx +++ b/package/src/contexts/messagesContext/MessagesContext.tsx @@ -65,7 +65,6 @@ export type DeletedMessagesVisibilityType = 'always' | 'never' | 'receiver' | 's export type MessagesContextValue< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = { - addReaction: (type: string, messageId: string) => void; /** * UI component for Attachment. * Defaults to: [Attachment](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/Attachment/Attachment.tsx) @@ -88,6 +87,9 @@ export type MessagesContextValue< * Defaults to: [DateHeader](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/MessageList/DateHeader.tsx) **/ DateHeader: React.ComponentType; + deleteMessage: (message: MessageResponse) => Promise; + deleteReaction: (type: string, messageId: string) => void; + /** Should keyboard be dismissed when messaged is touched */ dismissKeyboardOnMessageTouch: boolean; @@ -98,7 +100,6 @@ export type MessagesContextValue< * Defaults to: [FileAttachment](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/Attachment/FileAttachment.tsx) */ FileAttachment: React.ComponentType>; - /** * UI component to display group of File type attachments or multiple file attachments (in single message). * Defaults to: [FileAttachmentGroup](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/Attachment/FileAttachmentGroup.tsx) @@ -124,6 +125,7 @@ export type MessagesContextValue< * The giphy version to render - check the keys of the [Image Object](https://developers.giphy.com/docs/api/schema#image-object) for possible values. Uses 'fixed_height' by default * */ giphyVersion: keyof NonNullable; + /** * The indicator rendered when loading an image fails. */ @@ -138,7 +140,6 @@ export type MessagesContextValue< * When true, messageList will be scrolled at first unread message, when opened. */ initialScrollToFirstUnreadMessage: boolean; - /** * UI component for Message Date Separator Component * Defaults to: [InlineDateSeparator](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/MessageList/InlineDateSeparator.tsx) @@ -149,8 +150,8 @@ export type MessagesContextValue< * Defaults to: [InlineUnreadIndicator](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/Message/MessageSimple/InlineUnreadIndicator.tsx) **/ InlineUnreadIndicator: React.ComponentType; - Message: React.ComponentType>; + Message: React.ComponentType>; /** * UI component for MessageAvatar * Defaults to: [MessageAvatar](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/Message/MessageSimple/MessageAvatar.tsx) @@ -177,12 +178,12 @@ export type MessagesContextValue< * Custom message pinned component */ MessagePinnedHeader: React.ComponentType>; + /** * UI component for MessageReplies * Defaults to: [MessageReplies](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/MessageSimple/MessageReplies.tsx) */ MessageReplies: React.ComponentType>; - /** * UI Component for MessageRepliesAvatars * Defaults to: [MessageRepliesAvatars](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/MessageSimple/MessageRepliesAvatars.tsx) @@ -213,7 +214,6 @@ export type MessagesContextValue< */ ReactionList: React.ComponentType>; removeMessage: (message: { id: string; parent_id?: string }) => void; - removeReaction: (type: string, messageId: string) => void; /** * UI component for Reply * Defaults to: [Reply](https://getstream.io/chat/docs/sdk/reactnative/ui-components/reply/) @@ -228,6 +228,7 @@ export type MessagesContextValue< * Defaults to: [ScrollToBottomButton](https://getstream.io/chat/docs/sdk/reactnative/ui-components/scroll-to-bottom-button/) */ ScrollToBottomButton: React.ComponentType; + sendReaction: (type: string, messageId: string) => void; setEditingState: (message: MessageType) => void; setQuotedMessageState: (message: MessageType) => void; supportedReactions: ReactionData[]; diff --git a/package/src/store/apis/updateMessage.ts b/package/src/store/apis/updateMessage.ts index 0e00c13b07..562fbe21c3 100644 --- a/package/src/store/apis/updateMessage.ts +++ b/package/src/store/apis/updateMessage.ts @@ -1,4 +1,4 @@ -import type { MessageResponse } from 'stream-chat'; +import type { FormatMessageResponse, MessageResponse } from 'stream-chat'; import { mapMessageToStorable } from '../mappers/mapMessageToStorable'; import { mapReactionToStorable } from '../mappers/mapReactionToStorable'; @@ -14,7 +14,7 @@ export const updateMessage = ({ flush = true, message, }: { - message: MessageResponse; + message: MessageResponse | FormatMessageResponse; flush?: boolean; }) => { const queries: PreparedQueries[] = []; diff --git a/package/src/store/mappers/mapStorableToTask.ts b/package/src/store/mappers/mapStorableToTask.ts index 493fbb1067..c6c9825940 100644 --- a/package/src/store/mappers/mapStorableToTask.ts +++ b/package/src/store/mappers/mapStorableToTask.ts @@ -1,6 +1,6 @@ -import type { TableRowJoinedUser } from '../types'; +import type { PendingTask, TableRowJoinedUser } from '../types'; -export const mapStorableToTask = (row: TableRowJoinedUser<'pendingTasks'>) => { +export const mapStorableToTask = (row: TableRowJoinedUser<'pendingTasks'>): PendingTask => { const { channelId, channelType, id, type } = row; return { channelId, diff --git a/package/src/store/types.ts b/package/src/store/types.ts index 02cb0c87a4..9754b5d484 100644 --- a/package/src/store/types.ts +++ b/package/src/store/types.ts @@ -1,3 +1,5 @@ +import type { Channel, StreamChat } from 'stream-chat'; + import type { Schema } from './schema'; export type Table = keyof Schema; @@ -14,6 +16,22 @@ export type PreparedQueries = [string] | [string, Array]; export type PendingTask = { channelId: string; channelType: string; - payload: Array; - type: string; -}; + id?: number; +} & ( + | { + payload: Parameters; + type: 'send-reaction'; + } + | { + payload: Parameters; + type: 'send-message'; + } + | { + payload: Parameters; + type: 'delete-message'; + } + | { + payload: Parameters; + type: 'delete-reaction'; + } +); diff --git a/package/src/utils/pendingTaskUtils.ts b/package/src/utils/pendingTaskUtils.ts index 60e1a1cd75..48e9459548 100644 --- a/package/src/utils/pendingTaskUtils.ts +++ b/package/src/utils/pendingTaskUtils.ts @@ -19,9 +19,11 @@ export const queueTask = async < }) => { const removeFromApi = addPendingTask(task); - await executeTask({ client, task }); + const response = await executeTask({ client, task }); removeFromApi(); + + return response; }; const executeTask = async < @@ -34,16 +36,15 @@ const executeTask = async < task: PendingTask; }) => { const channel = client.channel(task.channelType, task.channelId); + let response; switch (task.type) { case 'send-reaction': - // @ts-ignore - await channel.sendReaction(...task.payload); + response = await channel.sendReaction(...task.payload); break; case 'delete-reaction': try { - // @ts-ignore - await channel.deleteReaction(...task.payload); + response = await channel.deleteReaction(...task.payload); } catch (e) { if ((e as AxiosError)?.response?.data?.code === 16) { // Error code 16 - reaction doesn't exist. @@ -53,9 +54,35 @@ const executeTask = async < } } break; + case 'send-message': + try { + response = await channel.sendMessage(...task.payload); + } catch (e) { + if ((e as AxiosError)?.response?.data?.code === 4) { + // Error code 16 - message already exists + // ignore + } else { + throw e; + } + } + break; + case 'delete-message': + try { + response = await client.deleteMessage(...task.payload); + } catch (e) { + if ((e as AxiosError)?.response?.data?.code === 4) { + // Error code 16 - message doesn't exist. + // ignore + } else { + throw e; + } + } + break; default: break; } + + return response; }; export const executePendingTasks = async < @@ -65,7 +92,7 @@ export const executePendingTasks = async < ) => { const queue = getPendingTasks(); for (const task of queue) { - await executeTask({ + await executeTask({ client, task, });