Added local link preview

This commit is contained in:
Mikunj 2019-02-08 11:51:34 +11:00
parent 0417f0ffad
commit 8f8e25bb3e
3 changed files with 217 additions and 5 deletions

View file

@ -730,6 +730,7 @@
<script type='text/javascript' src='js/expire.js'></script>
<script type='text/javascript' src='js/conversation_controller.js'></script>
<script type='text/javascript' src='js/blocked_number_controller.js'></script>
<script type='text/javascript' src='js/link_previews_helper.js'></script>
<script type='text/javascript' src='js/views/react_wrapper_view.js'></script>
<script type='text/javascript' src='js/views/whisper_view.js'></script>

151
js/link_previews_helper.js Normal file
View file

@ -0,0 +1,151 @@
/* global
Signal,
textsecure,
dcodeIO,
*/
// eslint-disable-next-line func-names
(function() {
'use strict';
window.Signal = window.Signal || {};
window.Signal.LinkPreviews = window.Signal.LinkPreviews || {};
const base64ImageCache = {};
function getBase64Image(preview) {
const { url, image } = preview;
if (!url || !image || !image.data) return null;
// Return the cached value
if (base64ImageCache[url]) return base64ImageCache[url];
// Set the cache and return the value
const contentType = image.contentType || 'image/jpeg';
const base64 = dcodeIO.ByteBuffer.wrap(image.data).toString('base64');
const data = `data:${contentType};base64, ${base64}`;
base64ImageCache[url] = data;
return data;
}
async function makeChunkedRequest(url) {
const PARALLELISM = 3;
const size = await textsecure.messaging.getProxiedSize(url);
const chunks = await Signal.LinkPreviews.getChunkPattern(size);
let results = [];
const jobs = chunks.map(chunk => async () => {
const { start, end } = chunk;
const result = await textsecure.messaging.makeProxiedRequest(url, {
start,
end,
returnArrayBuffer: true,
});
return {
...chunk,
...result,
};
});
while (jobs.length > 0) {
const activeJobs = [];
for (let i = 0, max = PARALLELISM; i < max; i += 1) {
if (!jobs.length) {
break;
}
const job = jobs.shift();
activeJobs.push(job());
}
// eslint-disable-next-line no-await-in-loop
results = results.concat(await Promise.all(activeJobs));
}
if (!results.length) {
throw new Error('No responses received');
}
const { contentType } = results[0];
const data = Signal.LinkPreviews.assembleChunks(results);
return {
contentType,
data,
};
}
async function getPreview(url) {
let html;
try {
html = await textsecure.messaging.makeProxiedRequest(url);
} catch (error) {
if (error.code >= 300) {
return null;
}
}
const title = window.Signal.LinkPreviews.getTitleMetaTag(html);
const imageUrl = window.Signal.LinkPreviews.getImageMetaTag(html);
let image;
let objectUrl;
try {
if (imageUrl) {
if (!Signal.LinkPreviews.isMediaLinkInWhitelist(imageUrl)) {
const primaryDomain = Signal.LinkPreviews.getDomain(url);
const imageDomain = Signal.LinkPreviews.getDomain(imageUrl);
throw new Error(
`imageUrl for domain ${primaryDomain} did not match media whitelist. Domain: ${imageDomain}`
);
}
const data = await makeChunkedRequest(imageUrl);
// Calculate dimensions
const file = new Blob([data.data], {
type: data.contentType,
});
objectUrl = URL.createObjectURL(file);
const dimensions = await Signal.Types.VisualAttachment.getImageDimensions(
{
objectUrl,
logger: window.log,
}
);
image = {
...data,
...dimensions,
contentType: file.type,
};
}
} catch (error) {
// We still want to show the preview if we failed to get an image
window.log.error(
'getPreview failed to get image for link preview:',
error.message
);
} finally {
if (objectUrl) {
URL.revokeObjectURL(objectUrl);
}
}
return {
title,
url,
image,
};
}
window.Signal.LinkPreviews.helper = {
getPreview,
getBase64Image,
}
})();

View file

@ -8,6 +8,7 @@
/* global Signal: false */
/* global textsecure: false */
/* global Whisper: false */
/* global dcodeIO: false */
/* eslint-disable more/no-then */
@ -84,6 +85,8 @@
this.on('unload', this.unload);
this.on('expired', this.onExpired);
this.setToExpire();
this.updatePreviews();
},
idForLogging() {
return `${this.get('source')}.${this.get('sourceDevice')} ${this.get(
@ -109,6 +112,40 @@
// eslint-disable-next-line no-bitwise
return !!(this.get('flags') & flag);
},
async updatePreviews() {
if (this.updatingPreview) return;
// Only update the preview if we don't have any set
const preview = this.get('preview');
if (!_.isEmpty(preview)) return;
// Make sure we have links we can preview
const links = Signal.LinkPreviews.findLinks(this.get('body'));
const firstLink = links.find(link => Signal.LinkPreviews.isLinkInWhitelist(link));
if (!firstLink) return;
this.updatingPreview = true;
try {
const result = await Signal.LinkPreviews.helper.getPreview(firstLink);
if (!result) {
this.updatingPreview = false;
return;
}
if (!result.image && !result.title) {
// A link preview isn't worth showing unless we have either a title or an image
this.updatingPreview = false;
return;
}
this.set({ preview: [result] });
} catch (e) {
window.log.warn(`Failed to load previews for message: ${this.id}`);
} finally {
this.updatingPreview = false;
}
},
getEndSessionTranslationKey() {
const sessionType = this.get('endSessionType');
if (sessionType === 'ongoing') {
@ -616,11 +653,34 @@
getPropsForPreview() {
const previews = this.get('preview') || [];
return previews.map(preview => ({
return previews.map(preview => {
let image = {};
// Try set the image from the attachment otherwise just pass in the object
if (preview.image) {
try {
const attachmentProps = this.getPropsForAttachment(preview.image);
if (attachmentProps.url) {
image = attachmentProps;
}
} catch (e) {
// Only set the image if we have a url to display
const url = Signal.LinkPreviews.helper.getBase64Image(preview);
if (preview.image.url || url) {
image = {
...preview.image,
url: preview.image.url || url,
}
}
}
}
return {
...preview,
domain: window.Signal.LinkPreviews.getDomain(preview.url),
image: preview.image ? this.getPropsForAttachment(preview.image) : null,
}));
image,
};
});
},
getPropsForQuote() {
const quote = this.get('quote');