From d0c35dda756cdd2473eee828e0c304105b788b42 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 31 Dec 2024 13:51:21 +0200 Subject: [PATCH 01/15] web/roomlist: close room when switching space --- web/src/api/statestore/main.ts | 2 +- web/src/api/statestore/space.ts | 7 ++++++- web/src/ui/roomlist/RoomList.tsx | 8 +++++++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/web/src/api/statestore/main.ts b/web/src/api/statestore/main.ts index 50b07d1..84ad5ff 100644 --- a/web/src/api/statestore/main.ts +++ b/web/src/api/statestore/main.ts @@ -77,7 +77,7 @@ export class StateStore { readonly spaceEdges: Map = new Map() readonly spaceOrphans = new SpaceOrphansSpace(this) readonly directChatsSpace = new DirectChatSpace() - readonly unreadsSpace = new UnreadsSpace() + readonly unreadsSpace = new UnreadsSpace(this) readonly pseudoSpaces = [ this.spaceOrphans, this.directChatsSpace, diff --git a/web/src/api/statestore/space.ts b/web/src/api/statestore/space.ts index 8e7eb02..f54cc4c 100644 --- a/web/src/api/statestore/space.ts +++ b/web/src/api/statestore/space.ts @@ -79,8 +79,13 @@ export class DirectChatSpace extends Space { export class UnreadsSpace extends Space { id = "fi.mau.gomuks.unreads" + constructor(private parent: StateStore) { + super() + } + include(room: RoomListEntry): boolean { - return Boolean(room.unread_messages + return Boolean(room.room_id === this.parent.activeRoomID + || room.unread_messages || room.unread_notifications || room.unread_highlights || room.marked_unread) diff --git a/web/src/ui/roomlist/RoomList.tsx b/web/src/ui/roomlist/RoomList.tsx index 04aff8d..4353524 100644 --- a/web/src/ui/roomlist/RoomList.tsx +++ b/web/src/ui/roomlist/RoomList.tsx @@ -49,7 +49,13 @@ const RoomList = ({ activeRoomID }: RoomListProps) => { const setSpace = useCallback((space: RoomListFilter | null) => { directSetSpace(space) client.store.currentRoomListFilter = space - }, [client]) + if (client.store.activeRoomID && space) { + const entry = client.store.rooms.get(client.store.activeRoomID)?.roomListEntry + if (entry && !space.include(entry)) { + mainScreen.setActiveRoom(null) + } + } + }, [client, mainScreen]) const onClickSpace = useCallback((evt: React.MouseEvent) => { const store = client.store.getSpaceStore(evt.currentTarget.getAttribute("data-target-space")!) setSpace(store) From 43f25727e64c68c82f11b8d28cabbf8d92279d27 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 31 Dec 2024 13:51:59 +0200 Subject: [PATCH 02/15] web/roomlist: add title for pseudo-spaces --- web/src/ui/roomlist/FakeSpace.tsx | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/web/src/ui/roomlist/FakeSpace.tsx b/web/src/ui/roomlist/FakeSpace.tsx index 00ac3a7..59120e8 100644 --- a/web/src/ui/roomlist/FakeSpace.tsx +++ b/web/src/ui/roomlist/FakeSpace.tsx @@ -30,27 +30,28 @@ export interface FakeSpaceProps { onClickUnread?: (evt: React.MouseEvent | null, space: Space | null) => void } -const getFakeSpaceIcon = (space: RoomListFilter | null): JSX.Element | null => { +const getFakeSpaceMeta = (space: RoomListFilter | null): [string | undefined, JSX.Element | null] => { switch (space?.id) { case undefined: - return + return ["Home", ] case "fi.mau.gomuks.direct_chats": - return + return ["Direct chats", ] case "fi.mau.gomuks.unreads": - return + return ["Unread chats", ] case "fi.mau.gomuks.space_orphans": - return + return ["Rooms outside spaces", ] default: - return null + return [undefined, null] } } const FakeSpace = ({ space, setSpace, isActive, onClickUnread }: FakeSpaceProps) => { const unreads = useEventAsState(space?.counts) const onClickUnreadWrapped = onClickUnread ? () => onClickUnread(null, space) : undefined - return
setSpace(space)}> + const [title, icon] = getFakeSpaceMeta(space) + return
setSpace(space)} title={title}> - {getFakeSpaceIcon(space)} + {icon}
} From 59e1b760d6f69d928a41db715c211bf19ec6691d Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 1 Jan 2025 01:22:33 +0200 Subject: [PATCH 03/15] web/statestore: fix clearing unread count after accepting invite --- web/src/api/statestore/main.ts | 15 ++++++++++----- web/src/api/statestore/room.ts | 3 +-- web/src/ui/roomlist/RoomList.tsx | 2 +- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/web/src/api/statestore/main.ts b/web/src/api/statestore/main.ts index 84ad5ff..3274b5e 100644 --- a/web/src/api/statestore/main.ts +++ b/web/src/api/statestore/main.ts @@ -73,6 +73,7 @@ export class StateStore { readonly rooms: Map = new Map() readonly inviteRooms: Map = new Map() readonly roomList = new NonNullCachedEventDispatcher([]) + readonly roomListEntries = new Map() readonly topLevelSpaces = new NonNullCachedEventDispatcher([]) readonly spaceEdges: Map = new Map() readonly spaceOrphans = new SpaceOrphansSpace(this) @@ -212,11 +213,11 @@ export class StateStore { const changedRoomListEntries = new Map() for (const data of sync.invited_rooms ?? []) { const room = new InvitedRoomStore(data, this) - const oldEntry = this.inviteRooms.get(room.room_id) this.inviteRooms.set(room.room_id, room) if (!resyncRoomList) { changedRoomListEntries.set(room.room_id, room) - this.#applyUnreadModification(room, oldEntry) + this.#applyUnreadModification(room, this.roomListEntries.get(room.room_id)) + this.roomListEntries.set(room.room_id, room) } if (this.activeRoomID === room.room_id) { this.switchRoom?.(room.room_id) @@ -239,8 +240,12 @@ export class StateStore { if (roomListEntryChanged) { const entry = this.#makeRoomListEntry(data, room) changedRoomListEntries.set(roomID, entry) - this.#applyUnreadModification(entry, room.roomListEntry) - room.roomListEntry = entry + this.#applyUnreadModification(entry, this.roomListEntries.get(roomID)) + if (entry) { + this.roomListEntries.set(roomID, entry) + } else { + this.roomListEntries.delete(roomID) + } } if (!resyncRoomList) { // When we join a valid replacement room, hide the tombstoned room. @@ -289,7 +294,7 @@ export class StateStore { updatedRoomList.sort((r1, r2) => r1.sorting_timestamp - r2.sorting_timestamp) for (const entry of updatedRoomList) { this.#applyUnreadModification(entry, undefined) - this.rooms.get(entry.room_id)!.roomListEntry = entry + this.roomListEntries.set(entry.room_id, entry) } } else if (changedRoomListEntries.size > 0) { updatedRoomList = this.roomList.current.filter(entry => !changedRoomListEntries.has(entry.room_id)) diff --git a/web/src/api/statestore/room.ts b/web/src/api/statestore/room.ts index 73903a8..7419a55 100644 --- a/web/src/api/statestore/room.ts +++ b/web/src/api/statestore/room.ts @@ -42,7 +42,7 @@ import { UserID, roomStateGUIDToString, } from "../types" -import type { RoomListEntry, StateStore } from "./main.ts" +import type { StateStore } from "./main.ts" function arraysAreEqual(arr1?: T[], arr2?: T[]): boolean { if (!arr1 || !arr2) { @@ -126,7 +126,6 @@ export class RoomStateStore { readUpToRow = -1 hasMoreHistory = true hidden = false - roomListEntry: RoomListEntry | undefined | null constructor(meta: DBRoom, private parent: StateStore) { this.roomID = meta.room_id diff --git a/web/src/ui/roomlist/RoomList.tsx b/web/src/ui/roomlist/RoomList.tsx index 4353524..2cbd9be 100644 --- a/web/src/ui/roomlist/RoomList.tsx +++ b/web/src/ui/roomlist/RoomList.tsx @@ -50,7 +50,7 @@ const RoomList = ({ activeRoomID }: RoomListProps) => { directSetSpace(space) client.store.currentRoomListFilter = space if (client.store.activeRoomID && space) { - const entry = client.store.rooms.get(client.store.activeRoomID)?.roomListEntry + const entry = client.store.roomListEntries.get(client.store.activeRoomID) if (entry && !space.include(entry)) { mainScreen.setActiveRoom(null) } From 8b7d0fe6b6dc9ec92cc7ac1c39411e773a2d3d97 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 1 Jan 2025 12:21:10 +0200 Subject: [PATCH 04/15] web/roomlist: restore open space when using browser history --- web/src/api/statestore/index.ts | 1 + web/src/api/statestore/main.ts | 17 ++++++++++++++ web/src/ui/MainScreen.tsx | 35 ++++++++++++++++++++++++----- web/src/ui/MainScreenContext.ts | 5 +++++ web/src/ui/roomlist/RoomList.tsx | 24 ++++++-------------- web/src/ui/roomlist/UnreadCount.tsx | 2 +- 6 files changed, 61 insertions(+), 23 deletions(-) diff --git a/web/src/api/statestore/index.ts b/web/src/api/statestore/index.ts index 3bbe512..106a3f4 100644 --- a/web/src/api/statestore/index.ts +++ b/web/src/api/statestore/index.ts @@ -1,3 +1,4 @@ export * from "./main.ts" export * from "./room.ts" export * from "./hooks.ts" +export * from "./space.ts" diff --git a/web/src/api/statestore/main.ts b/web/src/api/statestore/main.ts index 3274b5e..78dadf0 100644 --- a/web/src/api/statestore/main.ts +++ b/web/src/api/statestore/main.ts @@ -111,6 +111,23 @@ export class StateStore { return true } + getSpaceByID(spaceID: string | undefined): RoomListFilter | null { + if (!spaceID) { + return null + } + const realSpace = this.spaceEdges.get(spaceID) + if (realSpace) { + return realSpace + } + for (const pseudoSpace of this.pseudoSpaces) { + if (pseudoSpace.id === spaceID) { + return pseudoSpace + } + } + console.warn("Failed to find space", spaceID) + return null + } + get roomListFilterFunc(): ((entry: RoomListEntry) => boolean) | null { if (!this.currentRoomListFilter && !this.currentRoomListQuery) { return null diff --git a/web/src/ui/MainScreen.tsx b/web/src/ui/MainScreen.tsx index 05f8b59..c735548 100644 --- a/web/src/ui/MainScreen.tsx +++ b/web/src/ui/MainScreen.tsx @@ -16,7 +16,7 @@ import { JSX, use, useEffect, useMemo, useReducer, useRef, useState } from "react" import { SyncLoader } from "react-spinners" import Client from "@/api/client.ts" -import { RoomStateStore } from "@/api/statestore" +import { RoomListFilter, RoomStateStore } from "@/api/statestore" import type { RoomID } from "@/api/types" import { useEventAsState } from "@/util/eventdispatcher.ts" import { ensureString, ensureStringArray, parseMatrixURI } from "@/util/validation.ts" @@ -52,6 +52,7 @@ class ContextFields implements MainScreenContextFields { constructor( private directSetRightPanel: (props: RightPanelProps | null) => void, private directSetActiveRoom: (room: RoomStateStore | RoomPreviewProps | null) => void, + private directSetSpace: (space: RoomListFilter | null) => void, private client: Client, ) { this.keybindings = new Keybindings(client.store, this) @@ -109,6 +110,24 @@ class ContextFields implements MainScreenContextFields { } } + setSpace = (space: RoomListFilter | null, pushState = true) => { + if (space === this.client.store.currentRoomListFilter) { + return + } + console.log("Switching to space", space?.id) + this.directSetSpace(space) + this.client.store.currentRoomListFilter = space + if (pushState) { + if (this.client.store.activeRoomID && space) { + const entry = this.client.store.roomListEntries.get(this.client.store.activeRoomID) + if (entry && !space.include(entry)) { + this.setActiveRoom(null) + } + } + history.replaceState({ ...(history.state || {}), space_id: space?.id }, "") + } + } + #setPreviewRoom(roomID: RoomID, pushState: boolean, meta?: Partial) { const invite = this.client.store.inviteRooms.get(roomID) this.#closeActiveRoom(false) @@ -120,6 +139,7 @@ class ContextFields implements MainScreenContextFields { room_id: roomID, source_via: meta?.via, source_alias: meta?.alias, + space_id: history.state.space_id, }, "") } } @@ -148,7 +168,7 @@ class ContextFields implements MainScreenContextFields { .querySelector(`div.room-entry[data-room-id="${CSS.escape(room.roomID)}"]`) ?.scrollIntoView({ block: "nearest" }) if (pushState) { - history.pushState({ room_id: room.roomID }, "") + history.pushState({ room_id: room.roomID, space_id: history.state.space_id }, "") } let roomNameForTitle = room.meta.current.name if (roomNameForTitle && roomNameForTitle.length > 48) { @@ -166,7 +186,7 @@ class ContextFields implements MainScreenContextFields { this.client.store.activeRoomIsPreview = false this.keybindings.activeRoom = null if (pushState) { - history.pushState({}, "") + history.pushState({ space_id: history.state.space_id }, "") } document.title = this.#getWindowTitle() } @@ -279,12 +299,13 @@ const activeRoomReducer = ( const MainScreen = () => { const [[prevActiveRoom, activeRoom], directSetActiveRoom] = useReducer(activeRoomReducer, [null, null]) + const [space, directSetSpace] = useState(null) const skipNextTransitionRef = useRef(false) const [rightPanel, directSetRightPanel] = useState(null) const client = use(ClientContext)! const syncStatus = useEventAsState(client.syncStatus) const context = useMemo( - () => new ContextFields(directSetRightPanel, directSetActiveRoom, client), + () => new ContextFields(directSetRightPanel, directSetActiveRoom, directSetSpace, client), [client], ) useEffect(() => { @@ -292,6 +313,10 @@ const MainScreen = () => { const listener = (evt: PopStateEvent) => { skipNextTransitionRef.current = evt.hasUAVisualTransition const roomID = evt.state?.room_id ?? null + const spaceID = evt.state?.space_id ?? undefined + if (spaceID !== client.store.currentRoomListFilter?.id) { + context.setSpace(client.store.getSpaceByID(spaceID), false) + } if (roomID !== client.store.activeRoomID) { context.setActiveRoom(roomID, { alias: ensureString(evt.state?.source_alias) || undefined, @@ -372,7 +397,7 @@ const MainScreen = () => {
- + {resizeHandle1} {renderedRoom ? renderedRoom instanceof RoomStateStore diff --git a/web/src/ui/MainScreenContext.ts b/web/src/ui/MainScreenContext.ts index 67c0b0b..28aa97d 100644 --- a/web/src/ui/MainScreenContext.ts +++ b/web/src/ui/MainScreenContext.ts @@ -14,12 +14,14 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import { createContext } from "react" +import { RoomListFilter } from "@/api/statestore" import type { RoomID } from "@/api/types" import type { RightPanelProps } from "./rightpanel/RightPanel.tsx" import type { RoomPreviewProps } from "./roomview/RoomPreview.tsx" export interface MainScreenContextFields { setActiveRoom: (roomID: RoomID | null, previewMeta?: Partial) => void + setSpace: (space: RoomListFilter | null, pushState?: boolean) => void clickRoom: (evt: React.MouseEvent) => void clearActiveRoom: () => void @@ -32,6 +34,9 @@ const stubContext = { get setActiveRoom(): never { throw new Error("MainScreenContext used outside main screen") }, + get setSpace(): never { + throw new Error("MainScreenContext used outside main screen") + }, get clickRoom(): never { throw new Error("MainScreenContext used outside main screen") }, diff --git a/web/src/ui/roomlist/RoomList.tsx b/web/src/ui/roomlist/RoomList.tsx index 2cbd9be..b4c0b78 100644 --- a/web/src/ui/roomlist/RoomList.tsx +++ b/web/src/ui/roomlist/RoomList.tsx @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import React, { use, useCallback, useRef, useState } from "react" -import { RoomListFilter, Space as SpaceStore, SpaceUnreadCounts } from "@/api/statestore/space.ts" +import { RoomListFilter, Space as SpaceStore, SpaceUnreadCounts } from "@/api/statestore" import type { RoomID } from "@/api/types" import { useEventAsState } from "@/util/eventdispatcher.ts" import reverseMap from "@/util/reversemap.ts" @@ -31,35 +31,25 @@ import "./RoomList.css" interface RoomListProps { activeRoomID: RoomID | null + space: RoomListFilter | null } -const RoomList = ({ activeRoomID }: RoomListProps) => { +const RoomList = ({ activeRoomID, space }: RoomListProps) => { const client = use(ClientContext)! const mainScreen = use(MainScreenContext) const roomList = useEventAsState(client.store.roomList) const spaces = useEventAsState(client.store.topLevelSpaces) const searchInputRef = useRef(null) const [query, directSetQuery] = useState("") - const [space, directSetSpace] = useState(null) const setQuery = (evt: React.ChangeEvent) => { client.store.currentRoomListQuery = toSearchableString(evt.target.value) directSetQuery(evt.target.value) } - const setSpace = useCallback((space: RoomListFilter | null) => { - directSetSpace(space) - client.store.currentRoomListFilter = space - if (client.store.activeRoomID && space) { - const entry = client.store.roomListEntries.get(client.store.activeRoomID) - if (entry && !space.include(entry)) { - mainScreen.setActiveRoom(null) - } - } - }, [client, mainScreen]) const onClickSpace = useCallback((evt: React.MouseEvent) => { const store = client.store.getSpaceStore(evt.currentTarget.getAttribute("data-target-space")!) - setSpace(store) - }, [setSpace, client]) + mainScreen.setSpace(store) + }, [mainScreen, client]) const onClickSpaceUnread = useCallback(( evt: React.MouseEvent | null, space?: SpaceStore | null, ) => { @@ -130,11 +120,11 @@ const RoomList = ({ activeRoomID }: RoomListProps) => {
- + {client.store.pseudoSpaces.map(pseudoSpace => )} diff --git a/web/src/ui/roomlist/UnreadCount.tsx b/web/src/ui/roomlist/UnreadCount.tsx index 82807c0..b1fb141 100644 --- a/web/src/ui/roomlist/UnreadCount.tsx +++ b/web/src/ui/roomlist/UnreadCount.tsx @@ -13,7 +13,7 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import { SpaceUnreadCounts } from "@/api/statestore/space.ts" +import { SpaceUnreadCounts } from "@/api/statestore" interface UnreadCounts extends SpaceUnreadCounts { marked_unread?: boolean From 6d1c5f6277395e5fa2c463b5e200ca99e4439820 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 1 Jan 2025 13:11:31 +0200 Subject: [PATCH 05/15] web/roomlist: use margin instead of padding for room avatars This allows adding a background for avatars using ```css .avatar { background-color: var(--background-color); } ``` --- web/src/ui/roomlist/RoomList.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/ui/roomlist/RoomList.css b/web/src/ui/roomlist/RoomList.css index f7f8e33..6241a65 100644 --- a/web/src/ui/roomlist/RoomList.css +++ b/web/src/ui/roomlist/RoomList.css @@ -119,7 +119,7 @@ div.room-entry { width: 3rem; > img.room-avatar { - padding: 4px; + margin: .25rem; } } From ddf20b34d26bbb17aecd611561f78ba3505ac44f Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 1 Jan 2025 15:44:44 +0200 Subject: [PATCH 06/15] web/mainscreen: fix pushing history states when outside a space --- web/src/ui/MainScreen.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/src/ui/MainScreen.tsx b/web/src/ui/MainScreen.tsx index c735548..3bb15e6 100644 --- a/web/src/ui/MainScreen.tsx +++ b/web/src/ui/MainScreen.tsx @@ -139,7 +139,7 @@ class ContextFields implements MainScreenContextFields { room_id: roomID, source_via: meta?.via, source_alias: meta?.alias, - space_id: history.state.space_id, + space_id: history.state?.space_id, }, "") } } @@ -168,7 +168,7 @@ class ContextFields implements MainScreenContextFields { .querySelector(`div.room-entry[data-room-id="${CSS.escape(room.roomID)}"]`) ?.scrollIntoView({ block: "nearest" }) if (pushState) { - history.pushState({ room_id: room.roomID, space_id: history.state.space_id }, "") + history.pushState({ room_id: room.roomID, space_id: history.state?.space_id }, "") } let roomNameForTitle = room.meta.current.name if (roomNameForTitle && roomNameForTitle.length > 48) { @@ -186,7 +186,7 @@ class ContextFields implements MainScreenContextFields { this.client.store.activeRoomIsPreview = false this.keybindings.activeRoom = null if (pushState) { - history.pushState({ space_id: history.state.space_id }, "") + history.pushState({ space_id: history.state?.space_id }, "") } document.title = this.#getWindowTitle() } From 8c9925959afc261ba2ff9890003640e2bae75583 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 1 Jan 2025 15:47:08 +0200 Subject: [PATCH 07/15] web/roomlist: don't allow selecting unread counter text --- web/src/ui/roomlist/RoomList.css | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/ui/roomlist/RoomList.css b/web/src/ui/roomlist/RoomList.css index 6241a65..0d084d2 100644 --- a/web/src/ui/roomlist/RoomList.css +++ b/web/src/ui/roomlist/RoomList.css @@ -172,6 +172,7 @@ div.room-entry-unreads { justify-content: center; border-radius: var(--unread-count-size); color: var(--unread-counter-text-color); + user-select: none; background-color: var(--unread-counter-message-bg); height: var(--unread-count-size); From 7f94bbf39ea5cec471b4a36228e27d70372bb508 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 1 Jan 2025 15:51:15 +0200 Subject: [PATCH 08/15] web/timeline: add background for read receipt avatars --- web/src/ui/timeline/ReadReceipts.css | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/ui/timeline/ReadReceipts.css b/web/src/ui/timeline/ReadReceipts.css index 7e8579d..df23657 100644 --- a/web/src/ui/timeline/ReadReceipts.css +++ b/web/src/ui/timeline/ReadReceipts.css @@ -20,6 +20,7 @@ div.timeline-event > div.read-receipts { > img { margin-left: -.35rem; border: 1px solid var(--background-color); + background-color: var(--background-color); } } From c3899d0b50dddeda9bbb18a642ae5e4abcca40b5 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 1 Jan 2025 18:07:57 +0200 Subject: [PATCH 09/15] web/settings: add button to log into css.gomuks.app --- desktop/go.mod | 2 +- desktop/go.sum | 4 ++-- go.mod | 2 +- go.sum | 4 ++-- pkg/hicli/json-commands.go | 2 ++ web/src/api/rpc.ts | 5 +++++ web/src/api/types/mxtypes.ts | 7 +++++++ web/src/ui/settings/SettingsView.tsx | 11 +++++++++++ 8 files changed, 31 insertions(+), 6 deletions(-) diff --git a/desktop/go.mod b/desktop/go.mod index 70137e5..8291c8f 100644 --- a/desktop/go.mod +++ b/desktop/go.mod @@ -75,7 +75,7 @@ require ( gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - maunium.net/go/mautrix v0.22.2-0.20241222121030-33b4e823c5e5 // indirect + maunium.net/go/mautrix v0.22.2-0.20250101160025-077716a4ec7c // indirect mvdan.cc/xurls/v2 v2.5.0 // indirect ) diff --git a/desktop/go.sum b/desktop/go.sum index ef86ad9..c04dd75 100644 --- a/desktop/go.sum +++ b/desktop/go.sum @@ -250,7 +250,7 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -maunium.net/go/mautrix v0.22.2-0.20241222121030-33b4e823c5e5 h1:3aZCApUF3wlboN1BcAX6u6MGhju4OpsmhyfvxdO6tO0= -maunium.net/go/mautrix v0.22.2-0.20241222121030-33b4e823c5e5/go.mod h1:1rhqwH34Rz54ZqzdQYkmNW6rQUymNeTdaLA4l9LK6AI= +maunium.net/go/mautrix v0.22.2-0.20250101160025-077716a4ec7c h1:rVm1DvtGXvjy46pKmO6HYPDLMT6Wd83D1jtUhbnuPoM= +maunium.net/go/mautrix v0.22.2-0.20250101160025-077716a4ec7c/go.mod h1:1rhqwH34Rz54ZqzdQYkmNW6rQUymNeTdaLA4l9LK6AI= mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8= mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE= diff --git a/go.mod b/go.mod index bc8de98..a7347ed 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( golang.org/x/text v0.21.0 gopkg.in/yaml.v3 v3.0.1 maunium.net/go/mauflag v1.0.0 - maunium.net/go/mautrix v0.22.2-0.20241222121030-33b4e823c5e5 + maunium.net/go/mautrix v0.22.2-0.20250101160025-077716a4ec7c mvdan.cc/xurls/v2 v2.5.0 ) diff --git a/go.sum b/go.sum index 1897c4d..51e82a9 100644 --- a/go.sum +++ b/go.sum @@ -91,7 +91,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= -maunium.net/go/mautrix v0.22.2-0.20241222121030-33b4e823c5e5 h1:3aZCApUF3wlboN1BcAX6u6MGhju4OpsmhyfvxdO6tO0= -maunium.net/go/mautrix v0.22.2-0.20241222121030-33b4e823c5e5/go.mod h1:1rhqwH34Rz54ZqzdQYkmNW6rQUymNeTdaLA4l9LK6AI= +maunium.net/go/mautrix v0.22.2-0.20250101160025-077716a4ec7c h1:rVm1DvtGXvjy46pKmO6HYPDLMT6Wd83D1jtUhbnuPoM= +maunium.net/go/mautrix v0.22.2-0.20250101160025-077716a4ec7c/go.mod h1:1rhqwH34Rz54ZqzdQYkmNW6rQUymNeTdaLA4l9LK6AI= mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8= mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE= diff --git a/pkg/hicli/json-commands.go b/pkg/hicli/json-commands.go index 0a07c25..26b8e89 100644 --- a/pkg/hicli/json-commands.go +++ b/pkg/hicli/json-commands.go @@ -153,6 +153,8 @@ func (h *HiClient) handleJSONCommand(ctx context.Context, req *JSONCommand) (any return unmarshalAndCall(req.Data, func(params *resolveAliasParams) (*mautrix.RespAliasResolve, error) { return h.Client.ResolveAlias(ctx, params.Alias) }) + case "request_openid_token": + return h.Client.RequestOpenIDToken(ctx) case "logout": if h.LogoutFunc == nil { return nil, errors.New("logout not supported") diff --git a/web/src/api/rpc.ts b/web/src/api/rpc.ts index b967010..a52a3c3 100644 --- a/web/src/api/rpc.ts +++ b/web/src/api/rpc.ts @@ -32,6 +32,7 @@ import type { ReceiptType, RelatesTo, ResolveAliasResponse, + RespOpenIDToken, RespRoomJoin, RoomAlias, RoomID, @@ -257,4 +258,8 @@ export default abstract class RPCClient { verify(recovery_key: string): Promise { return this.request("verify", { recovery_key }) } + + requestOpenIDToken(): Promise { + return this.request("request_openid_token", {}) + } } diff --git a/web/src/api/types/mxtypes.ts b/web/src/api/types/mxtypes.ts index 5932e3e..81261ea 100644 --- a/web/src/api/types/mxtypes.ts +++ b/web/src/api/types/mxtypes.ts @@ -279,3 +279,10 @@ export interface RoomSummary { export interface RespRoomJoin { room_id: RoomID } + +export interface RespOpenIDToken { + access_token: string + expires_in: number + matrix_server_name: string + token_type: "Bearer" +} diff --git a/web/src/ui/settings/SettingsView.tsx b/web/src/ui/settings/SettingsView.tsx index 6f9aefc..02c5ff3 100644 --- a/web/src/ui/settings/SettingsView.tsx +++ b/web/src/ui/settings/SettingsView.tsx @@ -332,6 +332,16 @@ const SettingsView = ({ room }: SettingsViewProps) => { ) } } + const onClickOpenCSSApp = () => { + client.rpc.requestOpenIDToken().then( + resp => window.open( + `https://css.gomuks.app/login?token=${resp.access_token}&server_name=${resp.matrix_server_name}`, + "_blank", + "noreferrer noopener", + ), + err => window.alert(`Failed to request OpenID token: ${err}`), + ) + } usePreferences(client.store, room) const globalServer = client.store.serverPreferenceCache const globalLocal = client.store.localPreferenceCache @@ -381,6 +391,7 @@ const SettingsView = ({ room }: SettingsViewProps) => {
+ {window.Notification && } From 021236592f3ac59d3814df1a38dc2429875851fd Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 2 Jan 2025 11:07:04 +0200 Subject: [PATCH 10/15] web/statestore: clear unread counts when clearing state --- web/src/api/statestore/main.ts | 1 + web/src/api/statestore/space.ts | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/web/src/api/statestore/main.ts b/web/src/api/statestore/main.ts index 78dadf0..ec1ecb5 100644 --- a/web/src/api/statestore/main.ts +++ b/web/src/api/statestore/main.ts @@ -537,6 +537,7 @@ export class StateStore { this.rooms.clear() this.inviteRooms.clear() this.spaceEdges.clear() + this.pseudoSpaces.forEach(space => space.clearUnreads()) this.roomList.emit([]) this.topLevelSpaces.emit([]) this.accountData.clear() diff --git a/web/src/api/statestore/space.ts b/web/src/api/statestore/space.ts index f54cc4c..96b37b8 100644 --- a/web/src/api/statestore/space.ts +++ b/web/src/api/statestore/space.ts @@ -40,6 +40,10 @@ export abstract class Space implements RoomListFilter { abstract id: string abstract include(room: RoomListEntry): boolean + clearUnreads() { + this.counts.emit(emptyUnreadCounts) + } + applyUnreads(newCounts?: SpaceUnreadCounts | null, oldCounts?: SpaceUnreadCounts | null) { const mergedCounts: SpaceUnreadCounts = { unread_messages: this.counts.current.unread_messages From 3c3e2456e26c4157d8f41547eae8eed0f06dec74 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 2 Jan 2025 11:16:26 +0200 Subject: [PATCH 11/15] web/roomlist: switch space in unread click handler On desktop it worked without this via event propagation, but that doesn't work on mobile (possibly because the room view opens immediately and hides the space bar?) --- web/src/ui/roomlist/FakeSpace.tsx | 6 ++++-- web/src/ui/roomlist/RoomList.tsx | 12 +++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/web/src/ui/roomlist/FakeSpace.tsx b/web/src/ui/roomlist/FakeSpace.tsx index 59120e8..bb46319 100644 --- a/web/src/ui/roomlist/FakeSpace.tsx +++ b/web/src/ui/roomlist/FakeSpace.tsx @@ -27,7 +27,7 @@ export interface FakeSpaceProps { space: Space | null setSpace: (space: RoomListFilter | null) => void isActive: boolean - onClickUnread?: (evt: React.MouseEvent | null, space: Space | null) => void + onClickUnread?: (evt: React.MouseEvent, space: Space | null) => void } const getFakeSpaceMeta = (space: RoomListFilter | null): [string | undefined, JSX.Element | null] => { @@ -47,7 +47,9 @@ const getFakeSpaceMeta = (space: RoomListFilter | null): [string | undefined, JS const FakeSpace = ({ space, setSpace, isActive, onClickUnread }: FakeSpaceProps) => { const unreads = useEventAsState(space?.counts) - const onClickUnreadWrapped = onClickUnread ? () => onClickUnread(null, space) : undefined + const onClickUnreadWrapped = onClickUnread + ? (evt: React.MouseEvent) => onClickUnread(evt, space) + : undefined const [title, icon] = getFakeSpaceMeta(space) return
setSpace(space)} title={title}> diff --git a/web/src/ui/roomlist/RoomList.tsx b/web/src/ui/roomlist/RoomList.tsx index b4c0b78..39d4cb5 100644 --- a/web/src/ui/roomlist/RoomList.tsx +++ b/web/src/ui/roomlist/RoomList.tsx @@ -51,17 +51,17 @@ const RoomList = ({ activeRoomID, space }: RoomListProps) => { mainScreen.setSpace(store) }, [mainScreen, client]) const onClickSpaceUnread = useCallback(( - evt: React.MouseEvent | null, space?: SpaceStore | null, + evt: React.MouseEvent, space?: SpaceStore | null, ) => { - if (evt) { + if (!space) { const targetSpace = evt.currentTarget.closest("div.space-entry")?.getAttribute("data-target-space") if (!targetSpace) { return } space = client.store.getSpaceStore(targetSpace) - } - if (!space) { - return + if (!space) { + return + } } const counts = space.counts.current let wantedField: keyof SpaceUnreadCounts @@ -78,6 +78,8 @@ const RoomList = ({ activeRoomID, space }: RoomListProps) => { const entry = client.store.roomList.current[i] if (entry[wantedField] > 0 && space.include(entry)) { mainScreen.setActiveRoom(entry.room_id) + mainScreen.setSpace(space) + evt.stopPropagation() break } } From d8f0a82ffc79b9c1706da5c68f33526ebe8d4706 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 2 Jan 2025 11:17:22 +0200 Subject: [PATCH 12/15] web/roomlist: fix unread counter overflow condition --- web/src/ui/roomlist/UnreadCount.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/ui/roomlist/UnreadCount.tsx b/web/src/ui/roomlist/UnreadCount.tsx index b1fb141..4ad48f5 100644 --- a/web/src/ui/roomlist/UnreadCount.tsx +++ b/web/src/ui/roomlist/UnreadCount.tsx @@ -38,9 +38,9 @@ const UnreadCount = ({ counts, space, onClick }: UnreadCountProps) => { const countIsBig = !space && Boolean(counts.unread_notifications || counts.unread_highlights || counts.marked_unread) let unreadCountDisplay = unreadCount.toString() - if (unreadCount > 999 && countIsBig) { + if (unreadCount > 999 && (countIsBig || space)) { unreadCountDisplay = "99+" - } else if (unreadCount > 9999 && countIsBig) { + } else if (unreadCount > 9999) { unreadCountDisplay = "999+" } const classNames = ["unread-count"] From ac6f2713e55e5d72df4547438ec1274e86eeeed5 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 2 Jan 2025 11:34:14 +0200 Subject: [PATCH 13/15] web/eslint: make line max length an error --- web/eslint.config.js | 2 +- web/src/ui/modal/Modal.tsx | 2 +- web/src/ui/settings/SettingsView.tsx | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/web/eslint.config.js b/web/eslint.config.js index 7654e8d..41ba85f 100644 --- a/web/eslint.config.js +++ b/web/eslint.config.js @@ -74,7 +74,7 @@ export default tseslint.config( "quotes": ["error", "double", {allowTemplateLiterals: true}], "semi": ["error", "never"], "comma-dangle": ["error", "always-multiline"], - "max-len": ["warn", 120], + "max-len": ["error", 120], "space-before-function-paren": ["error", { "anonymous": "never", "named": "never", diff --git a/web/src/ui/modal/Modal.tsx b/web/src/ui/modal/Modal.tsx index 1fc3ab6..4d0baf9 100644 --- a/web/src/ui/modal/Modal.tsx +++ b/web/src/ui/modal/Modal.tsx @@ -60,7 +60,7 @@ const ModalWrapper = ({ children }: { children: React.ReactNode }) => { } window.addEventListener("popstate", listener) return () => window.removeEventListener("popstate", listener) - }, []) + }, [onClickWrapper]) let modal: JSX.Element | null = null if (state) { let content = {state.content} diff --git a/web/src/ui/settings/SettingsView.tsx b/web/src/ui/settings/SettingsView.tsx index 02c5ff3..daf5c44 100644 --- a/web/src/ui/settings/SettingsView.tsx +++ b/web/src/ui/settings/SettingsView.tsx @@ -237,7 +237,9 @@ const CustomCSSInput = ({ setPref, room }: { setPref: SetPrefFunc, room: RoomSta }
{vscodeOpen ?
-
}> +
+ }> Date: Thu, 2 Jan 2025 23:18:00 +0200 Subject: [PATCH 14/15] hicli/database: store DM user ID in database --- pkg/hicli/database/room.go | 35 ++++++++++++------- .../database/upgrades/00-latest-revision.sql | 3 +- pkg/hicli/database/upgrades/11-dm-user-id.sql | 19 ++++++++++ pkg/hicli/sync.go | 33 ++++++++++++----- web/src/api/media.ts | 12 ++----- web/src/api/statestore/main.ts | 3 +- web/src/api/statestore/room.ts | 1 + web/src/api/types/hitypes.ts | 1 + web/src/ui/rightpanel/UserInfoMutualRooms.tsx | 3 +- 9 files changed, 73 insertions(+), 37 deletions(-) create mode 100644 pkg/hicli/database/upgrades/11-dm-user-id.sql diff --git a/pkg/hicli/database/room.go b/pkg/hicli/database/room.go index a27de72..6e46001 100644 --- a/pkg/hicli/database/room.go +++ b/pkg/hicli/database/room.go @@ -21,7 +21,8 @@ import ( const ( getRoomBaseQuery = ` - SELECT room_id, creation_content, tombstone_content, name, name_quality, avatar, explicit_avatar, topic, canonical_alias, + SELECT room_id, creation_content, tombstone_content, name, name_quality, + avatar, explicit_avatar, dm_user_id, topic, canonical_alias, lazy_load_summary, encryption_event, has_member_list, preview_event_rowid, sorting_timestamp, unread_highlights, unread_notifications, unread_messages, marked_unread, prev_batch FROM room @@ -42,18 +43,19 @@ const ( name_quality = CASE WHEN $4 IS NOT NULL THEN $5 ELSE room.name_quality END, avatar = COALESCE($6, room.avatar), explicit_avatar = CASE WHEN $6 IS NOT NULL THEN $7 ELSE room.explicit_avatar END, - topic = COALESCE($8, room.topic), - canonical_alias = COALESCE($9, room.canonical_alias), - lazy_load_summary = COALESCE($10, room.lazy_load_summary), - encryption_event = COALESCE($11, room.encryption_event), - has_member_list = room.has_member_list OR $12, - preview_event_rowid = COALESCE($13, room.preview_event_rowid), - sorting_timestamp = COALESCE($14, room.sorting_timestamp), - unread_highlights = COALESCE($15, room.unread_highlights), - unread_notifications = COALESCE($16, room.unread_notifications), - unread_messages = COALESCE($17, room.unread_messages), - marked_unread = COALESCE($18, room.marked_unread), - prev_batch = COALESCE($19, room.prev_batch) + dm_user_id = COALESCE($8, room.dm_user_id), + topic = COALESCE($9, room.topic), + canonical_alias = COALESCE($10, room.canonical_alias), + lazy_load_summary = COALESCE($11, room.lazy_load_summary), + encryption_event = COALESCE($12, room.encryption_event), + has_member_list = room.has_member_list OR $13, + preview_event_rowid = COALESCE($14, room.preview_event_rowid), + sorting_timestamp = COALESCE($15, room.sorting_timestamp), + unread_highlights = COALESCE($16, room.unread_highlights), + unread_notifications = COALESCE($17, room.unread_notifications), + unread_messages = COALESCE($18, room.unread_messages), + marked_unread = COALESCE($19, room.marked_unread), + prev_batch = COALESCE($20, room.prev_batch) WHERE room_id = $1 ` setRoomPrevBatchQuery = ` @@ -153,6 +155,7 @@ type Room struct { NameQuality NameQuality `json:"name_quality"` Avatar *id.ContentURI `json:"avatar,omitempty"` ExplicitAvatar bool `json:"explicit_avatar"` + DMUserID *id.UserID `json:"dm_user_id,omitempty"` Topic *string `json:"topic,omitempty"` CanonicalAlias *id.RoomAlias `json:"canonical_alias,omitempty"` @@ -188,6 +191,10 @@ func (r *Room) CheckChangesAndCopyInto(other *Room) (hasChanges bool) { other.ExplicitAvatar = r.ExplicitAvatar hasChanges = true } + if r.DMUserID != nil { + other.DMUserID = r.DMUserID + hasChanges = true + } if r.Topic != nil { other.Topic = r.Topic hasChanges = true @@ -250,6 +257,7 @@ func (r *Room) Scan(row dbutil.Scannable) (*Room, error) { &r.NameQuality, &r.Avatar, &r.ExplicitAvatar, + &r.DMUserID, &r.Topic, &r.CanonicalAlias, dbutil.JSON{Data: &r.LazyLoadSummary}, @@ -281,6 +289,7 @@ func (r *Room) sqlVariables() []any { r.NameQuality, r.Avatar, r.ExplicitAvatar, + r.DMUserID, r.Topic, r.CanonicalAlias, dbutil.JSONPtr(r.LazyLoadSummary), diff --git a/pkg/hicli/database/upgrades/00-latest-revision.sql b/pkg/hicli/database/upgrades/00-latest-revision.sql index 2df49d2..97f98d4 100644 --- a/pkg/hicli/database/upgrades/00-latest-revision.sql +++ b/pkg/hicli/database/upgrades/00-latest-revision.sql @@ -1,4 +1,4 @@ --- v0 -> v10 (compatible with v10+): Latest revision +-- v0 -> v11 (compatible with v10+): Latest revision CREATE TABLE account ( user_id TEXT NOT NULL PRIMARY KEY, device_id TEXT NOT NULL, @@ -18,6 +18,7 @@ CREATE TABLE room ( name_quality INTEGER NOT NULL DEFAULT 0, avatar TEXT, explicit_avatar INTEGER NOT NULL DEFAULT 0, + dm_user_id TEXT, topic TEXT, canonical_alias TEXT, lazy_load_summary TEXT, diff --git a/pkg/hicli/database/upgrades/11-dm-user-id.sql b/pkg/hicli/database/upgrades/11-dm-user-id.sql new file mode 100644 index 0000000..3377f0c --- /dev/null +++ b/pkg/hicli/database/upgrades/11-dm-user-id.sql @@ -0,0 +1,19 @@ +-- v11 (compatible with v10+): Store direct chat user ID in database +ALTER TABLE room ADD COLUMN dm_user_id TEXT; +WITH dm_user_ids AS ( + SELECT room_id, value + FROM room + INNER JOIN json_each(lazy_load_summary, '$."m.heroes"') + WHERE value NOT IN (SELECT value FROM json_each(( + SELECT event.content + FROM current_state cs + INNER JOIN event ON cs.event_rowid = event.rowid + WHERE cs.room_id=room.room_id AND cs.event_type='io.element.functional_members' AND cs.state_key='' + ), '$.service_members')) + GROUP BY room_id + HAVING COUNT(*) = 1 +) +UPDATE room +SET dm_user_id=value +FROM dm_user_ids du +WHERE room.room_id=du.room_id; diff --git a/pkg/hicli/sync.go b/pkg/hicli/sync.go index 4813f1b..316da7e 100644 --- a/pkg/hicli/sync.go +++ b/pkg/hicli/sync.go @@ -894,10 +894,11 @@ func (h *HiClient) processStateAndTimeline( } // Calculate name from participants if participants changed and current name was generated from participants, or if the room name was unset if (heroesChanged && updatedRoom.NameQuality <= database.NameQualityParticipants) || updatedRoom.NameQuality == database.NameQualityNil { - name, dmAvatarURL, err := h.calculateRoomParticipantName(ctx, room.ID, summary) + name, dmAvatarURL, dmUserID, err := h.calculateRoomParticipantName(ctx, room.ID, summary) if err != nil { return fmt.Errorf("failed to calculate room name: %w", err) } + updatedRoom.DMUserID = &dmUserID updatedRoom.Name = &name updatedRoom.NameQuality = database.NameQualityParticipants if !dmAvatarURL.IsEmpty() && !room.ExplicitAvatar { @@ -966,15 +967,15 @@ func joinMemberNames(names []string, totalCount int) string { } } -func (h *HiClient) calculateRoomParticipantName(ctx context.Context, roomID id.RoomID, summary *mautrix.LazyLoadSummary) (string, id.ContentURI, error) { +func (h *HiClient) calculateRoomParticipantName(ctx context.Context, roomID id.RoomID, summary *mautrix.LazyLoadSummary) (string, id.ContentURI, id.UserID, error) { var primaryAvatarURL id.ContentURI if summary == nil || len(summary.Heroes) == 0 { - return "Empty room", primaryAvatarURL, nil + return "Empty room", primaryAvatarURL, "", nil } var functionalMembers []id.UserID functionalMembersEvt, err := h.DB.CurrentState.Get(ctx, roomID, event.StateElementFunctionalMembers, "") if err != nil { - return "", primaryAvatarURL, fmt.Errorf("failed to get %s event: %w", event.StateElementFunctionalMembers.Type, err) + return "", primaryAvatarURL, "", fmt.Errorf("failed to get %s event: %w", event.StateElementFunctionalMembers.Type, err) } else if functionalMembersEvt != nil { mautrixEvt := functionalMembersEvt.AsRawMautrix() _ = mautrixEvt.Content.ParseRaw(mautrixEvt.Type) @@ -990,16 +991,21 @@ func (h *HiClient) calculateRoomParticipantName(ctx context.Context, roomID id.R } else if summary.InvitedMemberCount != nil { memberCount = *summary.InvitedMemberCount } + var dmUserID id.UserID for _, hero := range summary.Heroes { if slices.Contains(functionalMembers, hero) { + // TODO save member count so push rule evaluation would use the subtracted one? memberCount-- continue } else if len(members) >= 5 { break } + if dmUserID == "" { + dmUserID = hero + } heroEvt, err := h.DB.CurrentState.Get(ctx, roomID, event.StateMember, hero.String()) if err != nil { - return "", primaryAvatarURL, fmt.Errorf("failed to get %s's member event: %w", hero, err) + return "", primaryAvatarURL, "", fmt.Errorf("failed to get %s's member event: %w", hero, err) } else if heroEvt == nil { leftMembers = append(leftMembers, hero.String()) continue @@ -1015,19 +1021,28 @@ func (h *HiClient) calculateRoomParticipantName(ctx context.Context, roomID id.R } if membership == "join" || membership == "invite" { members = append(members, name) + dmUserID = hero } else { leftMembers = append(leftMembers, name) } } - if len(members)+len(leftMembers) > 1 || !primaryAvatarURL.IsValid() { + if !primaryAvatarURL.IsValid() { primaryAvatarURL = id.ContentURI{} } if len(members) > 0 { - return joinMemberNames(members, memberCount), primaryAvatarURL, nil + if len(members) > 1 { + primaryAvatarURL = id.ContentURI{} + dmUserID = "" + } + return joinMemberNames(members, memberCount), primaryAvatarURL, dmUserID, nil } else if len(leftMembers) > 0 { - return fmt.Sprintf("Empty room (was %s)", joinMemberNames(leftMembers, memberCount)), primaryAvatarURL, nil + if len(leftMembers) > 1 { + primaryAvatarURL = id.ContentURI{} + dmUserID = "" + } + return fmt.Sprintf("Empty room (was %s)", joinMemberNames(leftMembers, memberCount)), primaryAvatarURL, "", nil } else { - return "Empty room", primaryAvatarURL, nil + return "Empty room", primaryAvatarURL, "", nil } } diff --git a/web/src/api/media.ts b/web/src/api/media.ts index 33ad9ea..5e3180d 100644 --- a/web/src/api/media.ts +++ b/web/src/api/media.ts @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import { parseMXC } from "@/util/validation.ts" -import { ContentURI, LazyLoadSummary, RoomID, UserID, UserProfile } from "./types" +import { ContentURI, RoomID, UserID, UserProfile } from "./types" export const getMediaURL = (mxc?: string, encrypted: boolean = false): string | undefined => { const [server, mediaID] = parseMXC(mxc) @@ -93,20 +93,12 @@ interface RoomForAvatarURL { room_id: RoomID name?: string dm_user_id?: UserID - lazy_load_summary?: LazyLoadSummary avatar?: ContentURI avatar_url?: ContentURI } export const getRoomAvatarURL = (room: RoomForAvatarURL, avatarOverride?: ContentURI): string | undefined => { - let dmUserID: UserID | undefined - if ("dm_user_id" in room) { - dmUserID = room.dm_user_id - } else if ("lazy_load_summary" in room) { - dmUserID = room.lazy_load_summary?.["m.heroes"]?.length === 1 - ? room.lazy_load_summary["m.heroes"][0] : undefined - } - return getAvatarURL(dmUserID ?? room.room_id, { + return getAvatarURL(room.dm_user_id ?? room.room_id, { displayname: room.name, avatar_url: avatarOverride ?? room.avatar ?? room.avatar_url, }) diff --git a/web/src/api/statestore/main.ts b/web/src/api/statestore/main.ts index ec1ecb5..e81ff2a 100644 --- a/web/src/api/statestore/main.ts +++ b/web/src/api/statestore/main.ts @@ -187,8 +187,7 @@ export class StateStore { const name = entry.meta.name ?? "Unnamed room" return { room_id: entry.meta.room_id, - dm_user_id: entry.meta.lazy_load_summary?.["m.heroes"]?.length === 1 - ? entry.meta.lazy_load_summary["m.heroes"][0] : undefined, + dm_user_id: entry.meta.dm_user_id, sorting_timestamp: entry.meta.sorting_timestamp, preview_event, preview_sender, diff --git a/web/src/api/statestore/room.ts b/web/src/api/statestore/room.ts index 7419a55..5252a3c 100644 --- a/web/src/api/statestore/room.ts +++ b/web/src/api/statestore/room.ts @@ -70,6 +70,7 @@ function visibleMetaIsEqual(meta1: DBRoom, meta2: DBRoom): boolean { meta1.avatar === meta2.avatar && meta1.topic === meta2.topic && meta1.canonical_alias === meta2.canonical_alias && + meta1.dm_user_id === meta2.dm_user_id && llSummaryIsEqual(meta1.lazy_load_summary, meta2.lazy_load_summary) && meta1.encryption_event?.algorithm === meta2.encryption_event?.algorithm && meta1.has_member_list === meta2.has_member_list diff --git a/web/src/api/types/hitypes.ts b/web/src/api/types/hitypes.ts index 521a9dd..637f38a 100644 --- a/web/src/api/types/hitypes.ts +++ b/web/src/api/types/hitypes.ts @@ -54,6 +54,7 @@ export interface DBRoom { name_quality: RoomNameQuality avatar?: ContentURI explicit_avatar: boolean + dm_user_id?: UserID topic?: string canonical_alias?: RoomAlias lazy_load_summary?: LazyLoadSummary diff --git a/web/src/ui/rightpanel/UserInfoMutualRooms.tsx b/web/src/ui/rightpanel/UserInfoMutualRooms.tsx index fadd041..1a11b55 100644 --- a/web/src/ui/rightpanel/UserInfoMutualRooms.tsx +++ b/web/src/ui/rightpanel/UserInfoMutualRooms.tsx @@ -40,8 +40,7 @@ const MutualRooms = ({ client, userID }: MutualRoomsProps) => { } return { room_id: roomID, - dm_user_id: roomData.meta.current.lazy_load_summary?.["m.heroes"]?.length === 1 - ? roomData.meta.current.lazy_load_summary["m.heroes"][0] : undefined, + dm_user_id: roomData.meta.current.dm_user_id, name: roomData.meta.current.name ?? "Unnamed room", avatar: roomData.meta.current.avatar, search_name: "", From 5d25d839f8c89e85b7917746fcf0ac23614187af Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 3 Jan 2025 12:12:33 +0200 Subject: [PATCH 15/15] web: switch to first matching space when opening room Fixes #582 --- web/src/api/statestore/main.ts | 18 +++++++++++++++++- web/src/ui/MainScreen.tsx | 32 ++++++++++++++++++++++++-------- web/src/ui/MainScreenContext.ts | 4 ++-- web/src/ui/roomlist/RoomList.tsx | 3 +-- 4 files changed, 44 insertions(+), 13 deletions(-) diff --git a/web/src/api/statestore/main.ts b/web/src/api/statestore/main.ts index e81ff2a..31fe657 100644 --- a/web/src/api/statestore/main.ts +++ b/web/src/api/statestore/main.ts @@ -39,7 +39,7 @@ import { } from "../types" import { InvitedRoomStore } from "./invitedroom.ts" import { RoomStateStore } from "./room.ts" -import { DirectChatSpace, RoomListFilter, SpaceEdgeStore, SpaceOrphansSpace, UnreadsSpace } from "./space.ts" +import { DirectChatSpace, RoomListFilter, Space, SpaceEdgeStore, SpaceOrphansSpace, UnreadsSpace } from "./space.ts" export interface RoomListEntry { room_id: RoomID @@ -128,6 +128,22 @@ export class StateStore { return null } + findMatchingSpace(room: RoomListEntry): Space | null { + if (this.spaceOrphans.include(room)) { + return this.spaceOrphans + } + for (const spaceID of this.topLevelSpaces.current) { + const space = this.spaceEdges.get(spaceID) + if (space?.include(room)) { + return space + } + } + if (this.directChatsSpace.include(room)) { + return this.directChatsSpace + } + return null + } + get roomListFilterFunc(): ((entry: RoomListEntry) => boolean) | null { if (!this.currentRoomListFilter && !this.currentRoomListQuery) { return null diff --git a/web/src/ui/MainScreen.tsx b/web/src/ui/MainScreen.tsx index 3bb15e6..e3220a6 100644 --- a/web/src/ui/MainScreen.tsx +++ b/web/src/ui/MainScreen.tsx @@ -96,12 +96,17 @@ class ContextFields implements MainScreenContextFields { } } - setActiveRoom = (roomID: RoomID | null, previewMeta?: Partial, pushState = true) => { + setActiveRoom = ( + roomID: RoomID | null, + previewMeta?: Partial, + toSpace?: RoomListFilter, + pushState = true, + ) => { console.log("Switching to room", roomID) if (roomID) { const room = this.client.store.rooms.get(roomID) if (room) { - this.#setActiveRoom(room, pushState) + this.#setActiveRoom(room, toSpace, pushState) } else { this.#setPreviewRoom(roomID, pushState, previewMeta) } @@ -151,10 +156,21 @@ class ContextFields implements MainScreenContextFields { return room.preferences.room_window_title.replace("$room", name!) } - #setActiveRoom(room: RoomStateStore, pushState: boolean) { + #setActiveRoom(room: RoomStateStore, space: RoomListFilter | undefined | null, pushState: boolean) { window.activeRoom = room this.directSetActiveRoom(room) this.directSetRightPanel(null) + if (!space && this.client.store.currentRoomListFilter) { + const roomListEntry = this.client.store.roomListEntries.get(room.roomID) + if (roomListEntry && !this.client.store.currentRoomListFilter.include(roomListEntry)) { + space = this.client.store.findMatchingSpace(roomListEntry) + } + } + if (space && space !== this.client.store.currentRoomListFilter) { + console.log("Switching to space", space?.id) + this.directSetSpace(space) + this.client.store.currentRoomListFilter = space + } this.rightPanelStack = [] this.client.store.activeRoomID = room.roomID this.client.store.activeRoomIsPreview = false @@ -168,7 +184,7 @@ class ContextFields implements MainScreenContextFields { .querySelector(`div.room-entry[data-room-id="${CSS.escape(room.roomID)}"]`) ?.scrollIntoView({ block: "nearest" }) if (pushState) { - history.pushState({ room_id: room.roomID, space_id: history.state?.space_id }, "") + history.pushState({ room_id: room.roomID, space_id: space?.id ?? history.state?.space_id }, "") } let roomNameForTitle = room.meta.current.name if (roomNameForTitle && roomNameForTitle.length > 48) { @@ -217,7 +233,7 @@ class ContextFields implements MainScreenContextFields { const SYNC_ERROR_HIDE_DELAY = 30 * 1000 -const handleURLHash = (client: Client) => { +const handleURLHash = (client: Client, context: MainScreenContextFields) => { if (!location.hash.startsWith("#/uri/")) { if (location.search) { const currentETag = ( @@ -268,7 +284,7 @@ const handleURLHash = (client: Client) => { // TODO loading indicator or something for this? client.rpc.resolveAlias(uri.identifier).then( res => { - window.mainScreenContext.setActiveRoom(res.room_id, { + context.setActiveRoom(res.room_id, { alias: uri.identifier, via: res.servers.slice(0, 3), }) @@ -321,13 +337,13 @@ const MainScreen = () => { context.setActiveRoom(roomID, { alias: ensureString(evt.state?.source_alias) || undefined, via: ensureStringArray(evt.state?.source_via), - }, false) + }, undefined, false) } context.setRightPanel(evt.state?.right_panel ?? null, false) } window.addEventListener("popstate", listener) const initHandle = () => { - const state = handleURLHash(client) + const state = handleURLHash(client, context) listener({ state } as PopStateEvent) } let cancel = () => {} diff --git a/web/src/ui/MainScreenContext.ts b/web/src/ui/MainScreenContext.ts index 28aa97d..de71425 100644 --- a/web/src/ui/MainScreenContext.ts +++ b/web/src/ui/MainScreenContext.ts @@ -13,14 +13,14 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import { createContext } from "react" +import React, { createContext } from "react" import { RoomListFilter } from "@/api/statestore" import type { RoomID } from "@/api/types" import type { RightPanelProps } from "./rightpanel/RightPanel.tsx" import type { RoomPreviewProps } from "./roomview/RoomPreview.tsx" export interface MainScreenContextFields { - setActiveRoom: (roomID: RoomID | null, previewMeta?: Partial) => void + setActiveRoom: (roomID: RoomID | null, previewMeta?: Partial, toSpace?: RoomListFilter) => void setSpace: (space: RoomListFilter | null, pushState?: boolean) => void clickRoom: (evt: React.MouseEvent) => void clearActiveRoom: () => void diff --git a/web/src/ui/roomlist/RoomList.tsx b/web/src/ui/roomlist/RoomList.tsx index 39d4cb5..c83be1d 100644 --- a/web/src/ui/roomlist/RoomList.tsx +++ b/web/src/ui/roomlist/RoomList.tsx @@ -77,8 +77,7 @@ const RoomList = ({ activeRoomID, space }: RoomListProps) => { for (let i = client.store.roomList.current.length - 1; i >= 0; i--) { const entry = client.store.roomList.current[i] if (entry[wantedField] > 0 && space.include(entry)) { - mainScreen.setActiveRoom(entry.room_id) - mainScreen.setSpace(space) + mainScreen.setActiveRoom(entry.room_id, undefined, space) evt.stopPropagation() break }