mirror of
https://github.com/tulir/gomuks.git
synced 2025-04-20 18:43:41 -05:00
web/roomlist: add filter bar
This commit is contained in:
parent
8f43d00d06
commit
c281ba90ee
7 changed files with 65 additions and 10 deletions
8
web/package-lock.json
generated
8
web/package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,3 +30,7 @@ button {
|
|||
--error-color: red;
|
||||
--error-color-light: #ff6666;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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}
|
||||
/>,
|
||||
|
|
Loading…
Add table
Reference in a new issue