Render link previews in Vue

This commit is contained in:
Pavel Djundik 2018-07-10 14:57:11 +03:00 committed by Pavel Djundik
parent 5f5b5fef3d
commit 595915fefd
6 changed files with 118 additions and 104 deletions

View 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>

View File

@ -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 {

View File

@ -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);
}
});

View File

@ -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);

View File

@ -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}}

View File

@ -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) {