mirror of
https://github.com/tulir/gomuks.git
synced 2025-04-19 18:13:41 -05:00
web: more file reorganization
This commit is contained in:
parent
929bfbc882
commit
da675fb578
12 changed files with 151 additions and 71 deletions
|
@ -20,6 +20,7 @@ import Client from "./api/client.ts"
|
|||
import WSClient from "./api/wsclient.ts"
|
||||
import { LoginScreen, VerificationScreen } from "./ui/login"
|
||||
import MainScreen from "./ui/MainScreen.tsx"
|
||||
import { ClientContext } from "./ui/ClientContext.ts"
|
||||
|
||||
function App() {
|
||||
const client = useMemo(() => new Client(new WSClient("/_gomuks/websocket")), [])
|
||||
|
@ -55,7 +56,7 @@ function App() {
|
|||
} else if (!clientState.is_verified) {
|
||||
return <VerificationScreen client={client} clientState={clientState}/>
|
||||
} else {
|
||||
return <MainScreen client={client} />
|
||||
return <ClientContext value={client}><MainScreen /></ClientContext>
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,12 +13,12 @@
|
|||
//
|
||||
// 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 { EventDispatcher } from "../util/eventdispatcher.ts"
|
||||
import { EventDispatcher, CachedEventDispatcher } from "../util/eventdispatcher.ts"
|
||||
import { CancellablePromise } from "../util/promise.ts"
|
||||
import { RPCEvent } from "./types/hievents.ts"
|
||||
|
||||
export interface RPCClient {
|
||||
connect: EventDispatcher<ConnectionEvent>
|
||||
connect: CachedEventDispatcher<ConnectionEvent>
|
||||
event: EventDispatcher<RPCEvent>
|
||||
start(): void
|
||||
stop(): void
|
||||
|
|
|
@ -58,12 +58,14 @@ function visibleMetaIsEqual(meta1: DBRoom, meta2: DBRoom): boolean {
|
|||
}
|
||||
|
||||
export class RoomStateStore {
|
||||
readonly roomID: RoomID
|
||||
readonly meta: NonNullCachedEventDispatcher<DBRoom>
|
||||
readonly timeline = new NonNullCachedEventDispatcher<TimelineRowTuple[]>([])
|
||||
readonly eventsByRowID: Map<EventRowID, DBEvent> = new Map()
|
||||
readonly eventsByID: Map<EventID, DBEvent> = new Map()
|
||||
|
||||
constructor(meta: DBRoom) {
|
||||
this.roomID = meta.room_id
|
||||
this.meta = new NonNullCachedEventDispatcher(meta)
|
||||
}
|
||||
|
||||
|
|
19
web/src/ui/ClientContext.ts
Normal file
19
web/src/ui/ClientContext.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
// 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 { createContext } from "react"
|
||||
import type Client from "../api/client.ts"
|
||||
|
||||
export const ClientContext = createContext<Client | null>(null)
|
|
@ -13,23 +13,19 @@
|
|||
//
|
||||
// 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 { useState } from "react"
|
||||
import type Client from "../api/client.ts"
|
||||
import { useState, use } from "react"
|
||||
import type { RoomID } from "../api/types/hitypes.ts"
|
||||
import RoomList from "./RoomList.tsx"
|
||||
import RoomList from "./roomlist/RoomList.tsx"
|
||||
import RoomView from "./RoomView.tsx"
|
||||
import { ClientContext } from "./ClientContext.ts"
|
||||
import "./MainScreen.css"
|
||||
|
||||
export interface MainScreenProps {
|
||||
client: Client
|
||||
}
|
||||
|
||||
const MainScreen = ({ client }: MainScreenProps) => {
|
||||
const MainScreen = () => {
|
||||
const [activeRoomID, setActiveRoomID] = useState<RoomID | null>(null)
|
||||
const activeRoom = activeRoomID && client.store.rooms.get(activeRoomID)
|
||||
const activeRoom = activeRoomID && use(ClientContext)!.store.rooms.get(activeRoomID)
|
||||
return <main className="matrix-main">
|
||||
<RoomList client={client} setActiveRoom={setActiveRoomID} />
|
||||
{activeRoom && <RoomView client={client} room={activeRoom} />}
|
||||
<RoomList setActiveRoom={setActiveRoomID} />
|
||||
{activeRoom && <RoomView key={activeRoomID} room={activeRoom} />}
|
||||
</main>
|
||||
}
|
||||
|
||||
|
|
|
@ -13,26 +13,20 @@
|
|||
//
|
||||
// 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 Client from "../api/client.ts"
|
||||
import { RoomStateStore } from "../api/statestore.ts"
|
||||
import { useNonNullEventAsState } from "../util/eventdispatcher.ts"
|
||||
import "./RoomView.css"
|
||||
import TimelineEvent from "./timeline/TimelineEvent.tsx"
|
||||
import TimelineView from "./timeline/TimelineView.tsx"
|
||||
|
||||
export interface RoomViewProps {
|
||||
client: Client
|
||||
interface RoomViewProps {
|
||||
room: RoomStateStore
|
||||
}
|
||||
|
||||
const RoomView = ({ client, room }: RoomViewProps) => {
|
||||
const RoomView = ({ room }: RoomViewProps) => {
|
||||
const roomMeta = useNonNullEventAsState(room.meta)
|
||||
const timeline = useNonNullEventAsState(room.timeline)
|
||||
return <div className="room-view">
|
||||
{roomMeta.room_id}
|
||||
<button onClick={() => client.loadMoreHistory(roomMeta.room_id)}>Load history</button>
|
||||
{timeline.map(entry => <TimelineEvent
|
||||
key={entry.event_rowid} client={client} room={room} eventRowID={entry.event_rowid}
|
||||
/>)}
|
||||
<TimelineView room={room} />
|
||||
</div>
|
||||
}
|
||||
|
||||
|
|
|
@ -13,47 +13,10 @@
|
|||
//
|
||||
// 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, { useMemo } from "react"
|
||||
import Client from "../api/client.ts"
|
||||
import { DBEvent, RoomID } from "../api/types/hitypes.ts"
|
||||
import { useNonNullEventAsState } from "../util/eventdispatcher.ts"
|
||||
import { RoomListEntry } from "../api/statestore.ts"
|
||||
import "./RoomList.css"
|
||||
|
||||
export interface RoomListProps {
|
||||
client: Client
|
||||
setActiveRoom: (room_id: RoomID) => void
|
||||
}
|
||||
|
||||
const RoomList = ({ client, setActiveRoom }: RoomListProps) => {
|
||||
const roomList = useNonNullEventAsState(client.store.roomList)
|
||||
const clickRoom = useMemo(() => (evt: React.MouseEvent) => {
|
||||
const roomID = evt.currentTarget.getAttribute("data-room-id")
|
||||
if (roomID) {
|
||||
setActiveRoom(roomID)
|
||||
} else {
|
||||
console.warn("No room ID :(", evt.currentTarget)
|
||||
}
|
||||
}, [setActiveRoom])
|
||||
|
||||
return <div className="room-list">
|
||||
{reverseMap(roomList, room =>
|
||||
<RoomEntry
|
||||
key={room.room_id}
|
||||
client={client}
|
||||
room={room}
|
||||
setActiveRoom={clickRoom}
|
||||
/>,
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
|
||||
function reverseMap<T, O>(arg: T[], fn: (a: T) => O) {
|
||||
return arg.map((_, i, arr) => fn(arr[arr.length - i - 1]))
|
||||
}
|
||||
import type { RoomListEntry } from "../../api/statestore.ts"
|
||||
import type { DBEvent } from "../../api/types/hitypes.ts"
|
||||
|
||||
export interface RoomListEntryProps {
|
||||
client: Client
|
||||
room: RoomListEntry
|
||||
setActiveRoom: (evt: React.MouseEvent) => void
|
||||
}
|
||||
|
@ -85,7 +48,7 @@ const getAvatarURL = (avatar?: string): string | undefined => {
|
|||
return `_gomuks/media/${match[1]}/${match[2]}`
|
||||
}
|
||||
|
||||
const RoomEntry = ({ room, setActiveRoom }: RoomListEntryProps) => {
|
||||
const Entry = ({ room, setActiveRoom }: RoomListEntryProps) => {
|
||||
const previewText = makePreviewText(room.preview_event)
|
||||
return <div className="room-entry" onClick={setActiveRoom} data-room-id={room.room_id}>
|
||||
<div className="room-entry-left">
|
||||
|
@ -98,4 +61,4 @@ const RoomEntry = ({ room, setActiveRoom }: RoomListEntryProps) => {
|
|||
</div>
|
||||
}
|
||||
|
||||
export default RoomList
|
||||
export default Entry
|
49
web/src/ui/roomlist/RoomList.tsx
Normal file
49
web/src/ui/roomlist/RoomList.tsx
Normal file
|
@ -0,0 +1,49 @@
|
|||
// 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, { use, useMemo } from "react"
|
||||
import type { RoomID } from "../../api/types/hitypes.ts"
|
||||
import { useNonNullEventAsState } from "../../util/eventdispatcher.ts"
|
||||
import { ClientContext } from "../ClientContext.ts"
|
||||
import Entry from "./Entry.tsx"
|
||||
import "./RoomList.css"
|
||||
|
||||
interface RoomListProps {
|
||||
setActiveRoom: (room_id: RoomID) => void
|
||||
}
|
||||
|
||||
const RoomList = ({ setActiveRoom }: RoomListProps) => {
|
||||
const roomList = useNonNullEventAsState(use(ClientContext)!.store.roomList)
|
||||
const clickRoom = useMemo(() => (evt: React.MouseEvent) => {
|
||||
const roomID = evt.currentTarget.getAttribute("data-room-id")
|
||||
if (roomID) {
|
||||
setActiveRoom(roomID)
|
||||
} else {
|
||||
console.warn("No room ID :(", evt.currentTarget)
|
||||
}
|
||||
}, [setActiveRoom])
|
||||
|
||||
return <div className="room-list">
|
||||
{reverseMap(roomList, room =>
|
||||
<Entry key={room.room_id} room={room} setActiveRoom={clickRoom}/>,
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
|
||||
function reverseMap<T, O>(arg: T[], fn: (a: T) => O) {
|
||||
return arg.map((_, i, arr) => fn(arr[arr.length - i - 1]))
|
||||
}
|
||||
|
||||
export default RoomList
|
|
@ -13,10 +13,11 @@
|
|||
//
|
||||
// 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 { RoomViewProps } from "../RoomView.tsx"
|
||||
import { RoomStateStore } from "../../api/statestore.ts"
|
||||
import "./TimelineEvent.css"
|
||||
|
||||
export interface TimelineEventProps extends RoomViewProps {
|
||||
export interface TimelineEventProps {
|
||||
room: RoomStateStore
|
||||
eventRowID: number
|
||||
}
|
||||
|
||||
|
|
41
web/src/ui/timeline/TimelineView.tsx
Normal file
41
web/src/ui/timeline/TimelineView.tsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
// 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 { use, useMemo } from "react"
|
||||
import { RoomStateStore } from "../../api/statestore.ts"
|
||||
import { useNonNullEventAsState } from "../../util/eventdispatcher.ts"
|
||||
import { ClientContext } from "../ClientContext.ts"
|
||||
import TimelineEvent from "./TimelineEvent.tsx"
|
||||
|
||||
interface TimelineViewProps {
|
||||
room: RoomStateStore
|
||||
}
|
||||
|
||||
const TimelineView = ({ room }: TimelineViewProps) => {
|
||||
const timeline = useNonNullEventAsState(room.timeline)
|
||||
const client = use(ClientContext)!
|
||||
const loadHistory = useMemo(() => () => {
|
||||
client.loadMoreHistory(room.roomID)
|
||||
.catch(err => console.error("Failed to load history", err))
|
||||
}, [client, room.roomID])
|
||||
return <div className="timeline-view">
|
||||
<button onClick={loadHistory}>Load history</button>
|
||||
{timeline.map(entry => <TimelineEvent
|
||||
key={entry.event_rowid} room={room} eventRowID={entry.event_rowid}
|
||||
/>)}
|
||||
</div>
|
||||
}
|
||||
|
||||
export default TimelineView
|
|
@ -13,7 +13,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 { useEffect, useState } from "react"
|
||||
import { useEffect, useState, useSyncExternalStore } from "react"
|
||||
|
||||
export function useEventAsState<T>(dispatcher?: EventDispatcher<T>): T | null {
|
||||
const [state, setState] = useState<T | null>(null)
|
||||
|
@ -21,16 +21,30 @@ export function useEventAsState<T>(dispatcher?: EventDispatcher<T>): T | null {
|
|||
return state
|
||||
}
|
||||
|
||||
export function useCachedEventAsState<T>(dispatcher: CachedEventDispatcher<T>): T | null {
|
||||
return useSyncExternalStore(
|
||||
dispatcher.listenChange,
|
||||
() => dispatcher.current,
|
||||
)
|
||||
}
|
||||
|
||||
export function useNonNullEventAsState<T>(dispatcher: NonNullCachedEventDispatcher<T>): T {
|
||||
const [state, setState] = useState<T>(dispatcher.current)
|
||||
useEffect(() => dispatcher.listen(setState), [dispatcher])
|
||||
return state
|
||||
return useSyncExternalStore(
|
||||
dispatcher.listenChange,
|
||||
() => dispatcher.current,
|
||||
)
|
||||
}
|
||||
|
||||
export class EventDispatcher<T> {
|
||||
#listeners: ((data: T) => void)[] = []
|
||||
|
||||
listenChange = (listener: () => void) => this.#listen(listener)
|
||||
|
||||
listen(listener: (data: T) => void): () => void {
|
||||
return this.#listen(listener)
|
||||
}
|
||||
|
||||
#listen(listener: (data: T) => void): () => void {
|
||||
this.#listeners.push(listener)
|
||||
return () => {
|
||||
const idx = this.#listeners.indexOf(listener)
|
||||
|
|
Loading…
Add table
Reference in a new issue