From c2d0020c8c99801748ada5981fb74066c7801c7d Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 9 Nov 2024 12:52:22 +0100 Subject: [PATCH] web/timeline: add click handlers for matrix URIs --- web/src/ui/MainScreen.tsx | 1 + web/src/ui/MainScreenContext.ts | 7 +++- web/src/ui/rightpanel/RightPanel.tsx | 30 +++++++++----- .../ui/timeline/content/TextMessageBody.tsx | 39 ++++++++++++++++--- web/src/vite-env.d.ts | 2 + 5 files changed, 62 insertions(+), 17 deletions(-) diff --git a/web/src/ui/MainScreen.tsx b/web/src/ui/MainScreen.tsx index 764971f..0d715cb 100644 --- a/web/src/ui/MainScreen.tsx +++ b/web/src/ui/MainScreen.tsx @@ -43,6 +43,7 @@ class ContextFields implements MainScreenContextFields { ) { this.keybindings = new Keybindings(client.store, this) client.store.switchRoom = this.setActiveRoom + window.mainScreenContext = this } setActiveRoom = (roomID: RoomID | null) => { diff --git a/web/src/ui/MainScreenContext.ts b/web/src/ui/MainScreenContext.ts index a892194..8823f4b 100644 --- a/web/src/ui/MainScreenContext.ts +++ b/web/src/ui/MainScreenContext.ts @@ -27,7 +27,7 @@ export interface MainScreenContextFields { clickRightPanelOpener: (evt: React.MouseEvent) => void } -const MainScreenContext = createContext({ +const stubContext = { get setActiveRoom(): never { throw new Error("MainScreenContext used outside main screen") }, @@ -46,6 +46,9 @@ const MainScreenContext = createContext({ get clickRightPanelOpener(): never { throw new Error("MainScreenContext used outside main screen") }, -}) +} + +const MainScreenContext = createContext(stubContext) +window.mainScreenContext = stubContext export default MainScreenContext diff --git a/web/src/ui/rightpanel/RightPanel.tsx b/web/src/ui/rightpanel/RightPanel.tsx index 8aca7fd..fe68933 100644 --- a/web/src/ui/rightpanel/RightPanel.tsx +++ b/web/src/ui/rightpanel/RightPanel.tsx @@ -14,43 +14,55 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import { JSX, use } from "react" +import type { UserID } from "@/api/types" import MainScreenContext from "../MainScreenContext.ts" import PinnedMessages from "./PinnedMessages.tsx" import CloseButton from "@/icons/close.svg?react" import "./RightPanel.css" -export type RightPanelType = "pinned-messages" | "members" +export type RightPanelType = "pinned-messages" | "members" | "user" -export interface RightPanelProps { - type: RightPanelType +interface RightPanelSimpleProps { + type: "pinned-messages" | "members" } +interface RightPanelUserProps { + type: "user" + userID: UserID +} + +export type RightPanelProps = RightPanelUserProps | RightPanelSimpleProps + function getTitle(type: RightPanelType): string { switch (type) { case "pinned-messages": return "Pinned Messages" case "members": return "Room Members" + case "user": + return "User Info" } } -function renderRightPanelContent({ type }: RightPanelProps): JSX.Element | null { - switch (type) { +function renderRightPanelContent(props: RightPanelProps): JSX.Element | null { + switch (props.type) { case "pinned-messages": return case "members": return <>Member list is not yet implemented + case "user": + return <>{props.userID} } } -const RightPanel = ({ type, ...rest }: RightPanelProps) => { +const RightPanel = (props: RightPanelProps) => { return
-
{getTitle(type)}
+
{getTitle(props.type)}
-
- {renderRightPanelContent({ type, ...rest })} +
+ {renderRightPanelContent(props)}
} diff --git a/web/src/ui/timeline/content/TextMessageBody.tsx b/web/src/ui/timeline/content/TextMessageBody.tsx index 9b4c2db..ab5ad64 100644 --- a/web/src/ui/timeline/content/TextMessageBody.tsx +++ b/web/src/ui/timeline/content/TextMessageBody.tsx @@ -20,15 +20,42 @@ function isImageElement(elem: EventTarget): elem is HTMLImageElement { return (elem as HTMLImageElement).tagName === "IMG" } -const onClickHTML = (evt: React.MouseEvent) => { - if (isImageElement(evt.target)) { - window.openLightbox({ - src: evt.target.src, - alt: evt.target.alt, +function isAnchorElement(elem: EventTarget): elem is HTMLAnchorElement { + return (elem as HTMLAnchorElement).tagName === "A" +} + +function onClickMatrixURI(href: string) { + const url = new URL(href) + const pathParts = url.pathname.split("/") + switch (pathParts[0]) { + case "u": + return window.mainScreenContext.setRightPanel({ + type: "user", + userID: pathParts[1], }) - } else if ((evt.target as HTMLElement).closest?.("span.hicli-spoiler")?.classList.toggle("spoiler-revealed")) { + case "roomid": + return window.mainScreenContext.setActiveRoom(pathParts[1]) + case "r": + return window.client.rpc.resolveAlias(`#${pathParts[1]}`).then( + res => window.mainScreenContext.setActiveRoom(res.room_id), + err => window.alert(`Failed to resolve room alias #${pathParts[1]}: ${err}`), + ) + } +} + +const onClickHTML = (evt: React.MouseEvent) => { + const targetElem = evt.target as HTMLElement + if (isImageElement(targetElem)) { + window.openLightbox({ + src: targetElem.src, + alt: targetElem.alt, + }) + } else if (targetElem.closest?.("span.hicli-spoiler")?.classList.toggle("spoiler-revealed")) { // When unspoilering, don't trigger links and other clickables inside the spoiler evt.preventDefault() + } else if (isAnchorElement(targetElem) && targetElem.href.startsWith("matrix:")) { + onClickMatrixURI(targetElem.href) + evt.preventDefault() } } diff --git a/web/src/vite-env.d.ts b/web/src/vite-env.d.ts index 27dd206..dc847ca 100644 --- a/web/src/vite-env.d.ts +++ b/web/src/vite-env.d.ts @@ -2,10 +2,12 @@ /// import type Client from "@/api/client.ts" +import type { MainScreenContextFields } from "@/ui/MainScreenContext.ts" declare global { interface Window { client: Client + mainScreenContext: MainScreenContextFields openLightbox: (params: { src: string, alt: string }) => void } }