-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
New room list: add partial keyboard shortcuts support #29783
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
Merged
florianduros
merged 4 commits into
develop
from
florianduros/new-room-list/keyboard-navigation
Apr 22, 2025
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
c24ea5d
feat: add support to `Action.ViewRoomDelta`
florianduros 3a8369a
test: add tests for support of `Action.ViewRoomDelta`
florianduros cb52283
test(e2e): add tests for shortcuts
florianduros e648475
doc: improve comments in `useRoomListNavigation`
florianduros 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
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
56 changes: 56 additions & 0 deletions
56
src/components/viewmodels/roomlist/useRoomListNavigation.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,56 @@ | ||
| /* | ||
| * Copyright 2025 New Vector Ltd. | ||
| * | ||
| * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial | ||
| * Please see LICENSE files in the repository root for full details. | ||
| */ | ||
|
|
||
| import { type Room } from "matrix-js-sdk/src/matrix"; | ||
|
|
||
| import dispatcher from "../../../dispatcher/dispatcher"; | ||
| import { useDispatcher } from "../../../hooks/useDispatcher"; | ||
| import { Action } from "../../../dispatcher/actions"; | ||
| import { type ViewRoomDeltaPayload } from "../../../dispatcher/payloads/ViewRoomDeltaPayload"; | ||
| import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; | ||
| import { SdkContextClass } from "../../../contexts/SDKContext"; | ||
| import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; | ||
|
|
||
| /** | ||
| * Hook to navigate the room list using keyboard shortcuts. | ||
| * It listens to the ViewRoomDelta action and updates the room list accordingly. | ||
| * @param rooms | ||
| */ | ||
| export function useRoomListNavigation(rooms: Room[]): void { | ||
| useDispatcher(dispatcher, (payload) => { | ||
| if (payload.action !== Action.ViewRoomDelta) return; | ||
| const roomId = SdkContextClass.instance.roomViewStore.getRoomId(); | ||
| if (!roomId) return; | ||
|
|
||
| const { delta, unread } = payload as ViewRoomDeltaPayload; | ||
| const filteredRooms = unread | ||
| ? // Filter the rooms to only include unread ones and the active room | ||
| rooms.filter((room) => { | ||
| const state = RoomNotificationStateStore.instance.getRoomState(room); | ||
| return room.roomId === roomId || state.isUnread; | ||
| }) | ||
| : rooms; | ||
|
|
||
| const currentIndex = filteredRooms.findIndex((room) => room.roomId === roomId); | ||
| if (currentIndex === -1) return; | ||
|
|
||
| // Get the next/previous new room according to the delta | ||
| // Use slice to loop on the list | ||
| // If delta is -1 at the start of the list, it will go to the end | ||
| // If delta is 1 at the end of the list, it will go to the start | ||
| const [newRoom] = filteredRooms.slice((currentIndex + delta) % filteredRooms.length); | ||
| if (!newRoom) return; | ||
|
|
||
| dispatcher.dispatch<ViewRoomPayload>({ | ||
| action: Action.ViewRoom, | ||
| room_id: newRoom.roomId, | ||
| show_room_tile: true, // to make sure the room gets scrolled into view | ||
| metricsTrigger: "WebKeyboardShortcut", | ||
| metricsViaKeyboard: true, | ||
| }); | ||
| }); | ||
| } | ||
152 changes: 152 additions & 0 deletions
152
test/unit-tests/components/viewmodels/roomlist/useRoomListNavigation-test.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,152 @@ | ||
| /* | ||
| * Copyright 2025 New Vector Ltd. | ||
| * | ||
| * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial | ||
| * Please see LICENSE files in the repository root for full details. | ||
| */ | ||
|
|
||
| import { renderHook } from "jest-matrix-react"; | ||
| import { type Room } from "matrix-js-sdk/src/matrix"; | ||
| import { waitFor } from "@testing-library/dom"; | ||
|
|
||
| import { SdkContextClass } from "../../../../../src/contexts/SDKContext"; | ||
| import dispatcher from "../../../../../src/dispatcher/dispatcher"; | ||
| import { mkStubRoom, stubClient } from "../../../../test-utils"; | ||
| import { useRoomListNavigation } from "../../../../../src/components/viewmodels/roomlist/useRoomListNavigation"; | ||
| import { Action } from "../../../../../src/dispatcher/actions"; | ||
| import DMRoomMap from "../../../../../src/utils/DMRoomMap"; | ||
| import { RoomNotificationStateStore } from "../../../../../src/stores/notifications/RoomNotificationStateStore"; | ||
| import { type RoomNotificationState } from "../../../../../src/stores/notifications/RoomNotificationState"; | ||
|
|
||
| describe("useRoomListNavigation", () => { | ||
| let rooms: Room[]; | ||
|
|
||
| beforeEach(() => { | ||
| const matrixClient = stubClient(); | ||
| rooms = [ | ||
| mkStubRoom("room1", "Room 1", matrixClient), | ||
| mkStubRoom("room2", "Room 2", matrixClient), | ||
| mkStubRoom("room3", "Room 3", matrixClient), | ||
| ]; | ||
|
|
||
| DMRoomMap.makeShared(matrixClient); | ||
| jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(null); | ||
| jest.spyOn(dispatcher, "dispatch"); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| jest.clearAllMocks(); | ||
| }); | ||
|
|
||
| it("should navigate to the next room based on delta", async () => { | ||
| jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("room1"); | ||
|
|
||
| renderHook(() => useRoomListNavigation(rooms)); | ||
| dispatcher.dispatch({ | ||
| action: Action.ViewRoomDelta, | ||
| delta: 1, | ||
| unread: false, | ||
| }); | ||
|
|
||
| await waitFor(() => | ||
| expect(dispatcher.dispatch).toHaveBeenCalledWith({ | ||
| action: Action.ViewRoom, | ||
| room_id: "room2", | ||
| show_room_tile: true, | ||
| metricsTrigger: "WebKeyboardShortcut", | ||
| metricsViaKeyboard: true, | ||
| }), | ||
| ); | ||
| }); | ||
|
|
||
| it("should navigate to the previous room based on delta", async () => { | ||
| jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("room2"); | ||
|
|
||
| renderHook(() => useRoomListNavigation(rooms)); | ||
| dispatcher.dispatch({ | ||
| action: Action.ViewRoomDelta, | ||
| delta: -1, | ||
| unread: false, | ||
| }); | ||
|
|
||
| await waitFor(() => | ||
| expect(dispatcher.dispatch).toHaveBeenCalledWith({ | ||
| action: Action.ViewRoom, | ||
| room_id: "room1", | ||
| show_room_tile: true, | ||
| metricsTrigger: "WebKeyboardShortcut", | ||
| metricsViaKeyboard: true, | ||
| }), | ||
| ); | ||
| }); | ||
|
|
||
| it("should wrap around to the first room when navigating past the last room", async () => { | ||
| jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("room3"); | ||
|
|
||
| renderHook(() => useRoomListNavigation(rooms)); | ||
| dispatcher.dispatch({ | ||
| action: Action.ViewRoomDelta, | ||
| delta: 1, | ||
| unread: false, | ||
| }); | ||
|
|
||
| await waitFor(() => | ||
| expect(dispatcher.dispatch).toHaveBeenCalledWith({ | ||
| action: Action.ViewRoom, | ||
| room_id: "room1", | ||
| show_room_tile: true, | ||
| metricsTrigger: "WebKeyboardShortcut", | ||
| metricsViaKeyboard: true, | ||
| }), | ||
| ); | ||
| }); | ||
|
|
||
| it("should wrap around to the last room when navigating before the first room", async () => { | ||
| jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("room1"); | ||
|
|
||
| renderHook(() => useRoomListNavigation(rooms)); | ||
| dispatcher.dispatch({ | ||
| action: Action.ViewRoomDelta, | ||
| delta: -1, | ||
| unread: false, | ||
| }); | ||
|
|
||
| await waitFor(() => | ||
| expect(dispatcher.dispatch).toHaveBeenCalledWith({ | ||
| action: Action.ViewRoom, | ||
| room_id: "room3", | ||
| show_room_tile: true, | ||
| metricsTrigger: "WebKeyboardShortcut", | ||
| metricsViaKeyboard: true, | ||
| }), | ||
| ); | ||
| }); | ||
|
|
||
| it("should filter rooms to only unread when unread=true", async () => { | ||
| jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("room1"); | ||
| jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockImplementation( | ||
| (room) => | ||
| ({ | ||
| isUnread: room.roomId !== "room1", | ||
| }) as RoomNotificationState, | ||
| ); | ||
|
|
||
| renderHook(() => useRoomListNavigation(rooms)); | ||
|
|
||
| dispatcher.dispatch({ | ||
| action: Action.ViewRoomDelta, | ||
| delta: 1, | ||
| unread: true, | ||
| }); | ||
|
|
||
| await waitFor(() => | ||
| expect(dispatcher.dispatch).toHaveBeenCalledWith({ | ||
| action: Action.ViewRoom, | ||
| room_id: "room2", | ||
| show_room_tile: true, | ||
| metricsTrigger: "WebKeyboardShortcut", | ||
| metricsViaKeyboard: true, | ||
| }), | ||
| ); | ||
| }); | ||
| }); |
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.