mirror of
https://github.com/thelounge/thelounge.git
synced 2024-06-04 14:52:19 +02:00
Merge 57141f983a
into 549c445853
This commit is contained in:
commit
fd62f334b8
|
@ -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,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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: "",
|
||||||
|
|
Loading…
Reference in a new issue