diff --git a/package/package.json b/package/package.json index 83e89f5add..ce3a333ac8 100644 --- a/package/package.json +++ b/package/package.json @@ -77,7 +77,7 @@ "path": "0.12.7", "react-native-markdown-package": "1.8.2", "react-native-url-polyfill": "^1.3.0", - "stream-chat": "8.42.0" + "stream-chat": "8.44.0" }, "peerDependencies": { "@op-engineering/op-sqlite": ">=9.3.0", diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx index f44ad45dc2..486a031a96 100644 --- a/package/src/components/Channel/Channel.tsx +++ b/package/src/components/Channel/Channel.tsx @@ -1203,6 +1203,7 @@ const ChannelWithContext = < await channel.query({}, 'latest'); } await channel.state.loadMessageIntoState('latest'); + setMessages([...channel.state.messages]); }); const loadChannel = () => @@ -1399,13 +1400,17 @@ const ChannelWithContext = < }, [enableOfflineSupport, shouldSyncChannel]); const reloadChannel = () => - channelQueryCallRef.current(async () => { - setLoading(true); - await loadLatestMessagesRef.current(true); - setLoading(false); - channel?.state.setIsUpToDate(true); - setHasNoMoreRecentMessagesToLoad(true); - }); + channelQueryCallRef.current( + async () => { + setLoading(true); + await loadLatestMessagesRef.current(true); + setLoading(false); + }, + () => { + channel?.state.setIsUpToDate(true); + setHasNoMoreRecentMessagesToLoad(true); + }, + ); // In case the channel is disconnected which may happen when channel is deleted, // underlying js client throws an error. Following function ensures that Channel component @@ -1795,10 +1800,12 @@ const ChannelWithContext = < const latestLengthBeforeMerge = latestMessageSet?.messages.length || 0; const didMerge = mergeOverlappingMessageSetsRef.current(true); if (didMerge) { - if (latestMessageSet && latestLengthBeforeMerge >= limit) { + if (latestMessageSet && latestLengthBeforeMerge > 0) { + const shouldSetStateUpToDate = + latestMessageSet.messages.length < limit && latestMessageSet.isCurrent; setLoadingMoreRecent(true); - channel.state.setIsUpToDate(true); - setHasNoMoreRecentMessagesToLoad(true); + channel.state.setIsUpToDate(shouldSetStateUpToDate); + setHasNoMoreRecentMessagesToLoad(shouldSetStateUpToDate); loadMoreRecentFinished(channel.state.messages); restartSetsMergeFuncRef.current(); return; diff --git a/package/src/components/ChannelPreview/ChannelPreview.tsx b/package/src/components/ChannelPreview/ChannelPreview.tsx index 928d44c357..7eec3e32c7 100644 --- a/package/src/components/ChannelPreview/ChannelPreview.tsx +++ b/package/src/components/ChannelPreview/ChannelPreview.tsx @@ -1,7 +1,8 @@ -import React, { useEffect, useState } from 'react'; +import React from 'react'; -import type { Channel, ChannelState, Event, MessageResponse } from 'stream-chat'; +import type { Channel } from 'stream-chat'; +import { useChannelPreviewData } from './hooks/useChannelPreviewData'; import { useLatestMessagePreview } from './hooks/useLatestMessagePreview'; import { @@ -12,120 +13,40 @@ import { ChatContextValue, useChatContext } from '../../contexts/chatContext/Cha import type { DefaultStreamChatGenerics } from '../../types/types'; -export type ChannelPreviewPropsWithContext< +export type ChannelPreviewProps< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, -> = Pick, 'client'> & - Pick, 'Preview' | 'forceUpdate'> & { +> = Partial, 'client'>> & + Partial, 'Preview' | 'forceUpdate'>> & { /** * Instance of Channel from stream-chat package. */ channel: Channel; }; -/** - * This component manages state for the ChannelPreviewMessenger UI component and receives - * all props from the ChannelListMessenger component. - */ -const ChannelPreviewWithContext = < +export const ChannelPreview = < StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( - props: ChannelPreviewPropsWithContext, + props: ChannelPreviewProps, ) => { - const { channel, client, forceUpdate: channelListForceUpdate, Preview } = props; + const { channel, client: propClient, forceUpdate: propForceUpdate, Preview: propPreview } = props; - const [lastMessage, setLastMessage] = useState< - | ReturnType['formatMessage']> - | MessageResponse - | undefined - >(channel.state.messages[channel.state.messages.length - 1]); + const { client: contextClient } = useChatContext(); + const { forceUpdate: contextForceUpdate, Preview: contextPreview } = + useChannelsContext(); - const [forceUpdate, setForceUpdate] = useState(0); - const [unread, setUnread] = useState(channel.countUnread()); + 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 channelLastMessage = channel.lastMessage(); - const channelLastMessageString = `${channelLastMessage?.id}${channelLastMessage?.updated_at}`; - - useEffect(() => { - const { unsubscribe } = client.on('notification.mark_read', () => { - setUnread(channel.countUnread()); - }); - return unsubscribe; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - 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 - }, [channelLastMessageString, channelListForceUpdate]); - - useEffect(() => { - const handleNewMessageEvent = (event: Event) => { - const message = event.message; - if (message && (!message.parent_id || message.show_in_channel)) { - setLastMessage(event.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', handleNewMessageEvent), - channel.on('message.updated', handleUpdatedOrDeletedMessage), - channel.on('message.deleted', handleUpdatedOrDeletedMessage), - ]; - - return () => listeners.forEach((l) => l.unsubscribe()); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - const handleReadEvent = (event: Event) => { - if (event.user?.id === client.userID) { - setUnread(0); - } else if (event.user?.id) { - setForceUpdate((prev) => prev + 1); - } - }; - - const listener = channel.on('message.read', handleReadEvent); - return () => listener.unsubscribe(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return ; -}; - -export type ChannelPreviewProps< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, -> = Partial, 'channel'>> & - Pick, 'channel'>; - -export const ChannelPreview = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, ->( - props: ChannelPreviewProps, -) => { - const { client } = useChatContext(); - const { forceUpdate, Preview } = useChannelsContext(); - - return ; + return ( + + ); }; diff --git a/package/src/components/ChannelPreview/ChannelPreviewMessenger.tsx b/package/src/components/ChannelPreview/ChannelPreviewMessenger.tsx index 641f7c4c19..2805b61325 100644 --- a/package/src/components/ChannelPreview/ChannelPreviewMessenger.tsx +++ b/package/src/components/ChannelPreview/ChannelPreviewMessenger.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { StyleSheet, View } from 'react-native'; import { TouchableOpacity } from 'react-native-gesture-handler'; @@ -17,7 +17,6 @@ import { ChannelsContextValue, useChannelsContext, } from '../../contexts/channelsContext/ChannelsContext'; -import { useChatContext } from '../../contexts/chatContext/ChatContext'; import { useTheme } from '../../contexts/themeContext/ThemeContext'; import { useViewport } from '../../hooks/useViewport'; import type { DefaultStreamChatGenerics } from '../../types/types'; @@ -95,6 +94,8 @@ export type ChannelPreviewMessengerPropsWithContext< * default formatted date. This default logic is part of ChannelPreview component. */ formatLatestMessageDate?: (date: Date) => string; + /** If the channel is muted. */ + muted?: boolean; /** Number of unread messages on the channel */ unread?: number; }; @@ -109,6 +110,7 @@ const ChannelPreviewMessengerWithContext = < formatLatestMessageDate, latestMessagePreview, maxUnreadCount, + muted, onSelect, PreviewAvatar = ChannelAvatar, PreviewMessage = ChannelPreviewMessage, @@ -129,23 +131,11 @@ const ChannelPreviewMessengerWithContext = < }, } = useTheme(); - const { client } = useChatContext(); - const displayName = useChannelPreviewDisplayName( channel, Math.floor(maxWidth / ((title.fontSize || styles.title.fontSize) / 2)), ); - const [isChannelMuted, setIsChannelMuted] = useState(() => channel.muteStatus().muted); - - useEffect(() => { - const handleEvent = () => setIsChannelMuted(channel.muteStatus().muted); - - client.on('notification.channel_mutes_updated', handleEvent); - return () => client.off('notification.channel_mutes_updated', handleEvent); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [client]); - return ( { @@ -168,7 +158,7 @@ const ChannelPreviewMessengerWithContext = < - {isChannelMuted && } + {muted && } diff --git a/package/src/components/ChannelPreview/ChannelPreviewMutedStatus.tsx b/package/src/components/ChannelPreview/ChannelPreviewMutedStatus.tsx index a3d26ee52a..6cecff6ed5 100644 --- a/package/src/components/ChannelPreview/ChannelPreviewMutedStatus.tsx +++ b/package/src/components/ChannelPreview/ChannelPreviewMutedStatus.tsx @@ -20,16 +20,11 @@ export const ChannelPreviewMutedStatus = () => { channelPreview: { mutedStatus: { height, iconStyle, width }, }, - colors: { grey_dark }, + colors: { grey }, }, } = useTheme(); return ( - + ); }; diff --git a/package/src/components/ChannelPreview/__tests__/ChannelPreview.test.tsx b/package/src/components/ChannelPreview/__tests__/ChannelPreview.test.tsx index b6c1dd50bb..e9ad31c93f 100644 --- a/package/src/components/ChannelPreview/__tests__/ChannelPreview.test.tsx +++ b/package/src/components/ChannelPreview/__tests__/ChannelPreview.test.tsx @@ -11,8 +11,9 @@ import { } from '../../../mock-builders/api/getOrCreateChannel'; import { useMockedApis } from '../../../mock-builders/api/useMockedApis'; import dispatchMessageNewEvent from '../../../mock-builders/event/messageNew'; -import dispatchMessageReadEvent from '../../../mock-builders/event/messageRead'; -import { generateChannelResponse } from '../../../mock-builders/generator/channel'; +import dispatchNotificationMarkRead from '../../../mock-builders/event/notificationMarkRead'; +import dispatchNotificationMarkUnread from '../../../mock-builders/event/notificationMarkUnread'; +import { generateChannel, generateChannelResponse } from '../../../mock-builders/generator/channel'; import { generateMessage } from '../../../mock-builders/generator/message'; import { generateUser } from '../../../mock-builders/generator/user'; import { getTestClientWithUser } from '../../../mock-builders/mock'; @@ -84,22 +85,238 @@ describe('ChannelPreview', () => { channel = null; }); - it('should mark channel as read, when message.read event is received for current user', async () => { - const c = generateChannelResponse(); - await useInitializeChannel(c); + describe('notification.mark_read event', () => { + 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() }); - if (channel !== null) { - channel.countUnread = () => 20; - } + const c = generateChannel({ + countUnread: jest.fn().mockReturnValue(10), + muteStatus: jest.fn().mockReturnValue({ muted: false }), + on: channelOnMock, + }); + + channel = c as unknown as Channel; + + const { getByTestId } = render(); + + await waitFor(() => getByTestId('channel-id')); + + await waitFor(() => { + expect(getByTestId('unread-count')).toHaveTextContent('10'); + }); + + act(() => { + dispatchNotificationMarkRead(chatClient, { cid: 'channel-id' }); + }); + + await waitFor(() => { + expect(getByTestId('unread-count')).toHaveTextContent('10'); + }); + }); + + it('should update the unread count to 0', async () => { + const channelOnMock = jest.fn().mockReturnValue({ unsubscribe: jest.fn() }); + + const c = generateChannel({ + countUnread: jest.fn().mockReturnValue(10), + muteStatus: jest.fn().mockReturnValue({ muted: false }), + on: channelOnMock, + }); + + channel = c as unknown as Channel; + + const { getByTestId } = render(); + + await waitFor(() => getByTestId('channel-id')); + + await waitFor(() => { + expect(getByTestId('unread-count')).toHaveTextContent('10'); + }); + + act(() => { + dispatchNotificationMarkRead(chatClient, channel || {}); + }); + + await waitFor(() => { + expect(getByTestId('unread-count')).toHaveTextContent('0'); + }); + }); + }); + + describe('notification.mark_unread event', () => { + 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 }), + on: channelOnMock, + }); + + channel = c as unknown as Channel; + + const { getByTestId } = render(); + + await waitFor(() => getByTestId('channel-id')); + + await waitFor(() => { + expect(getByTestId('unread-count')).toHaveTextContent('0'); + }); + + act(() => { + dispatchNotificationMarkUnread( + chatClient, + {}, + { + unread_channels: 2, + unread_messages: 5, + }, + ); + }); + + await waitFor(() => { + expect(getByTestId('unread-count')).toHaveTextContent('0'); + }); + }); + + 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 }), + on: channelOnMock, + }); + + channel = c as unknown as Channel; + + const { getByTestId } = render(); + + await waitFor(() => getByTestId('channel-id')); + + await waitFor(() => { + expect(getByTestId('unread-count')).toHaveTextContent('0'); + }); + + act(() => { + dispatchNotificationMarkUnread( + chatClient, + { cid: 'channel-id' }, + { + unread_channels: 2, + unread_messages: 5, + }, + ); + }); + + await waitFor(() => { + expect(getByTestId('unread-count')).toHaveTextContent('0'); + }); + }); + + 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 }), + on: channelOnMock, + }); + + channel = c as unknown as Channel; + + const { getByTestId } = render(); + + await waitFor(() => getByTestId('channel-id')); + + await waitFor(() => { + expect(getByTestId('unread-count')).toHaveTextContent('0'); + }); + + act(() => { + dispatchNotificationMarkUnread( + chatClient, + { cid: channel?.cid }, + { + unread_channels: 2, + unread_messages: 5, + user: { id: 'random-id' }, + }, + ); + }); + + await waitFor(() => { + expect(getByTestId('unread-count')).toHaveTextContent('0'); + }); + }); + + it("should update the unread count if the event's user id matches the client's user id", async () => { + const c = generateChannelResponse(); + await useInitializeChannel(c); + const channelOnMock = jest.fn().mockReturnValue({ unsubscribe: jest.fn() }); + + const testChannel = { + ...channel, + countUnread: jest.fn().mockReturnValue(0), + muteStatus: jest.fn().mockReturnValue({ muted: false }), + on: channelOnMock, + }; + + const { getByTestId } = render(); + + await waitFor(() => getByTestId('channel-id')); + + await waitFor(() => { + expect(getByTestId('unread-count')).toHaveTextContent('0'); + }); + + act(() => { + dispatchNotificationMarkUnread( + chatClient, + { cid: testChannel?.cid }, + { + unread_channels: 2, + unread_messages: 5, + user: { id: clientUser.id }, + }, + ); + }); + + await waitFor(() => { + expect(getByTestId('unread-count')).toHaveTextContent('5'); + }); + }); + }); + + 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({ + countUnread: jest.fn().mockReturnValue(10), + muteStatus: jest.fn().mockReturnValue({ muted: false }), + on: channelOnMock, + }); + + channel = c as unknown as Channel; const { getByTestId } = render(); await waitFor(() => getByTestId('channel-id')); - expect(getByTestId('unread-count')).toHaveTextContent('20'); + await waitFor(() => { + expect(getByTestId('unread-count')).toHaveTextContent('10'); + }); act(() => { - dispatchMessageReadEvent(chatClient, clientUser, channel || {}); + dispatchNotificationMarkUnread( + chatClient, + { cid: channel?.cid }, + { + unread_channels: 2, + unread_messages: 5, + user: { id: clientUser.id }, + }, + ); }); await waitFor(() => { diff --git a/package/src/components/ChannelPreview/hooks/__tests__/useChannelPreviewDisplayName.test.tsx b/package/src/components/ChannelPreview/hooks/__tests__/useChannelPreviewDisplayName.test.tsx index fb341a4bdf..7865355361 100644 --- a/package/src/components/ChannelPreview/hooks/__tests__/useChannelPreviewDisplayName.test.tsx +++ b/package/src/components/ChannelPreview/hooks/__tests__/useChannelPreviewDisplayName.test.tsx @@ -3,7 +3,6 @@ import { Text } from 'react-native'; import { render, screen, waitFor } from '@testing-library/react-native'; -import type { DefaultStreamChatGenerics } from 'src/types/types'; import type { Channel, ChannelMemberResponse, DefaultGenerics, StreamChat } from 'stream-chat'; import { @@ -13,6 +12,7 @@ import { } from '../../../../mock-builders/api/queryMembers'; import { generateUser } from '../../../../mock-builders/generator/user'; import { getTestClientWithUser } from '../../../../mock-builders/mock'; +import type { DefaultStreamChatGenerics } from '../../../../types/types'; import { getChannelPreviewDisplayName, useChannelPreviewDisplayName, diff --git a/package/src/components/ChannelPreview/hooks/__tests__/useChannelPreviewMuted.test.tsx b/package/src/components/ChannelPreview/hooks/__tests__/useChannelPreviewMuted.test.tsx new file mode 100644 index 0000000000..f39d283162 --- /dev/null +++ b/package/src/components/ChannelPreview/hooks/__tests__/useChannelPreviewMuted.test.tsx @@ -0,0 +1,57 @@ +import { act, renderHook, waitFor } from '@testing-library/react-native'; +import { Channel, DefaultGenerics, StreamChat } from 'stream-chat'; + +import * as ChatContext from '../../../../contexts/chatContext/ChatContext'; +import dispatchNotificationChannelMutesUpdated from '../../../../mock-builders/event/notificationChannelMutesUpdated'; +import { generateUser } from '../../../../mock-builders/generator/user'; +import { getTestClientWithUser } from '../../../../mock-builders/mock'; +import { DefaultStreamChatGenerics } from '../../../../types/types'; +import { useIsChannelMuted } from '../useIsChannelMuted'; + +describe('useChannelPreviewMuted', () => { + const clientUser = generateUser(); + let chatClient: StreamChat | StreamChat; + + beforeEach(async () => { + chatClient = await getTestClientWithUser(clientUser); + jest.spyOn(ChatContext, 'useChatContext').mockImplementation( + jest.fn( + () => + ({ + client: chatClient, + } as unknown as ChatContext.ChatContextValue), + ), + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + const mockChannel = { + muteStatus: jest.fn().mockReturnValue(false), + } as unknown as Channel; + + it('should return the correct mute status', () => { + const { result } = renderHook(() => useIsChannelMuted(mockChannel)); + expect(result.current).toBe(false); + }); + + it("should update the mute status when the notification.channel_mutes_updated event is emitted'", () => { + renderHook(() => useIsChannelMuted(mockChannel)); + + act(() => dispatchNotificationChannelMutesUpdated(chatClient, mockChannel)); + + expect(mockChannel.muteStatus).toHaveBeenCalledTimes(2); + }); + + it('should clean up the event listener when the component is unmounted', async () => { + const { unmount } = renderHook(() => useIsChannelMuted(mockChannel)); + + unmount(); + + await waitFor(() => { + expect(mockChannel.muteStatus).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/package/src/components/ChannelPreview/hooks/useChannelPreviewData.ts b/package/src/components/ChannelPreview/hooks/useChannelPreviewData.ts new file mode 100644 index 0000000000..5fe2dae305 --- /dev/null +++ b/package/src/components/ChannelPreview/hooks/useChannelPreviewData.ts @@ -0,0 +1,85 @@ +import { useEffect, useMemo, useState } from 'react'; + +import throttle from 'lodash/throttle'; +import type { Channel, ChannelState, Event, MessageResponse, StreamChat } from 'stream-chat'; + +import { useIsChannelMuted } from './useIsChannelMuted'; + +import type { DefaultStreamChatGenerics } from '../../../types/types'; + +export const useChannelPreviewData = < + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +>( + channel: Channel, + client: StreamChat, + forceUpdate?: number, +) => { + 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); + + /** + * This effect listens for the `notification.mark_read` event and sets the unread count to 0 + */ + useEffect(() => { + const handleReadEvent = (event: Event) => { + if (!event.cid) return; + if (channel.cid === event.cid) setUnread(0); + }; + const { unsubscribe } = client.on('notification.mark_read', handleReadEvent); + return unsubscribe; + }, [client, channel]); + + /** + * This effect listens for the `notification.mark_unread` event and updates the unread count + */ + useEffect(() => { + const handleUnreadEvent = (event: Event) => { + if (!event.cid) return; + if (channel.cid !== event.cid) return; + if (event.user?.id !== client.user?.id) return; + setUnread(channel.countUnread()); + }; + const { unsubscribe } = client.on('notification.mark_unread', handleUnreadEvent); + return unsubscribe; + }, [client, channel]); + + const refreshUnreadCount = useMemo( + () => + throttle(() => { + if (muted) { + setUnread(0); + } else { + setUnread(channel.countUnread()); + } + }, 400), + [channel, muted], + ); + + /** + * This effect listens for the `message.new`, `message.updated`, `message.deleted`, `message.undeleted`, and `channel.truncated` events + */ + useEffect(() => { + refreshUnreadCount(); + + const handleEvent = () => { + setLastMessage(channel.state.latestMessages[channel.state.latestMessages.length - 1]); + refreshUnreadCount(); + }; + + const listeners = [ + channel.on('message.new', handleEvent), + channel.on('message.updated', handleEvent), + channel.on('message.deleted', handleEvent), + channel.on('message.undeleted', handleEvent), + channel.on('channel.truncated', handleEvent), + ]; + + return () => listeners.forEach((l) => l.unsubscribe()); + }, [channel, refreshUnreadCount, forceUpdate]); + + return { lastMessage, muted, unread }; +}; diff --git a/package/src/components/ChannelPreview/hooks/useIsChannelMuted.ts b/package/src/components/ChannelPreview/hooks/useIsChannelMuted.ts new file mode 100644 index 0000000000..9b8e87fc3a --- /dev/null +++ b/package/src/components/ChannelPreview/hooks/useIsChannelMuted.ts @@ -0,0 +1,29 @@ +import { useEffect, useState } from 'react'; + +import type { Channel } from 'stream-chat'; + +import { useChatContext } from '../../../contexts/chatContext/ChatContext'; + +import type { DefaultStreamChatGenerics } from '../../../types/types'; + +export const useIsChannelMuted = < + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +>( + channel: Channel, +) => { + const { client } = useChatContext(); + + const [muted, setMuted] = useState(channel.muteStatus()); + + useEffect(() => { + const handleEvent = () => { + setMuted(channel.muteStatus()); + }; + + client.on('notification.channel_mutes_updated', handleEvent); + return () => client.off('notification.channel_mutes_updated', handleEvent); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [muted]); + + return muted; +}; diff --git a/package/src/components/MessageList/MessageList.tsx b/package/src/components/MessageList/MessageList.tsx index e0cd15e34d..e2c0ecde58 100644 --- a/package/src/components/MessageList/MessageList.tsx +++ b/package/src/components/MessageList/MessageList.tsx @@ -862,9 +862,7 @@ const MessageListWithContext = < const goToNewMessages = async () => { const isNotLatestSet = channel.state.messages !== channel.state.latestMessages; - if (isNotLatestSet && hasNoMoreRecentMessagesToLoad) { - loadChannelAroundMessage({}); - } else if (!hasNoMoreRecentMessagesToLoad) { + if (isNotLatestSet) { resetPaginationTrackersRef.current(); await reloadChannel(); } else if (flatListRef.current) { diff --git a/package/src/components/ThreadList/ThreadList.tsx b/package/src/components/ThreadList/ThreadList.tsx index 003b054ff5..952cc31c3f 100644 --- a/package/src/components/ThreadList/ThreadList.tsx +++ b/package/src/components/ThreadList/ThreadList.tsx @@ -103,8 +103,6 @@ export const ThreadList = (props: ThreadListProps) => { client.threads.reload({ force: true }); }); - client.threads.reload({ force: true }); - return () => { client.threads.deactivate(); listener.unsubscribe(); diff --git a/package/src/components/index.ts b/package/src/components/index.ts index c2fa0e8d93..79fd95691f 100644 --- a/package/src/components/index.ts +++ b/package/src/components/index.ts @@ -67,6 +67,8 @@ export * from './ChannelPreview/hooks/useChannelPreviewDisplayAvatar'; export * from './ChannelPreview/hooks/useChannelPreviewDisplayName'; export * from './ChannelPreview/hooks/useChannelPreviewDisplayPresence'; export * from './ChannelPreview/hooks/useLatestMessagePreview'; +export * from './ChannelPreview/hooks/useChannelPreviewData'; +export * from './ChannelPreview/hooks/useIsChannelMuted'; export * from './Chat/Chat'; export * from './Chat/hooks/useCreateChatClient'; diff --git a/package/src/mock-builders/event/notificationChannelMutesUpdated.js b/package/src/mock-builders/event/notificationChannelMutesUpdated.js new file mode 100644 index 0000000000..3600092681 --- /dev/null +++ b/package/src/mock-builders/event/notificationChannelMutesUpdated.js @@ -0,0 +1,7 @@ +export default (client, channel = {}) => { + client.dispatchEvent({ + channel, + cid: channel.cid, + type: 'notification.channel_mutes_updated', + }); +}; diff --git a/package/src/mock-builders/event/notificationMarkRead.js b/package/src/mock-builders/event/notificationMarkRead.js new file mode 100644 index 0000000000..8978706f8f --- /dev/null +++ b/package/src/mock-builders/event/notificationMarkRead.js @@ -0,0 +1,7 @@ +export default (client, channel = {}) => { + client.dispatchEvent({ + channel, + cid: channel.cid, + type: 'notification.mark_read', + }); +}; diff --git a/package/src/mock-builders/event/notificationMarkUnread.js b/package/src/mock-builders/event/notificationMarkUnread.js new file mode 100644 index 0000000000..862802cf18 --- /dev/null +++ b/package/src/mock-builders/event/notificationMarkUnread.js @@ -0,0 +1,9 @@ +export default (client, channel = {}, payload = {}, user = {}) => { + client.dispatchEvent({ + channel, + cid: channel.cid, + type: 'notification.mark_unread', + user, + ...payload, + }); +}; diff --git a/package/src/mock-builders/generator/channel.ts b/package/src/mock-builders/generator/channel.ts index d86a2b763c..8fde5071fb 100644 --- a/package/src/mock-builders/generator/channel.ts +++ b/package/src/mock-builders/generator/channel.ts @@ -58,6 +58,7 @@ const getChannelDefaults = ( { id, type }: { [key: string]: any } = { id: uuidv4(), type: 'messaging' }, ) => ({ _client: {}, + cid: `${type}:${id}`, data: { cid: `${type}:${id}`, config: {