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"
|
class="text"
|
||||||
v-html="$options.filters.parse(message.text, message.users)"/>
|
v-html="$options.filters.parse(message.text, message.users)"/>
|
||||||
|
|
||||||
<div
|
<LinkPreview
|
||||||
v-for="preview in message.previews"
|
v-for="preview in message.previews"
|
||||||
:key="preview.link"
|
:key="preview.link"
|
||||||
:data-url="preview.link"
|
:link="preview"/>
|
||||||
class="preview"/>
|
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@ -44,8 +43,10 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Username from "./Username.vue";
|
import Username from "./Username.vue";
|
||||||
|
import LinkPreview from "./LinkPreview.vue";
|
||||||
import MessageTypes from "./MessageTypes";
|
import MessageTypes from "./MessageTypes";
|
||||||
|
|
||||||
|
MessageTypes.LinkPreview = LinkPreview;
|
||||||
MessageTypes.Username = Username;
|
MessageTypes.Username = Username;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -1,11 +1,19 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const $ = require("jquery");
|
|
||||||
const renderPreview = require("../renderPreview");
|
|
||||||
const socket = require("../socket");
|
const socket = require("../socket");
|
||||||
const utils = require("../utils");
|
const {vueApp, findChannel} = require("../vue");
|
||||||
|
|
||||||
socket.on("msg:preview", function(data) {
|
socket.on("msg:preview", function(data) {
|
||||||
// Previews are not as important, we can wait longer for them to appear
|
const {channel} = findChannel(data.chan);
|
||||||
utils.requestIdleCallback(() => renderPreview(data.preview, $("#msg-" + data.id)), 6000);
|
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 localetime = require("./libs/handlebars/localetime");
|
||||||
const localedate = require("./libs/handlebars/localedate");
|
const localedate = require("./libs/handlebars/localedate");
|
||||||
const friendlydate = require("./libs/handlebars/friendlydate");
|
const friendlydate = require("./libs/handlebars/friendlydate");
|
||||||
|
const friendlysize = require("./libs/handlebars/friendlysize");
|
||||||
const colorClass = require("./libs/handlebars/colorClass");
|
const colorClass = require("./libs/handlebars/colorClass");
|
||||||
|
|
||||||
Vue.filter("parse", parse);
|
Vue.filter("parse", parse);
|
||||||
@ -15,6 +16,7 @@ Vue.filter("tz", tz);
|
|||||||
Vue.filter("localetime", localetime);
|
Vue.filter("localetime", localetime);
|
||||||
Vue.filter("localedate", localedate);
|
Vue.filter("localedate", localedate);
|
||||||
Vue.filter("friendlydate", friendlydate);
|
Vue.filter("friendlydate", friendlydate);
|
||||||
|
Vue.filter("friendlysize", friendlysize);
|
||||||
Vue.filter("colorClass", colorClass);
|
Vue.filter("colorClass", colorClass);
|
||||||
Vue.filter("roundBadgeNumber", roundBadgeNumber);
|
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",
|
accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||||
language: client.language,
|
language: client.language,
|
||||||
}).then((res) => {
|
}).then((res) => {
|
||||||
parse(msg, preview, res, client);
|
parse(msg, chan, preview, res, client);
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
preview.type = "error";
|
preview.type = "error";
|
||||||
preview.error = "message";
|
preview.error = "message";
|
||||||
preview.message = err.message;
|
preview.message = err.message;
|
||||||
handlePreview(client, msg, preview, null);
|
handlePreview(client, chan, msg, preview, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
return cleanLinks;
|
return cleanLinks;
|
||||||
@ -80,7 +80,7 @@ function parseHtml(preview, res, client) {
|
|||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const $ = cheerio.load(res.data);
|
const $ = cheerio.load(res.data);
|
||||||
|
|
||||||
return parseHtmlMedia($, preview, res, client)
|
return parseHtmlMedia($, preview, client)
|
||||||
.then((newRes) => resolve(newRes))
|
.then((newRes) => resolve(newRes))
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
preview.type = "link";
|
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
let foundMedia = false;
|
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;
|
let promise;
|
||||||
|
|
||||||
switch (res.type) {
|
switch (res.type) {
|
||||||
@ -239,15 +239,15 @@ function parse(msg, preview, res, client) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!promise) {
|
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) {
|
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
|
// Get the correct file extension for the provided content-type
|
||||||
@ -262,17 +262,17 @@ function handlePreview(client, msg, preview, res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
preview.thumb = "";
|
preview.thumb = "";
|
||||||
return emitPreview(client, msg, preview);
|
return emitPreview(client, chan, msg, preview);
|
||||||
}
|
}
|
||||||
|
|
||||||
storage.store(res.data, extension, (uri) => {
|
storage.store(res.data, extension, (uri) => {
|
||||||
preview.thumb = 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
|
// If there is no title but there is preview or description, set title
|
||||||
// otherwise bail out and show no preview
|
// otherwise bail out and show no preview
|
||||||
if (!preview.head.length && preview.type === "link") {
|
if (!preview.head.length && preview.type === "link") {
|
||||||
@ -283,8 +283,11 @@ function emitPreview(client, msg, preview) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = msg.id;
|
client.emit("msg:preview", {
|
||||||
client.emit("msg:preview", {id, preview});
|
id: msg.id,
|
||||||
|
chan: chan.id,
|
||||||
|
preview: preview,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function removePreview(msg, preview) {
|
function removePreview(msg, preview) {
|
||||||
|
Loading…
Reference in New Issue
Block a user