diff --git a/package/expo-package/src/optionalDependencies/pickImage.ts b/package/expo-package/src/optionalDependencies/pickImage.ts index 9d2ce11533..fa6b98d034 100644 --- a/package/expo-package/src/optionalDependencies/pickImage.ts +++ b/package/expo-package/src/optionalDependencies/pickImage.ts @@ -53,8 +53,10 @@ export const pickImage = ImagePicker return { cancelled: true }; } } + return { cancelled: true }; } catch (error) { console.log('Error while picking image', error); + return { cancelled: true }; } } : null; diff --git a/package/native-package/src/optionalDependencies/pickImage.ts b/package/native-package/src/optionalDependencies/pickImage.ts index 574c1538e9..5ab35fcd76 100644 --- a/package/native-package/src/optionalDependencies/pickImage.ts +++ b/package/native-package/src/optionalDependencies/pickImage.ts @@ -35,6 +35,7 @@ export const pickImage = ImagePicker } } catch (error) { console.log('Error picking image: ', error); + return { cancelled: true }; } } : null; diff --git a/package/src/components/ChannelPreview/ChannelPreview.tsx b/package/src/components/ChannelPreview/ChannelPreview.tsx index 7eec3e32c7..fb8edbbed9 100644 --- a/package/src/components/ChannelPreview/ChannelPreview.tsx +++ b/package/src/components/ChannelPreview/ChannelPreview.tsx @@ -3,7 +3,6 @@ import React from 'react'; import type { Channel } from 'stream-chat'; import { useChannelPreviewData } from './hooks/useChannelPreviewData'; -import { useLatestMessagePreview } from './hooks/useLatestMessagePreview'; import { ChannelsContextValue, @@ -31,15 +30,16 @@ export const ChannelPreview = < const { channel, client: propClient, forceUpdate: propForceUpdate, Preview: propPreview } = props; const { client: contextClient } = useChatContext(); - const { forceUpdate: contextForceUpdate, Preview: contextPreview } = - useChannelsContext(); + const { Preview: contextPreview } = useChannelsContext(); const client = propClient || contextClient; - const forceUpdate = propForceUpdate || contextForceUpdate; const Preview = propPreview || contextPreview; - const { lastMessage, muted, unread } = useChannelPreviewData(channel, client, forceUpdate); - const latestMessagePreview = useLatestMessagePreview(channel, forceUpdate, lastMessage); + const { latestMessagePreview, muted, unread } = useChannelPreviewData( + channel, + client, + propForceUpdate, + ); return ( { ); }; + const generateChannelWrapper = (overrides: Record) => + generateChannel({ + countUnread: jest.fn().mockReturnValue(0), + initialized: true, + lastMessage: jest.fn().mockReturnValue(generateMessage()), + muteStatus: jest.fn().mockReturnValue({ muted: false }), + ...overrides, + }); + const useInitializeChannel = async (c: GetOrCreateChannelApiParams) => { useMockedApis(chatClient, [getOrCreateChannelApi(c)]); @@ -89,9 +98,8 @@ describe('ChannelPreview', () => { it("should not update the unread count if the event's cid does not match the channel's cid", async () => { const channelOnMock = jest.fn().mockReturnValue({ unsubscribe: jest.fn() }); - const c = generateChannel({ + const c = generateChannelWrapper({ countUnread: jest.fn().mockReturnValue(10), - muteStatus: jest.fn().mockReturnValue({ muted: false }), on: channelOnMock, }); @@ -117,9 +125,8 @@ describe('ChannelPreview', () => { it('should update the unread count to 0', async () => { const channelOnMock = jest.fn().mockReturnValue({ unsubscribe: jest.fn() }); - const c = generateChannel({ + const c = generateChannelWrapper({ countUnread: jest.fn().mockReturnValue(10), - muteStatus: jest.fn().mockReturnValue({ muted: false }), on: channelOnMock, }); @@ -147,9 +154,7 @@ describe('ChannelPreview', () => { it("should not update the unread count if the event's cid is undefined", async () => { const channelOnMock = jest.fn().mockReturnValue({ unsubscribe: jest.fn() }); - const c = generateChannel({ - countUnread: jest.fn().mockReturnValue(0), - muteStatus: jest.fn().mockReturnValue({ muted: false }), + const c = generateChannelWrapper({ on: channelOnMock, }); @@ -182,9 +187,7 @@ describe('ChannelPreview', () => { it("should not update the unread count if the event's cid does not match the channel's cid", async () => { const channelOnMock = jest.fn().mockReturnValue({ unsubscribe: jest.fn() }); - const c = generateChannel({ - countUnread: jest.fn().mockReturnValue(0), - muteStatus: jest.fn().mockReturnValue({ muted: false }), + const c = generateChannelWrapper({ on: channelOnMock, }); @@ -217,9 +220,7 @@ describe('ChannelPreview', () => { it("should not update the unread count if the event's user id does not match the client's user id", async () => { const channelOnMock = jest.fn().mockReturnValue({ unsubscribe: jest.fn() }); - const c = generateChannel({ - countUnread: jest.fn().mockReturnValue(0), - muteStatus: jest.fn().mockReturnValue({ muted: false }), + const c = generateChannelWrapper({ on: channelOnMock, }); @@ -255,12 +256,10 @@ describe('ChannelPreview', () => { await useInitializeChannel(c); const channelOnMock = jest.fn().mockReturnValue({ unsubscribe: jest.fn() }); - const testChannel = { + const testChannel = generateChannelWrapper({ ...channel, - countUnread: jest.fn().mockReturnValue(0), - muteStatus: jest.fn().mockReturnValue({ muted: false }), on: channelOnMock, - }; + }); const { getByTestId } = render(); @@ -291,9 +290,9 @@ describe('ChannelPreview', () => { it('should update the unread count to 0 if the channel is muted', async () => { const channelOnMock = jest.fn().mockReturnValue({ unsubscribe: jest.fn() }); - const c = generateChannel({ + const c = generateChannelWrapper({ countUnread: jest.fn().mockReturnValue(10), - muteStatus: jest.fn().mockReturnValue({ muted: false }), + muteStatus: jest.fn().mockReturnValue({ muted: true }), on: channelOnMock, }); @@ -304,7 +303,7 @@ describe('ChannelPreview', () => { await waitFor(() => getByTestId('channel-id')); await waitFor(() => { - expect(getByTestId('unread-count')).toHaveTextContent('10'); + expect(getByTestId('unread-count')).toHaveTextContent('0'); }); act(() => { diff --git a/package/src/components/ChannelPreview/hooks/useChannelPreviewData.ts b/package/src/components/ChannelPreview/hooks/useChannelPreviewData.ts index 5fe2dae305..a678e7042a 100644 --- a/package/src/components/ChannelPreview/hooks/useChannelPreviewData.ts +++ b/package/src/components/ChannelPreview/hooks/useChannelPreviewData.ts @@ -5,6 +5,9 @@ import type { Channel, ChannelState, Event, MessageResponse, StreamChat } from ' import { useIsChannelMuted } from './useIsChannelMuted'; +import { useLatestMessagePreview } from './useLatestMessagePreview'; + +import { useChannelsContext } from '../../../contexts'; import type { DefaultStreamChatGenerics } from '../../../types/types'; export const useChannelPreviewData = < @@ -12,14 +15,40 @@ export const useChannelPreviewData = < >( channel: Channel, client: StreamChat, - forceUpdate?: number, + forceUpdateOverride?: number, ) => { + const [forceUpdate, setForceUpdate] = useState(0); const [lastMessage, setLastMessage] = useState< | ReturnType['formatMessage']> | MessageResponse >(channel.state.messages[channel.state.messages.length - 1]); const [unread, setUnread] = useState(channel.countUnread()); const { muted } = useIsChannelMuted(channel); + const { forceUpdate: contextForceUpdate } = useChannelsContext(); + const channelListForceUpdate = forceUpdateOverride ?? contextForceUpdate; + + const channelLastMessage = channel.lastMessage(); + const channelLastMessageString = `${channelLastMessage?.id}${channelLastMessage?.updated_at}`; + + useEffect(() => { + const { unsubscribe } = client.on('notification.mark_read', () => { + setUnread(channel.countUnread()); + }); + return unsubscribe; + }, [channel, client]); + + useEffect(() => { + if ( + channelLastMessage && + (channelLastMessage.id !== lastMessage?.id || + channelLastMessage.updated_at !== lastMessage?.updated_at) + ) { + setLastMessage(channelLastMessage); + } + const newUnreadCount = channel.countUnread(); + setUnread(newUnreadCount); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [channel, channelLastMessageString, channelListForceUpdate]); /** * This effect listens for the `notification.mark_read` event and sets the unread count to 0 @@ -27,9 +56,14 @@ export const useChannelPreviewData = < useEffect(() => { const handleReadEvent = (event: Event) => { if (!event.cid) return; - if (channel.cid === event.cid) setUnread(0); + if (channel.cid !== event.cid) return; + if (event?.user?.id === client.userID) { + setUnread(0); + } else if (event?.user?.id) { + setForceUpdate((prev) => prev + 1); + } }; - const { unsubscribe } = client.on('notification.mark_read', handleReadEvent); + const { unsubscribe } = client.on('message.read', handleReadEvent); return unsubscribe; }, [client, channel]); @@ -70,16 +104,35 @@ export const useChannelPreviewData = < refreshUnreadCount(); }; + const handleNewMessageEvent = (event: Event) => { + const message = event.message; + if (message && (!message.parent_id || message.show_in_channel)) { + setLastMessage(message); + setUnread(channel.countUnread()); + } + }; + + const handleUpdatedOrDeletedMessage = (event: Event) => { + setLastMessage((prevLastMessage) => { + if (prevLastMessage?.id === event.message?.id) { + return event.message; + } + return prevLastMessage; + }); + }; + const listeners = [ - channel.on('message.new', handleEvent), - channel.on('message.updated', handleEvent), - channel.on('message.deleted', handleEvent), + channel.on('message.new', handleNewMessageEvent), + channel.on('message.updated', handleUpdatedOrDeletedMessage), + channel.on('message.deleted', handleUpdatedOrDeletedMessage), channel.on('message.undeleted', handleEvent), channel.on('channel.truncated', handleEvent), ]; return () => listeners.forEach((l) => l.unsubscribe()); - }, [channel, refreshUnreadCount, forceUpdate]); + }, [channel, refreshUnreadCount, forceUpdate, channelListForceUpdate]); + + const latestMessagePreview = useLatestMessagePreview(channel, forceUpdate, lastMessage); - return { lastMessage, muted, unread }; + return { latestMessagePreview, muted, unread }; }; diff --git a/package/src/components/Message/MessageSimple/utils/generateMarkdownText.ts b/package/src/components/Message/MessageSimple/utils/generateMarkdownText.ts index 2851be1376..4e116cc4f2 100644 --- a/package/src/components/Message/MessageSimple/utils/generateMarkdownText.ts +++ b/package/src/components/Message/MessageSimple/utils/generateMarkdownText.ts @@ -35,5 +35,11 @@ export const generateMarkdownText = (text?: string) => { resultText = resultText.replace(/[<"'>]/g, '\\$&'); + // Remove whitespaces that come directly after newlines except in code blocks where we deem this allowed. + resultText = resultText.replace(/(```[\s\S]*?```|`.*?`)|\n[ ]{2,}/g, (_, code) => { + if (code) return code; + return '\n'; + }); + return resultText; };