forked from Mirrors/gomuks
parent
5483b077c7
commit
5a8139685d
12 changed files with 302 additions and 35 deletions
|
@ -12,5 +12,16 @@
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="src/main.tsx"></script>
|
<script type="module" src="src/main.tsx"></script>
|
||||||
<audio id="default-notification-sound" preload="auto" src="sounds/bright.flac"></audio>
|
<audio id="default-notification-sound" preload="auto" src="sounds/bright.flac"></audio>
|
||||||
|
<svg style="position: absolute; width: 0; height: 0;" viewBox="0 0 1 1" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<clipPath id="squircle" clipPathUnits="objectBoundingBox">
|
||||||
|
<path d="M 0,0.5
|
||||||
|
C 0,0 0,0 0.5,0
|
||||||
|
1,0 1,0 1,0.5
|
||||||
|
1,1 1,1 0.5,1
|
||||||
|
0,1 0,1 0,0.5"></path>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -39,6 +39,7 @@ import {
|
||||||
} from "../types"
|
} from "../types"
|
||||||
import { InvitedRoomStore } from "./invitedroom.ts"
|
import { InvitedRoomStore } from "./invitedroom.ts"
|
||||||
import { RoomStateStore } from "./room.ts"
|
import { RoomStateStore } from "./room.ts"
|
||||||
|
import { RoomListFilter, SpaceEdgeStore } from "./space.ts"
|
||||||
|
|
||||||
export interface RoomListEntry {
|
export interface RoomListEntry {
|
||||||
room_id: RoomID
|
room_id: RoomID
|
||||||
|
@ -72,7 +73,10 @@ export class StateStore {
|
||||||
readonly rooms: Map<RoomID, RoomStateStore> = new Map()
|
readonly rooms: Map<RoomID, RoomStateStore> = new Map()
|
||||||
readonly inviteRooms: Map<RoomID, InvitedRoomStore> = new Map()
|
readonly inviteRooms: Map<RoomID, InvitedRoomStore> = new Map()
|
||||||
readonly roomList = new NonNullCachedEventDispatcher<RoomListEntry[]>([])
|
readonly roomList = new NonNullCachedEventDispatcher<RoomListEntry[]>([])
|
||||||
currentRoomListFilter: string = ""
|
readonly topLevelSpaces = new NonNullCachedEventDispatcher<RoomID[]>([])
|
||||||
|
readonly spaceEdges: Map<RoomID, SpaceEdgeStore> = new Map()
|
||||||
|
currentRoomListQuery: string = ""
|
||||||
|
currentRoomListFilter: RoomListFilter | null = null
|
||||||
readonly accountData: Map<string, UnknownEventContent> = new Map()
|
readonly accountData: Map<string, UnknownEventContent> = new Map()
|
||||||
readonly accountDataSubs = new MultiSubscribable()
|
readonly accountDataSubs = new MultiSubscribable()
|
||||||
readonly emojiRoomsSub = new Subscribable()
|
readonly emojiRoomsSub = new Subscribable()
|
||||||
|
@ -89,11 +93,25 @@ export class StateStore {
|
||||||
activeRoomIsPreview: boolean = false
|
activeRoomIsPreview: boolean = false
|
||||||
imageAuthToken?: string
|
imageAuthToken?: string
|
||||||
|
|
||||||
getFilteredRoomList(): RoomListEntry[] {
|
#roomListFilterFunc = (entry: RoomListEntry) => {
|
||||||
if (!this.currentRoomListFilter) {
|
if (this.currentRoomListQuery && !entry.search_name.includes(this.currentRoomListQuery)) {
|
||||||
return this.roomList.current
|
return false
|
||||||
|
} else if (this.currentRoomListFilter && !this.currentRoomListFilter.include(entry)) {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
return this.roomList.current.filter(entry => entry.search_name.includes(this.currentRoomListFilter))
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
get roomListFilterFunc(): ((entry: RoomListEntry) => boolean) | null {
|
||||||
|
if (!this.currentRoomListFilter && !this.currentRoomListQuery) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return this.#roomListFilterFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
getFilteredRoomList(): RoomListEntry[] {
|
||||||
|
const fn = this.roomListFilterFunc
|
||||||
|
return fn ? this.roomList.current.filter(fn) : this.roomList.current
|
||||||
}
|
}
|
||||||
|
|
||||||
#shouldHideRoom(entry: SyncRoom): boolean {
|
#shouldHideRoom(entry: SyncRoom): boolean {
|
||||||
|
@ -259,6 +277,12 @@ export class StateStore {
|
||||||
if (updatedRoomList) {
|
if (updatedRoomList) {
|
||||||
this.roomList.emit(updatedRoomList)
|
this.roomList.emit(updatedRoomList)
|
||||||
}
|
}
|
||||||
|
for (const [spaceID, children] of Object.entries(sync.space_edges ?? {})) {
|
||||||
|
this.getSpaceStore(spaceID, true).children = children
|
||||||
|
}
|
||||||
|
if (sync.top_level_spaces) {
|
||||||
|
this.topLevelSpaces.emit(sync.top_level_spaces)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
invalidateEmojiPackKeyCache() {
|
invalidateEmojiPackKeyCache() {
|
||||||
|
@ -324,6 +348,20 @@ export class StateStore {
|
||||||
return this.#watchedRoomEmojiPacks ?? {}
|
return this.#watchedRoomEmojiPacks ?? {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSpaceStore(spaceID: RoomID, force: true): SpaceEdgeStore
|
||||||
|
getSpaceStore(spaceID: RoomID): SpaceEdgeStore | null
|
||||||
|
getSpaceStore(spaceID: RoomID, force?: true): SpaceEdgeStore | null {
|
||||||
|
let store = this.spaceEdges.get(spaceID)
|
||||||
|
if (!store) {
|
||||||
|
if (!force && this.rooms.get(spaceID)?.meta.current.creation_content?.type !== "m.space") {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
store = new SpaceEdgeStore(spaceID, this)
|
||||||
|
this.spaceEdges.set(spaceID, store)
|
||||||
|
}
|
||||||
|
return store
|
||||||
|
}
|
||||||
|
|
||||||
get frequentlyUsedEmoji(): Map<string, number> {
|
get frequentlyUsedEmoji(): Map<string, number> {
|
||||||
if (this.#frequentlyUsedEmoji === null) {
|
if (this.#frequentlyUsedEmoji === null) {
|
||||||
const emojiData = this.accountData.get("io.element.recent_emoji")
|
const emojiData = this.accountData.get("io.element.recent_emoji")
|
||||||
|
@ -433,9 +471,12 @@ export class StateStore {
|
||||||
clear() {
|
clear() {
|
||||||
this.rooms.clear()
|
this.rooms.clear()
|
||||||
this.inviteRooms.clear()
|
this.inviteRooms.clear()
|
||||||
|
this.spaceEdges.clear()
|
||||||
this.roomList.emit([])
|
this.roomList.emit([])
|
||||||
|
this.topLevelSpaces.emit([])
|
||||||
this.accountData.clear()
|
this.accountData.clear()
|
||||||
this.currentRoomListFilter = ""
|
this.currentRoomListQuery = ""
|
||||||
|
this.currentRoomListFilter = null
|
||||||
this.#frequentlyUsedEmoji = null
|
this.#frequentlyUsedEmoji = null
|
||||||
this.#emojiPackKeys = null
|
this.#emojiPackKeys = null
|
||||||
this.#watchedRoomEmojiPacks = null
|
this.#watchedRoomEmojiPacks = null
|
||||||
|
|
109
web/src/api/statestore/space.ts
Normal file
109
web/src/api/statestore/space.ts
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
// gomuks - A Matrix client written in Go.
|
||||||
|
// Copyright (C) 2024 Tulir Asokan
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
import { RoomListEntry, StateStore } from "@/api/statestore/main.ts"
|
||||||
|
import { DBSpaceEdge, RoomID } from "@/api/types"
|
||||||
|
|
||||||
|
export interface RoomListFilter {
|
||||||
|
id: unknown
|
||||||
|
include(room: RoomListEntry): boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SpaceEdgeStore {
|
||||||
|
#children: DBSpaceEdge[] = []
|
||||||
|
#childRooms: Set<RoomID> = new Set()
|
||||||
|
#flattenedRooms: Set<RoomID> = new Set()
|
||||||
|
#childSpaces: Set<SpaceEdgeStore> = new Set()
|
||||||
|
readonly #parentSpaces: Set<SpaceEdgeStore> = new Set()
|
||||||
|
|
||||||
|
constructor(public id: RoomID, private parent: StateStore) {
|
||||||
|
}
|
||||||
|
|
||||||
|
addParent(parent: SpaceEdgeStore) {
|
||||||
|
this.#parentSpaces.add(parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
removeParent(parent: SpaceEdgeStore) {
|
||||||
|
this.#parentSpaces.delete(parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
include(room: RoomListEntry) {
|
||||||
|
return this.#flattenedRooms.has(room.room_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
get children() {
|
||||||
|
return this.#children
|
||||||
|
}
|
||||||
|
|
||||||
|
#updateFlattened(recalculate: boolean, added: Set<RoomID>) {
|
||||||
|
if (recalculate) {
|
||||||
|
let flattened = new Set(this.#childRooms)
|
||||||
|
for (const space of this.#childSpaces) {
|
||||||
|
flattened = flattened.union(space.#flattenedRooms)
|
||||||
|
}
|
||||||
|
this.#flattenedRooms = flattened
|
||||||
|
} else if (added.size > 50) {
|
||||||
|
this.#flattenedRooms = this.#flattenedRooms.union(added)
|
||||||
|
} else if (added.size > 0) {
|
||||||
|
for (const room of added) {
|
||||||
|
this.#flattenedRooms.add(room)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#notifyParentsOfChange(recalculate: boolean, added: Set<RoomID>, stack: WeakSet<SpaceEdgeStore>) {
|
||||||
|
if (stack.has(this)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stack.add(this)
|
||||||
|
for (const parent of this.#parentSpaces) {
|
||||||
|
parent.#updateFlattened(recalculate, added)
|
||||||
|
parent.#notifyParentsOfChange(recalculate, added, stack)
|
||||||
|
}
|
||||||
|
stack.delete(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
set children(newChildren: DBSpaceEdge[]) {
|
||||||
|
const newChildRooms = new Set<RoomID>()
|
||||||
|
const newChildSpaces = new Set<SpaceEdgeStore>()
|
||||||
|
for (const child of newChildren) {
|
||||||
|
const spaceStore = this.parent.getSpaceStore(child.child_id)
|
||||||
|
if (spaceStore) {
|
||||||
|
newChildSpaces.add(spaceStore)
|
||||||
|
spaceStore.addParent(this)
|
||||||
|
} else {
|
||||||
|
newChildRooms.add(child.child_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const space of this.#childSpaces) {
|
||||||
|
if (!newChildSpaces.has(space)) {
|
||||||
|
space.removeParent(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const addedRooms = newChildRooms.difference(this.#childRooms)
|
||||||
|
const removedRooms = this.#childRooms.difference(newChildRooms)
|
||||||
|
this.#children = newChildren
|
||||||
|
this.#childRooms = newChildRooms
|
||||||
|
this.#childSpaces = newChildSpaces
|
||||||
|
if (this.#childSpaces.size > 0) {
|
||||||
|
this.#updateFlattened(removedRooms.size > 0, addedRooms)
|
||||||
|
} else {
|
||||||
|
this.#flattenedRooms = newChildRooms
|
||||||
|
}
|
||||||
|
if (this.#parentSpaces.size > 0) {
|
||||||
|
this.#notifyParentsOfChange(removedRooms.size > 0, addedRooms, new WeakSet())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -91,7 +91,7 @@ export interface SyncCompleteData {
|
||||||
invited_rooms: DBInvitedRoom[] | null
|
invited_rooms: DBInvitedRoom[] | null
|
||||||
left_rooms: RoomID[] | null
|
left_rooms: RoomID[] | null
|
||||||
account_data: Record<EventType, DBAccountData> | null
|
account_data: Record<EventType, DBAccountData> | null
|
||||||
space_edges: Record<RoomID, Omit<DBSpaceEdge, "space_id">[]> | null
|
space_edges: Record<RoomID, DBSpaceEdge[]> | null
|
||||||
top_level_spaces: RoomID[] | null
|
top_level_spaces: RoomID[] | null
|
||||||
since?: string
|
since?: string
|
||||||
clear_state?: boolean
|
clear_state?: boolean
|
||||||
|
|
|
@ -72,7 +72,7 @@ export interface DBRoom {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DBSpaceEdge {
|
export interface DBSpaceEdge {
|
||||||
space_id: RoomID
|
// space_id: RoomID
|
||||||
child_id: RoomID
|
child_id: RoomID
|
||||||
|
|
||||||
child_event_rowid?: EventRowID
|
child_event_rowid?: EventRowID
|
||||||
|
|
1
web/src/icons/home.svg
Normal file
1
web/src/icons/home.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M240-200h120v-240h240v240h120v-360L480-740 240-560v360Zm-80 80v-480l320-240 320 240v480H520v-240h-80v240H160Zm320-350Z"/></svg>
|
After Width: | Height: | Size: 244 B |
|
@ -1,5 +1,5 @@
|
||||||
main.matrix-main {
|
main.matrix-main {
|
||||||
--room-list-width: 300px;
|
--room-list-width: 350px;
|
||||||
--right-panel-width: 300px;
|
--right-panel-width: 300px;
|
||||||
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|
|
@ -318,7 +318,7 @@ const MainScreen = () => {
|
||||||
}, [context, client])
|
}, [context, client])
|
||||||
useEffect(() => context.keybindings.listen(), [context])
|
useEffect(() => context.keybindings.listen(), [context])
|
||||||
const [roomListWidth, resizeHandle1] = useResizeHandle(
|
const [roomListWidth, resizeHandle1] = useResizeHandle(
|
||||||
300, 48, Math.min(900, window.innerWidth * 0.4),
|
350, 96, Math.min(900, window.innerWidth * 0.4),
|
||||||
"roomListWidth", { className: "room-list-resizer" },
|
"roomListWidth", { className: "room-list-resizer" },
|
||||||
)
|
)
|
||||||
const [rightPanelWidth, resizeHandle2] = useResizeHandle(
|
const [rightPanelWidth, resizeHandle2] = useResizeHandle(
|
||||||
|
|
|
@ -5,14 +5,53 @@ div.room-list-wrapper {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
scrollbar-color: var(--room-list-scrollbar-color);
|
scrollbar-color: var(--room-list-scrollbar-color);
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-direction: column;
|
grid-template:
|
||||||
|
"spacebar search" 3.5rem
|
||||||
|
"spacebar roomlist" 1fr
|
||||||
|
/ 3rem 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.room-list {
|
div.room-list {
|
||||||
background-color: var(--room-list-background-overlay);
|
background-color: var(--room-list-background-overlay);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
flex: 1;
|
grid-area: roomlist;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.space-bar {
|
||||||
|
background-color: var(--space-list-background-overlay);
|
||||||
|
grid-area: spacebar;
|
||||||
|
overflow: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
|
||||||
|
> div.space-entry {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
padding: .25rem;
|
||||||
|
margin: .25rem;
|
||||||
|
border-radius: .25rem;
|
||||||
|
cursor: var(--clickable-cursor);
|
||||||
|
|
||||||
|
&:hover, &:focus {
|
||||||
|
background-color: var(--room-list-entry-hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: var(--room-list-entry-selected-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
> svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
> img.avatar {
|
||||||
|
border-radius: 0;
|
||||||
|
clip-path: url(#squircle);
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
div.room-search-wrapper {
|
div.room-search-wrapper {
|
||||||
|
@ -21,6 +60,7 @@ div.room-search-wrapper {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 3.5rem;
|
height: 3.5rem;
|
||||||
background-color: var(--room-list-search-background-overlay);
|
background-color: var(--room-list-search-background-overlay);
|
||||||
|
grid-area: search;
|
||||||
|
|
||||||
> input {
|
> input {
|
||||||
padding: 0 0 0 1rem;
|
padding: 0 0 0 1rem;
|
||||||
|
|
|
@ -13,7 +13,8 @@
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import React, { use, useRef, useState } from "react"
|
import React, { use, useCallback, useRef, useState } from "react"
|
||||||
|
import type { RoomListFilter } from "@/api/statestore/space.ts"
|
||||||
import type { RoomID } from "@/api/types"
|
import type { RoomID } from "@/api/types"
|
||||||
import { useEventAsState } from "@/util/eventdispatcher.ts"
|
import { useEventAsState } from "@/util/eventdispatcher.ts"
|
||||||
import reverseMap from "@/util/reversemap.ts"
|
import reverseMap from "@/util/reversemap.ts"
|
||||||
|
@ -22,7 +23,9 @@ import ClientContext from "../ClientContext.ts"
|
||||||
import MainScreenContext from "../MainScreenContext.ts"
|
import MainScreenContext from "../MainScreenContext.ts"
|
||||||
import { keyToString } from "../keybindings.ts"
|
import { keyToString } from "../keybindings.ts"
|
||||||
import Entry from "./Entry.tsx"
|
import Entry from "./Entry.tsx"
|
||||||
|
import Space from "./Space.tsx"
|
||||||
import CloseIcon from "@/icons/close.svg?react"
|
import CloseIcon from "@/icons/close.svg?react"
|
||||||
|
import HomeIcon from "@/icons/home.svg?react"
|
||||||
import SearchIcon from "@/icons/search.svg?react"
|
import SearchIcon from "@/icons/search.svg?react"
|
||||||
import "./RoomList.css"
|
import "./RoomList.css"
|
||||||
|
|
||||||
|
@ -34,20 +37,27 @@ const RoomList = ({ activeRoomID }: RoomListProps) => {
|
||||||
const client = use(ClientContext)!
|
const client = use(ClientContext)!
|
||||||
const mainScreen = use(MainScreenContext)
|
const mainScreen = use(MainScreenContext)
|
||||||
const roomList = useEventAsState(client.store.roomList)
|
const roomList = useEventAsState(client.store.roomList)
|
||||||
const roomFilterRef = useRef<HTMLInputElement>(null)
|
const spaces = useEventAsState(client.store.topLevelSpaces)
|
||||||
const [roomFilter, setRoomFilter] = useState("")
|
const searchInputRef = useRef<HTMLInputElement>(null)
|
||||||
const [realRoomFilter, setRealRoomFilter] = useState("")
|
const [query, directSetQuery] = useState("")
|
||||||
|
const [space, directSetSpace] = useState<RoomListFilter | null>(null)
|
||||||
|
|
||||||
const updateRoomFilter = (evt: React.ChangeEvent<HTMLInputElement>) => {
|
const setQuery = (evt: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setRoomFilter(evt.target.value)
|
client.store.currentRoomListQuery = toSearchableString(evt.target.value)
|
||||||
client.store.currentRoomListFilter = toSearchableString(evt.target.value)
|
directSetQuery(evt.target.value)
|
||||||
setRealRoomFilter(client.store.currentRoomListFilter)
|
|
||||||
}
|
}
|
||||||
|
const setSpace = useCallback((space: RoomListFilter | null) => {
|
||||||
|
directSetSpace(space)
|
||||||
|
client.store.currentRoomListFilter = space
|
||||||
|
}, [client])
|
||||||
|
const onClickSpace = useCallback((evt: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
const store = client.store.getSpaceStore(evt.currentTarget.getAttribute("data-target-space")!)
|
||||||
|
setSpace(store)
|
||||||
|
}, [setSpace, client])
|
||||||
const clearQuery = () => {
|
const clearQuery = () => {
|
||||||
setRoomFilter("")
|
client.store.currentRoomListQuery = ""
|
||||||
client.store.currentRoomListFilter = ""
|
directSetQuery("")
|
||||||
setRealRoomFilter("")
|
searchInputRef.current?.focus()
|
||||||
roomFilterRef.current?.focus()
|
|
||||||
}
|
}
|
||||||
const onKeyDown = (evt: React.KeyboardEvent<HTMLInputElement>) => {
|
const onKeyDown = (evt: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
const key = keyToString(evt)
|
const key = keyToString(evt)
|
||||||
|
@ -64,28 +74,40 @@ const RoomList = ({ activeRoomID }: RoomListProps) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const roomListFilter = client.store.roomListFilterFunc
|
||||||
return <div className="room-list-wrapper">
|
return <div className="room-list-wrapper">
|
||||||
<div className="room-search-wrapper">
|
<div className="room-search-wrapper">
|
||||||
<input
|
<input
|
||||||
value={roomFilter}
|
value={query}
|
||||||
onChange={updateRoomFilter}
|
onChange={setQuery}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
className="room-search"
|
className="room-search"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search rooms"
|
placeholder="Search rooms"
|
||||||
ref={roomFilterRef}
|
ref={searchInputRef}
|
||||||
id="room-search"
|
id="room-search"
|
||||||
/>
|
/>
|
||||||
<button onClick={clearQuery} disabled={roomFilter === ""}>
|
<button onClick={clearQuery} disabled={query === ""}>
|
||||||
{roomFilter !== "" ? <CloseIcon/> : <SearchIcon/>}
|
{query !== "" ? <CloseIcon/> : <SearchIcon/>}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-bar">
|
||||||
|
<div className={`space-entry ${space === null ? "active" : ""}`} onClick={() => setSpace(null)}>
|
||||||
|
<HomeIcon />
|
||||||
|
</div>
|
||||||
|
{spaces.map(roomID => <Space
|
||||||
|
roomID={roomID}
|
||||||
|
client={client}
|
||||||
|
onClick={onClickSpace}
|
||||||
|
isActive={space?.id === roomID}
|
||||||
|
/>)}
|
||||||
|
</div>
|
||||||
<div className="room-list">
|
<div className="room-list">
|
||||||
{reverseMap(roomList, room =>
|
{reverseMap(roomList, room =>
|
||||||
<Entry
|
<Entry
|
||||||
key={room.room_id}
|
key={room.room_id}
|
||||||
isActive={room.room_id === activeRoomID}
|
isActive={room.room_id === activeRoomID}
|
||||||
hidden={roomFilter ? !room.search_name.includes(realRoomFilter) : false}
|
hidden={roomListFilter ? !roomListFilter(room) : false}
|
||||||
room={room}
|
room={room}
|
||||||
/>,
|
/>,
|
||||||
)}
|
)}
|
||||||
|
|
40
web/src/ui/roomlist/Space.tsx
Normal file
40
web/src/ui/roomlist/Space.tsx
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
// gomuks - A Matrix client written in Go.
|
||||||
|
// Copyright (C) 2024 Tulir Asokan
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
import React from "react"
|
||||||
|
import Client from "@/api/client.ts"
|
||||||
|
import { getRoomAvatarURL } from "@/api/media.ts"
|
||||||
|
import type { RoomID } from "@/api/types"
|
||||||
|
import { useEventAsState } from "@/util/eventdispatcher.ts"
|
||||||
|
import "./RoomList.css"
|
||||||
|
|
||||||
|
export interface SpaceProps {
|
||||||
|
roomID: RoomID
|
||||||
|
client: Client
|
||||||
|
onClick: (evt: React.MouseEvent<HTMLDivElement>) => void
|
||||||
|
isActive: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const Space = ({ roomID, client, onClick, isActive }: SpaceProps) => {
|
||||||
|
const room = useEventAsState(client.store.rooms.get(roomID)?.meta)
|
||||||
|
if (!room) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return <div className={`space-entry ${isActive ? "active" : ""}`} onClick={onClick} data-target-space={roomID}>
|
||||||
|
<img src={getRoomAvatarURL(room)} alt={room.name} title={room.name} className="avatar" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Space
|
|
@ -15,12 +15,15 @@
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import { useSyncExternalStore } from "react"
|
import { useSyncExternalStore } from "react"
|
||||||
|
|
||||||
|
const noop = () => {}
|
||||||
|
const noopListen = () => noop
|
||||||
|
|
||||||
export function useEventAsState<T>(dispatcher: NonNullCachedEventDispatcher<T>): T
|
export function useEventAsState<T>(dispatcher: NonNullCachedEventDispatcher<T>): T
|
||||||
export function useEventAsState<T>(dispatcher: CachedEventDispatcher<T>): T | null
|
export function useEventAsState<T>(dispatcher?: CachedEventDispatcher<T>): T | null
|
||||||
export function useEventAsState<T>(dispatcher: CachedEventDispatcher<T>): T | null {
|
export function useEventAsState<T>(dispatcher?: CachedEventDispatcher<T>): T | null {
|
||||||
return useSyncExternalStore(
|
return useSyncExternalStore(
|
||||||
dispatcher.listenChange,
|
dispatcher ? dispatcher.listenChange : noopListen,
|
||||||
() => dispatcher.current,
|
() => dispatcher ? dispatcher.current : null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue