This commit is contained in:
Nachtalb 2024-04-21 15:13:33 +02:00 committed by GitHub
commit fd62f334b8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 148 additions and 11 deletions

View file

@ -8,7 +8,11 @@
> >
<div <div
ref="content" ref="content"
:class="['toggle-content', 'toggle-type-' + link.type, {opened: isContentShown}]" :class="[
'toggle-content',
'toggle-type-' + link.type,
{opened: isContentShown, 'with-filename': link.filename},
]"
> >
<template v-if="link.type === 'link'"> <template v-if="link.type === 'link'">
<a <a
@ -64,13 +68,20 @@
<template v-else-if="link.type === 'image'"> <template v-else-if="link.type === 'image'">
<a <a
:href="link.link" :href="link.link"
class="toggle-thumbnail" :title="link.filename"
:class="['toggle-thumbnail', {'wide-view': useWideImageView}]"
target="_blank" target="_blank"
rel="noopener" rel="noopener"
@click="onThumbnailClick" @click="onThumbnailClick"
> >
<div v-if="link.filename" class="image-filename">
<span class="inner-image-filename">
{{ link.filename }}
</span>
</div>
<img <img
v-show="link.sourceLoaded" v-show="link.sourceLoaded"
ref="image"
:src="link.thumb" :src="link.thumb"
decoding="async" decoding="async"
alt="" alt=""
@ -79,6 +90,7 @@
</a> </a>
</template> </template>
<template v-else-if="link.type === 'video'"> <template v-else-if="link.type === 'video'">
<span v-if="link.filename" class="video-filename">{{ link.filename }}</span>
<video <video
v-show="link.sourceLoaded" v-show="link.sourceLoaded"
preload="metadata" preload="metadata"
@ -89,14 +101,28 @@
</video> </video>
</template> </template>
<template v-else-if="link.type === 'audio'"> <template v-else-if="link.type === 'audio'">
<audio <div>
v-show="link.sourceLoaded" <a
controls v-if="link.filename"
preload="metadata" :href="link.link"
@canplay="onPreviewReady" :title="link.filename"
> target="_blank"
<source :src="link.media" :type="link.mediaType" /> rel="noopener"
</audio> class="audio-filename"
>
<span class="inner-audio-filename">
{{ link.filename }}
</span>
</a>
<audio
v-show="link.sourceLoaded"
controls
preload="metadata"
@canplay="onPreviewReady"
>
<source :src="link.media" :type="link.mediaType" />
</audio>
</div>
</template> </template>
<template v-else-if="link.type === 'error'"> <template v-else-if="link.type === 'error'">
<em v-if="link.error === 'image-too-big'"> <em v-if="link.error === 'image-too-big'">
@ -167,7 +193,9 @@ export default defineComponent({
const showMoreButton = ref(false); const showMoreButton = ref(false);
const isContentShown = ref(false); const isContentShown = ref(false);
const useWideImageView = ref(false);
const imageViewer = inject(imageViewerKey); const imageViewer = inject(imageViewerKey);
const image = ref(null);
onBeforeRouteUpdate((to, from, next) => { onBeforeRouteUpdate((to, from, next) => {
// cancel the navigation if the user is trying to close the image viewer // cancel the navigation if the user is trying to close the image viewer
@ -217,6 +245,20 @@ export default defineComponent({
} }
}; };
const updateWideImageViewDecision = () => {
if (window.innerWidth < 480) {
// Mobile
useWideImageView.value =
(image.value && image.value.naturalWidth / image.value.naturalHeight <= 1.34) ||
false; // aspect ratio around 4:3 and slimmer
} else {
// Desktop
useWideImageView.value =
(image.value && image.value.naturalWidth / image.value.naturalHeight <= 1.6) ||
false; // aspect ratio 16:10 and slimmer
}
};
const onPreviewUpdate = () => { const onPreviewUpdate = () => {
// Don't display previews while they are loading on the server // Don't display previews while they are loading on the server
if (props.link.type === "loading") { if (props.link.type === "loading") {
@ -233,6 +275,8 @@ export default defineComponent({
handleResize(); handleResize();
props.keepScrollPosition(); props.keepScrollPosition();
} }
updateWideImageViewDecision();
}; };
const onThumbnailError = () => { const onThumbnailError = () => {
@ -321,8 +365,11 @@ export default defineComponent({
onPreviewUpdate, onPreviewUpdate,
showMoreButton, showMoreButton,
isContentShown, isContentShown,
useWideImageView,
image,
content, content,
container, container,
updateWideImageViewDecision,
}; };
}, },
}); });

View file

@ -1624,6 +1624,24 @@ textarea.input {
white-space: normal; white-space: normal;
} }
#chat .toggle-content .wide-view {
display: flex;
flex-direction: row;
}
#chat .toggle-content .wide-view .image-filename {
order: 2;
}
#chat .toggle-content .wide-view .image-filename span {
width: auto;
max-width: 500px;
}
#chat .toggle-content .wide-view img {
order: 1;
}
/* This applies to images of preview-type-image and thumbnails of preview-type-link */ /* This applies to images of preview-type-image and thumbnails of preview-type-link */
#chat .toggle-content img { #chat .toggle-content img {
max-width: 100%; max-width: 100%;
@ -1718,8 +1736,50 @@ textarea.input {
max-width: 100%; max-width: 100%;
} }
#chat .toggle-type-image > a {
position: relative;
font-weight: 700;
color: inherit;
}
#chat .toggle-type-image.with-filename .image-filename {
padding: 8px 10px 10px;
display: flex;
}
#chat .toggle-type-image .inner-image-filename,
#chat .toggle-type-audio .inner-audio-filename {
width: 0;
flex-grow: 1;
}
#chat .toggle-content.toggle-type-audio.with-filename {
padding: 8px 10px 10px;
}
#chat .toggle-content.toggle-type-audio .audio-filename {
font-weight: 700;
padding-bottom: 10px;
color: inherit;
display: flex;
}
#chat .toggle-type-audio .head {
padding-bottom: 10px;
}
#chat .toggle-type-video { #chat .toggle-type-video {
max-width: 640px; max-width: 640px;
position: relative;
}
#chat .toggle-type-video .video-filename {
position: absolute;
top: 0;
width: 100%;
padding: 1em;
background: linear-gradient(black, transparent);
color: white;
} }
#chat video { #chat video {

