Render link previews in Vue
This commit is contained in:
parent
5f5b5fef3d
commit
595915fefd
82
client/components/LinkPreview.vue
Normal file
82
client/components/LinkPreview.vue
Normal file
@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<div class="preview">
|
||||
<div :class="['toggle-content', 'toggle-type-' + link.type, {show: link.shown}]">
|
||||
<template v-if="link.type === 'link'">
|
||||
<a v-if="link.thumb" class="toggle-thumbnail" :href="link.link" target="_blank" rel="noopener">
|
||||
<img :src="link.thumb" decoding="async" alt="" class="thumb">
|
||||
</a>
|
||||
<div class="toggle-text">
|
||||
<div class="head">
|
||||
<div class="overflowable">
|
||||
<a :href="link.link" target="_blank" rel="noopener" :title="link.head">{{link.head}}</a>
|
||||
</div>
|
||||
|
||||
<button class="more"
|
||||
aria-expanded="false"
|
||||
aria-label="More"
|
||||
data-closed-text="More"
|
||||
data-opened-text="Less"
|
||||
>
|
||||
<span class="more-caret"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="body overflowable">
|
||||
<a :href="link.link" target="_blank" rel="noopener" :title="link.body">{{link.body}}</a>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="link.type === 'image'">
|
||||
<a class="toggle-thumbnail" :href="link.link" target="_blank" rel="noopener">
|
||||
<img :src="link.thumb" decoding="async" alt="">
|
||||
</a>
|
||||
</template>
|
||||
<template v-else-if="link.type === 'video'">
|
||||
<video preload="metadata" controls>
|
||||
<source :src="link.media" :type="link.mediaType">
|
||||
</video>
|
||||
</template>
|
||||
<template v-else-if="link.type === 'audio'">
|
||||
<audio controls preload="metadata">
|
||||
<source :src="link.media" :type="link.mediaType">
|
||||
</audio>
|
||||
</template>
|
||||
<template v-else-if="link.type === 'error'">
|
||||
<em v-if="link.error === 'image-too-big'">
|
||||
This image is larger than {{maxSize | friendlysize}} and cannot be
|
||||
previewed.
|
||||
<a :href="link.link" target="_blank" rel="noopener">Click here</a>
|
||||
to open it in a new window.
|
||||
</em>
|
||||
<template v-else-if="link.error === 'message'">
|
||||
<div>
|
||||
<em>
|
||||
A preview could not be loaded.
|
||||
<a :href="link.link" target="_blank" rel="noopener">Click here</a>
|
||||
to open it in a new window.
|
||||
</em>
|
||||
<br>
|
||||
<pre class="prefetch-error">{{link.message}}</pre>
|
||||
</div>
|
||||
|
||||
<button class="more"
|
||||
aria-expanded="false"
|
||||
aria-label="More"
|
||||
data-closed-text="More"
|
||||
data-opened-text="Less"
|
||||
><span class="more-caret"/></button>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "LinkPreview",
|
||||
props: {
|
||||
link: Object,
|
||||
},
|
||||
};
|
||||
</script>
|
@ -32,11 +32,10 @@
|
||||
class="text"
|
||||
v-html="$options.filters.parse(message.text, message.users)"/>
|
||||
|
||||
<div
|
||||
<LinkPreview
|
||||
v-for="preview in message.previews"
|
||||
:key="preview.link"
|
||||
:data-url="preview.link"
|
||||
class="preview"/>
|
||||
:link="preview"/>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
@ -44,8 +43,10 @@
|
||||
|
||||
<script>
|
||||
import Username from "./Username.vue";
|
||||
import LinkPreview from "./LinkPreview.vue";
|
||||
import MessageTypes from "./MessageTypes";
|
||||
|
||||
MessageTypes.LinkPreview = LinkPreview;
|
||||
MessageTypes.Username = Username;
|
||||
|
||||
export default {
|
||||
|
@ -1,11 +1,19 @@
|
||||
"use strict";
|
||||
|
||||
const $ = require("jquery");
|
||||
const renderPreview = require("../renderPreview");
|
||||
const socket = require("../socket");
|
||||
const utils = require("../utils");
|
||||
const {vueApp, findChannel} = require("../vue");
|
||||
|
||||
socket.on("msg:preview", function(data) {
|
||||
// Previews are not as important, we can wait longer for them to appear
|
||||
utils.requestIdleCallback(() => renderPreview(data.preview, $("#msg-" + data.id)), 6000);
|
||||
const {channel} = findChannel(data.chan);
|
||||
const message = channel.messages.find((m) => m.id === data.id);
|
||||
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
const previewIndex = message.previews.findIndex((m) => m.link === data.preview.link);
|
||||
|
||||
if (previewIndex > -1) {
|
||||
vueApp.$set(message.previews, previewIndex, data.preview);
|
||||
}
|
||||
});
|
||||
|
@ -8,6 +8,7 @@ const tz = require("./libs/handlebars/tz");
|
||||
const localetime = require("./libs/handlebars/localetime");
|
||||
const localedate = require("./libs/handlebars/localedate");
|
||||
const friendlydate = require("./libs/handlebars/friendlydate");
|
||||
const friendlysize = require("./libs/handlebars/friendlysize");
|
||||
const colorClass = require("./libs/handlebars/colorClass");
|
||||
|
||||
Vue.filter("parse", parse);
|
||||
@ -15,6 +16,7 @@ Vue.filter("tz", tz);
|
||||
Vue.filter("localetime", localetime);
|
||||
Vue.filter("localedate", localedate);
|
||||
Vue.filter("friendlydate", friendlydate);
|
||||
Vue.filter("friendlysize", friendlysize);
|
||||
Vue.filter("colorClass", colorClass);
|
||||
Vue.filter("roundBadgeNumber", roundBadgeNumber);
|
||||
|
||||
|
@ -1,82 +0,0 @@
|
||||
{{#preview}}
|
||||
<div class="toggle-content toggle-type-{{type}}{{#if shown}} show{{/if}}">
|
||||
{{#equal type "image"}}
|
||||
<a class="toggle-thumbnail" href="{{link}}" target="_blank" rel="noopener">
|
||||
<img src="{{thumb}}" decoding="async" alt="">
|
||||
</a>
|
||||
{{/equal}}
|
||||
{{#equal type "audio"}}
|
||||
<audio controls preload="metadata">
|
||||
<source src="{{media}}" type="{{mediaType}}">
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
{{/equal}}
|
||||
{{#equal type "video"}}
|
||||
<video preload="metadata" controls>
|
||||
<source src="{{media}}" type="{{mediaType}}">
|
||||
Your browser does not support the video element.
|
||||
</video>
|
||||
{{/equal}}
|
||||
{{#equal type "link"}}
|
||||
{{#if thumb}}
|
||||
<a class="toggle-thumbnail" href="{{link}}" target="_blank" rel="noopener">
|
||||
<img src="{{thumb}}" decoding="async" alt="" class="thumb">
|
||||
</a>
|
||||
{{/if}}
|
||||
<div class="toggle-text">
|
||||
<div class="head">
|
||||
<div class="overflowable">
|
||||
<a href="{{link}}" target="_blank" rel="noopener" title="{{head}}">
|
||||
{{head}}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<button class="more"
|
||||
aria-expanded="false"
|
||||
aria-label="More"
|
||||
data-closed-text="More"
|
||||
data-opened-text="Less"
|
||||
>
|
||||
<span class="more-caret"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="body overflowable">
|
||||
<a href="{{link}}" target="_blank" rel="noopener" title="{{body}}">
|
||||
{{body}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{{/equal}}
|
||||
{{#equal type "error"}}
|
||||
{{#equal error "image-too-big"}}
|
||||
<em>
|
||||
This image is larger than {{friendlysize maxSize}} and cannot be
|
||||
previewed.
|
||||
<a href="{{link}}" target="_blank" rel="noopener">Click here</a>
|
||||
to open it in a new window.
|
||||
</em>
|
||||
{{/equal}}
|
||||
{{#equal error "message"}}
|
||||
<div>
|
||||
<em>
|
||||
A preview could not be loaded.
|
||||
<a href="{{link}}" target="_blank" rel="noopener">Click here</a>
|
||||
to open it in a new window.
|
||||
</em>
|
||||
<br>
|
||||
<pre class="prefetch-error">{{message}}</pre>
|
||||
</div>
|
||||
|
||||
<button class="more"
|
||||
aria-expanded="false"
|
||||
aria-label="More"
|
||||
data-closed-text="More"
|
||||
data-opened-text="Less"
|
||||
>
|
||||
<span class="more-caret"></span>
|
||||
</button>
|
||||
{{/equal}}
|
||||
{{/equal}}
|
||||
</div>
|
||||
{{/preview}}
|
@ -64,12 +64,12 @@ module.exports = function(client, chan, msg) {
|
||||
accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
language: client.language,
|
||||
}).then((res) => {
|
||||
parse(msg, preview, res, client);
|
||||
parse(msg, chan, preview, res, client);
|
||||
}).catch((err) => {
|
||||
preview.type = "error";
|
||||
preview.error = "message";
|
||||
preview.message = err.message;
|
||||
handlePreview(client, msg, preview, null);
|
||||
handlePreview(client, chan, msg, preview, null);
|
||||
});
|
||||
|
||||
return cleanLinks;
|
||||
@ -80,7 +80,7 @@ function parseHtml(preview, res, client) {
|
||||
return new Promise((resolve) => {
|
||||
const $ = cheerio.load(res.data);
|
||||
|
||||
return parseHtmlMedia($, preview, res, client)
|
||||
return parseHtmlMedia($, preview, client)
|
||||
.then((newRes) => resolve(newRes))
|
||||
.catch(() => {
|
||||
preview.type = "link";
|
||||
@ -124,7 +124,7 @@ function parseHtml(preview, res, client) {
|
||||
});
|
||||
}
|
||||
|
||||
function parseHtmlMedia($, preview, res, client) {
|
||||
function parseHtmlMedia($, preview, client) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let foundMedia = false;
|
||||
|
||||
@ -178,7 +178,7 @@ function parseHtmlMedia($, preview, res, client) {
|
||||
});
|
||||
}
|
||||
|
||||
function parse(msg, preview, res, client) {
|
||||
function parse(msg, chan, preview, res, client) {
|
||||
let promise;
|
||||
|
||||
switch (res.type) {
|
||||
@ -239,15 +239,15 @@ function parse(msg, preview, res, client) {
|
||||
}
|
||||
|
||||
if (!promise) {
|
||||
return handlePreview(client, msg, preview, res);
|
||||
return handlePreview(client, chan, msg, preview, res);
|
||||
}
|
||||
|
||||
promise.then((newRes) => handlePreview(client, msg, preview, newRes));
|
||||
promise.then((newRes) => handlePreview(client, chan, msg, preview, newRes));
|
||||
}
|
||||
|
||||
function handlePreview(client, msg, preview, res) {
|
||||
function handlePreview(client, chan, msg, preview, res) {
|
||||
if (!preview.thumb.length || !Helper.config.prefetchStorage) {
|
||||
return emitPreview(client, msg, preview);
|
||||
return emitPreview(client, chan, msg, preview);
|
||||
}
|
||||
|
||||
// Get the correct file extension for the provided content-type
|
||||
@ -262,17 +262,17 @@ function handlePreview(client, msg, preview, res) {
|
||||
}
|
||||
|
||||
preview.thumb = "";
|
||||
return emitPreview(client, msg, preview);
|
||||
return emitPreview(client, chan, msg, preview);
|
||||
}
|
||||
|
||||
storage.store(res.data, extension, (uri) => {
|
||||
preview.thumb = uri;
|
||||
|
||||
emitPreview(client, msg, preview);
|
||||
emitPreview(client, chan, msg, preview);
|
||||
});
|
||||
}
|
||||
|
||||
function emitPreview(client, msg, preview) {
|
||||
function emitPreview(client, chan, msg, preview) {
|
||||
// If there is no title but there is preview or description, set title
|
||||
// otherwise bail out and show no preview
|
||||
if (!preview.head.length && preview.type === "link") {
|
||||
@ -283,8 +283,11 @@ function emitPreview(client, msg, preview) {
|
||||
}
|
||||
}
|
||||
|
||||
const id = msg.id;
|
||||
client.emit("msg:preview", {id, preview});
|
||||
client.emit("msg:preview", {
|
||||
id: msg.id,
|
||||
chan: chan.id,
|
||||
preview: preview,
|
||||
});
|
||||
}
|
||||
|
||||
function removePreview(msg, preview) {
|
||||
|
Loading…
Reference in New Issue
Block a user