1
0
Fork 0
forked from Mirrors/gomuks

web/roomlist: add filter bar

This commit is contained in:
Tulir Asokan 2024-10-13 20:52:20 +03:00
parent 8f43d00d06
commit c281ba90ee
7 changed files with 65 additions and 10 deletions

8
web/package-lock.json generated
View file

@ -15,7 +15,8 @@
"react": "^19.0.0-rc-0751fac7-20241002",
"react-dom": "^19.0.0-rc-0751fac7-20241002",
"react-spinners": "^0.14.1",
"sanitize-html": "^2.13.1"
"sanitize-html": "^2.13.1",
"unhomoglyph": "^1.0.6"
},
"devDependencies": {
"@eslint/js": "^9.11.1",
@ -4849,6 +4850,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/unhomoglyph": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/unhomoglyph/-/unhomoglyph-1.0.6.tgz",
"integrity": "sha512-7uvcWI3hWshSADBu4JpnyYbTVc7YlhF5GDW/oPD5AxIxl34k4wXR3WDkPnzLxkN32LiTCTKMQLtKVZiwki3zGg=="
},
"node_modules/update-browserslist-db": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz",

View file

@ -17,7 +17,8 @@
"react": "^19.0.0-rc-0751fac7-20241002",
"react-dom": "^19.0.0-rc-0751fac7-20241002",
"react-spinners": "^0.14.1",
"sanitize-html": "^2.13.1"
"sanitize-html": "^2.13.1",
"unhomoglyph": "^1.0.6"
},
"devDependencies": {
"@eslint/js": "^9.11.1",

View file

@ -14,6 +14,7 @@
// 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 { useSyncExternalStore } from "react"
import unhomoglyph from "unhomoglyph"
import { NonNullCachedEventDispatcher } from "../util/eventdispatcher.ts"
import type {
ContentURI,
@ -220,9 +221,20 @@ export interface RoomListEntry {
preview_event?: MemDBEvent
preview_sender?: MemDBEvent
name: string
search_name: string
avatar?: ContentURI
}
// eslint-disable-next-line no-misleading-character-class
const removeHiddenCharsRegex = /[\u2000-\u200F\u202A-\u202F\u0300-\u036F\uFEFF\u061C\u2800\u2062-\u2063\s]/g
export function toSearchableString(str: string): string {
return unhomoglyph(str.normalize("NFD").replace(removeHiddenCharsRegex, ""))
.toLowerCase()
.replace(/[\\'!"#$%&()*+,\-./:;<=>?@[\]^_`{|}~\u2000-\u206f\u2e00-\u2e7f]/g, "")
.toLowerCase()
}
export class StateStore {
readonly rooms: Map<RoomID, RoomStateStore> = new Map()
readonly roomList = new NonNullCachedEventDispatcher<RoomListEntry[]>([])
@ -239,12 +251,14 @@ export class StateStore {
}
const preview_event = room?.eventsByRowID.get(entry.meta.preview_event_rowid)
const preview_sender = preview_event && room?.getStateEvent("m.room.member", preview_event.sender)
const name = entry.meta.name ?? "Unnamed room"
return {
room_id: entry.meta.room_id,
sorting_timestamp: entry.meta.sorting_timestamp,
preview_event,
preview_sender,
name: entry.meta.name ?? "Unnamed room",
name,
search_name: toSearchableString(name),
avatar: entry.meta.avatar,
}
}

View file

@ -30,3 +30,7 @@ button {
--error-color: red;
--error-color-light: #ff6666;
}
.hidden {
display: none !important;
}

View file

@ -23,6 +23,7 @@ export interface RoomListEntryProps {
room: RoomListEntry
setActiveRoom: (evt: React.MouseEvent) => void
isActive: boolean
hidden: boolean
}
function usePreviewText(evt?: MemDBEvent, senderMemberEvt?: MemDBEvent): [string, string] {
@ -45,10 +46,10 @@ function usePreviewText(evt?: MemDBEvent, senderMemberEvt?: MemDBEvent): [string
return ["", ""]
}
const Entry = ({ room, setActiveRoom, isActive }: RoomListEntryProps) => {
const Entry = ({ room, setActiveRoom, isActive, hidden }: RoomListEntryProps) => {
const [previewText, croppedPreviewText] = usePreviewText(room.preview_event, room.preview_sender)
return <div
className={`room-entry ${isActive ? "active" : ""}`}
className={`room-entry ${isActive ? "active" : ""} ${hidden ? "hidden" : ""}`}
onClick={setActiveRoom}
data-room-id={room.room_id}
>

View file

@ -2,12 +2,25 @@ div.room-list-wrapper {
grid-area: roomlist;
background: linear-gradient(in hsl longer hue, red 0 0, magenta);
box-sizing: border-box;
overflow-y: auto;
overflow: hidden;
scrollbar-color: rgba(0, 0, 0, 0.4) rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
}
div.room-list {
background-color: hsla(0, 0%, 96%, .9);
min-height: 100vh;
overflow-y: auto;
flex: 1;
}
input.room-search {
width: 100%;
padding: 1rem;
box-sizing: border-box;
border: none;
background-color: hsla(0, 0%, 96%, .9);
outline: none;
}
div.room-entry {
@ -19,11 +32,11 @@ div.room-entry {
cursor: pointer;
&:hover {
background-color: rgba(5, 38, 87, 0.1);
background-color: rgba(0, 0, 0, 0.075);
}
&.active {
background-color: rgba(5, 38, 87, 0.15);
background-color: rgba(0, 0, 0, 0.125);
}
> div.room-entry-left {

View file

@ -13,7 +13,8 @@
//
// 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, { use, useCallback } from "react"
import React, { use, useCallback, useState } from "react"
import { toSearchableString } from "@/api/statestore.ts"
import type { RoomID } from "@/api/types"
import { useNonNullEventAsState } from "@/util/eventdispatcher.ts"
import { ClientContext } from "../ClientContext.ts"
@ -27,6 +28,8 @@ interface RoomListProps {
const RoomList = ({ setActiveRoom, activeRoomID }: RoomListProps) => {
const roomList = useNonNullEventAsState(use(ClientContext)!.store.roomList)
const [roomFilter, setRoomFilter] = useState("")
const [realRoomFilter, setRealRoomFilter] = useState("")
const clickRoom = useCallback((evt: React.MouseEvent) => {
const roomID = evt.currentTarget.getAttribute("data-room-id")
if (roomID) {
@ -36,12 +39,25 @@ const RoomList = ({ setActiveRoom, activeRoomID }: RoomListProps) => {
}
}, [setActiveRoom])
const updateRoomFilter = useCallback((evt: React.ChangeEvent<HTMLInputElement>) => {
setRoomFilter(evt.target.value)
setRealRoomFilter(toSearchableString(evt.target.value))
}, [])
return <div className="room-list-wrapper">
<input
value={roomFilter}
onChange={updateRoomFilter}
className="room-search"
type="text"
placeholder="Search rooms"
/>
<div className="room-list">
{reverseMap(roomList, room =>
<Entry
key={room.room_id}
isActive={room.room_id === activeRoomID}
hidden={roomFilter ? !room.search_name.includes(realRoomFilter) : false}
room={room}
setActiveRoom={clickRoom}
/>,