View file

@ -10,11 +10,14 @@ import storage from "../storage";
import Client from "../../client"; import Client from "../../client";
import Chan from "../../models/chan"; import Chan from "../../models/chan";
import Msg from "../../models/msg"; import Msg from "../../models/msg";
import contentDisposition from "content-disposition";
import path from "path";
type FetchRequest = { type FetchRequest = {
data: Buffer; data: Buffer;
type: string; type: string;
size: number; size: number;
filename: string | null;
}; };
const currentFetchPromises = new Map<string, Promise<FetchRequest>>(); const currentFetchPromises = new Map<string, Promise<FetchRequest>>();
const imageTypeRegex = /^image\/.+/; const imageTypeRegex = /^image\/.+/;
@ -30,6 +33,7 @@ export type LinkPreview = {
shown?: boolean | null; shown?: boolean | null;
error?: string; error?: string;
message?: string; message?: string;
filename: string | null;
media?: string; media?: string;
mediaType?: string; mediaType?: string;
@ -68,6 +72,7 @@ export default function (client: Client, chan: Chan, msg: Msg, cleanText: string
size: -1, size: -1,
link: link.link, // Send original matched link to the client link: link.link, // Send original matched link to the client
shown: null, shown: null,
filename: null,
}; };
cleanLinks.push(preview); cleanLinks.push(preview);
@ -243,6 +248,7 @@ function parse(msg: Msg, chan: Chan, preview: LinkPreview, res: FetchRequest, cl
let promise: Promise<FetchRequest | null> | null = null; let promise: Promise<FetchRequest | null> | null = null;
preview.size = res.size; preview.size = res.size;
preview.filename = res.filename;
switch (res.type) { switch (res.type) {
case "text/html": case "text/html":
@ -431,6 +437,7 @@ function fetch(uri: string, headers: Record<string, string>) {
let contentLength = 0; let contentLength = 0;
let contentType: string | undefined; let contentType: string | undefined;
let limit = Config.values.prefetchMaxImageSize * 1024; let limit = Config.values.prefetchMaxImageSize * 1024;
let filename: string | null = null;
try { try {
const gotStream = got.stream(uri, { const gotStream = got.stream(uri, {
@ -445,6 +452,17 @@ function fetch(uri: string, headers: Record<string, string>) {
contentLength = parseInt(res.headers["content-length"], 10) || 0; contentLength = parseInt(res.headers["content-length"], 10) || 0;
contentType = res.headers["content-type"]; contentType = res.headers["content-type"];
filename =
"content-disposition" in res.headers
? contentDisposition?.parse(res.headers["content-disposition"])
.parameters.filename || null
: null;
if (filename === null) {
const basename = decodeURI(path.basename(new URL(uri).pathname));
filename = basename.indexOf(".") > 0 ? basename : null;
}
if (contentType && imageTypeRegex.test(contentType)) { if (contentType && imageTypeRegex.test(contentType)) {
// response is an image // response is an image
// if Content-Length header reports a size exceeding the prefetch limit, abort fetch // if Content-Length header reports a size exceeding the prefetch limit, abort fetch
@ -488,7 +506,7 @@ function fetch(uri: string, headers: Record<string, string>) {
type = contentType.split(/ *; */).shift() || ""; type = contentType.split(/ *; */).shift() || "";
} }
resolve({data: buffer, type, size}); resolve({data: buffer, type, size, filename});
}); });
} catch (e: any) { } catch (e: any) {
return reject(e); return reject(e);

View file

@ -20,6 +20,7 @@ const inlineContentDispositionTypes = {
"audio/mpeg": "audio.mp3", "audio/mpeg": "audio.mp3",
"audio/ogg": "audio.ogg", "audio/ogg": "audio.ogg",
"audio/vnd.wave": "audio.wav", "audio/vnd.wave": "audio.wav",
"audio/flac": "audio.flac",
"audio/x-flac": "audio.flac", "audio/x-flac": "audio.flac",
"audio/x-m4a": "audio.m4a", "audio/x-m4a": "audio.m4a",
"image/bmp": "image.bmp", "image/bmp": "image.bmp",
@ -124,6 +125,12 @@ class Uploader {
detectedMimeType = "video/mp4"; detectedMimeType = "video/mp4";
} }
if (detectedMimeType === "audio/x-flac") {
// Send a more common mime type for wave audio files
// so that browsers can play them correctly
detectedMimeType = "audio/flac";
}
res.setHeader("Content-Disposition", disposition); res.setHeader("Content-Disposition", disposition);
res.setHeader("Cache-Control", "max-age=86400"); res.setHeader("Cache-Control", "max-age=86400");
res.contentType(detectedMimeType); res.contentType(detectedMimeType);

View file

@ -56,6 +56,7 @@ Vivamus bibendum vulputate tincidunt. Sed vitae ligula felis.`;
expect(message.previews).to.deep.equal([ expect(message.previews).to.deep.equal([
{ {
body: "", body: "",
filename: null,
head: "", head: "",
link: url, link: url,
thumb: "", thumb: "",
@ -93,6 +94,7 @@ Vivamus bibendum vulputate tincidunt. Sed vitae ligula felis.`;
expect(message.previews).to.deep.equal([ expect(message.previews).to.deep.equal([
{ {
body: "", body: "",
filename: null,
head: "", head: "",
link: url, link: url,
thumb: "", thumb: "",
@ -425,6 +427,7 @@ Vivamus bibendum vulputate tincidunt. Sed vitae ligula felis.`;
expect(message.previews).to.eql([ expect(message.previews).to.eql([
{ {
body: "", body: "",
filename: null,
head: "", head: "",
link: url_one, link: url_one,
thumb: "", thumb: "",
@ -434,6 +437,7 @@ Vivamus bibendum vulputate tincidunt. Sed vitae ligula felis.`;
}, },
{ {
body: "", body: "",
filename: null,
head: "", head: "",
link: url_two, link: url_two,
thumb: "", thumb: "",
@ -608,6 +612,7 @@ Vivamus bibendum vulputate tincidunt. Sed vitae ligula felis.`;
expect(message.previews).to.deep.equal([ expect(message.previews).to.deep.equal([
{ {
type: "loading", type: "loading",
filename: null,
head: "", head: "",
body: "", body: "",
thumb: "", thumb: "",