mirror of
https://github.com/oxen-io/session-desktop.git
synced 2023-12-14 02:12:57 +01:00
Merge pull request #186 from Mikunj/local-preview
Added local link previews
This commit is contained in:
commit
1a810f4e4f
14 changed files with 301 additions and 7 deletions
|
@ -1102,6 +1102,22 @@
|
|||
"message": "General",
|
||||
"description": "Header for general options on the settings screen"
|
||||
},
|
||||
"linkPreviews": {
|
||||
"message": "Link Previews",
|
||||
"description":
|
||||
"Option to control creation and send of link previews in setting screen"
|
||||
},
|
||||
"linkPreviewsDescription": {
|
||||
"message":
|
||||
"Previews are supported for Imgur, Instagram, Reddit, and YouTube links.",
|
||||
"description":
|
||||
"Additional detail provided for Link Previews option in settings screen"
|
||||
},
|
||||
"linkPreviewsSettingDescription": {
|
||||
"message":
|
||||
"Enable local link previews (Restart for changes to take effect).",
|
||||
"description": "Description shown for the Link Preview option "
|
||||
},
|
||||
"spellCheckDescription": {
|
||||
"message": "Enable spell check of text entered in message composition box",
|
||||
"description": "Description of the media permission description"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -254,6 +254,10 @@
|
|||
getReadReceiptSetting: () => storage.get('read-receipt-setting'),
|
||||
setReadReceiptSetting: value =>
|
||||
storage.put('read-receipt-setting', value),
|
||||
|
||||
getLinkPreviewSetting: () => storage.get('linkPreviews', false),
|
||||
setLinkPreviewSetting: value => storage.put('linkPreviews', value),
|
||||
|
||||
getNotificationSetting: () =>
|
||||
storage.get('notification-setting', 'message'),
|
||||
setNotificationSetting: value =>
|
||||
|
|
142
js/link_previews_helper.js
Normal file
142
js/link_previews_helper.js
Normal file
|
@ -0,0 +1,142 @@
|
|||
/* global
|
||||
Signal,
|
||||
textsecure,
|
||||
StringView
|
||||
*/
|
||||
|
||||
/* eslint-disable no-bitwise */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.Signal = window.Signal || {};
|
||||
window.Signal.LinkPreviews = window.Signal.LinkPreviews || {};
|
||||
|
||||
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 sha256(string) {
|
||||
const arraybuffer = new TextEncoder('utf-8').encode(string);
|
||||
const digest = await window.crypto.subtle.digest('SHA-256', arraybuffer);
|
||||
return StringView.arrayBufferToHex(digest);
|
||||
}
|
||||
|
||||
async function getPreview(url) {
|
||||
let html;
|
||||
try {
|
||||
html = await textsecure.messaging.makeProxiedRequest(url);
|
||||
} catch (error) {
|
||||
if (error.code >= 300) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const title = Signal.LinkPreviews.getTitleMetaTag(html);
|
||||
const imageUrl = 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);
|
||||
}
|
||||
}
|
||||
|
||||
const hash = await sha256(url);
|
||||
|
||||
return {
|
||||
title,
|
||||
url,
|
||||
image,
|
||||
hash,
|
||||
};
|
||||
}
|
||||
|
||||
window.Signal.LinkPreviews.helper = {
|
||||
getPreview,
|
||||
};
|
||||
})();
|
|
@ -17,7 +17,12 @@
|
|||
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
const { Message: TypedMessage, Contact, PhoneNumber } = Signal.Types;
|
||||
const {
|
||||
Message: TypedMessage,
|
||||
Contact,
|
||||
PhoneNumber,
|
||||
Attachment,
|
||||
} = Signal.Types;
|
||||
const {
|
||||
deleteAttachmentData,
|
||||
deleteExternalMessageFiles,
|
||||
|
@ -26,6 +31,7 @@
|
|||
loadQuoteData,
|
||||
loadPreviewData,
|
||||
writeNewAttachmentData,
|
||||
writeAttachment,
|
||||
} = window.Signal.Migrations;
|
||||
|
||||
window.AccountCache = Object.create(null);
|
||||
|
@ -84,6 +90,8 @@
|
|||
this.on('unload', this.unload);
|
||||
this.on('expired', this.onExpired);
|
||||
this.setToExpire();
|
||||
|
||||
this.updatePreview();
|
||||
},
|
||||
idForLogging() {
|
||||
return `${this.get('source')}.${this.get('sourceDevice')} ${this.get(
|
||||
|
@ -109,6 +117,73 @@
|
|||
// eslint-disable-next-line no-bitwise
|
||||
return !!(this.get('flags') & flag);
|
||||
},
|
||||
async updatePreview() {
|
||||
// Don't generate link previews if user has turned them off
|
||||
if (!storage.get('linkPreviews', false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
const { image, title, hash } = result;
|
||||
|
||||
// A link preview isn't worth showing unless we have either a title or an image
|
||||
if (!result || !(image || title)) {
|
||||
this.updatingPreview = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Save the image to disk
|
||||
const { data } = image;
|
||||
const extension = Attachment.getFileExtension(image);
|
||||
if (data && extension) {
|
||||
const hash32 = hash.substring(0, 32);
|
||||
try {
|
||||
const filePath = await writeAttachment({
|
||||
data,
|
||||
path: `previews/${hash32}.${extension}`,
|
||||
});
|
||||
|
||||
// return the image without the data
|
||||
result.image = _.omit({ ...image, path: filePath }, 'data');
|
||||
} catch (e) {
|
||||
window.log.warn('Failed to write preview to disk', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Save it!!
|
||||
this.set({ preview: [result] });
|
||||
await window.Signal.Data.saveMessage(this.attributes, {
|
||||
Message: Whisper.Message,
|
||||
});
|
||||
} 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') {
|
||||
|
@ -614,13 +689,29 @@
|
|||
});
|
||||
},
|
||||
getPropsForPreview() {
|
||||
// Don't generate link previews if user has turned them off
|
||||
if (!storage.get('linkPreviews', false)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const previews = this.get('preview') || [];
|
||||
|
||||
return previews.map(preview => ({
|
||||
...preview,
|
||||
domain: window.Signal.LinkPreviews.getDomain(preview.url),
|
||||
image: preview.image ? this.getPropsForAttachment(preview.image) : null,
|
||||
}));
|
||||
return previews.map(preview => {
|
||||
let image = null;
|
||||
try {
|
||||
if (preview.image) {
|
||||
image = this.getPropsForAttachment(preview.image);
|
||||
}
|
||||
} catch (e) {
|
||||
window.log.info('Failed to show preview');
|
||||
}
|
||||
|
||||
return {
|
||||
...preview,
|
||||
domain: window.Signal.LinkPreviews.getDomain(preview.url),
|
||||
image,
|
||||
};
|
||||
});
|
||||
},
|
||||
getPropsForQuote() {
|
||||
const quote = this.get('quote');
|
||||
|
@ -1307,6 +1398,9 @@
|
|||
schemaVersion: dataMessage.schemaVersion,
|
||||
});
|
||||
|
||||
// Update the previews if we need to
|
||||
message.updatePreview();
|
||||
|
||||
if (type === 'outgoing') {
|
||||
const receipts = Whisper.DeliveryReceipts.forMessage(
|
||||
conversation,
|
||||
|
|
|
@ -169,6 +169,8 @@ function initializeMigrations({
|
|||
logger,
|
||||
}),
|
||||
writeNewAttachmentData: createWriterForNew(attachmentsPath),
|
||||
writeAttachment: ({ data, path }) =>
|
||||
createWriterForExisting(attachmentsPath)({ data, path }),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -208,6 +208,7 @@ exports.deleteData = deleteOnDisk => {
|
|||
|
||||
exports.isVoiceMessage = AttachmentTS.isVoiceMessage;
|
||||
exports.save = AttachmentTS.save;
|
||||
exports.getFileExtension = AttachmentTS.getFileExtension;
|
||||
|
||||
const THUMBNAIL_SIZE = 150;
|
||||
const THUMBNAIL_CONTENT_TYPE = 'image/png';
|
||||
|
|
|
@ -20,6 +20,7 @@ const getInitialData = async () => ({
|
|||
|
||||
messageTTL: await window.getMessageTTL(),
|
||||
readReceiptSetting: await window.getReadReceiptSetting(),
|
||||
linkPreviewSetting: await window.getLinkPreviewSetting(),
|
||||
notificationSetting: await window.getNotificationSetting(),
|
||||
audioNotification: await window.getAudioNotification(),
|
||||
|
||||
|
|
|
@ -155,6 +155,12 @@
|
|||
value: window.initialData.hideMenuBar,
|
||||
setFn: window.setHideMenuBar,
|
||||
});
|
||||
new CheckboxView({
|
||||
el: this.$('.link-preview-setting'),
|
||||
name: 'link-preview-setting',
|
||||
value: window.initialData.linkPreviewSetting,
|
||||
setFn: window.setLinkPreviewSetting,
|
||||
});
|
||||
new MediaPermissionsSettingView({
|
||||
el: this.$('.media-permissions'),
|
||||
value: window.initialData.mediaPermissions,
|
||||
|
@ -212,8 +218,9 @@
|
|||
spellCheckHeader: i18n('spellCheck'),
|
||||
spellCheckDescription: i18n('spellCheckDescription'),
|
||||
blockedHeader: 'Blocked Users',
|
||||
sendLinkPreviews: i18n('sendLinkPreviews'),
|
||||
linkPreviews: i18n('linkPreviews'),
|
||||
linkPreviewsDescription: i18n('linkPreviewsDescription'),
|
||||
linkPreviewsSettingDescription: i18n('linkPreviewsSettingDescription'),
|
||||
};
|
||||
},
|
||||
onClose() {
|
||||
|
|
|
@ -307,6 +307,14 @@ MessageSender.prototype = {
|
|||
const message = new Message(attrs);
|
||||
const silent = false;
|
||||
|
||||
// Remove this when we add support for attachments
|
||||
message.attachments = [];
|
||||
message.attachmentPointers = [];
|
||||
message.preview = [];
|
||||
if (message.quote) {
|
||||
message.quote.attachments = [];
|
||||
}
|
||||
|
||||
return Promise.all([
|
||||
this.uploadAttachments(message),
|
||||
this.uploadThumbnails(message),
|
||||
|
|
3
main.js
3
main.js
|
@ -1091,6 +1091,9 @@ installSettingsSetter('notification-setting');
|
|||
installSettingsGetter('audio-notification');
|
||||
installSettingsSetter('audio-notification');
|
||||
|
||||
installSettingsGetter('link-preview-setting');
|
||||
installSettingsSetter('link-preview-setting');
|
||||
|
||||
installSettingsGetter('spell-check');
|
||||
installSettingsSetter('spell-check');
|
||||
|
||||
|
|
|
@ -171,6 +171,9 @@ installSetter('notification-setting', 'setNotificationSetting');
|
|||
installGetter('audio-notification', 'getAudioNotification');
|
||||
installSetter('audio-notification', 'setAudioNotification');
|
||||
|
||||
installGetter('link-preview-setting', 'getLinkPreviewSetting');
|
||||
installSetter('link-preview-setting', 'setLinkPreviewSetting');
|
||||
|
||||
installGetter('spell-check', 'getSpellCheck');
|
||||
installSetter('spell-check', 'setSpellCheck');
|
||||
|
||||
|
|
|
@ -98,7 +98,16 @@
|
|||
<input type='checkbox' name='spell-check-setting' id='spell-check-setting' />
|
||||
<label for='spell-check-setting'>{{ spellCheckDescription }}</label>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<h3>{{ linkPreviews }}</h3>
|
||||
<div>{{ linkPreviewsDescription }}<div>
|
||||
</br>
|
||||
<div class='link-preview-setting'>
|
||||
<input type='checkbox' name='link-preview-setting' id='link-preview-setting' />
|
||||
<label for='link-preview-setting'>{{ linkPreviewsSettingDescription }}</label>
|
||||
</div>
|
||||
<hr>
|
||||
<div class='permissions-setting'>
|
||||
<h3>{{ permissions }}</h3>
|
||||
<div class='media-permissions'>
|
||||
|
|
|
@ -52,6 +52,9 @@ window.setNotificationSetting = makeSetter('notification-setting');
|
|||
window.getAudioNotification = makeGetter('audio-notification');
|
||||
window.setAudioNotification = makeSetter('audio-notification');
|
||||
|
||||
window.getLinkPreviewSetting = makeGetter('link-preview-setting');
|
||||
window.setLinkPreviewSetting = makeSetter('link-preview-setting');
|
||||
|
||||
window.getMediaPermissions = makeGetter('media-permissions');
|
||||
window.setMediaPermissions = makeSetter('media-permissions');
|
||||
|
||||
|
|
Loading…
Reference in a new issue