forked from Mirrors/gomuks
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": "^19.0.0-rc-0751fac7-20241002",
|
||||||
"react-dom": "^19.0.0-rc-0751fac7-20241002",
|
"react-dom": "^19.0.0-rc-0751fac7-20241002",
|
||||||
"react-spinners": "^0.14.1",
|
"react-spinners": "^0.14.1",
|
||||||
"sanitize-html": "^2.13.1"
|
"sanitize-html": "^2.13.1",
|
||||||
|
"unhomoglyph": "^1.0.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.11.1",
|
"@eslint/js": "^9.11.1",
|
||||||
|
@ -4849,6 +4850,11 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/update-browserslist-db": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz",
|
"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": "^19.0.0-rc-0751fac7-20241002",
|
||||||
"react-dom": "^19.0.0-rc-0751fac7-20241002",
|
"react-dom": "^19.0.0-rc-0751fac7-20241002",
|
||||||
"react-spinners": "^0.14.1",
|
"react-spinners": "^0.14.1",
|
||||||
"sanitize-html": "^2.13.1"
|
"sanitize-html": "^2.13.1",
|
||||||
|
"unhomoglyph": "^1.0.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.11.1",
|
"@eslint/js": "^9.11.1",
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
// 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 { useSyncExternalStore } from "react"
|
import { useSyncExternalStore } from "react"
|
||||||
|
import unhomoglyph from "unhomoglyph"
|
||||||
import { NonNullCachedEventDispatcher } from "../util/eventdispatcher.ts"
|
import { NonNullCachedEventDispatcher } from "../util/eventdispatcher.ts"
|
||||||
import type {
|
import type {
|
||||||
ContentURI,
|
ContentURI,
|
||||||
|
@ -220,9 +221,20 @@ export interface RoomListEntry {
|
||||||
preview_event?: MemDBEvent
|
preview_event?: MemDBEvent
|
||||||
preview_sender?: MemDBEvent
|
preview_sender?: MemDBEvent
|
||||||
name: string
|
name: string
|
||||||
|
search_name: string
|
||||||
avatar?: ContentURI
|
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 {
|
export class StateStore {
|
||||||
readonly rooms: Map<RoomID, RoomStateStore> = new Map()
|
readonly rooms: Map<RoomID, RoomStateStore> = new Map()
|
||||||
readonly roomList = new NonNullCachedEventDispatcher<RoomListEntry[]>([])
|
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_event = room?.eventsByRowID.get(entry.meta.preview_event_rowid)
|
||||||
const preview_sender = preview_event && room?.getStateEvent("m.room.member", preview_event.sender)
|
const preview_sender = preview_event && room?.getStateEvent("m.room.member", preview_event.sender)
|
||||||
|
const name = entry.meta.name ?? "Unnamed room"
|
||||||
return {
|
return {
|
||||||
room_id: entry.meta.room_id,
|
room_id: entry.meta.room_id,
|
||||||
sorting_timestamp: entry.meta.sorting_timestamp,
|
sorting_timestamp: entry.meta.sorting_timestamp,
|
||||||
preview_event,
|
preview_event,
|
||||||
preview_sender,
|
preview_sender,
|
||||||
name: entry.meta.name ?? "Unnamed room",
|
name,
|
||||||
|
search_name: toSearchableString(name),
|
||||||
avatar: entry.meta.avatar,
|
avatar: entry.meta.avatar,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,3 +30,7 @@ button {
|
||||||
--error-color: red;
|
--error-color: red;
|
||||||
--error-color-light: #ff6666;
|
--error-color-light: #ff6666;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ export interface RoomListEntryProps {
|
||||||
room: RoomListEntry
|
room: RoomListEntry
|
||||||
setActiveRoom: (evt: React.MouseEvent) => void
|
setActiveRoom: (evt: React.MouseEvent) => void
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
|
hidden: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
function usePreviewText(evt?: MemDBEvent, senderMemberEvt?: MemDBEvent): [string, string] {
|
function usePreviewText(evt?: MemDBEvent, senderMemberEvt?: MemDBEvent): [string, string] {
|
||||||
|
@ -45,10 +46,10 @@ function usePreviewText(evt?: MemDBEvent, senderMemberEvt?: MemDBEvent): [string
|
||||||
return ["", ""]
|
return ["", ""]
|
||||||
}
|
}
|
||||||
|
|
||||||
const Entry = ({ room, setActiveRoom, isActive }: RoomListEntryProps) => {
|
const Entry = ({ room, setActiveRoom, isActive, hidden }: RoomListEntryProps) => {
|
||||||
const [previewText, croppedPreviewText] = usePreviewText(room.preview_event, room.preview_sender)
|
const [previewText, croppedPreviewText] = usePreviewText(room.preview_event, room.preview_sender)
|
||||||
return <div
|
return <div
|
||||||
className={`room-entry ${isActive ? "active" : ""}`}
|
className={`room-entry ${isActive ? "active" : ""} ${hidden ? "hidden" : ""}`}
|
||||||
onClick={setActiveRoom}
|
onClick={setActiveRoom}
|
||||||
data-room-id={room.room_id}
|
data-room-id={room.room_id}
|
||||||
>
|
>
|
||||||
|
|
|
@ -2,12 +2,25 @@ div.room-list-wrapper {
|
||||||
grid-area: roomlist;
|
grid-area: roomlist;
|
||||||
background: linear-gradient(in hsl longer hue, red 0 0, magenta);
|
background: linear-gradient(in hsl longer hue, red 0 0, magenta);
|
||||||
box-sizing: border-box;
|
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 {
|
div.room-list {
|
||||||
background-color: hsla(0, 0%, 96%, .9);
|
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 {
|
div.room-entry {
|
||||||
|
@ -19,11 +32,11 @@ div.room-entry {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba(5, 38, 87, 0.1);
|
background-color: rgba(0, 0, 0, 0.075);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
background-color: rgba(5, 38, 87, 0.15);
|
background-color: rgba(0, 0, 0, 0.125);
|
||||||
}
|
}
|
||||||
|
|
||||||
> div.room-entry-left {
|
> div.room-entry-left {
|
||||||
|
|
|
@ -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, useCallback } from "react"
|
import React, { use, useCallback, useState } from "react"
|
||||||
|
import { toSearchableString } from "@/api/statestore.ts"
|
||||||
import type { RoomID } from "@/api/types"
|
import type { RoomID } from "@/api/types"
|
||||||
import { useNonNullEventAsState } from "@/util/eventdispatcher.ts"
|
import { useNonNullEventAsState } from "@/util/eventdispatcher.ts"
|
||||||
import { ClientContext } from "../ClientContext.ts"
|
import { ClientContext } from "../ClientContext.ts"
|
||||||
|
@ -27,6 +28,8 @@ interface RoomListProps {
|
||||||
|
|
||||||
const RoomList = ({ setActiveRoom, activeRoomID }: RoomListProps) => {
|
const RoomList = ({ setActiveRoom, activeRoomID }: RoomListProps) => {
|
||||||
const roomList = useNonNullEventAsState(use(ClientContext)!.store.roomList)
|
const roomList = useNonNullEventAsState(use(ClientContext)!.store.roomList)
|
||||||
|
const [roomFilter, setRoomFilter] = useState("")
|
||||||
|
const [realRoomFilter, setRealRoomFilter] = useState("")
|
||||||
const clickRoom = useCallback((evt: React.MouseEvent) => {
|
const clickRoom = useCallback((evt: React.MouseEvent) => {
|
||||||
const roomID = evt.currentTarget.getAttribute("data-room-id")
|
const roomID = evt.currentTarget.getAttribute("data-room-id")
|
||||||
if (roomID) {
|
if (roomID) {
|
||||||
|
@ -36,12 +39,25 @@ const RoomList = ({ setActiveRoom, activeRoomID }: RoomListProps) => {
|
||||||
}
|
}
|
||||||
}, [setActiveRoom])
|
}, [setActiveRoom])
|
||||||
|
|
||||||
|
const updateRoomFilter = useCallback((evt: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setRoomFilter(evt.target.value)
|
||||||
|
setRealRoomFilter(toSearchableString(evt.target.value))
|
||||||
|
}, [])
|
||||||
|
|
||||||
return <div className="room-list-wrapper">
|
return <div className="room-list-wrapper">
|
||||||
|
<input
|
||||||
|
value={roomFilter}
|
||||||
|
onChange={updateRoomFilter}
|
||||||
|
className="room-search"
|
||||||
|
type="text"
|
||||||
|
placeholder="Search rooms"
|
||||||
|
/>
|
||||||
<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}
|
||||||
room={room}
|
room={room}
|
||||||
setActiveRoom={clickRoom}
|
setActiveRoom={clickRoom}
|
||||||
/>,
|
/>,
|
||||||
|
|
Loading…
Add table
Reference in a new issue