diff --git a/app/views/RoomView/List/hooks/useMessages.test.tsx b/app/views/RoomView/List/hooks/__tests__/useMessages.test.tsx similarity index 95% rename from app/views/RoomView/List/hooks/useMessages.test.tsx rename to app/views/RoomView/List/hooks/__tests__/useMessages.test.tsx index 2a57262b0b0..5ff5b095bea 100644 --- a/app/views/RoomView/List/hooks/useMessages.test.tsx +++ b/app/views/RoomView/List/hooks/__tests__/useMessages.test.tsx @@ -2,20 +2,20 @@ import { act, renderHook, waitFor } from '@testing-library/react-native'; import { Provider } from 'react-redux'; import { type ReactNode } from 'react'; -import { ROOM } from '../../../../actions/actionsTypes'; -import { type IRoomHistoryRequest } from '../../../../actions/room'; -import { type RoomType, type TAnyMessageModel } from '../../../../definitions'; -import database from '../../../../lib/database'; -import { getMessageById } from '../../../../lib/database/services/Message'; -import { getThreadById } from '../../../../lib/database/services/Thread'; -import { MessageTypeLoad } from '../../../../lib/constants/messageTypeLoad'; -import { readThreads } from '../../../../lib/services/restApi'; -import { mockedStore } from '../../../../reducers/mockedStore'; -import { MAX_AUTO_LOADS, QUERY_SIZE } from '../constants'; -import { buildVisibleSystemTypesClause } from '../visibleSystemMessages'; -import { useMessages } from './useMessages'; - -jest.mock('../../../../lib/database', () => ({ +import { ROOM } from '../../../../../actions/actionsTypes'; +import { type IRoomHistoryRequest } from '../../../../../actions/room'; +import { type RoomType, type TAnyMessageModel } from '../../../../../definitions'; +import database from '../../../../../lib/database'; +import { getMessageById } from '../../../../../lib/database/services/Message'; +import { getThreadById } from '../../../../../lib/database/services/Thread'; +import { MessageTypeLoad } from '../../../../../lib/constants/messageTypeLoad'; +import { readThreads } from '../../../../../lib/services/restApi'; +import { mockedStore } from '../../../../../reducers/mockedStore'; +import { MAX_AUTO_LOADS, QUERY_SIZE } from '../../constants'; +import { buildVisibleSystemTypesClause } from '../../visibleSystemMessages'; +import { useMessages } from '../useMessages'; + +jest.mock('../../../../../lib/database', () => ({ __esModule: true, default: { active: { @@ -24,20 +24,20 @@ jest.mock('../../../../lib/database', () => ({ } })); -jest.mock('../../../../lib/database/services/Message', () => ({ +jest.mock('../../../../../lib/database/services/Message', () => ({ getMessageById: jest.fn(() => Promise.resolve(null)) })); -jest.mock('../../../../lib/database/services/Thread', () => ({ +jest.mock('../../../../../lib/database/services/Thread', () => ({ getThreadById: jest.fn(() => Promise.resolve(null)) })); -jest.mock('../../../../lib/services/restApi', () => ({ +jest.mock('../../../../../lib/services/restApi', () => ({ readThreads: jest.fn(() => Promise.resolve()) })); -jest.mock('../../../../lib/methods/helpers', () => { - const actual = jest.requireActual('../../../../lib/methods/helpers'); +jest.mock('../../../../../lib/methods/helpers', () => { + const actual = jest.requireActual('../../../../../lib/methods/helpers'); return { ...actual, useDebounce: (fn: (...args: unknown[]) => unknown) => Object.assign(fn, { cancel: jest.fn() }) diff --git a/app/views/RoomView/List/hooks/useScroll.test.tsx b/app/views/RoomView/List/hooks/__tests__/useScroll.test.tsx similarity index 99% rename from app/views/RoomView/List/hooks/useScroll.test.tsx rename to app/views/RoomView/List/hooks/__tests__/useScroll.test.tsx index 154e7bc88f0..b38eb2b854f 100644 --- a/app/views/RoomView/List/hooks/useScroll.test.tsx +++ b/app/views/RoomView/List/hooks/__tests__/useScroll.test.tsx @@ -1,8 +1,8 @@ import { act, renderHook, waitFor } from '@testing-library/react-native'; -import { type TAnyMessageModel } from '../../../../definitions'; -import { type TListRef, type TMessagesIdsRef } from '../../definitions'; -import { useScroll } from './useScroll'; +import { type TAnyMessageModel } from '../../../../../definitions'; +import { type TListRef, type TMessagesIdsRef } from '../../../definitions'; +import { useScroll } from '../useScroll'; type Row = { id: string }; diff --git a/app/views/RoomView/hooks/useCanPlaceLivechatOnHold.ts b/app/views/RoomView/hooks/useCanPlaceLivechatOnHold.ts index 3a130713aea..14cd450d318 100644 --- a/app/views/RoomView/hooks/useCanPlaceLivechatOnHold.ts +++ b/app/views/RoomView/hooks/useCanPlaceLivechatOnHold.ts @@ -1,10 +1,13 @@ +import { useShallow } from 'zustand/react/shallow'; + import { useSetting } from '../../../lib/hooks/useSetting'; import { useRoomStoreByRid } from '../stores/RoomStore'; export function useCanPlaceLivechatOnHold(rid?: string): boolean { const livechatAllowManualOnHold = useSetting('Livechat_allow_manual_on_hold') as boolean; - const t = useRoomStoreByRid(rid, s => s.room.t); - const lastMessageFromAgent = useRoomStoreByRid(rid, s => s.lastMessageFromAgent); - const onHold = useRoomStoreByRid(rid, s => s.roomUpdate.onHold); + const { t, lastMessageFromAgent, onHold } = useRoomStoreByRid( + rid, + useShallow(s => ({ t: s.room.t, lastMessageFromAgent: s.lastMessageFromAgent, onHold: s.roomUpdate.onHold })) + ); return t === 'l' && !!livechatAllowManualOnHold && lastMessageFromAgent && !onHold; } diff --git a/app/views/RoomView/hooks/useGoRoomActionsView.ts b/app/views/RoomView/hooks/useGoRoomActionsView.ts index a401cba0937..c423272ec98 100644 --- a/app/views/RoomView/hooks/useGoRoomActionsView.ts +++ b/app/views/RoomView/hooks/useGoRoomActionsView.ts @@ -1,4 +1,5 @@ import { type NavigatorScreenParams, useNavigation } from '@react-navigation/native'; +import { useShallow } from 'zustand/react/shallow'; import { events, logEvent } from '../../../lib/methods/helpers/log'; import { useMasterDetail } from '../../../lib/hooks/useMasterDetail'; @@ -13,13 +14,17 @@ import { useCanPlaceLivechatOnHold } from './useCanPlaceLivechatOnHold'; export const useGoRoomActionsView = (rid?: string): ((screen?: keyof ModalStackParamList) => void) => { const navigation = useNavigation(); const isMasterDetail = useMasterDetail(); - // `t` comes from the store (seeded at mount) rather than route.params, which navigation can wipe. - const room = useRoomStoreByRid(rid, s => s.room); + const { room, member, joined, canForwardGuest, canViewCannedResponse } = useRoomStoreByRid( + rid, + useShallow(s => ({ + room: s.room, + member: s.member, + joined: s.joined, + canForwardGuest: s.canForwardGuest, + canViewCannedResponse: s.canViewCannedResponse + })) + ); const t = room.t; - const member = useRoomStoreByRid(rid, s => s.member); - const joined = useRoomStoreByRid(rid, s => s.joined); - const canForwardGuest = useRoomStoreByRid(rid, s => s.canForwardGuest); - const canViewCannedResponse = useRoomStoreByRid(rid, s => s.canViewCannedResponse); const canReturnQueue = useCanReturnQueue(t === 'l'); const canPlaceLivechatOnHold = useCanPlaceLivechatOnHold(rid); diff --git a/app/views/RoomView/hooks/useHeader.tsx b/app/views/RoomView/hooks/useHeader.tsx index ec2b5acf516..24e4dbd0a3e 100644 --- a/app/views/RoomView/hooks/useHeader.tsx +++ b/app/views/RoomView/hooks/useHeader.tsx @@ -101,6 +101,26 @@ export const useHeader = ({ rid, tmid, name: roomName }: IUseHeaderParams): void } const headerProps = getRoomHeaderProps({ room, tmid, roomName, roomUserId, onPress: goRoomActionsView }); - navigation.setOptions({ headerTitle: () => }); + navigation.setOptions({ + headerTitle: () => ( + + ) + }); }, [rid, tmid, roomName, room, roomUpdate, roomUserId, navigation, goRoomActionsView]); }; diff --git a/app/views/RoomView/hooks/useJumpToMessage.ts b/app/views/RoomView/hooks/useJumpToMessage.ts index c3e6977835d..8c364e2246a 100644 --- a/app/views/RoomView/hooks/useJumpToMessage.ts +++ b/app/views/RoomView/hooks/useJumpToMessage.ts @@ -88,7 +88,6 @@ export function useJumpToMessage({ // Live-mirrored (see useLiveRef) so the mount effect can key on [tmid] despite these being unstable. const consumeJumpParamRef = useLiveRef(consumeJumpParam); - const onJumpParamChangedRef = useLiveRef(onJumpParamChanged); const navToThreadRef = useLiveRef(navToThread); useEffect(() => { @@ -114,8 +113,8 @@ export function useJumpToMessage({ return () => task.cancel(); }, [navToThreadRef]); - useChangedParam(route.params?.jumpToMessageId, id => onJumpParamChangedRef.current(id)); - useChangedParam(route.params?.jumpToThreadId, id => navToThreadRef.current({ tmid: id })); + useChangedParam(route.params?.jumpToMessageId, onJumpParamChanged); + useChangedParam(route.params?.jumpToThreadId, id => navToThread({ tmid: id })); return { jumpToMessage, cancelJumpToMessage, onThreadMessagesLoaded }; } diff --git a/app/views/RoomView/hooks/useRoomMessageHandlers.tsx b/app/views/RoomView/hooks/useRoomMessageHandlers.tsx index 7050001d6c0..dfc6d753ef0 100644 --- a/app/views/RoomView/hooks/useRoomMessageHandlers.tsx +++ b/app/views/RoomView/hooks/useRoomMessageHandlers.tsx @@ -36,8 +36,8 @@ export function useRoomMessageHandlers({ const isMasterDetail = useMasterDetail(); const { showActionSheet } = useActionSheet(); - const rid = useRoomStore(s => s.room.rid); const room = useRoomStore(s => s.room); + const rid = room.rid; const onDiscussionPress = async (drid: TAnyMessageModel['drid']) => { if (!drid) return; diff --git a/app/views/RoomView/hooks/useRoomNavigation.ts b/app/views/RoomView/hooks/useRoomNavigation.ts index dfb415df7f3..25b238359a6 100644 --- a/app/views/RoomView/hooks/useRoomNavigation.ts +++ b/app/views/RoomView/hooks/useRoomNavigation.ts @@ -54,7 +54,7 @@ export function useRoomNavigation({ useEffect(() => { cancelJumpToMessageRef.current = cancelJumpToMessage; - }); + }, [cancelJumpToMessage]); const onThreadPress = useDebounce((item: TAnyMessageModel) => navToThread(item), 1000, { leading: true, trailing: false }); diff --git a/app/views/RoomView/hooks/useUnreadsCount.ts b/app/views/RoomView/hooks/useUnreadsCount.ts index 8868c79560a..ed63e89b8bc 100644 --- a/app/views/RoomView/hooks/useUnreadsCount.ts +++ b/app/views/RoomView/hooks/useUnreadsCount.ts @@ -1,32 +1,29 @@ -import { useEffect, useState } from 'react'; +import { useMemo } from 'react'; import { Q } from '@nozbe/watermelondb'; import database from '../../../lib/database'; import { isIOS } from '../../../lib/methods/helpers'; +import { useObservable } from '../../../lib/hooks/useObservable'; import { type TSubscriptionModel } from '../../../definitions'; export function useUnreadsCount(rid?: string): number | null { - const [unreadsCount, setUnreadsCount] = useState(null); + const observable = useMemo( + () => + isIOS && rid + ? database.active + .get('subscriptions') + .query(Q.where('archived', false), Q.where('open', true), Q.where('rid', Q.notEq(rid))) + .observeWithColumns(['unread']) + : undefined, + [rid] + ); + const rooms = useObservable(observable); - useEffect(() => { - if (!isIOS || !rid) { - return; - } - const observable = database.active - .get('subscriptions') - .query(Q.where('archived', false), Q.where('open', true), Q.where('rid', Q.notEq(rid))) - .observeWithColumns(['unread']); - - const subscription = observable.subscribe((rooms: TSubscriptionModel[]) => { - const nextUnreadsCount = rooms.reduce( - (unreadCount, item) => (item.unread > 0 && !item.hideUnreadStatus ? unreadCount + item.unread : unreadCount), - 0 - ); - setUnreadsCount(nextUnreadsCount); - }); - - return () => subscription.unsubscribe(); - }, [rid]); - - return unreadsCount; + if (!rooms) { + return null; + } + return rooms.reduce( + (unreadCount, item) => (item.unread > 0 && !item.hideUnreadStatus ? unreadCount + item.unread : unreadCount), + 0 + ); }