Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/app/features/room/RoomTimeline.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,25 @@ describe('unread read marker (sliding sync)', () => {
});

describe('scroll-edge pagination', () => {
it('marks the timeline as at the bottom when jumping to latest', async () => {
const { getByText, queryByText } = renderTimeline();

await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 150));
});

// Simulate a stale virtualizer measurement reporting the viewport above the
// bottom. A programmatic jump may not emit a follow-up scroll event.
act(() => lastOnScroll?.(0));
expect(getByText('Jump to Latest')).toBeTruthy();

act(() => {
getByText('Jump to Latest').click();
});

expect(queryByText('Jump to Latest')).toBeNull();
});

it('paginates backwards near the top only while a pagination token exists', () => {
const { rerender } = renderTimeline();

Expand Down
1 change: 1 addition & 0 deletions src/app/features/room/RoomTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1331,6 +1331,7 @@ export function RoomTimeline({
onClick={() => {
if (eventId) navigateRoom(room.roomId, undefined, { replace: true });
timelineSync.setTimeline(getInitialTimeline(room));
setAtBottom(true);
scrollToBottom();
}}
style={{
Expand Down
16 changes: 16 additions & 0 deletions src/app/hooks/timeline/useProcessedTimeline.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,22 @@ const dividerIds = (processed: ProcessedEvent[]) =>
processed.filter((e) => e.willRenderNewDivider).map((e) => e.id);

describe('useProcessedTimeline new-messages divider', () => {
it('renders an event that is still encrypted', () => {
const processed = processTimeline(
[
createEvent({ id: '$a' }),
createEvent({
id: '$encrypted',
type: EventType.RoomMessageEncrypted as string,
content: { algorithm: 'm.megolm.v1.aes-sha2', ciphertext: 'AwgAEnB...' },
}),
],
undefined
);

expect(renderedIds(processed)).toEqual(['$a', '$encrypted']);
});

it('keeps poll start events with default hidden-event settings', () => {
const processed = processTimeline(
[
Expand Down
15 changes: 5 additions & 10 deletions src/app/hooks/timeline/useProcessedTimeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,19 +69,16 @@ export function getProcessedRowIndexForRawTimelineIndex(
return bestRowIndex >= 0 ? { rowIndex: bestRowIndex, focusRawIndex: bestRawIndex } : undefined;
}

// Decrypted room-message events are re-typed to this app-internal value; there is no SDK constant for it.
const ROOM_MESSAGE_DECRYPTED = 'm.room.message.encrypted';

const MESSAGE_EVENT_TYPES = new Set([
const MESSAGE_EVENT_TYPES = new Set<string>([
EventType.RoomMessage,
ROOM_MESSAGE_DECRYPTED,
EventType.Sticker,
EventType.RoomMessageEncrypted,
]);

export const STANDARD_RENDERED_EVENT_TYPES = new Set([
export const STANDARD_RENDERED_EVENT_TYPES = new Set<string>([
EventType.RoomMessage,
ROOM_MESSAGE_DECRYPTED,
// getType() reports this until decryption resolves the real type.
EventType.RoomMessageEncrypted,
EventType.Sticker,
M_POLL_START.name,
EventType.RoomMember,
Expand All @@ -92,9 +89,7 @@ export const STANDARD_RENDERED_EVENT_TYPES = new Set([
]);

const normalizeMessageType = (t: string): string =>
t === (EventType.RoomMessageEncrypted as string) || t === ROOM_MESSAGE_DECRYPTED
? EventType.RoomMessage
: t;
t === (EventType.RoomMessageEncrypted as string) ? EventType.RoomMessage : t;

const isMessageRow = (mEvent: MatrixEvent): boolean =>
MESSAGE_EVENT_TYPES.has(mEvent.getType()) && !isEditEvent(mEvent);
Expand Down
79 changes: 56 additions & 23 deletions src/app/hooks/timeline/useTimelineSync.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { act, renderHook } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { faker } from '@faker-js/faker';
import type { Room } from '$types/matrix-sdk';
import { Direction, RoomEvent } from '$types/matrix-sdk';
import { Direction, MatrixEventEvent, RoomEvent } from '$types/matrix-sdk';
import { countVisibleAmongNewest, useTimelineSync } from './useTimelineSync';
import { getRoomUnreadInfo } from '$utils/timeline';
import type * as TimelineUtils from '$utils/timeline';
Expand Down Expand Up @@ -132,6 +132,15 @@ function createPaginableRoom(roomId = '!room:test') {
return { room, timelineSet, events };
}

const mxEmitter = new EventEmitter();
const makeMx = (extra: Record<string, unknown> = {}) =>
({
getUserId: () => '@alice:test',
on: mxEmitter.on.bind(mxEmitter),
removeListener: mxEmitter.removeListener.bind(mxEmitter),
...extra,
}) as never;

function makeEvent(sender: string, roomId: string) {
return {
threadRootId: undefined,
Expand Down Expand Up @@ -229,7 +238,7 @@ describe('useTimelineSync', () => {
renderHook(() =>
useTimelineSync({
room: room as Room,
mx: { getUserId: () => '@alice:test' } as never,
mx: makeMx(),
isAtBottom: false,
isAtBottomRef: { current: false },
scrollToBottom,
Expand Down Expand Up @@ -261,7 +270,7 @@ describe('useTimelineSync', () => {
renderHook(() =>
useTimelineSync({
room: room as Room,
mx: { getUserId: () => '@alice:test' } as never,
mx: makeMx(),
isAtBottom: true,
isAtBottomRef: { current: true },
scrollToBottom,
Expand Down Expand Up @@ -289,7 +298,7 @@ describe('useTimelineSync', () => {
({ room, eventId }) =>
useTimelineSync({
room,
mx: { getUserId: () => '@alice:test' } as never,
mx: makeMx(),
eventId,
isAtBottom: false,
isAtBottomRef: { current: false },
Expand Down Expand Up @@ -326,7 +335,7 @@ describe('useTimelineSync', () => {
({ room, eventId }) =>
useTimelineSync({
room,
mx: { getUserId: () => '@alice:test' } as never,
mx: makeMx(),
eventId,
isAtBottom: false,
isAtBottomRef: { current: false },
Expand Down Expand Up @@ -361,7 +370,7 @@ describe('useTimelineSync', () => {
({ room }) =>
useTimelineSync({
room,
mx: { getUserId: () => '@alice:test' } as never,
mx: makeMx(),
eventId: undefined,
isAtBottom: false,
isAtBottomRef: { current: false },
Expand Down Expand Up @@ -395,7 +404,7 @@ describe('useTimelineSync', () => {
renderHook(() =>
useTimelineSync({
room: room as Room,
mx: { getUserId: () => '@alice:test' } as never,
mx: makeMx(),
isAtBottom: true,
isAtBottomRef: { current: true },
scrollToBottom,
Expand All @@ -422,7 +431,7 @@ describe('useTimelineSync', () => {
renderHook(() =>
useTimelineSync({
room: room as Room,
mx: { getUserId: () => '@alice:test' } as never,
mx: makeMx(),
isAtBottom: true,
isAtBottomRef: { current: true },
scrollToBottom,
Expand Down Expand Up @@ -452,7 +461,7 @@ describe('useTimelineSync', () => {
renderHook(() =>
useTimelineSync({
room: room as Room,
mx: { getUserId: () => '@alice:test' } as never,
mx: makeMx(),
isAtBottom: true,
isAtBottomRef: { current: true },
scrollToBottom,
Expand Down Expand Up @@ -480,7 +489,7 @@ describe('useTimelineSync', () => {
renderHook(() =>
useTimelineSync({
room: room as Room,
mx: { getUserId: () => '@alice:test' } as never,
mx: makeMx(),
isAtBottom: false,
isAtBottomRef: { current: false },
scrollToBottom,
Expand All @@ -506,7 +515,7 @@ describe('useTimelineSync', () => {
renderHook(() =>
useTimelineSync({
room: room as Room,
mx: { getUserId: () => '@alice:test' } as never,
mx: makeMx(),
isAtBottom: true,
isAtBottomRef: { current: true },
scrollToBottom,
Expand Down Expand Up @@ -536,7 +545,7 @@ describe('useTimelineSync', () => {
renderHook(() =>
useTimelineSync({
room: room as Room,
mx: { getUserId: () => '@alice:test' } as never,
mx: makeMx(),
isAtBottom: true,
isAtBottomRef: { current: true },
scrollToBottom,
Expand Down Expand Up @@ -572,7 +581,7 @@ const syncOpts = (
isEventVisible?: () => boolean
) => ({
room: room as Room,
mx: { getUserId: () => '@alice:test', paginateEventTimeline } as never,
mx: makeMx({ paginateEventTimeline }),
isAtBottom: true,
isAtBottomRef: { current: true },
scrollToBottom: vi.fn<() => void>(),
Expand Down Expand Up @@ -786,7 +795,7 @@ const renderSyncHook = (
const { result } = renderHook(() =>
useTimelineSync({
room: room as Room,
mx: (options.mx ?? { getUserId: () => '@alice:test' }) as never,
mx: (options.mx ?? makeMx()) as never,
isAtBottom,
isAtBottomRef: { current: isAtBottom },
scrollToBottom,
Expand Down Expand Up @@ -947,7 +956,7 @@ describe('live-arrive edge cases', () => {
renderHook(() =>
useTimelineSync({
room: room as Room,
mx: { getUserId: () => '@alice:test' } as never,
mx: makeMx(),
isAtBottom: false,
isAtBottomRef: { current: false },
scrollToBottom,
Expand All @@ -971,6 +980,32 @@ describe('live-arrive edge cases', () => {
expect(scrollToBottom).not.toHaveBeenCalled();
});

it('re-renders when an event finishes decrypting', async () => {
const { room } = createRoom();
const { result } = renderSyncHook(room);
const before = result.current.timeline;

await act(async () => {
mxEmitter.emit(MatrixEventEvent.Decrypted, { getRoomId: () => room.roomId });
await Promise.resolve();
});

expect(result.current.timeline).not.toBe(before);
});

it('ignores decryption of an event in another room', async () => {
const { room } = createRoom();
const { result } = renderSyncHook(room);
const before = result.current.timeline;

await act(async () => {
mxEmitter.emit(MatrixEventEvent.Decrypted, { getRoomId: () => '!other:test' });
await Promise.resolve();
});

expect(result.current.timeline).toBe(before);
});

it('re-renders when a late local echo updates (slow send acknowledgement)', async () => {
const { room } = createRoom();
const { result } = renderSyncHook(room);
Expand Down Expand Up @@ -1016,12 +1051,11 @@ describe('event jump recovery', () => {
targetTimeline,
roomInitialSync,
getLatestTimeline,
mx: {
getUserId: () => '@alice:test',
mx: makeMx({
roomInitialSync,
getLatestTimeline,
getEventTimeline: vi.fn<() => Promise<unknown>>(() => Promise.resolve(targetTimeline)),
},
}),
};
};

Expand Down Expand Up @@ -1067,13 +1101,12 @@ describe('event jump recovery', () => {
vi.useFakeTimers();
try {
const { room } = createRoom();
const mx = {
getUserId: () => '@alice:test',
const mx = makeMx({
roomInitialSync: vi.fn<() => Promise<unknown>>(() => Promise.resolve(undefined)),
getLatestTimeline: vi.fn<() => Promise<unknown>>(() => Promise.resolve(undefined)),
// The homeserver never answers /context: hit the 12 s timeout.
getEventTimeline: () => new Promise(() => {}),
};
});
const { result, scrollToBottom } = renderSyncHook(room, { isAtBottom: false, mx });

await act(async () => {
Expand All @@ -1099,7 +1132,7 @@ describe('sliding sync chain relink', () => {
undefined;
const paginateEventTimeline = vi.fn<() => Promise<boolean>>(() => Promise.resolve(false));
const { result } = renderSyncHook(room, {
mx: { getUserId: () => '@alice:test', paginateEventTimeline },
mx: makeMx({ paginateEventTimeline }),
});

expect(result.current.eventsLength).toBe(1);
Expand Down Expand Up @@ -1150,7 +1183,7 @@ describe('sync transport fuzz', () => {
const { result, unmount } = renderHook(() =>
useTimelineSync({
room: room as Room,
mx: { getUserId: () => '@alice:test', paginateEventTimeline } as never,
mx: makeMx({ paginateEventTimeline }),
isAtBottom: false,
isAtBottomRef: { current: false },
scrollToBottom,
Expand Down
18 changes: 17 additions & 1 deletion src/app/hooks/timeline/useTimelineSync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@ import type {
IRoomTimelineData,
RoomEventHandlerMap,
} from '$types/matrix-sdk';
import { Direction, RoomEvent, RelationType, ThreadEvent } from '$types/matrix-sdk';
import {
Direction,
MatrixEventEvent,
RoomEvent,
RelationType,
ThreadEvent,
} from '$types/matrix-sdk';

import { useAlive } from '$hooks/useAlive';
import { useMatrixEvent } from '$hooks/useMatrixEvent';
Expand Down Expand Up @@ -574,6 +580,16 @@ export function useTimelineSync({

useMatrixEvent(room, RoomEvent.LocalEchoUpdated, handleLocalEchoUpdated);

const handleDecrypted = useCallback(
(mEvent: MatrixEvent) => {
if (mEvent.getRoomId() !== room.roomId) return;
setTimeline((ct) => ({ ...ct }));
},
[room, setTimeline]
);

useMatrixEvent(mx, MatrixEventEvent.Decrypted, handleDecrypted);

useLiveTimelineRefresh(
room,
useCallback(() => {
Expand Down
Loading