mirror of
https://github.com/tulir/gomuks.git
synced 2025-04-20 18:43:41 -05:00
web/timeline: use blurhash before image loaded and for spoilers
Signed-off-by: Sumner Evans <me@sumnerevans.com>
This commit is contained in:
parent
e9f0d7e2a7
commit
b31c92def3
7 changed files with 101 additions and 10 deletions
|
@ -1,4 +1,4 @@
|
||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
cd web > /dev/null
|
cd web > /dev/null
|
||||||
if [[ -f "./node_modules/.bin/eslint" ]]; then
|
if [[ -f "./node_modules/.bin/eslint" ]]; then
|
||||||
ARGS=("$@")
|
ARGS=("$@")
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
cd web > /dev/null
|
cd web > /dev/null
|
||||||
if [[ -f "./node_modules/.bin/tsc" ]]; then
|
if [[ -f "./node_modules/.bin/tsc" ]]; then
|
||||||
tsc --build --noEmit
|
tsc --build --noEmit
|
||||||
|
|
18
web/package-lock.json
generated
18
web/package-lock.json
generated
|
@ -12,8 +12,10 @@
|
||||||
"@types/react": "npm:types-react@rc",
|
"@types/react": "npm:types-react@rc",
|
||||||
"@types/react-dom": "npm:types-react-dom@rc",
|
"@types/react-dom": "npm:types-react-dom@rc",
|
||||||
"@wailsio/runtime": "^3.0.0-alpha.29",
|
"@wailsio/runtime": "^3.0.0-alpha.29",
|
||||||
|
"blurhash": "^2.0.5",
|
||||||
"katex": "^0.16.11",
|
"katex": "^0.16.11",
|
||||||
"react": "^19.0.0-rc.1",
|
"react": "^19.0.0-rc.1",
|
||||||
|
"react-blurhash": "^0.3.0",
|
||||||
"react-dom": "^19.0.0-rc.1",
|
"react-dom": "^19.0.0-rc.1",
|
||||||
"react-spinners": "^0.14.1",
|
"react-spinners": "^0.14.1",
|
||||||
"unhomoglyph": "^1.0.6"
|
"unhomoglyph": "^1.0.6"
|
||||||
|
@ -2271,6 +2273,12 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/blurhash": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/blurhash/-/blurhash-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-cRygWd7kGBQO3VEhPiTgq4Wc43ctsM+o46urrmPOiuAe+07fzlSB9OJVdpgDL0jPqXUVQ9ht7aq7kxOeJHRK+w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||||
|
@ -4641,6 +4649,16 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-blurhash": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-blurhash/-/react-blurhash-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-XlKr4Ns1iYFRnk6DkAblNbAwN/bTJvxTVoxMvmTcURdc5oLoXZwqAF9N3LZUh/HT+QFlq5n6IS6VsDGsviYAiQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"blurhash": "^2.0.3",
|
||||||
|
"react": ">=15"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-dom": {
|
"node_modules/react-dom": {
|
||||||
"version": "19.0.0-rc.1",
|
"version": "19.0.0-rc.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0-rc.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0-rc.1.tgz",
|
||||||
|
|
|
@ -14,8 +14,10 @@
|
||||||
"@types/react": "npm:types-react@rc",
|
"@types/react": "npm:types-react@rc",
|
||||||
"@types/react-dom": "npm:types-react-dom@rc",
|
"@types/react-dom": "npm:types-react-dom@rc",
|
||||||
"@wailsio/runtime": "^3.0.0-alpha.29",
|
"@wailsio/runtime": "^3.0.0-alpha.29",
|
||||||
|
"blurhash": "^2.0.5",
|
||||||
"katex": "^0.16.11",
|
"katex": "^0.16.11",
|
||||||
"react": "^19.0.0-rc.1",
|
"react": "^19.0.0-rc.1",
|
||||||
|
"react-blurhash": "^0.3.0",
|
||||||
"react-dom": "^19.0.0-rc.1",
|
"react-dom": "^19.0.0-rc.1",
|
||||||
"react-spinners": "^0.14.1",
|
"react-spinners": "^0.14.1",
|
||||||
"unhomoglyph": "^1.0.6"
|
"unhomoglyph": "^1.0.6"
|
||||||
|
|
|
@ -126,6 +126,11 @@ export interface RelatesTo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ContentWarning {
|
||||||
|
type: string
|
||||||
|
description?: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface BaseMessageEventContent {
|
export interface BaseMessageEventContent {
|
||||||
msgtype: string
|
msgtype: string
|
||||||
body: string
|
body: string
|
||||||
|
@ -133,6 +138,8 @@ export interface BaseMessageEventContent {
|
||||||
format?: "org.matrix.custom.html"
|
format?: "org.matrix.custom.html"
|
||||||
"m.mentions"?: Mentions
|
"m.mentions"?: Mentions
|
||||||
"m.relates_to"?: RelatesTo
|
"m.relates_to"?: RelatesTo
|
||||||
|
"m.content_warning"?: ContentWarning
|
||||||
|
"town.robin.msc3725.content_warning"?: ContentWarning
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TextMessageEventContent extends BaseMessageEventContent {
|
export interface TextMessageEventContent extends BaseMessageEventContent {
|
||||||
|
@ -178,6 +185,7 @@ export interface MediaInfo {
|
||||||
|
|
||||||
"fi.mau.hide_controls"?: boolean
|
"fi.mau.hide_controls"?: boolean
|
||||||
"fi.mau.loop"?: boolean
|
"fi.mau.loop"?: boolean
|
||||||
|
"xyz.amorgan.blurhash"?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LocationMessageEventContent extends BaseMessageEventContent {
|
export interface LocationMessageEventContent extends BaseMessageEventContent {
|
||||||
|
|
|
@ -199,6 +199,21 @@ div.html-body {
|
||||||
}
|
}
|
||||||
|
|
||||||
div.media-container {
|
div.media-container {
|
||||||
|
&.image-container {
|
||||||
|
.placeholder {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.spoiler-indicator {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background-color: rgba(0,0,0,0.3);
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.video-container {
|
&.video-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 240px;
|
height: 240px;
|
||||||
|
|
|
@ -13,7 +13,8 @@
|
||||||
//
|
//
|
||||||
// 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 { CSSProperties, use } from "react"
|
import React, { CSSProperties, use } from "react"
|
||||||
|
import { Blurhash } from "react-blurhash"
|
||||||
import { getEncryptedMediaURL, getMediaURL } from "@/api/media.ts"
|
import { getEncryptedMediaURL, getMediaURL } from "@/api/media.ts"
|
||||||
import type { EventType, MediaMessageEventContent } from "@/api/types"
|
import type { EventType, MediaMessageEventContent } from "@/api/types"
|
||||||
import { ImageContainerSize, calculateMediaSize } from "@/util/mediasize.ts"
|
import { ImageContainerSize, calculateMediaSize } from "@/util/mediasize.ts"
|
||||||
|
@ -23,18 +24,65 @@ import DownloadIcon from "@/icons/download.svg?react"
|
||||||
export const useMediaContent = (
|
export const useMediaContent = (
|
||||||
content: MediaMessageEventContent, evtType: EventType, containerSize?: ImageContainerSize,
|
content: MediaMessageEventContent, evtType: EventType, containerSize?: ImageContainerSize,
|
||||||
): [React.ReactElement | null, string, CSSProperties] => {
|
): [React.ReactElement | null, string, CSSProperties] => {
|
||||||
|
const imgEl = React.useRef<HTMLImageElement>(null)
|
||||||
|
const [loaded, setLoaded] = React.useState(false)
|
||||||
|
const onImageLoaded = () => setLoaded(true)
|
||||||
|
|
||||||
|
const blurhashEl = React.useRef<Blurhash>(null)
|
||||||
|
const [spoilerShowing, setSpoilerShowing] = React.useState(false)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const imgElCurrent = imgEl.current
|
||||||
|
|
||||||
|
if (imgElCurrent) {
|
||||||
|
imgElCurrent.addEventListener("load", onImageLoaded)
|
||||||
|
return () => imgElCurrent.removeEventListener("load", onImageLoaded)
|
||||||
|
}
|
||||||
|
}, [imgEl])
|
||||||
|
|
||||||
const mediaURL = content.file?.url ? getEncryptedMediaURL(content.file.url) : getMediaURL(content.url)
|
const mediaURL = content.file?.url ? getEncryptedMediaURL(content.file.url) : getMediaURL(content.url)
|
||||||
const thumbnailURL = content.info?.thumbnail_file?.url
|
const thumbnailURL = content.info?.thumbnail_file?.url
|
||||||
? getEncryptedMediaURL(content.info.thumbnail_file.url) : getMediaURL(content.info?.thumbnail_url)
|
? getEncryptedMediaURL(content.info.thumbnail_file.url) : getMediaURL(content.info?.thumbnail_url)
|
||||||
if (content.msgtype === "m.image" || evtType === "m.sticker") {
|
if (content.msgtype === "m.image" || evtType === "m.sticker") {
|
||||||
const style = calculateMediaSize(content.info?.w, content.info?.h, containerSize)
|
const style = calculateMediaSize(content.info?.w, content.info?.h, containerSize)
|
||||||
return [<img
|
|
||||||
loading="lazy"
|
const blurhash = content.info ? content.info["xyz.amorgan.blurhash"] : undefined
|
||||||
style={style.media}
|
const isSpoiler = content["m.content_warning"]?.type === "m.spoiler" ||
|
||||||
src={mediaURL}
|
content["town.robin.msc3725.content_warning"]?.type === "town.robin.msc3725.spoiler"
|
||||||
alt={content.filename ?? content.body}
|
const showPlaceholder = (blurhash && !loaded) || (isSpoiler && !spoilerShowing)
|
||||||
onClick={use(LightboxContext)}
|
|
||||||
/>, "image-container", style.container]
|
const mediaStyle = Object.assign(
|
||||||
|
style.media,
|
||||||
|
showPlaceholder ? { display: "none" } : { display: "inline-block" })
|
||||||
|
|
||||||
|
return [
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
onClick={() => setSpoilerShowing(!spoilerShowing)}
|
||||||
|
style={!showPlaceholder ? { display: "none" } : {}}
|
||||||
|
className="placeholder"
|
||||||
|
>
|
||||||
|
{blurhash && <Blurhash
|
||||||
|
ref={blurhashEl}
|
||||||
|
hash={blurhash}
|
||||||
|
width={style.container.width}
|
||||||
|
height={style.container.height}
|
||||||
|
resolutionX={48}
|
||||||
|
resolutionY={48}
|
||||||
|
/>}
|
||||||
|
{!blurhash && <div style={style.container}>
|
||||||
|
</div>}
|
||||||
|
{isSpoiler && !spoilerShowing && <div className="spoiler-indicator">Spoiler</div>}
|
||||||
|
</div>
|
||||||
|
<img
|
||||||
|
ref={imgEl}
|
||||||
|
style={mediaStyle}
|
||||||
|
src={mediaURL}
|
||||||
|
alt={content.filename ?? content.body}
|
||||||
|
onClick={use(LightboxContext)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
, "image-container", style.container]
|
||||||
} else if (content.msgtype === "m.video") {
|
} else if (content.msgtype === "m.video") {
|
||||||
const autoplay = false
|
const autoplay = false
|
||||||
const controls = !content.info?.["fi.mau.hide_controls"]
|
const controls = !content.info?.["fi.mau.hide_controls"]
|
||||||
|
|
Loading…
Add table
Reference in a new issue