web/timeline: add click handlers for matrix URIs

This commit is contained in:
Tulir Asokan 2024-11-09 12:52:22 +01:00
parent c6992b0fca
commit c2d0020c8c
5 changed files with 62 additions and 17 deletions

View file

@ -43,6 +43,7 @@ class ContextFields implements MainScreenContextFields {
) { ) {
this.keybindings = new Keybindings(client.store, this) this.keybindings = new Keybindings(client.store, this)
client.store.switchRoom = this.setActiveRoom client.store.switchRoom = this.setActiveRoom
window.mainScreenContext = this
} }
setActiveRoom = (roomID: RoomID | null) => { setActiveRoom = (roomID: RoomID | null) => {

View file

@ -27,7 +27,7 @@ export interface MainScreenContextFields {
clickRightPanelOpener: (evt: React.MouseEvent) => void clickRightPanelOpener: (evt: React.MouseEvent) => void
} }
const MainScreenContext = createContext<MainScreenContextFields>({ const stubContext = {
get setActiveRoom(): never { get setActiveRoom(): never {
throw new Error("MainScreenContext used outside main screen") throw new Error("MainScreenContext used outside main screen")
}, },
@ -46,6 +46,9 @@ const MainScreenContext = createContext<MainScreenContextFields>({
get clickRightPanelOpener(): never { get clickRightPanelOpener(): never {
throw new Error("MainScreenContext used outside main screen") throw new Error("MainScreenContext used outside main screen")
}, },
}) }
const MainScreenContext = createContext<MainScreenContextFields>(stubContext)
window.mainScreenContext = stubContext
export default MainScreenContext export default MainScreenContext

View file

@ -14,43 +14,55 @@
// 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 { JSX, use } from "react" import { JSX, use } from "react"
import type { UserID } from "@/api/types"
import MainScreenContext from "../MainScreenContext.ts" import MainScreenContext from "../MainScreenContext.ts"
import PinnedMessages from "./PinnedMessages.tsx" import PinnedMessages from "./PinnedMessages.tsx"
import CloseButton from "@/icons/close.svg?react" import CloseButton from "@/icons/close.svg?react"
import "./RightPanel.css" import "./RightPanel.css"
export type RightPanelType = "pinned-messages" | "members" export type RightPanelType = "pinned-messages" | "members" | "user"
export interface RightPanelProps { interface RightPanelSimpleProps {
type: RightPanelType type: "pinned-messages" | "members"
} }
interface RightPanelUserProps {
type: "user"
userID: UserID
}
export type RightPanelProps = RightPanelUserProps | RightPanelSimpleProps
function getTitle(type: RightPanelType): string { function getTitle(type: RightPanelType): string {
switch (type) { switch (type) {
case "pinned-messages": case "pinned-messages":
return "Pinned Messages" return "Pinned Messages"
case "members": case "members":
return "Room Members" return "Room Members"
case "user":
return "User Info"
} }
} }
function renderRightPanelContent({ type }: RightPanelProps): JSX.Element | null { function renderRightPanelContent(props: RightPanelProps): JSX.Element | null {
switch (type) { switch (props.type) {
case "pinned-messages": case "pinned-messages":
return <PinnedMessages /> return <PinnedMessages />
case "members": case "members":
return <>Member list is not yet implemented</> return <>Member list is not yet implemented</>
case "user":
return <>{props.userID}</>
} }
} }
const RightPanel = ({ type, ...rest }: RightPanelProps) => { const RightPanel = (props: RightPanelProps) => {
return <div className="right-panel"> return <div className="right-panel">
<div className="right-panel-header"> <div className="right-panel-header">
<div className="panel-name">{getTitle(type)}</div> <div className="panel-name">{getTitle(props.type)}</div>
<button onClick={use(MainScreenContext).closeRightPanel}><CloseButton/></button> <button onClick={use(MainScreenContext).closeRightPanel}><CloseButton/></button>
</div> </div>
<div className={`right-panel-content ${type}`}> <div className={`right-panel-content ${props.type}`}>
{renderRightPanelContent({ type, ...rest })} {renderRightPanelContent(props)}
</div> </div>
</div> </div>
} }

View file

@ -20,15 +20,42 @@ function isImageElement(elem: EventTarget): elem is HTMLImageElement {
return (elem as HTMLImageElement).tagName === "IMG" return (elem as HTMLImageElement).tagName === "IMG"
} }
const onClickHTML = (evt: React.MouseEvent<HTMLDivElement>) => { function isAnchorElement(elem: EventTarget): elem is HTMLAnchorElement {
if (isImageElement(evt.target)) { return (elem as HTMLAnchorElement).tagName === "A"
window.openLightbox({ }
src: evt.target.src,
alt: evt.target.alt, 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<HTMLDivElement>) => {
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 // When unspoilering, don't trigger links and other clickables inside the spoiler
evt.preventDefault() evt.preventDefault()
} else if (isAnchorElement(targetElem) && targetElem.href.startsWith("matrix:")) {
onClickMatrixURI(targetElem.href)
evt.preventDefault()
} }
} }

View file

@ -2,10 +2,12 @@
/// <reference types="vite-plugin-svgr/client" /> /// <reference types="vite-plugin-svgr/client" />
import type Client from "@/api/client.ts" import type Client from "@/api/client.ts"
import type { MainScreenContextFields } from "@/ui/MainScreenContext.ts"
declare global { declare global {
interface Window { interface Window {
client: Client client: Client
mainScreenContext: MainScreenContextFields
openLightbox: (params: { src: string, alt: string }) => void openLightbox: (params: { src: string, alt: string }) => void
} }
} }