-
Notifications
You must be signed in to change notification settings - Fork 1.4k
fix: switch hide system message not working on mobile and loading on older server #7130
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
OtavioStasiak
wants to merge
19
commits into
develop
Choose a base branch
from
fix.load-older-messages-with-more-than-50-system-messages
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
8ec8f99
chore: update filter
OtavioStasiak 2cdac4a
chore: code improvements
OtavioStasiak c6fc775
fix: workspace <3.16 hide system message switch
OtavioStasiak 3a2b4fe
roomsHistoryRequest
OtavioStasiak f955180
fix: workspace > 3.16 hide system message switch
OtavioStasiak cc0b9a3
fix: tests
OtavioStasiak 62d0f03
Merge branch 'develop' into fix.load-older-messages-with-more-than-50…
OtavioStasiak 473cf4e
fix: lint
OtavioStasiak b083e1b
Merge branch 'develop' into fix.load-older-messages-with-more-than-50…
OtavioStasiak 9f60dcb
Merge branch 'develop' into fix.load-older-messages-with-more-than-50…
OtavioStasiak d87b8f6
fix: code improvements
OtavioStasiak 5c0e835
Merge branch 'develop' into fix.load-older-messages-with-more-than-50…
OtavioStasiak 643fbec
fix: unit test
OtavioStasiak ff49e7f
code improvements
OtavioStasiak c027cba
code improvements
OtavioStasiak 2b292d6
fix: snapshot
OtavioStasiak 1ccc8be
fix: derive messagesIds ref inside useMemo to prevent stale data unde…
OtavioStasiak 3f5fe57
fix: guard roomHistoryRequest dispatch to prevent re-firing on same l…
OtavioStasiak 22984d4
code improvements
OtavioStasiak File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
18 changes: 18 additions & 0 deletions
18
app/views/RoomView/List/hooks/buildVisibleSystemTypesClause.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| import { Q } from '@nozbe/watermelondb'; | ||
|
|
||
| import { MESSAGE_TYPE_ANY_LOAD } from '../../../../lib/constants/messageTypeLoad'; | ||
|
|
||
| /** | ||
| * When the user hides system message types, applying `take(N)` before filtering would return | ||
| * mostly hidden rows. This clause matches the JS filter `!m.t || !hideSystemMessages.includes(m.t)` | ||
| * so `take` applies to visible rows only. | ||
| */ | ||
| export function buildVisibleSystemTypesClause(hideSystemMessages: string[]): Q.Or | null { | ||
| if (!hideSystemMessages.length) { | ||
| return null; | ||
| } | ||
|
|
||
| const notHidden = Q.and(...hideSystemMessages.map(h => Q.where('t', Q.notEq(h)))); | ||
|
|
||
| return Q.or(Q.where('t', null), Q.where('t', Q.oneOf([...MESSAGE_TYPE_ANY_LOAD])), notHidden); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,215 @@ | ||
| import React from 'react'; | ||
| import { act, renderHook, waitFor } from '@testing-library/react-native'; | ||
| import { Provider } from 'react-redux'; | ||
|
|
||
| import { ROOM } from '../../../../actions/actionsTypes'; | ||
| 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 { mockedStore } from '../../../../reducers/mockedStore'; | ||
| import { useMessages } from './useMessages'; | ||
|
|
||
| jest.mock('../../../../lib/database', () => ({ | ||
| __esModule: true, | ||
| default: { | ||
| active: { | ||
| get: jest.fn() | ||
| } | ||
| } | ||
| })); | ||
|
|
||
| jest.mock('../../../../lib/database/services/Message', () => ({ | ||
| getMessageById: jest.fn(() => Promise.resolve(null)) | ||
| })); | ||
|
|
||
| jest.mock('../../../../lib/database/services/Thread', () => ({ | ||
| getThreadById: jest.fn(() => Promise.resolve(null)) | ||
| })); | ||
|
|
||
| jest.mock('../../../../lib/services/restApi', () => ({ | ||
| readThreads: jest.fn(() => Promise.resolve()) | ||
| })); | ||
|
|
||
| jest.mock('../../../../lib/methods/helpers', () => { | ||
| const actual = jest.requireActual('../../../../lib/methods/helpers'); | ||
| return { | ||
| ...actual, | ||
| useDebounce: (fn: (...args: unknown[]) => unknown) => fn | ||
| }; | ||
| }); | ||
|
|
||
| const mockDbGet = database.active.get as unknown as jest.Mock; | ||
| const mockGetThreadById = jest.mocked(getThreadById); | ||
| const mockGetMessageById = jest.mocked(getMessageById); | ||
|
|
||
| const baseArgs = { | ||
| rid: 'ROOM_ID', | ||
| showMessageInMainThread: true, | ||
| hideSystemMessages: [] as string[], | ||
| serverVersion: '3.0.0' as string | null, | ||
| t: 'c' as RoomType | ||
| }; | ||
|
|
||
| const msg = (overrides: Partial<TAnyMessageModel> & { id: string }): TAnyMessageModel => | ||
| ({ | ||
| ts: new Date('2024-01-01'), | ||
| ...overrides | ||
| } as TAnyMessageModel); | ||
|
|
||
| describe('useMessages', () => { | ||
| let emittedRows: TAnyMessageModel[]; | ||
|
|
||
| const wrapper = ({ children }: { children: React.ReactNode }) => <Provider store={mockedStore}>{children}</Provider>; | ||
|
|
||
| beforeEach(() => { | ||
| emittedRows = []; | ||
| jest.clearAllMocks(); | ||
| mockDbGet.mockImplementation(() => ({ | ||
| query: jest.fn().mockReturnValue({ | ||
| observe: () => ({ | ||
| subscribe: (onNext: (rows: TAnyMessageModel[]) => void) => { | ||
| onNext(emittedRows); | ||
| return { unsubscribe: jest.fn() }; | ||
| } | ||
| }) | ||
| }) | ||
| })); | ||
|
OtavioStasiak marked this conversation as resolved.
|
||
| }); | ||
|
|
||
| const renderUseMessages = (overrides: Partial<Parameters<typeof useMessages>[0]> = {}) => | ||
| renderHook(() => useMessages({ ...baseArgs, ...overrides }), { wrapper }); | ||
|
|
||
| it('returns fetchMessages as a function', async () => { | ||
| emittedRows = [msg({ id: 'm1' })]; | ||
| const { result } = renderUseMessages(); | ||
| await waitFor(() => { | ||
| expect(typeof result.current[2]).toBe('function'); | ||
| }); | ||
| }); | ||
|
|
||
| it('loads main room messages from the messages collection', async () => { | ||
| emittedRows = [msg({ id: 'a' }), msg({ id: 'b' })]; | ||
| const { result } = renderUseMessages({ rid: 'R1' }); | ||
| await waitFor(() => { | ||
| expect(result.current[0].map(m => m.id)).toEqual(['a', 'b']); | ||
| }); | ||
| expect(mockDbGet).toHaveBeenCalledWith('messages'); | ||
| }); | ||
|
|
||
| it('does not query the database when rid is empty', async () => { | ||
| emittedRows = [msg({ id: 'x' })]; | ||
| const { result } = renderUseMessages({ rid: '' }); | ||
| await act(async () => { | ||
| await result.current[2](); | ||
| }); | ||
| expect(mockDbGet).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('filters out system message types listed in hideSystemMessages', async () => { | ||
| emittedRows = [msg({ id: '1', t: 'uj' }), msg({ id: '2', t: undefined }), msg({ id: '3', t: 'room_changed_topic' })]; | ||
| const { result } = renderUseMessages({ hideSystemMessages: ['uj'] }); | ||
| await waitFor(() => { | ||
| expect(result.current[0].map(m => m.id)).toEqual(['2', '3']); | ||
| }); | ||
| }); | ||
|
|
||
| it('returns visibleMessagesIds aligned with visible messages', async () => { | ||
| emittedRows = [msg({ id: 'p' }), msg({ id: 'q' })]; | ||
| const { result } = renderUseMessages(); | ||
| await waitFor(() => { | ||
| expect(result.current[1].current).toEqual(['p', 'q']); | ||
| }); | ||
| }); | ||
|
|
||
| it('dispatches room history request when server is 3.16+, user hides system messages, and a load row exists', async () => { | ||
| const dispatchSpy = jest.spyOn(mockedStore, 'dispatch'); | ||
| emittedRows = [msg({ id: 'm1', t: undefined }), msg({ id: 'load-more-x', t: MessageTypeLoad.MORE })]; | ||
| renderUseMessages({ | ||
| serverVersion: '6.0.0', | ||
| hideSystemMessages: ['uj'] | ||
| }); | ||
| await waitFor(() => { | ||
| expect(dispatchSpy).toHaveBeenCalledWith( | ||
| expect.objectContaining({ | ||
| type: ROOM.HISTORY_REQUEST, | ||
| rid: 'ROOM_ID', | ||
| t: 'c', | ||
| loaderId: 'load-more-x' | ||
| }) | ||
| ); | ||
| }); | ||
| dispatchSpy.mockRestore(); | ||
| }); | ||
|
|
||
| it('does not dispatch roomHistoryRequest again when loaderId has not changed', async () => { | ||
| const dispatchSpy = jest.spyOn(mockedStore, 'dispatch'); | ||
| emittedRows = [msg({ id: 'load-more-x', t: MessageTypeLoad.MORE })]; | ||
|
|
||
| const { rerender } = renderUseMessages({ | ||
| serverVersion: '6.0.0', | ||
| hideSystemMessages: ['uj'] | ||
| }); | ||
|
|
||
| await waitFor(() => { | ||
| const historyDispatches = dispatchSpy.mock.calls.filter(([a]: any) => a?.type === ROOM.HISTORY_REQUEST); | ||
| expect(historyDispatches.length).toBe(1); | ||
| }); | ||
|
|
||
| // Simulate a new message arriving — visibleMessages changes but loaderId stays the same | ||
| emittedRows = [msg({ id: 'new-msg' }), msg({ id: 'load-more-x', t: MessageTypeLoad.MORE })]; | ||
| rerender({}); | ||
|
|
||
| await waitFor(() => { | ||
| const historyDispatches = dispatchSpy.mock.calls.filter(([a]: any) => a?.type === ROOM.HISTORY_REQUEST); | ||
| expect(historyDispatches.length).toBe(1); // still only once | ||
| }); | ||
|
|
||
| dispatchSpy.mockRestore(); | ||
| }); | ||
|
|
||
| it('does not dispatch room history request when server is below 3.16', async () => { | ||
| const dispatchSpy = jest.spyOn(mockedStore, 'dispatch'); | ||
| emittedRows = [msg({ id: 'load-more-x', t: MessageTypeLoad.MORE })]; | ||
| const { result } = renderUseMessages({ | ||
| serverVersion: '3.15.0', | ||
| hideSystemMessages: ['uj'] | ||
| }); | ||
| await waitFor(() => { | ||
| expect(result.current[0].length).toBeGreaterThan(0); | ||
| }); | ||
| const historyDispatches = dispatchSpy.mock.calls.filter(([a]: any) => a?.type === ROOM.HISTORY_REQUEST); | ||
| expect(historyDispatches.length).toBe(0); | ||
| dispatchSpy.mockRestore(); | ||
| }); | ||
|
|
||
| it('queries thread_messages and appends thread parent when tmid is set', async () => { | ||
| const parent = { | ||
| ...msg({ id: 'parent-thread', t: 'discussion-created' }), | ||
| collection: { table: 'threads' } | ||
| } as TAnyMessageModel; | ||
| mockGetThreadById.mockResolvedValueOnce(parent); | ||
| emittedRows = [msg({ id: 'tm1', tmid: 'THREAD_ID' })]; | ||
| const { result } = renderUseMessages({ tmid: 'THREAD_ID' }); | ||
| await waitFor(() => { | ||
| expect(mockDbGet).toHaveBeenCalledWith('thread_messages'); | ||
| }); | ||
| await waitFor(() => { | ||
| const ids = result.current[0].map(m => m.id); | ||
| expect(ids).toContain('parent-thread'); | ||
| expect(ids).toContain('tm1'); | ||
| }); | ||
| }); | ||
|
|
||
| it('falls back to getMessageById when thread record is missing', async () => { | ||
| const parent = msg({ id: 'fallback-parent', t: undefined }); | ||
| mockGetThreadById.mockResolvedValueOnce(null); | ||
| mockGetMessageById.mockResolvedValueOnce(parent as TAnyMessageModel); | ||
| emittedRows = [msg({ id: 'only-child', tmid: 'TM' })]; | ||
| const { result } = renderUseMessages({ tmid: 'TM' }); | ||
| await waitFor(() => { | ||
| expect(result.current[0].map(m => m.id)).toContain('fallback-parent'); | ||
| }); | ||
| }); | ||
| }); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.