mirror of
https://github.com/tulir/gomuks.git
synced 2025-04-19 18:13:41 -05:00
web: add lightbox for viewing images
This commit is contained in:
parent
33f67b65a8
commit
63268a0ccb
4 changed files with 171 additions and 5 deletions
|
@ -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 <VerificationScreen client={client} clientState={clientState}/>
|
||||
} else {
|
||||
return <ClientContext value={client}><MainScreen /></ClientContext>
|
||||
return <ClientContext value={client}>
|
||||
<LightboxWrapper>
|
||||
<MainScreen />
|
||||
</LightboxWrapper>
|
||||
</ClientContext>
|
||||
}
|
||||
}
|
||||
|
||||
|
|
13
web/src/ui/Lightbox.css
Normal file
13
web/src/ui/Lightbox.css
Normal file
|
@ -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%;
|
||||
}
|
||||
}
|
146
web/src/ui/Lightbox.tsx
Normal file
146
web/src/ui/Lightbox.tsx
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
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<HTMLImageElement>) => void
|
||||
|
||||
export const LightboxContext = createContext<openLightbox>(() =>
|
||||
console.error("Tried to open lightbox without being inside context"))
|
||||
|
||||
export const LightboxWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const [params, setParams] = useState<LightboxParams | null>(null)
|
||||
const onOpen = useCallback((params: LightboxParams | React.MouseEvent<HTMLImageElement>) => {
|
||||
if ((params as React.MouseEvent).target) {
|
||||
const evt = params as React.MouseEvent<HTMLImageElement>
|
||||
const target = evt.currentTarget as HTMLImageElement
|
||||
setParams({
|
||||
src: target.src,
|
||||
alt: target.alt,
|
||||
})
|
||||
} else {
|
||||
setParams(params as LightboxParams)
|
||||
}
|
||||
}, [])
|
||||
const onClose = useCallback(() => setParams(null), [])
|
||||
return <>
|
||||
<LightboxContext value={onOpen}>
|
||||
{children}
|
||||
</LightboxContext>
|
||||
{params && <Lightbox {...params} onClose={onClose} />}
|
||||
</>
|
||||
}
|
||||
|
||||
export interface LightboxProps extends LightboxParams {
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export class Lightbox extends Component<LightboxProps> {
|
||||
transform = { zoom: 1, x: 0, y: 0 }
|
||||
maybePanning = false
|
||||
readonly ref: RefObject<HTMLImageElement | null>
|
||||
|
||||
constructor(props: LightboxProps) {
|
||||
super(props)
|
||||
this.ref = createRef<HTMLImageElement>()
|
||||
}
|
||||
|
||||
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 <div
|
||||
className="overlay lightbox"
|
||||
onClick={this.onClick}
|
||||
onMouseMove={isTouchDevice ? undefined : this.onMouseMove}
|
||||
>
|
||||
<img
|
||||
onMouseDown={isTouchDevice ? undefined : this.onMouseDown}
|
||||
onWheel={isTouchDevice ? undefined : this.onWheel}
|
||||
src={this.props.src}
|
||||
alt={this.props.alt}
|
||||
ref={this.ref}
|
||||
style={this.style}
|
||||
draggable="false"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
}
|
|
@ -13,13 +13,14 @@
|
|||
//
|
||||
// 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 { 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 <div className="media-container" style={style.container}>
|
||||
<img style={style.media} src={getMediaURL(content.url)} alt={content.body}/>
|
||||
<img style={style.media} src={getMediaURL(content.url)} alt={content.body} onClick={openLightbox}/>
|
||||
</div>
|
||||
} else if (content.file) {
|
||||
return <div className="media-container" style={style.container}>
|
||||
<img style={style.media} src={getMediaURL(content.file.url)} alt={content.body}/>
|
||||
<img style={style.media} src={getMediaURL(content.file.url)} alt={content.body} onClick={openLightbox}/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue