diff --git a/web/src/App.tsx b/web/src/App.tsx index bc64f93..3338e23 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -18,6 +18,7 @@ import { ScaleLoader } from "react-spinners" import Client from "./api/client.ts" import WSClient from "./api/wsclient.ts" import { ClientContext } from "./ui/ClientContext.ts" +import { LightboxWrapper } from "./ui/Lightbox.tsx" import MainScreen from "./ui/MainScreen.tsx" import { LoginScreen, VerificationScreen } from "./ui/login" import { useEventAsState } from "./util/eventdispatcher.ts" @@ -56,7 +57,11 @@ function App() { } else if (!clientState.is_verified) { return } else { - return + return + + + + } } diff --git a/web/src/ui/Lightbox.css b/web/src/ui/Lightbox.css new file mode 100644 index 0000000..aa21535 --- /dev/null +++ b/web/src/ui/Lightbox.css @@ -0,0 +1,13 @@ +div.overlay { + position: fixed; + inset: 0; + background-color: rgba(0, 0, 0, 0.75); + display: flex; + align-items: center; + justify-content: center; + + &.lightbox > img { + max-width: 75%; + max-height: 75%; + } +} diff --git a/web/src/ui/Lightbox.tsx b/web/src/ui/Lightbox.tsx new file mode 100644 index 0000000..faf8bff --- /dev/null +++ b/web/src/ui/Lightbox.tsx @@ -0,0 +1,146 @@ +// 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 . +import React, { Component, RefObject, createContext, createRef, useCallback, useState } from "react" +import "./Lightbox.css" + +const isTouchDevice = window.ontouchstart !== undefined + +export interface LightboxParams { + src: string + alt: string +} + +type openLightbox = (params: LightboxParams | React.MouseEvent) => void + +export const LightboxContext = createContext(() => + console.error("Tried to open lightbox without being inside context")) + +export const LightboxWrapper = ({ children }: { children: React.ReactNode }) => { + const [params, setParams] = useState(null) + const onOpen = useCallback((params: LightboxParams | React.MouseEvent) => { + if ((params as React.MouseEvent).target) { + const evt = params as React.MouseEvent + const target = evt.currentTarget as HTMLImageElement + setParams({ + src: target.src, + alt: target.alt, + }) + } else { + setParams(params as LightboxParams) + } + }, []) + const onClose = useCallback(() => setParams(null), []) + return <> + + {children} + + {params && } + +} + +export interface LightboxProps extends LightboxParams { + onClose: () => void +} + +export class Lightbox extends Component { + transform = { zoom: 1, x: 0, y: 0 } + maybePanning = false + readonly ref: RefObject + + constructor(props: LightboxProps) { + super(props) + this.ref = createRef() + } + + transformString = () => { + return `translate(${this.transform.x}px, ${this.transform.y}px) scale(${this.transform.zoom})` + } + + onClick = () => { + if (!this.ref.current) { + return + } + if (this.ref.current.style.cursor === "grabbing") { + this.ref.current.style.cursor = "auto" + this.maybePanning = false + } else { + this.transform = { zoom: 1, x: 0, y: 0 } + this.props.onClose() + } + } + + onWheel = (evt: React.WheelEvent) => { + if (!this.ref.current) { + return + } + evt.preventDefault() + const oldZoom = this.transform.zoom + const delta = -evt.deltaY / 1000 + const newDelta = this.transform.zoom + delta * this.transform.zoom + this.transform.zoom = Math.min(Math.max(newDelta, 0.01), 10) + const zoomDelta = this.transform.zoom - oldZoom + this.transform.x += zoomDelta * (this.ref.current.clientWidth / 2 - evt.nativeEvent.offsetX) + this.transform.y += zoomDelta * (this.ref.current.clientHeight / 2 - evt.nativeEvent.offsetY) + this.ref.current.style.transform = this.transformString() + } + + onMouseDown = (evt: React.MouseEvent) => { + if (evt.buttons === 1) { + evt.preventDefault() + evt.stopPropagation() + this.maybePanning = true + } + } + + onMouseMove = (evt: React.MouseEvent) => { + if (!this.ref.current) { + return + } + if (evt.buttons !== 1 || !this.maybePanning) { + this.maybePanning = false + return + } + evt.preventDefault() + this.transform.x += evt.movementX + this.transform.y += evt.movementY + this.ref.current.style.transform = this.transformString() + this.ref.current.style.cursor = "grabbing" + } + + get style() { + return { + transform: this.transformString(), + } + } + + render() { + return
+ {this.props.alt} +
+ } +} diff --git a/web/src/ui/timeline/content/MessageBody.tsx b/web/src/ui/timeline/content/MessageBody.tsx index 3193339..f58e3d9 100644 --- a/web/src/ui/timeline/content/MessageBody.tsx +++ b/web/src/ui/timeline/content/MessageBody.tsx @@ -13,13 +13,14 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import { CSSProperties } from "react" +import { use } from "react" import sanitizeHtml from "sanitize-html" import { getMediaURL } from "../../../api/media.ts" import { ContentURI } from "../../../api/types" import { sanitizeHtmlParams } from "../../../util/html.ts" -import { EventContentProps } from "./props.ts" import { calculateMediaSize } from "../../../util/mediasize.ts" +import { LightboxContext } from "../../Lightbox.tsx" +import { EventContentProps } from "./props.ts" interface BaseMessageEventContent { msgtype: string @@ -76,14 +77,15 @@ const MessageBody = ({ event }: EventContentProps) => { } return content.body case "m.image": { + const openLightbox = use(LightboxContext) const style = calculateMediaSize(content.info?.w, content.info?.h) if (content.url) { return
- {content.body}/ + {content.body}
} else if (content.file) { return
- {content.body}/ + {content.body}
} }