diff --git a/web/package-lock.json b/web/package-lock.json index 8a5eb05..dc2632e 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -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", diff --git a/web/package.json b/web/package.json index 8125833..62c81fd 100644 --- a/web/package.json +++ b/web/package.json @@ -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", diff --git a/web/src/api/statestore.ts b/web/src/api/statestore.ts index 7c342f5..25e6ac0 100644 --- a/web/src/api/statestore.ts +++ b/web/src/api/statestore.ts @@ -14,6 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . 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 = new Map() readonly roomList = new NonNullCachedEventDispatcher([]) @@ -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, } } diff --git a/web/src/index.css b/web/src/index.css index 8d46438..b8d9294 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -30,3 +30,7 @@ button { --error-color: red; --error-color-light: #ff6666; } + +.hidden { + display: none !important; +} diff --git a/web/src/ui/roomlist/Entry.tsx b/web/src/ui/roomlist/Entry.tsx index 0541846..18ed7ef 100644 --- a/web/src/ui/roomlist/Entry.tsx +++ b/web/src/ui/roomlist/Entry.tsx @@ -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
diff --git a/web/src/ui/roomlist/RoomList.css b/web/src/ui/roomlist/RoomList.css index 8cb768c..c908f85 100644 --- a/web/src/ui/roomlist/RoomList.css +++ b/web/src/ui/roomlist/RoomList.css @@ -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 { diff --git a/web/src/ui/roomlist/RoomList.tsx b/web/src/ui/roomlist/RoomList.tsx index fa03624..041632e 100644 --- a/web/src/ui/roomlist/RoomList.tsx +++ b/web/src/ui/roomlist/RoomList.tsx @@ -13,7 +13,8 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -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) => { + setRoomFilter(evt.target.value) + setRealRoomFilter(toSearchableString(evt.target.value)) + }, []) + return
+
{reverseMap(roomList, room =>