mirror of
https://github.com/oxen-io/session-desktop.git
synced 2023-12-14 02:12:57 +01:00
commit
b226feaac7
20
.github/ISSUE_TEMPLATE.md
vendored
20
.github/ISSUE_TEMPLATE.md
vendored
|
@ -1,6 +1,6 @@
|
|||
<!--
|
||||
Please fill out this template with all the information you have. We can't do much without
|
||||
both logs and a detailed description of what you encountered. Please do your best!
|
||||
both the logs and a detailed description of what you've encountered. Please do your best!
|
||||
|
||||
Please note that this tracker is only for bugs and feature requests. Please try these
|
||||
locations if you have a question or comment:
|
||||
|
@ -21,11 +21,11 @@ Lastly, be sure to preview your issue before saving. Thanks!
|
|||
|
||||
---
|
||||
|
||||
### Bug description
|
||||
### Bug Description
|
||||
|
||||
<!-- Give an overall summary of the issue. -->
|
||||
|
||||
### Steps to reproduce
|
||||
### Steps to Reproduce
|
||||
|
||||
<!-- Using bullet points, list the steps that reproduce the bug. -->
|
||||
|
||||
|
@ -33,11 +33,11 @@ Lastly, be sure to preview your issue before saving. Thanks!
|
|||
2. step two
|
||||
3. step three
|
||||
|
||||
Actual result:
|
||||
Actual Result:
|
||||
|
||||
<!-- Describe the details of the buggy behaviour. -->
|
||||
|
||||
Expected result:
|
||||
Expected Result:
|
||||
|
||||
<!-- Describe in detail what the correct behavior should be. -->
|
||||
|
||||
|
@ -48,7 +48,7 @@ How to take screenshots on all OSes: https://www.take-a-screenshot.org/
|
|||
You can drag and drop images into this text box.
|
||||
-->
|
||||
|
||||
### Platform info
|
||||
### Platform Info
|
||||
|
||||
Loki Messenger version:
|
||||
|
||||
|
@ -58,16 +58,16 @@ Operating System:
|
|||
|
||||
<!-- Instructions for finding your OS version are here: http://whatsmyos.com/ -->
|
||||
|
||||
Linked device version:
|
||||
Linked Device Version:
|
||||
|
||||
<!-- Android: Settings -> Advanced, iOS: Settings -> General -> About -->
|
||||
|
||||
### Link to debug log
|
||||
### Link to Debug Log
|
||||
|
||||
<!--
|
||||
Immediately after the bug has happened, submit a debug log via View -> Debug Log, then copy that URL here.
|
||||
|
||||
In most cases, a log from your other devices is also useful:
|
||||
Android: https://support.signal.org/hc/en-us/articles/212535838
|
||||
iOS: https://support.signal.org/hc/en-us/articles/229676507
|
||||
Android: https://support.signal.org/hc/en-us/articles/360007318591#android_debug
|
||||
iOS: https://support.signal.org/hc/en-us/articles/360007318591#ios_debug
|
||||
-->
|
||||
|
|
|
@ -267,8 +267,8 @@ module.exports = grunt => {
|
|||
grunt.registerTask('getExpireTime', () => {
|
||||
grunt.task.requires('gitinfo');
|
||||
const gitinfo = grunt.config.get('gitinfo');
|
||||
const commited = gitinfo.local.branch.current.lastCommitTime;
|
||||
const time = Date.parse(commited) + 1000 * 60 * 60 * 24 * 90;
|
||||
const committed = gitinfo.local.branch.current.lastCommitTime;
|
||||
const time = Date.parse(committed) + 1000 * 60 * 60 * 24 * 90;
|
||||
grunt.file.write(
|
||||
'config/local-production.json',
|
||||
`${JSON.stringify({ buildExpiration: time })}\n`
|
||||
|
@ -307,7 +307,7 @@ module.exports = grunt => {
|
|||
app.client
|
||||
.execute(getMochaResults)
|
||||
.then(data => Boolean(data.value)),
|
||||
10000,
|
||||
25000,
|
||||
'Expected to find window.mochaResults set!'
|
||||
)
|
||||
)
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# Loki Messenger
|
||||
|
||||
[![Build Status](https://travis-ci.org/loki-project/loki-messenger.svg?branch=development)](https://travis-ci.org/loki-project/loki-messenger)
|
||||
|
||||
Loki Messenger allows for truly decentralized and end to end and private encrypted chats, Loki Messenger is built to handle both online and fully Asynchronous offline messages , Loki messenger implements the Signal protocol for message encryption, Our Client interface is a fork of [Signal Messenger](https://signal.org/). All communication that passes through Loki messenger is routed through [Lokinet](https://github.com/loki-project/loki-network).
|
||||
|
||||
## Summary
|
||||
|
|
|
@ -172,6 +172,10 @@
|
|||
"message": "Choose folder",
|
||||
"description": "Button to allow the user to find a folder on disk"
|
||||
},
|
||||
"chooseFile": {
|
||||
"message": "Choose file",
|
||||
"description": "Button to allow the user to find a file on disk"
|
||||
},
|
||||
"loadDataHeader": {
|
||||
"message": "Load your data",
|
||||
"description": "Header shown on the first screen in the data import process"
|
||||
|
@ -542,15 +546,37 @@
|
|||
"message": "Voice Message",
|
||||
"description": "Name for a voice message attachment"
|
||||
},
|
||||
"unsupportedFileType": {
|
||||
"message": "Unsupported file type",
|
||||
"description": "Displayed for outgoing unsupported attachment"
|
||||
},
|
||||
"dangerousFileType": {
|
||||
"message": "Attachment type not allowed for security reasons",
|
||||
"description":
|
||||
"Shown in toast when user attempts to send .exe file, for example"
|
||||
},
|
||||
"stagedImageAttachment": {
|
||||
"message": "Staged image attachment: $path$",
|
||||
"description": "Alt text for staged attachments",
|
||||
"placeholders": {
|
||||
"path": {
|
||||
"content": "$1",
|
||||
"example": "dog.jpg"
|
||||
}
|
||||
}
|
||||
},
|
||||
"oneNonImageAtATimeToast": {
|
||||
"message":
|
||||
"When including a non-image attachment, the limit is one attachment per message.",
|
||||
"description":
|
||||
"An error popup when the user has attempted to add an attachment"
|
||||
},
|
||||
"cannotMixImageAdnNonImageAttachments": {
|
||||
"message": "You cannot mix non-image and image attachments in one message.",
|
||||
"description":
|
||||
"An error popup when the user has attempted to add an attachment"
|
||||
},
|
||||
"maximumAttachments": {
|
||||
"message": "You cannot add any more attachments to this message.",
|
||||
"description":
|
||||
"An error popup when the user has attempted to add an attachment"
|
||||
},
|
||||
"fileSizeWarning": {
|
||||
"message": "Sorry, the selected file exceeds message size restrictions."
|
||||
},
|
||||
|
@ -732,6 +758,12 @@
|
|||
"description":
|
||||
"Shown in toast if user clicks on quote references messages not loaded in view, but in database"
|
||||
},
|
||||
"voiceNoteMustBeOnlyAttachment": {
|
||||
"message":
|
||||
"A voice note must be the only attachment included in a message.",
|
||||
"description":
|
||||
"Shown in toast if tries to record a voice note with any staged attachments"
|
||||
},
|
||||
"you": {
|
||||
"message": "You",
|
||||
"description":
|
||||
|
@ -936,6 +968,16 @@
|
|||
"description":
|
||||
"Used for the icon layered on top of an image in message bubbles"
|
||||
},
|
||||
"addACaption": {
|
||||
"message": "Add a caption...",
|
||||
"descripton":
|
||||
"Used as the placeholder text in the caption editor text field"
|
||||
},
|
||||
"save": {
|
||||
"message": "Save",
|
||||
"descripton":
|
||||
"Used as a 'commit changes' button in the Caption Editor for outgoing image attachments"
|
||||
},
|
||||
"fileIconAlt": {
|
||||
"message": "File icon",
|
||||
"description":
|
||||
|
|
141
app/sql.js
141
app/sql.js
|
@ -24,6 +24,7 @@ module.exports = {
|
|||
createOrUpdateGroup,
|
||||
getGroupById,
|
||||
getAllGroupIds,
|
||||
getAllGroups,
|
||||
bulkAddGroups,
|
||||
removeGroupById,
|
||||
removeAllGroups,
|
||||
|
@ -92,7 +93,10 @@ module.exports = {
|
|||
getAllConversationIds,
|
||||
getAllPrivateConversations,
|
||||
getAllGroupsInvolvingId,
|
||||
|
||||
searchConversations,
|
||||
searchMessages,
|
||||
searchMessagesInConversation,
|
||||
|
||||
getMessageCount,
|
||||
saveMessage,
|
||||
|
@ -540,6 +544,69 @@ async function updateToSchemaVersion7(currentVersion, instance) {
|
|||
console.log('updateToSchemaVersion7: success!');
|
||||
}
|
||||
|
||||
async function updateToSchemaVersion8(currentVersion, instance) {
|
||||
if (currentVersion >= 8) {
|
||||
return;
|
||||
}
|
||||
console.log('updateToSchemaVersion8: starting...');
|
||||
await instance.run('BEGIN TRANSACTION;');
|
||||
|
||||
// First, we pull a new body field out of the message table's json blob
|
||||
await instance.run(
|
||||
`ALTER TABLE messages
|
||||
ADD COLUMN body TEXT;`
|
||||
);
|
||||
await instance.run("UPDATE messages SET body = json_extract(json, '$.body')");
|
||||
|
||||
// Then we create our full-text search table and populate it
|
||||
await instance.run(`
|
||||
CREATE VIRTUAL TABLE messages_fts
|
||||
USING fts5(id UNINDEXED, body);
|
||||
`);
|
||||
await instance.run(`
|
||||
INSERT INTO messages_fts(id, body)
|
||||
SELECT id, body FROM messages;
|
||||
`);
|
||||
|
||||
// Then we set up triggers to keep the full-text search table up to date
|
||||
await instance.run(`
|
||||
CREATE TRIGGER messages_on_insert AFTER INSERT ON messages BEGIN
|
||||
INSERT INTO messages_fts (
|
||||
id,
|
||||
body
|
||||
) VALUES (
|
||||
new.id,
|
||||
new.body
|
||||
);
|
||||
END;
|
||||
`);
|
||||
await instance.run(`
|
||||
CREATE TRIGGER messages_on_delete AFTER DELETE ON messages BEGIN
|
||||
DELETE FROM messages_fts WHERE id = old.id;
|
||||
END;
|
||||
`);
|
||||
await instance.run(`
|
||||
CREATE TRIGGER messages_on_update AFTER UPDATE ON messages BEGIN
|
||||
DELETE FROM messages_fts WHERE id = old.id;
|
||||
INSERT INTO messages_fts(
|
||||
id,
|
||||
body
|
||||
) VALUES (
|
||||
new.id,
|
||||
new.body
|
||||
);
|
||||
END;
|
||||
`);
|
||||
|
||||
// For formatting search results:
|
||||
// https://sqlite.org/fts5.html#the_highlight_function
|
||||
// https://sqlite.org/fts5.html#the_snippet_function
|
||||
|
||||
await instance.run('PRAGMA schema_version = 8;');
|
||||
await instance.run('COMMIT TRANSACTION;');
|
||||
console.log('updateToSchemaVersion8: success!');
|
||||
}
|
||||
|
||||
const SCHEMA_VERSIONS = [
|
||||
updateToSchemaVersion1,
|
||||
updateToSchemaVersion2,
|
||||
|
@ -548,6 +615,7 @@ const SCHEMA_VERSIONS = [
|
|||
() => null, // version 5 was dropped
|
||||
updateToSchemaVersion6,
|
||||
updateToSchemaVersion7,
|
||||
updateToSchemaVersion8,
|
||||
];
|
||||
|
||||
async function updateSchema(instance) {
|
||||
|
@ -598,7 +666,7 @@ async function initialize({ configDir, key }) {
|
|||
const promisified = promisify(sqlInstance);
|
||||
|
||||
// promisified.on('trace', async statement => {
|
||||
// if (!db) {
|
||||
// if (!db || statement.startsWith('--')) {
|
||||
// console._log(statement);
|
||||
// return;
|
||||
// }
|
||||
|
@ -669,6 +737,10 @@ async function getAllGroupIds() {
|
|||
const rows = await db.all('SELECT id FROM groups ORDER BY id ASC;');
|
||||
return map(rows, row => row.id);
|
||||
}
|
||||
async function getAllGroups() {
|
||||
const rows = await db.all('SELECT id FROM groups ORDER BY id ASC;');
|
||||
return map(rows, row => jsonToObject(row.json));
|
||||
}
|
||||
async function bulkAddGroups(array) {
|
||||
return bulkAdd(GROUPS_TABLE, array);
|
||||
}
|
||||
|
@ -1232,9 +1304,11 @@ async function getAllGroupsInvolvingId(id) {
|
|||
async function searchConversations(query) {
|
||||
const rows = await db.all(
|
||||
`SELECT json FROM conversations WHERE
|
||||
id LIKE $id OR
|
||||
name LIKE $name OR
|
||||
profileName LIKE $profileName
|
||||
(
|
||||
id LIKE $id OR
|
||||
name LIKE $name OR
|
||||
profileName LIKE $profileName
|
||||
)
|
||||
ORDER BY id ASC;`,
|
||||
{
|
||||
$id: `%${query}%`,
|
||||
|
@ -1246,6 +1320,58 @@ async function searchConversations(query) {
|
|||
return map(rows, row => jsonToObject(row.json));
|
||||
}
|
||||
|
||||
async function searchMessages(query, { limit } = {}) {
|
||||
const rows = await db.all(
|
||||
`SELECT
|
||||
messages.json,
|
||||
snippet(messages_fts, -1, '<<left>>', '<<right>>', '...', 15) as snippet
|
||||
FROM messages_fts
|
||||
INNER JOIN messages on messages_fts.id = messages.id
|
||||
WHERE
|
||||
messages_fts match $query
|
||||
ORDER BY messages.received_at DESC
|
||||
LIMIT $limit;`,
|
||||
{
|
||||
$query: query,
|
||||
$limit: limit || 100,
|
||||
}
|
||||
);
|
||||
|
||||
return map(rows, row => ({
|
||||
...jsonToObject(row.json),
|
||||
snippet: row.snippet,
|
||||
}));
|
||||
}
|
||||
|
||||
async function searchMessagesInConversation(
|
||||
query,
|
||||
conversationId,
|
||||
{ limit } = {}
|
||||
) {
|
||||
const rows = await db.all(
|
||||
`SELECT
|
||||
messages.json,
|
||||
snippet(messages_fts, -1, '<<left>>', '<<right>>', '...', 15) as snippet
|
||||
FROM messages_fts
|
||||
INNER JOIN messages on messages_fts.id = messages.id
|
||||
WHERE
|
||||
messages_fts match $query AND
|
||||
messages.conversationId = $conversationId
|
||||
ORDER BY messages.received_at DESC
|
||||
LIMIT $limit;`,
|
||||
{
|
||||
$query: query,
|
||||
$conversationId: conversationId,
|
||||
$limit: limit || 100,
|
||||
}
|
||||
);
|
||||
|
||||
return map(rows, row => ({
|
||||
...jsonToObject(row.json),
|
||||
snippet: row.snippet,
|
||||
}));
|
||||
}
|
||||
|
||||
async function getMessageCount() {
|
||||
const row = await db.get('SELECT count(*) from messages;');
|
||||
|
||||
|
@ -1258,6 +1384,7 @@ async function getMessageCount() {
|
|||
|
||||
async function saveMessage(data, { forceSave } = {}) {
|
||||
const {
|
||||
body,
|
||||
conversationId,
|
||||
// eslint-disable-next-line camelcase
|
||||
expires_at,
|
||||
|
@ -1283,6 +1410,7 @@ async function saveMessage(data, { forceSave } = {}) {
|
|||
$id: id,
|
||||
$json: objectToJSON(data),
|
||||
|
||||
$body: body,
|
||||
$conversationId: conversationId,
|
||||
$expirationStartTimestamp: expirationStartTimestamp,
|
||||
$expires_at: expires_at,
|
||||
|
@ -1296,7 +1424,7 @@ async function saveMessage(data, { forceSave } = {}) {
|
|||
$sent_at: sent_at,
|
||||
$source: source,
|
||||
$sourceDevice: sourceDevice,
|
||||
$type: type,
|
||||
$type: type || '',
|
||||
$unread: unread,
|
||||
};
|
||||
|
||||
|
@ -1304,6 +1432,7 @@ async function saveMessage(data, { forceSave } = {}) {
|
|||
await db.run(
|
||||
`UPDATE messages SET
|
||||
json = $json,
|
||||
body = $body,
|
||||
conversationId = $conversationId,
|
||||
expirationStartTimestamp = $expirationStartTimestamp,
|
||||
expires_at = $expires_at,
|
||||
|
@ -1337,6 +1466,7 @@ async function saveMessage(data, { forceSave } = {}) {
|
|||
id,
|
||||
json,
|
||||
|
||||
body,
|
||||
conversationId,
|
||||
expirationStartTimestamp,
|
||||
expires_at,
|
||||
|
@ -1356,6 +1486,7 @@ async function saveMessage(data, { forceSave } = {}) {
|
|||
$id,
|
||||
$json,
|
||||
|
||||
$body,
|
||||
$conversationId,
|
||||
$expirationStartTimestamp,
|
||||
$expires_at,
|
||||
|
|
|
@ -17,6 +17,17 @@ function createTrayIcon(getMainWindow, messages) {
|
|||
|
||||
tray = new Tray(iconNoNewMessages);
|
||||
|
||||
tray.forceOnTop = mainWindow => {
|
||||
if (mainWindow) {
|
||||
// On some versions of GNOME the window may not be on top when restored.
|
||||
// This trick should fix it.
|
||||
// Thanks to: https://github.com/Enrico204/Whatsapp-Desktop/commit/6b0dc86b64e481b455f8fce9b4d797e86d000dc1
|
||||
mainWindow.setAlwaysOnTop(true);
|
||||
mainWindow.focus();
|
||||
mainWindow.setAlwaysOnTop(false);
|
||||
}
|
||||
};
|
||||
|
||||
tray.toggleWindowVisibility = () => {
|
||||
const mainWindow = getMainWindow();
|
||||
if (mainWindow) {
|
||||
|
@ -25,17 +36,24 @@ function createTrayIcon(getMainWindow, messages) {
|
|||
} else {
|
||||
mainWindow.show();
|
||||
|
||||
// On some versions of GNOME the window may not be on top when restored.
|
||||
// This trick should fix it.
|
||||
// Thanks to: https://github.com/Enrico204/Whatsapp-Desktop/commit/6b0dc86b64e481b455f8fce9b4d797e86d000dc1
|
||||
mainWindow.setAlwaysOnTop(true);
|
||||
mainWindow.focus();
|
||||
mainWindow.setAlwaysOnTop(false);
|
||||
tray.forceOnTop(mainWindow);
|
||||
}
|
||||
}
|
||||
tray.updateContextMenu();
|
||||
};
|
||||
|
||||
tray.showWindow = () => {
|
||||
const mainWindow = getMainWindow();
|
||||
if (mainWindow) {
|
||||
if (!mainWindow.isVisible()) {
|
||||
mainWindow.show();
|
||||
}
|
||||
|
||||
tray.forceOnTop(mainWindow);
|
||||
}
|
||||
tray.updateContextMenu();
|
||||
};
|
||||
|
||||
tray.updateContextMenu = () => {
|
||||
const mainWindow = getMainWindow();
|
||||
|
||||
|
@ -70,7 +88,7 @@ function createTrayIcon(getMainWindow, messages) {
|
|||
}
|
||||
};
|
||||
|
||||
tray.on('click', tray.toggleWindowVisibility);
|
||||
tray.on('click', tray.showWindow);
|
||||
|
||||
tray.setToolTip(messages.trayTooltip.message);
|
||||
tray.updateContextMenu();
|
||||
|
|
|
@ -143,9 +143,9 @@
|
|||
|
||||
<div class='bottom-bar' id='footer'>
|
||||
<div class='emoji-panel-container'></div>
|
||||
<div class='attachment-list'></div>
|
||||
<div class='compose'>
|
||||
<form class='send clearfix'>
|
||||
<div class='attachment-previews'></div>
|
||||
<form class='send clearfix file-input'>
|
||||
<div class='flex'>
|
||||
<button class='emoji' {{#disable-inputs}} disabled="disabled" {{/disable-inputs}}></button>
|
||||
<textarea maxlength='2000' class='send-message' placeholder='{{ send-message }}' rows='1' dir='auto' {{#disable-inputs}} disabled="disabled" {{/disable-inputs}}></textarea>
|
||||
|
@ -157,7 +157,7 @@
|
|||
</div>
|
||||
<div class='choose-file hide'>
|
||||
<button class='paperclip thumbnail' {{#disable-inputs}} disabled="disabled" {{/disable-inputs}}></button>
|
||||
<input type='file' class='file-input'>
|
||||
<input type='file' class='file-input' multiple='multiple'>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -735,7 +735,6 @@
|
|||
<script type='text/javascript' src='js/views/last_seen_indicator_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/scroll_down_button_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/toast_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/attachment_preview_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/file_input_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/list_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/conversation_list_item_view.js'></script>
|
||||
|
|
1
images/add-caption-24.svg
Normal file
1
images/add-caption-24.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg id="Shape" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>add-caption-24</title><rect x="16" y="14" width="7" height="1"/><rect x="16" y="14" width="7" height="1" transform="translate(5 34) rotate(-90)"/><rect x="2" y="11" width="15" height="1"/><rect x="2" y="8" width="18" height="1"/><rect x="2" y="14" width="12" height="1"/></svg>
|
After Width: | Height: | Size: 355 B |
1
images/plus-36.svg
Normal file
1
images/plus-36.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg id="Export" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><title>plus-36</title><polygon points="32 17.25 18.75 17.25 18.75 4 17.25 4 17.25 17.25 4 17.25 4 18.75 17.25 18.75 17.25 32 18.75 32 18.75 18.75 32 18.75 32 17.25"/></svg>
|
After Width: | Height: | Size: 244 B |
1
images/x-16.svg
Normal file
1
images/x-16.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg id="Shape" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><title>x-16</title><polygon points="14.35 2.35 13.65 1.65 8 7.29 2.35 1.65 1.65 2.35 7.29 8 1.65 13.65 2.35 14.35 8 8.71 13.65 14.35 14.35 13.65 8.71 8 14.35 2.35"/></svg>
|
After Width: | Height: | Size: 242 B |
1
images/x-shadow-16.svg
Normal file
1
images/x-shadow-16.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg id="Shape" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 12 12"><defs><style>.cls-1{opacity:0.5;filter:url(#shadow_blur_2);}.cls-2{fill:#fff;}</style><filter id="shadow_blur_2" name="shadow_blur_2"><feGaussianBlur stdDeviation="0.5" in="SourceGraphic"/></filter></defs><title>x-shadow-12</title><g class="cls-1"><polygon points="10.6 2.6 9.9 1.9 6 5.79 2.1 1.9 1.4 2.6 5.29 6.5 1.4 10.4 2.1 11.1 6 7.21 9.9 11.1 10.6 10.4 6.71 6.5 10.6 2.6"/></g><polygon class="cls-2" points="10.6 2.1 9.9 1.4 6 5.29 2.1 1.4 1.4 2.1 5.29 6 1.4 9.9 2.1 10.6 6 6.71 9.9 10.6 10.6 9.9 6.71 6 10.6 2.1"/></svg>
|
After Width: | Height: | Size: 641 B |
|
@ -786,6 +786,19 @@
|
|||
textsecure.storage.user.getDeviceId() != '1'
|
||||
) {
|
||||
window.getSyncRequest();
|
||||
|
||||
try {
|
||||
const manager = window.getAccountManager();
|
||||
await Promise.all([
|
||||
manager.maybeUpdateDeviceName(),
|
||||
manager.maybeDeleteSignalingKey(),
|
||||
]);
|
||||
} catch (e) {
|
||||
window.log.error(
|
||||
'Problem with account manager updates after starting new version: ',
|
||||
e && e.stack ? e.stack : e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// const udSupportKey = 'hasRegisterSupportForUnauthenticatedDelivery';
|
||||
|
|
|
@ -2063,6 +2063,21 @@
|
|||
return this.id;
|
||||
},
|
||||
|
||||
getInitials(name) {
|
||||
if (!name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cleaned = name.replace(/[^A-Za-z\s]+/g, '').replace(/\s+/g, ' ');
|
||||
const parts = cleaned.split(' ');
|
||||
const initials = parts.map(part => part.trim()[0]);
|
||||
if (!initials.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return initials.slice(0, 2).join('');
|
||||
},
|
||||
|
||||
isPrivate() {
|
||||
return this.get('type') === 'private';
|
||||
},
|
||||
|
@ -2099,7 +2114,7 @@
|
|||
const symbol = this.isValid() ? '#' : '!';
|
||||
return {
|
||||
color,
|
||||
content: title ? title.trim()[0] : symbol,
|
||||
content: this.getInitials(title) || symbol,
|
||||
};
|
||||
}
|
||||
return { url: 'images/group_default.png', color };
|
||||
|
|
|
@ -1266,9 +1266,12 @@
|
|||
conversation,
|
||||
message
|
||||
);
|
||||
receipts.forEach(() =>
|
||||
receipts.forEach(receipt =>
|
||||
message.set({
|
||||
delivered: (message.get('delivered') || 0) + 1,
|
||||
delivered_to: _.union(message.get('delivered_to') || [], [
|
||||
receipt.get('source'),
|
||||
]),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,22 +7,20 @@
|
|||
/* eslint-env browser */
|
||||
/* eslint-env node */
|
||||
|
||||
/* eslint-disable no-param-reassign, guard-for-in, no-unreachable */
|
||||
/* eslint-disable no-param-reassign, guard-for-in */
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const { map, fromPairs } = require('lodash');
|
||||
const tar = require('tar');
|
||||
const tmp = require('tmp');
|
||||
const pify = require('pify');
|
||||
const archiver = require('archiver');
|
||||
const rimraf = require('rimraf');
|
||||
const electronRemote = require('electron').remote;
|
||||
|
||||
const Attachment = require('./types/attachment');
|
||||
const crypto = require('./crypto');
|
||||
|
||||
const decompress = () => null;
|
||||
const { dialog, BrowserWindow } = electronRemote;
|
||||
|
||||
module.exports = {
|
||||
|
@ -111,100 +109,55 @@ function createOutputStream(writer) {
|
|||
};
|
||||
}
|
||||
|
||||
async function exportContactAndGroupsToFile(db, parent) {
|
||||
async function exportContactAndGroupsToFile(parent) {
|
||||
const writer = await createFileAndWriter(parent, 'db.json');
|
||||
return exportContactsAndGroups(db, writer);
|
||||
return exportContactsAndGroups(writer);
|
||||
}
|
||||
|
||||
function exportContactsAndGroups(db, fileWriter) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let storeNames = db.objectStoreNames;
|
||||
storeNames = _.without(
|
||||
storeNames,
|
||||
'messages',
|
||||
'items',
|
||||
'signedPreKeys',
|
||||
'preKeys',
|
||||
'identityKeys',
|
||||
'sessions',
|
||||
'unprocessed'
|
||||
);
|
||||
function writeArray(stream, array) {
|
||||
stream.write('[');
|
||||
|
||||
const exportedStoreNames = [];
|
||||
if (storeNames.length === 0) {
|
||||
throw new Error('No stores to export');
|
||||
for (let i = 0, max = array.length; i < max; i += 1) {
|
||||
if (i > 0) {
|
||||
stream.write(',');
|
||||
}
|
||||
window.log.info('Exporting from these stores:', storeNames.join(', '));
|
||||
|
||||
const stream = createOutputStream(fileWriter);
|
||||
const item = array[i];
|
||||
|
||||
stream.write('{');
|
||||
// We don't back up avatars; we'll get them in a future contact sync or profile fetch
|
||||
const cleaned = _.omit(item, ['avatar', 'profileAvatar']);
|
||||
|
||||
_.each(storeNames, storeName => {
|
||||
// Both the readwrite permission and the multi-store transaction are required to
|
||||
// keep this function working. They serve to serialize all of these transactions,
|
||||
// one per store to be exported.
|
||||
const transaction = db.transaction(storeNames, 'readwrite');
|
||||
transaction.onerror = () => {
|
||||
Whisper.Database.handleDOMException(
|
||||
`exportToJsonFile transaction error (store: ${storeName})`,
|
||||
transaction.error,
|
||||
reject
|
||||
);
|
||||
};
|
||||
transaction.oncomplete = () => {
|
||||
window.log.info('transaction complete');
|
||||
};
|
||||
stream.write(JSON.stringify(stringify(cleaned)));
|
||||
}
|
||||
|
||||
const store = transaction.objectStore(storeName);
|
||||
const request = store.openCursor();
|
||||
let count = 0;
|
||||
request.onerror = () => {
|
||||
Whisper.Database.handleDOMException(
|
||||
`exportToJsonFile request error (store: ${storeNames})`,
|
||||
request.error,
|
||||
reject
|
||||
);
|
||||
};
|
||||
request.onsuccess = async event => {
|
||||
if (count === 0) {
|
||||
window.log.info('cursor opened');
|
||||
stream.write(`"${storeName}": [`);
|
||||
}
|
||||
stream.write(']');
|
||||
}
|
||||
|
||||
const cursor = event.target.result;
|
||||
if (cursor) {
|
||||
if (count > 0) {
|
||||
stream.write(',');
|
||||
}
|
||||
function getPlainJS(collection) {
|
||||
return collection.map(model => model.attributes);
|
||||
}
|
||||
|
||||
// Preventing base64'd images from reaching the disk, making db.json too big
|
||||
const item = _.omit(cursor.value, ['avatar', 'profileAvatar']);
|
||||
async function exportContactsAndGroups(fileWriter) {
|
||||
const stream = createOutputStream(fileWriter);
|
||||
|
||||
const jsonString = JSON.stringify(stringify(item));
|
||||
stream.write(jsonString);
|
||||
cursor.continue();
|
||||
count += 1;
|
||||
} else {
|
||||
// no more
|
||||
stream.write(']');
|
||||
window.log.info('Exported', count, 'items from store', storeName);
|
||||
stream.write('{');
|
||||
|
||||
exportedStoreNames.push(storeName);
|
||||
if (exportedStoreNames.length < storeNames.length) {
|
||||
stream.write(',');
|
||||
} else {
|
||||
window.log.info('Exported all stores');
|
||||
stream.write('}');
|
||||
|
||||
await stream.close();
|
||||
window.log.info('Finished writing all stores to disk');
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
stream.write('"conversations": ');
|
||||
const conversations = await window.Signal.Data.getAllConversations({
|
||||
ConversationCollection: Whisper.ConversationCollection,
|
||||
});
|
||||
window.log.info(`Exporting ${conversations.length} conversations`);
|
||||
writeArray(stream, getPlainJS(conversations));
|
||||
|
||||
stream.write(',');
|
||||
|
||||
stream.write('"groups": ');
|
||||
const groups = await window.Signal.Data.getAllGroups();
|
||||
window.log.info(`Exporting ${groups.length} groups`);
|
||||
writeArray(stream, groups);
|
||||
|
||||
stream.write('}');
|
||||
await stream.close();
|
||||
}
|
||||
|
||||
async function importNonMessages(parent, options) {
|
||||
|
@ -414,6 +367,14 @@ function readFileAsText(parent, name) {
|
|||
});
|
||||
}
|
||||
|
||||
// Buffer instances are also Uint8Array instances, but they might be a view
|
||||
// https://nodejs.org/docs/latest/api/buffer.html#buffer_buffers_and_typedarray
|
||||
const toArrayBuffer = nodeBuffer =>
|
||||
nodeBuffer.buffer.slice(
|
||||
nodeBuffer.byteOffset,
|
||||
nodeBuffer.byteOffset + nodeBuffer.byteLength
|
||||
);
|
||||
|
||||
function readFileAsArrayBuffer(targetPath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// omitting the encoding to get a buffer back
|
||||
|
@ -422,9 +383,7 @@ function readFileAsArrayBuffer(targetPath) {
|
|||
return reject(error);
|
||||
}
|
||||
|
||||
// Buffer instances are also Uint8Array instances
|
||||
// https://nodejs.org/docs/latest/api/buffer.html#buffer_buffers_and_typedarray
|
||||
return resolve(buffer.buffer);
|
||||
return resolve(toArrayBuffer(buffer));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -468,7 +427,7 @@ function _getAnonymousAttachmentFileName(message, index) {
|
|||
return `${message.id}-${index}`;
|
||||
}
|
||||
|
||||
async function readAttachment(dir, attachment, name, options) {
|
||||
async function readEncryptedAttachment(dir, attachment, name, options) {
|
||||
options = options || {};
|
||||
const { key } = options;
|
||||
|
||||
|
@ -485,26 +444,29 @@ async function readAttachment(dir, attachment, name, options) {
|
|||
const isEncrypted = !_.isUndefined(key);
|
||||
|
||||
if (isEncrypted) {
|
||||
attachment.data = await crypto.decryptSymmetric(key, data);
|
||||
attachment.data = await crypto.decryptAttachment(
|
||||
key,
|
||||
attachment.path,
|
||||
data
|
||||
);
|
||||
} else {
|
||||
attachment.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
async function writeThumbnail(attachment, options) {
|
||||
async function writeQuoteThumbnail(attachment, options) {
|
||||
if (!attachment || !attachment.thumbnail || !attachment.thumbnail.path) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { dir, message, index, key, newKey } = options;
|
||||
const filename = `${_getAnonymousAttachmentFileName(
|
||||
message,
|
||||
index
|
||||
)}-thumbnail`;
|
||||
)}-quote-thumbnail`;
|
||||
const target = path.join(dir, filename);
|
||||
const { thumbnail } = attachment;
|
||||
|
||||
if (!thumbnail || !thumbnail.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
await writeEncryptedAttachment(target, thumbnail.data, {
|
||||
await writeEncryptedAttachment(target, attachment.thumbnail.path, {
|
||||
key,
|
||||
newKey,
|
||||
filename,
|
||||
|
@ -512,25 +474,13 @@ async function writeThumbnail(attachment, options) {
|
|||
});
|
||||
}
|
||||
|
||||
async function writeThumbnails(rawQuotedAttachments, options) {
|
||||
async function writeQuoteThumbnails(quotedAttachments, options) {
|
||||
const { name } = options;
|
||||
|
||||
const { loadAttachmentData } = Signal.Migrations;
|
||||
const promises = rawQuotedAttachments.map(async attachment => {
|
||||
if (!attachment || !attachment.thumbnail || !attachment.thumbnail.path) {
|
||||
return attachment;
|
||||
}
|
||||
|
||||
return Object.assign({}, attachment, {
|
||||
thumbnail: await loadAttachmentData(attachment.thumbnail),
|
||||
});
|
||||
});
|
||||
|
||||
const attachments = await Promise.all(promises);
|
||||
try {
|
||||
await Promise.all(
|
||||
_.map(attachments, (attachment, index) =>
|
||||
writeThumbnail(
|
||||
_.map(quotedAttachments, (attachment, index) =>
|
||||
writeQuoteThumbnail(
|
||||
attachment,
|
||||
Object.assign({}, options, {
|
||||
index,
|
||||
|
@ -550,26 +500,57 @@ async function writeThumbnails(rawQuotedAttachments, options) {
|
|||
}
|
||||
|
||||
async function writeAttachment(attachment, options) {
|
||||
if (!_.isString(attachment.path)) {
|
||||
throw new Error('writeAttachment: attachment.path was not a string!');
|
||||
}
|
||||
|
||||
const { dir, message, index, key, newKey } = options;
|
||||
const filename = _getAnonymousAttachmentFileName(message, index);
|
||||
const target = path.join(dir, filename);
|
||||
if (!Attachment.hasData(attachment)) {
|
||||
throw new TypeError("'attachment.data' is required");
|
||||
}
|
||||
|
||||
await writeEncryptedAttachment(target, attachment.data, {
|
||||
await writeEncryptedAttachment(target, attachment.path, {
|
||||
key,
|
||||
newKey,
|
||||
filename,
|
||||
dir,
|
||||
});
|
||||
|
||||
if (attachment.thumbnail && _.isString(attachment.thumbnail.path)) {
|
||||
const thumbnailName = `${_getAnonymousAttachmentFileName(
|
||||
message,
|
||||
index
|
||||
)}-thumbnail`;
|
||||
const thumbnailTarget = path.join(dir, thumbnailName);
|
||||
await writeEncryptedAttachment(thumbnailTarget, attachment.thumbnail.path, {
|
||||
key,
|
||||
newKey,
|
||||
filename: thumbnailName,
|
||||
dir,
|
||||
});
|
||||
}
|
||||
|
||||
if (attachment.screenshot && _.isString(attachment.screenshot.path)) {
|
||||
const screenshotName = `${_getAnonymousAttachmentFileName(
|
||||
message,
|
||||
index
|
||||
)}-screenshot`;
|
||||
const screenshotTarget = path.join(dir, screenshotName);
|
||||
await writeEncryptedAttachment(
|
||||
screenshotTarget,
|
||||
attachment.screenshot.path,
|
||||
{
|
||||
key,
|
||||
newKey,
|
||||
filename: screenshotName,
|
||||
dir,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function writeAttachments(rawAttachments, options) {
|
||||
async function writeAttachments(attachments, options) {
|
||||
const { name } = options;
|
||||
|
||||
const { loadAttachmentData } = Signal.Migrations;
|
||||
const attachments = await Promise.all(rawAttachments.map(loadAttachmentData));
|
||||
const promises = _.map(attachments, (attachment, index) =>
|
||||
writeAttachment(
|
||||
attachment,
|
||||
|
@ -591,17 +572,18 @@ async function writeAttachments(rawAttachments, options) {
|
|||
}
|
||||
}
|
||||
|
||||
async function writeAvatar(avatar, options) {
|
||||
const { dir, message, index, key, newKey } = options;
|
||||
const name = _getAnonymousAttachmentFileName(message, index);
|
||||
const filename = `${name}-contact-avatar`;
|
||||
|
||||
const target = path.join(dir, filename);
|
||||
if (!avatar || !avatar.path) {
|
||||
async function writeAvatar(contact, options) {
|
||||
const { avatar } = contact || {};
|
||||
if (!avatar || !avatar.avatar || !avatar.avatar.path) {
|
||||
return;
|
||||
}
|
||||
|
||||
await writeEncryptedAttachment(target, avatar.data, {
|
||||
const { dir, message, index, key, newKey } = options;
|
||||
const name = _getAnonymousAttachmentFileName(message, index);
|
||||
const filename = `${name}-contact-avatar`;
|
||||
const target = path.join(dir, filename);
|
||||
|
||||
await writeEncryptedAttachment(target, avatar.avatar.path, {
|
||||
key,
|
||||
newKey,
|
||||
filename,
|
||||
|
@ -612,23 +594,9 @@ async function writeAvatar(avatar, options) {
|
|||
async function writeContactAvatars(contact, options) {
|
||||
const { name } = options;
|
||||
|
||||
const { loadAttachmentData } = Signal.Migrations;
|
||||
const promises = contact.map(async item => {
|
||||
if (
|
||||
!item ||
|
||||
!item.avatar ||
|
||||
!item.avatar.avatar ||
|
||||
!item.avatar.avatar.path
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return loadAttachmentData(item.avatar.avatar);
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.all(
|
||||
_.map(await Promise.all(promises), (item, index) =>
|
||||
_.map(contact, (item, index) =>
|
||||
writeAvatar(
|
||||
item,
|
||||
Object.assign({}, options, {
|
||||
|
@ -648,7 +616,7 @@ async function writeContactAvatars(contact, options) {
|
|||
}
|
||||
}
|
||||
|
||||
async function writeEncryptedAttachment(target, data, options = {}) {
|
||||
async function writeEncryptedAttachment(target, source, options = {}) {
|
||||
const { key, newKey, filename, dir } = options;
|
||||
|
||||
if (fs.existsSync(target)) {
|
||||
|
@ -661,7 +629,9 @@ async function writeEncryptedAttachment(target, data, options = {}) {
|
|||
}
|
||||
}
|
||||
|
||||
const ciphertext = await crypto.encryptSymmetric(key, data);
|
||||
const { readAttachmentData } = Signal.Migrations;
|
||||
const data = await readAttachmentData(source);
|
||||
const ciphertext = await crypto.encryptAttachment(key, source, data);
|
||||
|
||||
const writer = await createFileAndWriter(dir, filename);
|
||||
const stream = createOutputStream(writer);
|
||||
|
@ -673,9 +643,9 @@ function _sanitizeFileName(filename) {
|
|||
return filename.toString().replace(/[^a-z0-9.,+()'#\- ]/gi, '_');
|
||||
}
|
||||
|
||||
async function exportConversation(db, conversation, options) {
|
||||
options = options || {};
|
||||
async function exportConversation(conversation, options = {}) {
|
||||
const { name, dir, attachmentsDir, key, newKey } = options;
|
||||
|
||||
if (!name) {
|
||||
throw new Error('Need a name!');
|
||||
}
|
||||
|
@ -691,143 +661,111 @@ async function exportConversation(db, conversation, options) {
|
|||
|
||||
window.log.info('exporting conversation', name);
|
||||
const writer = await createFileAndWriter(dir, 'messages.json');
|
||||
const stream = createOutputStream(writer);
|
||||
stream.write('{"messages":[');
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
// TODO: need to iterate through message ids, export using window.Signal.Data
|
||||
const transaction = db.transaction('messages', 'readwrite');
|
||||
transaction.onerror = () => {
|
||||
Whisper.Database.handleDOMException(
|
||||
`exportConversation transaction error (conversation: ${name})`,
|
||||
transaction.error,
|
||||
reject
|
||||
);
|
||||
};
|
||||
transaction.oncomplete = () => {
|
||||
// this doesn't really mean anything - we may have attachment processing to do
|
||||
};
|
||||
const CHUNK_SIZE = 50;
|
||||
let count = 0;
|
||||
let complete = false;
|
||||
|
||||
const store = transaction.objectStore('messages');
|
||||
const index = store.index('conversation');
|
||||
const range = window.IDBKeyRange.bound(
|
||||
[conversation.id, 0],
|
||||
[conversation.id, Number.MAX_VALUE]
|
||||
);
|
||||
// We're looping from the most recent to the oldest
|
||||
let lastReceivedAt = Number.MAX_VALUE;
|
||||
|
||||
let promiseChain = Promise.resolve();
|
||||
let count = 0;
|
||||
const request = index.openCursor(range);
|
||||
|
||||
const stream = createOutputStream(writer);
|
||||
stream.write('{"messages":[');
|
||||
|
||||
request.onerror = () => {
|
||||
Whisper.Database.handleDOMException(
|
||||
`exportConversation request error (conversation: ${name})`,
|
||||
request.error,
|
||||
reject
|
||||
);
|
||||
};
|
||||
request.onsuccess = async event => {
|
||||
const cursor = event.target.result;
|
||||
if (cursor) {
|
||||
const message = cursor.value;
|
||||
const { attachments } = message;
|
||||
|
||||
// skip message if it is disappearing, no matter the amount of time left
|
||||
if (message.expireTimer) {
|
||||
cursor.continue();
|
||||
return;
|
||||
}
|
||||
|
||||
if (count !== 0) {
|
||||
stream.write(',');
|
||||
}
|
||||
|
||||
// eliminate attachment data from the JSON, since it will go to disk
|
||||
// Note: this is for legacy messages only, which stored attachment data in the db
|
||||
message.attachments = _.map(attachments, attachment =>
|
||||
_.omit(attachment, ['data'])
|
||||
);
|
||||
// completely drop any attachments in messages cached in error objects
|
||||
// TODO: move to lodash. Sadly, a number of the method signatures have changed!
|
||||
message.errors = _.map(message.errors, error => {
|
||||
if (error && error.args) {
|
||||
error.args = [];
|
||||
}
|
||||
if (error && error.stack) {
|
||||
error.stack = '';
|
||||
}
|
||||
return error;
|
||||
});
|
||||
|
||||
const jsonString = JSON.stringify(stringify(message));
|
||||
stream.write(jsonString);
|
||||
|
||||
if (attachments && attachments.length > 0) {
|
||||
const exportAttachments = () =>
|
||||
writeAttachments(attachments, {
|
||||
dir: attachmentsDir,
|
||||
name,
|
||||
message,
|
||||
key,
|
||||
newKey,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line more/no-then
|
||||
promiseChain = promiseChain.then(exportAttachments);
|
||||
}
|
||||
|
||||
const quoteThumbnails = message.quote && message.quote.attachments;
|
||||
if (quoteThumbnails && quoteThumbnails.length > 0) {
|
||||
const exportQuoteThumbnails = () =>
|
||||
writeThumbnails(quoteThumbnails, {
|
||||
dir: attachmentsDir,
|
||||
name,
|
||||
message,
|
||||
key,
|
||||
newKey,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line more/no-then
|
||||
promiseChain = promiseChain.then(exportQuoteThumbnails);
|
||||
}
|
||||
|
||||
const { contact } = message;
|
||||
if (contact && contact.length > 0) {
|
||||
const exportContactAvatars = () =>
|
||||
writeContactAvatars(contact, {
|
||||
dir: attachmentsDir,
|
||||
name,
|
||||
message,
|
||||
key,
|
||||
newKey,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line more/no-then
|
||||
promiseChain = promiseChain.then(exportContactAvatars);
|
||||
}
|
||||
|
||||
count += 1;
|
||||
cursor.continue();
|
||||
} else {
|
||||
try {
|
||||
await Promise.all([stream.write(']}'), promiseChain, stream.close()]);
|
||||
} catch (error) {
|
||||
window.log.error(
|
||||
'exportConversation: error exporting conversation',
|
||||
name,
|
||||
':',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
window.log.info('done exporting conversation', name);
|
||||
resolve();
|
||||
while (!complete) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const collection = await window.Signal.Data.getMessagesByConversation(
|
||||
conversation.id,
|
||||
{
|
||||
limit: CHUNK_SIZE,
|
||||
receivedAt: lastReceivedAt,
|
||||
MessageCollection: Whisper.MessageCollection,
|
||||
}
|
||||
};
|
||||
});
|
||||
);
|
||||
const messages = getPlainJS(collection);
|
||||
|
||||
for (let i = 0, max = messages.length; i < max; i += 1) {
|
||||
const message = messages[i];
|
||||
if (count > 0) {
|
||||
stream.write(',');
|
||||
}
|
||||
|
||||
count += 1;
|
||||
|
||||
// skip message if it is disappearing, no matter the amount of time left
|
||||
if (message.expireTimer) {
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
const { attachments } = message;
|
||||
// eliminate attachment data from the JSON, since it will go to disk
|
||||
// Note: this is for legacy messages only, which stored attachment data in the db
|
||||
message.attachments = _.map(attachments, attachment =>
|
||||
_.omit(attachment, ['data'])
|
||||
);
|
||||
// completely drop any attachments in messages cached in error objects
|
||||
// TODO: move to lodash. Sadly, a number of the method signatures have changed!
|
||||
message.errors = _.map(message.errors, error => {
|
||||
if (error && error.args) {
|
||||
error.args = [];
|
||||
}
|
||||
if (error && error.stack) {
|
||||
error.stack = '';
|
||||
}
|
||||
return error;
|
||||
});
|
||||
|
||||
const jsonString = JSON.stringify(stringify(message));
|
||||
stream.write(jsonString);
|
||||
|
||||
if (attachments && attachments.length > 0) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await writeAttachments(attachments, {
|
||||
dir: attachmentsDir,
|
||||
name,
|
||||
message,
|
||||
key,
|
||||
newKey,
|
||||
});
|
||||
}
|
||||
|
||||
const quoteThumbnails = message.quote && message.quote.attachments;
|
||||
if (quoteThumbnails && quoteThumbnails.length > 0) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await writeQuoteThumbnails(quoteThumbnails, {
|
||||
dir: attachmentsDir,
|
||||
name,
|
||||
message,
|
||||
key,
|
||||
newKey,
|
||||
});
|
||||
}
|
||||
|
||||
const { contact } = message;
|
||||
if (contact && contact.length > 0) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await writeContactAvatars(contact, {
|
||||
dir: attachmentsDir,
|
||||
name,
|
||||
message,
|
||||
key,
|
||||
newKey,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const last = messages.length > 0 ? messages[messages.length - 1] : null;
|
||||
if (last) {
|
||||
lastReceivedAt = last.received_at;
|
||||
}
|
||||
|
||||
if (messages.length < CHUNK_SIZE) {
|
||||
complete = true;
|
||||
}
|
||||
}
|
||||
|
||||
stream.write(']}');
|
||||
await stream.close();
|
||||
}
|
||||
|
||||
// Goals for directory names:
|
||||
|
@ -857,74 +795,40 @@ function _getConversationLoggingName(conversation) {
|
|||
return name;
|
||||
}
|
||||
|
||||
function exportConversations(db, options) {
|
||||
async function exportConversations(options) {
|
||||
options = options || {};
|
||||
const { messagesDir, attachmentsDir, key, newKey } = options;
|
||||
|
||||
if (!messagesDir) {
|
||||
return Promise.reject(new Error('Need a messages directory!'));
|
||||
throw new Error('Need a messages directory!');
|
||||
}
|
||||
if (!attachmentsDir) {
|
||||
return Promise.reject(new Error('Need an attachments directory!'));
|
||||
throw new Error('Need an attachments directory!');
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction('conversations', 'readwrite');
|
||||
transaction.onerror = () => {
|
||||
Whisper.Database.handleDOMException(
|
||||
'exportConversations transaction error',
|
||||
transaction.error,
|
||||
reject
|
||||
);
|
||||
};
|
||||
transaction.oncomplete = () => {
|
||||
// not really very useful - fires at unexpected times
|
||||
};
|
||||
|
||||
let promiseChain = Promise.resolve();
|
||||
const store = transaction.objectStore('conversations');
|
||||
const request = store.openCursor();
|
||||
request.onerror = () => {
|
||||
Whisper.Database.handleDOMException(
|
||||
'exportConversations request error',
|
||||
request.error,
|
||||
reject
|
||||
);
|
||||
};
|
||||
request.onsuccess = async event => {
|
||||
const cursor = event.target.result;
|
||||
if (cursor && cursor.value) {
|
||||
const conversation = cursor.value;
|
||||
const dirName = _getConversationDirName(conversation);
|
||||
const name = _getConversationLoggingName(conversation);
|
||||
|
||||
const process = async () => {
|
||||
const dir = await createDirectory(messagesDir, dirName);
|
||||
return exportConversation(db, conversation, {
|
||||
name,
|
||||
dir,
|
||||
attachmentsDir,
|
||||
key,
|
||||
newKey,
|
||||
});
|
||||
};
|
||||
|
||||
window.log.info('scheduling export for conversation', name);
|
||||
// eslint-disable-next-line more/no-then
|
||||
promiseChain = promiseChain.then(process);
|
||||
cursor.continue();
|
||||
} else {
|
||||
window.log.info('Done scheduling conversation exports');
|
||||
try {
|
||||
await promiseChain;
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
const collection = await window.Signal.Data.getAllConversations({
|
||||
ConversationCollection: Whisper.ConversationCollection,
|
||||
});
|
||||
const conversations = collection.models;
|
||||
|
||||
for (let i = 0, max = conversations.length; i < max; i += 1) {
|
||||
const conversation = conversations[i];
|
||||
const dirName = _getConversationDirName(conversation);
|
||||
const name = _getConversationLoggingName(conversation);
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const dir = await createDirectory(messagesDir, dirName);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await exportConversation(conversation, {
|
||||
name,
|
||||
dir,
|
||||
attachmentsDir,
|
||||
key,
|
||||
newKey,
|
||||
});
|
||||
}
|
||||
|
||||
window.log.info('Done exporting conversations!');
|
||||
}
|
||||
|
||||
function getDirectory(options = {}) {
|
||||
|
@ -968,9 +872,30 @@ async function loadAttachments(dir, getName, options) {
|
|||
const { message } = options;
|
||||
|
||||
await Promise.all(
|
||||
_.map(message.attachments, (attachment, index) => {
|
||||
_.map(message.attachments, async (attachment, index) => {
|
||||
const name = getName(message, index, attachment);
|
||||
return readAttachment(dir, attachment, name, options);
|
||||
|
||||
await readEncryptedAttachment(dir, attachment, name, options);
|
||||
|
||||
if (attachment.thumbnail && _.isString(attachment.thumbnail.path)) {
|
||||
const thumbnailName = `${name}-thumbnail`;
|
||||
await readEncryptedAttachment(
|
||||
dir,
|
||||
attachment.thumbnail,
|
||||
thumbnailName,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
if (attachment.screenshot && _.isString(attachment.screenshot.path)) {
|
||||
const screenshotName = `${name}-screenshot`;
|
||||
await readEncryptedAttachment(
|
||||
dir,
|
||||
attachment.screenshot,
|
||||
screenshotName,
|
||||
options
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -982,8 +907,8 @@ async function loadAttachments(dir, getName, options) {
|
|||
return null;
|
||||
}
|
||||
|
||||
const name = `${getName(message, index)}-thumbnail`;
|
||||
return readAttachment(dir, thumbnail, name, options);
|
||||
const name = `${getName(message, index)}-quote-thumbnail`;
|
||||
return readEncryptedAttachment(dir, thumbnail, name, options);
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -996,7 +921,7 @@ async function loadAttachments(dir, getName, options) {
|
|||
}
|
||||
|
||||
const name = `${getName(message, index)}-contact-avatar`;
|
||||
return readAttachment(dir, avatar, name, options);
|
||||
return readEncryptedAttachment(dir, avatar, name, options);
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -1179,31 +1104,22 @@ function getDirectoryForExport() {
|
|||
return getDirectory();
|
||||
}
|
||||
|
||||
function createZip(zipDir, targetDir) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const target = path.join(zipDir, 'messages.zip');
|
||||
const output = fs.createWriteStream(target);
|
||||
const archive = archiver('zip', {
|
||||
async function compressArchive(file, targetDir) {
|
||||
const items = fs.readdirSync(targetDir);
|
||||
return tar.c(
|
||||
{
|
||||
gzip: true,
|
||||
file,
|
||||
cwd: targetDir,
|
||||
});
|
||||
},
|
||||
items
|
||||
);
|
||||
}
|
||||
|
||||
output.on('close', () => {
|
||||
resolve(target);
|
||||
});
|
||||
|
||||
archive.on('warning', error => {
|
||||
window.log.warn(`Archive generation warning: ${error.stack}`);
|
||||
});
|
||||
archive.on('error', reject);
|
||||
|
||||
archive.pipe(output);
|
||||
|
||||
// The empty string ensures that the base location of the files added to the zip
|
||||
// is nothing. If you provide null, you get the absolute path you pulled the files
|
||||
// from in the first place.
|
||||
archive.directory(targetDir, '');
|
||||
|
||||
archive.finalize();
|
||||
async function decompressArchive(file, targetDir) {
|
||||
return tar.x({
|
||||
file,
|
||||
cwd: targetDir,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1211,6 +1127,13 @@ function writeFile(targetPath, contents) {
|
|||
return pify(fs.writeFile)(targetPath, contents);
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
const UNIQUE_ID = new Uint8Array([
|
||||
1, 3, 4, 5, 6, 7, 8, 11,
|
||||
23, 34, 1, 34, 3, 5, 45, 45,
|
||||
1, 3, 4, 5, 6, 7, 8, 11,
|
||||
23, 34, 1, 34, 3, 5, 45, 45,
|
||||
]);
|
||||
async function encryptFile(sourcePath, targetPath, options) {
|
||||
options = options || {};
|
||||
|
||||
|
@ -1220,8 +1143,8 @@ async function encryptFile(sourcePath, targetPath, options) {
|
|||
}
|
||||
|
||||
const plaintext = await readFileAsArrayBuffer(sourcePath);
|
||||
const ciphertext = await crypto.encryptSymmetric(key, plaintext);
|
||||
return writeFile(targetPath, ciphertext);
|
||||
const ciphertext = await crypto.encryptFile(key, UNIQUE_ID, plaintext);
|
||||
return writeFile(targetPath, Buffer.from(ciphertext));
|
||||
}
|
||||
|
||||
async function decryptFile(sourcePath, targetPath, options) {
|
||||
|
@ -1233,7 +1156,7 @@ async function decryptFile(sourcePath, targetPath, options) {
|
|||
}
|
||||
|
||||
const ciphertext = await readFileAsArrayBuffer(sourcePath);
|
||||
const plaintext = await crypto.decryptSymmetric(key, ciphertext);
|
||||
const plaintext = await crypto.decryptFile(key, UNIQUE_ID, ciphertext);
|
||||
return writeFile(targetPath, Buffer.from(plaintext));
|
||||
}
|
||||
|
||||
|
@ -1246,9 +1169,9 @@ function deleteAll(pattern) {
|
|||
return pify(rimraf)(pattern);
|
||||
}
|
||||
|
||||
async function exportToDirectory(directory, options) {
|
||||
throw new Error('Encrypted export/import is disabled');
|
||||
const ARCHIVE_NAME = 'messages.tar.gz';
|
||||
|
||||
async function exportToDirectory(directory, options) {
|
||||
options = options || {};
|
||||
|
||||
if (!options.key) {
|
||||
|
@ -1261,20 +1184,19 @@ async function exportToDirectory(directory, options) {
|
|||
stagingDir = await createTempDir();
|
||||
encryptionDir = await createTempDir();
|
||||
|
||||
const db = await Whisper.Database.open();
|
||||
const attachmentsDir = await createDirectory(directory, 'attachments');
|
||||
|
||||
await exportContactAndGroupsToFile(db, stagingDir);
|
||||
await exportContactAndGroupsToFile(stagingDir);
|
||||
await exportConversations(
|
||||
db,
|
||||
Object.assign({}, options, {
|
||||
messagesDir: stagingDir,
|
||||
attachmentsDir,
|
||||
})
|
||||
);
|
||||
|
||||
const zip = await createZip(encryptionDir, stagingDir);
|
||||
await encryptFile(zip, path.join(directory, 'messages.zip'), options);
|
||||
const archivePath = path.join(directory, ARCHIVE_NAME);
|
||||
await compressArchive(archivePath, stagingDir);
|
||||
await encryptFile(archivePath, path.join(directory, ARCHIVE_NAME), options);
|
||||
|
||||
window.log.info('done backing up!');
|
||||
return directory;
|
||||
|
@ -1317,10 +1239,8 @@ async function importFromDirectory(directory, options) {
|
|||
groupLookup,
|
||||
});
|
||||
|
||||
const zipPath = path.join(directory, 'messages.zip');
|
||||
if (fs.existsSync(zipPath)) {
|
||||
throw new Error('Encrypted export/import is disabled');
|
||||
|
||||
const archivePath = path.join(directory, ARCHIVE_NAME);
|
||||
if (fs.existsSync(archivePath)) {
|
||||
// we're in the world of an encrypted, zipped backup
|
||||
if (!options.key) {
|
||||
throw new Error(
|
||||
|
@ -1336,9 +1256,9 @@ async function importFromDirectory(directory, options) {
|
|||
|
||||
const attachmentsDir = path.join(directory, 'attachments');
|
||||
|
||||
const decryptedZip = path.join(decryptionDir, 'messages.zip');
|
||||
await decryptFile(zipPath, decryptedZip, options);
|
||||
await decompress(decryptedZip, stagingDir);
|
||||
const decryptedArchivePath = path.join(decryptionDir, ARCHIVE_NAME);
|
||||
await decryptFile(archivePath, decryptedArchivePath, options);
|
||||
await decompressArchive(decryptedArchivePath, stagingDir);
|
||||
|
||||
options = Object.assign({}, options, {
|
||||
attachmentsDir,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/* eslint-env browser */
|
||||
/* global dcodeIO */
|
||||
/* global dcodeIO, libsignal */
|
||||
|
||||
/* eslint-disable camelcase, no-bitwise */
|
||||
|
||||
|
@ -10,9 +10,15 @@ module.exports = {
|
|||
concatenateBytes,
|
||||
constantTimeEqual,
|
||||
decryptAesCtr,
|
||||
decryptDeviceName,
|
||||
decryptAttachment,
|
||||
decryptFile,
|
||||
decryptSymmetric,
|
||||
deriveAccessKey,
|
||||
encryptAesCtr,
|
||||
encryptDeviceName,
|
||||
encryptAttachment,
|
||||
encryptFile,
|
||||
encryptSymmetric,
|
||||
fromEncodedBinaryToArrayBuffer,
|
||||
getAccessKeyVerifier,
|
||||
|
@ -28,8 +34,117 @@ module.exports = {
|
|||
verifyAccessKey,
|
||||
};
|
||||
|
||||
function arrayBufferToBase64(arrayBuffer) {
|
||||
return dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64');
|
||||
}
|
||||
function base64ToArrayBuffer(base64string) {
|
||||
return dcodeIO.ByteBuffer.wrap(base64string, 'base64').toArrayBuffer();
|
||||
}
|
||||
|
||||
function fromEncodedBinaryToArrayBuffer(key) {
|
||||
return dcodeIO.ByteBuffer.wrap(key, 'binary').toArrayBuffer();
|
||||
}
|
||||
|
||||
function bytesFromString(string) {
|
||||
return dcodeIO.ByteBuffer.wrap(string, 'utf8').toArrayBuffer();
|
||||
}
|
||||
function stringFromBytes(buffer) {
|
||||
return dcodeIO.ByteBuffer.wrap(buffer).toString('utf8');
|
||||
}
|
||||
|
||||
// High-level Operations
|
||||
|
||||
async function encryptDeviceName(deviceName, identityPublic) {
|
||||
const plaintext = bytesFromString(deviceName);
|
||||
const ephemeralKeyPair = await libsignal.KeyHelper.generateIdentityKeyPair();
|
||||
const masterSecret = await libsignal.Curve.async.calculateAgreement(
|
||||
identityPublic,
|
||||
ephemeralKeyPair.privKey
|
||||
);
|
||||
|
||||
const key1 = await hmacSha256(masterSecret, bytesFromString('auth'));
|
||||
const syntheticIv = _getFirstBytes(await hmacSha256(key1, plaintext), 16);
|
||||
|
||||
const key2 = await hmacSha256(masterSecret, bytesFromString('cipher'));
|
||||
const cipherKey = await hmacSha256(key2, syntheticIv);
|
||||
|
||||
const counter = getZeroes(16);
|
||||
const ciphertext = await encryptAesCtr(cipherKey, plaintext, counter);
|
||||
|
||||
return {
|
||||
ephemeralPublic: ephemeralKeyPair.pubKey,
|
||||
syntheticIv,
|
||||
ciphertext,
|
||||
};
|
||||
}
|
||||
|
||||
async function decryptDeviceName(
|
||||
{ ephemeralPublic, syntheticIv, ciphertext } = {},
|
||||
identityPrivate
|
||||
) {
|
||||
const masterSecret = await libsignal.Curve.async.calculateAgreement(
|
||||
ephemeralPublic,
|
||||
identityPrivate
|
||||
);
|
||||
|
||||
const key2 = await hmacSha256(masterSecret, bytesFromString('cipher'));
|
||||
const cipherKey = await hmacSha256(key2, syntheticIv);
|
||||
|
||||
const counter = getZeroes(16);
|
||||
const plaintext = await decryptAesCtr(cipherKey, ciphertext, counter);
|
||||
|
||||
const key1 = await hmacSha256(masterSecret, bytesFromString('auth'));
|
||||
const ourSyntheticIv = _getFirstBytes(await hmacSha256(key1, plaintext), 16);
|
||||
|
||||
if (!constantTimeEqual(ourSyntheticIv, syntheticIv)) {
|
||||
throw new Error('decryptDeviceName: synthetic IV did not match');
|
||||
}
|
||||
|
||||
return stringFromBytes(plaintext);
|
||||
}
|
||||
|
||||
// Path structure: 'fa/facdf99c22945b1c9393345599a276f4b36ad7ccdc8c2467f5441b742c2d11fa'
|
||||
function getAttachmentLabel(path) {
|
||||
const filename = path.slice(3);
|
||||
return base64ToArrayBuffer(filename);
|
||||
}
|
||||
|
||||
const PUB_KEY_LENGTH = 32;
|
||||
async function encryptAttachment(staticPublicKey, path, plaintext) {
|
||||
const uniqueId = getAttachmentLabel(path);
|
||||
return encryptFile(staticPublicKey, uniqueId, plaintext);
|
||||
}
|
||||
|
||||
async function decryptAttachment(staticPrivateKey, path, data) {
|
||||
const uniqueId = getAttachmentLabel(path);
|
||||
return decryptFile(staticPrivateKey, uniqueId, data);
|
||||
}
|
||||
|
||||
async function encryptFile(staticPublicKey, uniqueId, plaintext) {
|
||||
const ephemeralKeyPair = await libsignal.KeyHelper.generateIdentityKeyPair();
|
||||
const agreement = await libsignal.Curve.async.calculateAgreement(
|
||||
staticPublicKey,
|
||||
ephemeralKeyPair.privKey
|
||||
);
|
||||
const key = await hmacSha256(agreement, uniqueId);
|
||||
|
||||
const prefix = ephemeralKeyPair.pubKey.slice(1);
|
||||
return concatenateBytes(prefix, await encryptSymmetric(key, plaintext));
|
||||
}
|
||||
|
||||
async function decryptFile(staticPrivateKey, uniqueId, data) {
|
||||
const ephemeralPublicKey = _getFirstBytes(data, PUB_KEY_LENGTH);
|
||||
const ciphertext = _getBytes(data, PUB_KEY_LENGTH, data.byteLength);
|
||||
const agreement = await libsignal.Curve.async.calculateAgreement(
|
||||
ephemeralPublicKey,
|
||||
staticPrivateKey
|
||||
);
|
||||
|
||||
const key = await hmacSha256(agreement, uniqueId);
|
||||
|
||||
return decryptSymmetric(key, ciphertext);
|
||||
}
|
||||
|
||||
async function deriveAccessKey(profileKey) {
|
||||
const iv = getZeroes(12);
|
||||
const plaintext = getZeroes(16);
|
||||
|
@ -267,24 +382,6 @@ function trimBytes(buffer, length) {
|
|||
return _getFirstBytes(buffer, length);
|
||||
}
|
||||
|
||||
function arrayBufferToBase64(arrayBuffer) {
|
||||
return dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64');
|
||||
}
|
||||
function base64ToArrayBuffer(base64string) {
|
||||
return dcodeIO.ByteBuffer.wrap(base64string, 'base64').toArrayBuffer();
|
||||
}
|
||||
|
||||
function fromEncodedBinaryToArrayBuffer(key) {
|
||||
return dcodeIO.ByteBuffer.wrap(key, 'binary').toArrayBuffer();
|
||||
}
|
||||
|
||||
function bytesFromString(string) {
|
||||
return dcodeIO.ByteBuffer.wrap(string, 'utf8').toArrayBuffer();
|
||||
}
|
||||
function stringFromBytes(buffer) {
|
||||
return dcodeIO.ByteBuffer.wrap(buffer).toString('utf8');
|
||||
}
|
||||
|
||||
function getViewOfArrayBuffer(buffer, start, finish) {
|
||||
const source = new Uint8Array(buffer);
|
||||
const result = source.slice(start, finish);
|
||||
|
|
|
@ -52,6 +52,7 @@ module.exports = {
|
|||
createOrUpdateGroup,
|
||||
getGroupById,
|
||||
getAllGroupIds,
|
||||
getAllGroups,
|
||||
bulkAddGroups,
|
||||
removeGroupById,
|
||||
removeAllGroups,
|
||||
|
@ -428,6 +429,10 @@ async function getAllGroupIds() {
|
|||
const ids = await channels.getAllGroupIds();
|
||||
return ids;
|
||||
}
|
||||
async function getAllGroups() {
|
||||
const groups = await channels.getAllGroups();
|
||||
return groups;
|
||||
}
|
||||
async function bulkAddGroups(array) {
|
||||
await channels.bulkAddGroups(array);
|
||||
}
|
||||
|
|
|
@ -15,6 +15,10 @@ const Metadata = require('./metadata/SecretSessionCipher');
|
|||
const RefreshSenderCertificate = require('./refresh_sender_certificate');
|
||||
|
||||
// Components
|
||||
const {
|
||||
AttachmentList,
|
||||
} = require('../../ts/components/conversation/AttachmentList');
|
||||
const { CaptionEditor } = require('../../ts/components/CaptionEditor');
|
||||
const {
|
||||
ContactDetail,
|
||||
} = require('../../ts/components/conversation/ContactDetail');
|
||||
|
@ -136,6 +140,7 @@ function initializeMigrations({
|
|||
loadAttachmentData,
|
||||
loadQuoteData,
|
||||
loadMessage: MessageType.createAttachmentLoader(loadAttachmentData),
|
||||
readAttachmentData,
|
||||
run,
|
||||
upgradeMessageSchema: (message, options = {}) => {
|
||||
const { maxVersion } = options;
|
||||
|
@ -174,6 +179,8 @@ exports.setup = (options = {}) => {
|
|||
});
|
||||
|
||||
const Components = {
|
||||
AttachmentList,
|
||||
CaptionEditor,
|
||||
ContactDetail,
|
||||
ContactListItem,
|
||||
ContactName,
|
||||
|
|
|
@ -545,8 +545,6 @@ exports.createAttachmentDataWriter = ({
|
|||
});
|
||||
};
|
||||
|
||||
// TODO: need to handle attachment thumbnails and video screenshots
|
||||
|
||||
const messageWithoutAttachmentData = Object.assign(
|
||||
{},
|
||||
await writeThumbnails(message, { logger }),
|
||||
|
@ -555,7 +553,23 @@ exports.createAttachmentDataWriter = ({
|
|||
attachments: await Promise.all(
|
||||
(attachments || []).map(async attachment => {
|
||||
await writeExistingAttachmentData(attachment);
|
||||
return omit(attachment, ['data']);
|
||||
|
||||
if (attachment.screenshot && attachment.screenshot.data) {
|
||||
await writeExistingAttachmentData(attachment.screenshot);
|
||||
}
|
||||
if (attachment.thumbnail && attachment.thumbnail.data) {
|
||||
await writeExistingAttachmentData(attachment.thumbnail);
|
||||
}
|
||||
|
||||
return {
|
||||
...omit(attachment, ['data']),
|
||||
...(attachment.thumbnail
|
||||
? { thumbnail: omit(attachment.thumbnail, ['data']) }
|
||||
: null),
|
||||
...(attachment.screenshot
|
||||
? { screenshot: omit(attachment.screenshot, ['data']) }
|
||||
: null),
|
||||
};
|
||||
})
|
||||
),
|
||||
}
|
||||
|
|
|
@ -332,6 +332,8 @@ function HTTPError(message, providedCode, response, stack) {
|
|||
|
||||
const URL_CALLS = {
|
||||
accounts: 'v1/accounts',
|
||||
updateDeviceName: 'v1/accounts/name',
|
||||
removeSignalingKey: 'v1/accounts/signaling_key',
|
||||
attachment: 'v1/attachments',
|
||||
deliveryCert: 'v1/certificate/delivery',
|
||||
supportUnauthenticatedDelivery: 'v1/devices/unauthenticated_delivery',
|
||||
|
@ -393,6 +395,8 @@ function initialize({ url, cdnUrl, certificateAuthority, proxyUrl }) {
|
|||
sendMessages,
|
||||
sendMessagesUnauth,
|
||||
setSignedPreKey,
|
||||
updateDeviceName,
|
||||
removeSignalingKey,
|
||||
};
|
||||
|
||||
function _ajax(param) {
|
||||
|
@ -523,14 +527,12 @@ function initialize({ url, cdnUrl, certificateAuthority, proxyUrl }) {
|
|||
number,
|
||||
code,
|
||||
newPassword,
|
||||
signalingKey,
|
||||
registrationId,
|
||||
deviceName,
|
||||
options = {}
|
||||
) {
|
||||
const { accessKey } = options;
|
||||
const jsonData = {
|
||||
signalingKey: _btoa(_getString(signalingKey)),
|
||||
supportsSms: false,
|
||||
fetchesMessages: true,
|
||||
registrationId,
|
||||
|
@ -575,6 +577,23 @@ function initialize({ url, cdnUrl, certificateAuthority, proxyUrl }) {
|
|||
return response;
|
||||
}
|
||||
|
||||
function updateDeviceName(deviceName) {
|
||||
return _ajax({
|
||||
call: 'updateDeviceName',
|
||||
httpType: 'PUT',
|
||||
jsonData: {
|
||||
deviceName,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function removeSignalingKey() {
|
||||
return _ajax({
|
||||
call: 'removeSignalingKey',
|
||||
httpType: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
function getDevices() {
|
||||
return _ajax({
|
||||
call: 'devices',
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
/* global Whisper */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
Whisper.AttachmentPreviewView = Whisper.View.extend({
|
||||
className: 'attachment-preview',
|
||||
templateName: 'attachment-preview',
|
||||
render_attributes() {
|
||||
return { source: this.src };
|
||||
},
|
||||
});
|
||||
})();
|
|
@ -58,6 +58,11 @@
|
|||
return { toastMessage: i18n('messageFoundButNotLoaded') };
|
||||
},
|
||||
});
|
||||
Whisper.VoiceNoteMustBeOnlyAttachmentToast = Whisper.ToastView.extend({
|
||||
render_attributes() {
|
||||
return { toastMessage: i18n('voiceNoteMustBeOnlyAttachment') };
|
||||
},
|
||||
});
|
||||
|
||||
Whisper.ConversationLoadingScreen = Whisper.View.extend({
|
||||
templateName: 'conversation-loading-screen',
|
||||
|
@ -150,8 +155,16 @@
|
|||
|
||||
this.window = options.window;
|
||||
this.fileInput = new Whisper.FileInputView({
|
||||
el: this.$('form.send'),
|
||||
window: this.window,
|
||||
el: this.$('.attachment-list'),
|
||||
});
|
||||
this.listenTo(
|
||||
this.fileInput,
|
||||
'choose-attachment',
|
||||
this.onChooseAttachment
|
||||
);
|
||||
this.listenTo(this.fileInput, 'staged-attachments-changed', () => {
|
||||
this.view.resetScrollPosition();
|
||||
this.toggleMicrophone();
|
||||
});
|
||||
|
||||
const getHeaderProps = () => {
|
||||
|
@ -185,7 +198,7 @@
|
|||
onDeleteMessages: () => this.destroyMessages(),
|
||||
onResetSession: () => this.endSession(),
|
||||
|
||||
// These are view only and done update the Conversation model, so they
|
||||
// These are view only and don't update the Conversation model, so they
|
||||
// need a manual update call.
|
||||
onShowSafetyNumber: () => {
|
||||
this.showSafetyNumber();
|
||||
|
@ -290,15 +303,49 @@
|
|||
'farFromBottom .message-list': 'addScrollDownButton',
|
||||
'lazyScroll .message-list': 'onLazyScroll',
|
||||
'force-resize': 'forceUpdateMessageFieldSize',
|
||||
dragover: 'sendToFileInput',
|
||||
drop: 'sendToFileInput',
|
||||
dragleave: 'sendToFileInput',
|
||||
|
||||
'click button.paperclip': 'onChooseAttachment',
|
||||
'change input.file-input': 'onChoseAttachment',
|
||||
|
||||
dragover: 'onDragOver',
|
||||
dragleave: 'onDragLeave',
|
||||
drop: 'onDrop',
|
||||
paste: 'onPaste',
|
||||
},
|
||||
sendToFileInput(e) {
|
||||
if (e.originalEvent.dataTransfer.types[0] !== 'Files') {
|
||||
return;
|
||||
|
||||
onChooseAttachment(e) {
|
||||
if (e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
this.fileInput.$el.trigger(e);
|
||||
|
||||
this.$('input.file-input').click();
|
||||
},
|
||||
async onChoseAttachment() {
|
||||
const fileField = this.$('input.file-input');
|
||||
const files = fileField.prop('files');
|
||||
|
||||
for (let i = 0, max = files.length; i < max; i += 1) {
|
||||
const file = files[i];
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await this.fileInput.maybeAddAttachment(file);
|
||||
this.toggleMicrophone();
|
||||
}
|
||||
|
||||
fileField.val(null);
|
||||
},
|
||||
|
||||
onDragOver(e) {
|
||||
this.fileInput.onDragOver(e);
|
||||
},
|
||||
onDragLeave(e) {
|
||||
this.fileInput.onDragLeave(e);
|
||||
},
|
||||
onDrop(e) {
|
||||
this.fileInput.onDrop(e);
|
||||
},
|
||||
onPaste(e) {
|
||||
this.fileInput.onPaste(e);
|
||||
},
|
||||
|
||||
onPrune() {
|
||||
|
@ -546,6 +593,13 @@
|
|||
captureAudio(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.fileInput.hasFiles()) {
|
||||
const toast = new Whisper.VoiceNoteMustBeOnlyAttachmentToast();
|
||||
toast.$el.appendTo(this.$el);
|
||||
toast.render();
|
||||
return;
|
||||
}
|
||||
|
||||
// Note - clicking anywhere will close the audio capture panel, due to
|
||||
// the onClick handler in InboxView, which calls its closeRecording method.
|
||||
|
||||
|
@ -566,9 +620,11 @@
|
|||
this.$('.microphone').hide();
|
||||
},
|
||||
handleAudioCapture(blob) {
|
||||
this.fileInput.file = blob;
|
||||
this.fileInput.isVoiceNote = true;
|
||||
this.fileInput.previewImages();
|
||||
this.fileInput.addAttachment({
|
||||
contentType: blob.type,
|
||||
file: blob,
|
||||
isVoiceNote: true,
|
||||
});
|
||||
this.$('.bottom-bar form').submit();
|
||||
},
|
||||
endCaptureAudio() {
|
||||
|
@ -1229,6 +1285,7 @@
|
|||
const props = {
|
||||
objectURL: getAbsoluteAttachmentPath(path),
|
||||
contentType,
|
||||
caption: attachment.caption,
|
||||
onSave: () => this.downloadAttachment({ attachment, message }),
|
||||
};
|
||||
this.lightboxView = new Whisper.ReactWrapperView({
|
||||
|
@ -1496,7 +1553,6 @@
|
|||
if (event.key !== 'Escape') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.closeEmojiPanel();
|
||||
},
|
||||
openEmojiPanel() {
|
||||
|
@ -1504,6 +1560,7 @@
|
|||
this.emojiPanel = new EmojiPanel(this.$emojiPanelContainer[0], {
|
||||
onClick: this.insertEmoji.bind(this),
|
||||
});
|
||||
this.view.resetScrollPosition();
|
||||
this.updateMessageFieldSize({});
|
||||
},
|
||||
closeEmojiPanel() {
|
||||
|
@ -1513,6 +1570,7 @@
|
|||
|
||||
this.$emojiPanelContainer.empty().outerHeight(0);
|
||||
this.emojiPanel = null;
|
||||
this.view.resetScrollPosition();
|
||||
this.updateMessageFieldSize({});
|
||||
},
|
||||
insertEmoji(e) {
|
||||
|
@ -1560,6 +1618,7 @@
|
|||
this.quoteView = null;
|
||||
}
|
||||
if (!this.quotedMessage) {
|
||||
this.view.restoreBottomOffset();
|
||||
this.updateMessageFieldSize({});
|
||||
return;
|
||||
}
|
||||
|
@ -1583,16 +1642,18 @@
|
|||
this.quoteView = new Whisper.ReactWrapperView({
|
||||
className: 'quote-wrapper',
|
||||
Component: window.Signal.Components.Quote,
|
||||
elCallback: el => this.$('.send').prepend(el),
|
||||
props: Object.assign({}, props, {
|
||||
withContentAbove: true,
|
||||
onClose: () => {
|
||||
this.setQuoteMessage(null);
|
||||
},
|
||||
}),
|
||||
onInitialRender: () => {
|
||||
this.view.restoreBottomOffset();
|
||||
this.updateMessageFieldSize({});
|
||||
},
|
||||
});
|
||||
|
||||
this.$('.send').prepend(this.quoteView.el);
|
||||
this.updateMessageFieldSize({});
|
||||
},
|
||||
|
||||
async sendMessage(e) {
|
||||
|
@ -1647,7 +1708,7 @@
|
|||
this.setQuoteMessage(null);
|
||||
this.focusMessageFieldAndClearDisabled();
|
||||
this.forceUpdateMessageFieldSize(e);
|
||||
this.fileInput.deleteFiles();
|
||||
this.fileInput.clearAttachments();
|
||||
} catch (error) {
|
||||
window.log.error(
|
||||
'Error pulling attached files before send',
|
||||
|
|
|
@ -30,91 +30,412 @@
|
|||
},
|
||||
});
|
||||
|
||||
Whisper.UnsupportedFileTypeToast = Whisper.ToastView.extend({
|
||||
template: i18n('unsupportedFileType'),
|
||||
});
|
||||
|
||||
Whisper.DangerousFileTypeToast = Whisper.ToastView.extend({
|
||||
template: i18n('dangerousFileType'),
|
||||
});
|
||||
Whisper.OneNonImageAtATimeToast = Whisper.ToastView.extend({
|
||||
template: i18n('oneNonImageAtATimeToast'),
|
||||
});
|
||||
Whisper.CannotMixImageAndNonImageAttachmentsToast = Whisper.ToastView.extend({
|
||||
template: i18n('cannotMixImageAdnNonImageAttachments'),
|
||||
});
|
||||
Whisper.MaxAttachmentsToast = Whisper.ToastView.extend({
|
||||
template: i18n('maximumAttachments'),
|
||||
});
|
||||
|
||||
Whisper.FileInputView = Backbone.View.extend({
|
||||
tagName: 'span',
|
||||
className: 'file-input',
|
||||
initialize(options) {
|
||||
this.$input = this.$('input[type=file]');
|
||||
this.$input.click(e => {
|
||||
e.stopPropagation();
|
||||
initialize() {
|
||||
this.attachments = [];
|
||||
|
||||
this.attachmentListView = new Whisper.ReactWrapperView({
|
||||
el: this.el,
|
||||
Component: window.Signal.Components.AttachmentList,
|
||||
props: this.getPropsForAttachmentList(),
|
||||
});
|
||||
this.thumb = new Whisper.AttachmentPreviewView();
|
||||
this.$el.addClass('file-input');
|
||||
this.window = options.window;
|
||||
this.previewObjectUrl = null;
|
||||
},
|
||||
|
||||
events: {
|
||||
'change .choose-file': 'previewImages',
|
||||
'click .close': 'deleteFiles',
|
||||
'click .choose-file': 'open',
|
||||
drop: 'openDropped',
|
||||
dragover: 'showArea',
|
||||
dragleave: 'hideArea',
|
||||
paste: 'onPaste',
|
||||
remove() {
|
||||
if (this.attachmentListView) {
|
||||
this.attachmentListView.remove();
|
||||
}
|
||||
if (this.captionEditorView) {
|
||||
this.captionEditorView.remove();
|
||||
}
|
||||
|
||||
Backbone.View.prototype.remove.call(this);
|
||||
},
|
||||
|
||||
open(e) {
|
||||
render() {
|
||||
this.attachmentListView.update(this.getPropsForAttachmentList());
|
||||
this.trigger('staged-attachments-changed');
|
||||
},
|
||||
|
||||
getPropsForAttachmentList() {
|
||||
const { attachments } = this;
|
||||
|
||||
// We never want to display voice notes in our attachment list
|
||||
if (_.any(attachments, attachment => Boolean(attachment.isVoiceNote))) {
|
||||
return {
|
||||
attachments: [],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
attachments,
|
||||
onAddAttachment: this.onAddAttachment.bind(this),
|
||||
onClickAttachment: this.onClickAttachment.bind(this),
|
||||
onCloseAttachment: this.onCloseAttachment.bind(this),
|
||||
onClose: this.onClose.bind(this),
|
||||
};
|
||||
},
|
||||
|
||||
onClickAttachment(attachment) {
|
||||
const getProps = () => ({
|
||||
url: attachment.videoUrl || attachment.url,
|
||||
caption: attachment.caption,
|
||||
attachment,
|
||||
onSave,
|
||||
});
|
||||
|
||||
const onSave = caption => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
attachment.caption = caption;
|
||||
this.captionEditorView.remove();
|
||||
Signal.Backbone.Views.Lightbox.hide();
|
||||
this.render();
|
||||
};
|
||||
|
||||
this.captionEditorView = new Whisper.ReactWrapperView({
|
||||
className: 'attachment-list-wrapper',
|
||||
Component: window.Signal.Components.CaptionEditor,
|
||||
props: getProps(),
|
||||
onClose: () => Signal.Backbone.Views.Lightbox.hide(),
|
||||
});
|
||||
Signal.Backbone.Views.Lightbox.show(this.captionEditorView.el);
|
||||
},
|
||||
|
||||
onCloseAttachment(attachment) {
|
||||
this.attachments = _.without(this.attachments, attachment);
|
||||
this.render();
|
||||
},
|
||||
|
||||
onAddAttachment() {
|
||||
this.trigger('choose-attachment');
|
||||
},
|
||||
|
||||
onClose() {
|
||||
this.attachments = [];
|
||||
this.render();
|
||||
},
|
||||
|
||||
// These event handlers are called by ConversationView, which listens for these events
|
||||
|
||||
onDragOver(e) {
|
||||
if (e.originalEvent.dataTransfer.types[0] !== 'Files') {
|
||||
return;
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
// hack
|
||||
if (this.window && this.window.chrome && this.window.chrome.fileSystem) {
|
||||
this.window.chrome.fileSystem.chooseEntry(
|
||||
{ type: 'openFile' },
|
||||
entry => {
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
entry.file(file => {
|
||||
this.file = file;
|
||||
this.previewImages();
|
||||
});
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.$input.click();
|
||||
this.$el.addClass('dropoff');
|
||||
},
|
||||
|
||||
onDragLeave(e) {
|
||||
if (e.originalEvent.dataTransfer.types[0] !== 'Files') {
|
||||
return;
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.$el.removeClass('dropoff');
|
||||
},
|
||||
|
||||
async onDrop(e) {
|
||||
if (e.originalEvent.dataTransfer.types[0] !== 'Files') {
|
||||
return;
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
const { files } = e.originalEvent.dataTransfer;
|
||||
for (let i = 0, max = files.length; i < max; i += 1) {
|
||||
const file = files[i];
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await this.maybeAddAttachment(file);
|
||||
}
|
||||
|
||||
this.$el.removeClass('dropoff');
|
||||
},
|
||||
|
||||
onPaste(e) {
|
||||
const { items } = e.originalEvent.clipboardData;
|
||||
let imgBlob = null;
|
||||
for (let i = 0; i < items.length; i += 1) {
|
||||
if (items[i].type.split('/')[0] === 'image') {
|
||||
imgBlob = items[i].getAsFile();
|
||||
}
|
||||
}
|
||||
if (imgBlob !== null) {
|
||||
const file = imgBlob;
|
||||
this.maybeAddAttachment(file);
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
},
|
||||
|
||||
addThumb(src, options = {}) {
|
||||
_.defaults(options, { addPlayIcon: false });
|
||||
this.$('.avatar').hide();
|
||||
this.thumb.src = src;
|
||||
this.$('.attachment-previews').append(this.thumb.render().el);
|
||||
// Public interface
|
||||
|
||||
if (options.addPlayIcon) {
|
||||
this.$el.addClass('video-attachment');
|
||||
} else {
|
||||
this.$el.removeClass('video-attachment');
|
||||
}
|
||||
|
||||
this.thumb.$('img')[0].onload = () => {
|
||||
this.$el.trigger('force-resize');
|
||||
};
|
||||
this.thumb.$('img')[0].onerror = () => {
|
||||
this.unableToLoadAttachment();
|
||||
};
|
||||
hasFiles() {
|
||||
return this.attachments.length > 0;
|
||||
},
|
||||
|
||||
unableToLoadAttachment() {
|
||||
async getFiles() {
|
||||
const files = await Promise.all(
|
||||
this.attachments.map(attachment => this.getFile(attachment))
|
||||
);
|
||||
this.clearAttachments();
|
||||
return files;
|
||||
},
|
||||
|
||||
clearAttachments() {
|
||||
this.attachments.forEach(attachment => {
|
||||
if (attachment.url) {
|
||||
URL.revokeObjectURL(attachment.url);
|
||||
}
|
||||
if (attachment.videoUrl) {
|
||||
URL.revokeObjectURL(attachment.videoUrl);
|
||||
}
|
||||
});
|
||||
|
||||
this.attachments = [];
|
||||
this.render();
|
||||
this.$el.trigger('force-resize');
|
||||
},
|
||||
|
||||
// Show errors
|
||||
|
||||
showLoadFailure() {
|
||||
const toast = new Whisper.UnableToLoadToast();
|
||||
toast.$el.insertAfter(this.$el);
|
||||
toast.render();
|
||||
|
||||
this.deleteFiles();
|
||||
},
|
||||
|
||||
autoScale(file) {
|
||||
if (file.type.split('/')[0] !== 'image' || file.type === 'image/tiff') {
|
||||
showDangerousError() {
|
||||
const toast = new Whisper.DangerousFileTypeToast();
|
||||
toast.$el.insertAfter(this.$el);
|
||||
toast.render();
|
||||
},
|
||||
|
||||
showFileSizeError({ limit, units, u }) {
|
||||
const toast = new Whisper.FileSizeToast({
|
||||
model: { limit, units: units[u] },
|
||||
});
|
||||
toast.$el.insertAfter(this.$el);
|
||||
toast.render();
|
||||
},
|
||||
|
||||
showCannotMixError() {
|
||||
const toast = new Whisper.CannotMixImageAndNonImageAttachmentsToast();
|
||||
toast.$el.insertAfter(this.$el);
|
||||
toast.render();
|
||||
},
|
||||
|
||||
showMultipleNonImageError() {
|
||||
const toast = new Whisper.OneNonImageAtATimeToast();
|
||||
toast.$el.insertAfter(this.$el);
|
||||
toast.render();
|
||||
},
|
||||
|
||||
showMaximumAttachmentsError() {
|
||||
const toast = new Whisper.MaxAttachmentsToast();
|
||||
toast.$el.insertAfter(this.$el);
|
||||
toast.render();
|
||||
},
|
||||
|
||||
// Housekeeping
|
||||
|
||||
addAttachment(attachment) {
|
||||
if (attachment.isVoiceNote && this.attachments.length > 0) {
|
||||
throw new Error('A voice note cannot be sent with other attachments');
|
||||
}
|
||||
|
||||
this.attachments.push(attachment);
|
||||
this.render();
|
||||
},
|
||||
|
||||
async maybeAddAttachment(file) {
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileName = file.name;
|
||||
const contentType = file.type;
|
||||
|
||||
if (window.Signal.Util.isFileDangerous(fileName)) {
|
||||
this.showDangerousError();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.attachments.length >= 32) {
|
||||
this.showMaximumAttachmentsError();
|
||||
return;
|
||||
}
|
||||
|
||||
const haveNonImage = _.any(
|
||||
this.attachments,
|
||||
attachment => !MIME.isImage(attachment.contentType)
|
||||
);
|
||||
// You can't add another attachment if you already have a non-image staged
|
||||
if (haveNonImage) {
|
||||
this.showMultipleNonImageError();
|
||||
return;
|
||||
}
|
||||
|
||||
// You can't add a non-image attachment if you already have attachments staged
|
||||
if (!MIME.isImage(contentType) && this.attachments.length > 0) {
|
||||
this.showCannotMixError();
|
||||
return;
|
||||
}
|
||||
|
||||
const renderVideoPreview = async () => {
|
||||
const objectUrl = URL.createObjectURL(file);
|
||||
try {
|
||||
const type = 'image/png';
|
||||
const thumbnail = await VisualAttachment.makeVideoScreenshot({
|
||||
objectUrl,
|
||||
contentType: type,
|
||||
logger: window.log,
|
||||
});
|
||||
const data = await VisualAttachment.blobToArrayBuffer(thumbnail);
|
||||
const url = Signal.Util.arrayBufferToObjectURL({
|
||||
data,
|
||||
type,
|
||||
});
|
||||
this.addAttachment({
|
||||
file,
|
||||
size: file.size,
|
||||
fileName,
|
||||
contentType,
|
||||
videoUrl: objectUrl,
|
||||
url,
|
||||
});
|
||||
} catch (error) {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
}
|
||||
};
|
||||
|
||||
const renderImagePreview = async () => {
|
||||
if (!MIME.isJPEG(contentType)) {
|
||||
const url = URL.createObjectURL(file);
|
||||
if (!url) {
|
||||
throw new Error('Failed to create object url for image!');
|
||||
}
|
||||
this.addAttachment({
|
||||
file,
|
||||
size: file.size,
|
||||
fileName,
|
||||
contentType,
|
||||
url,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const url = await window.autoOrientImage(file);
|
||||
this.addAttachment({
|
||||
file,
|
||||
size: file.size,
|
||||
fileName,
|
||||
contentType,
|
||||
url,
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
const blob = await this.autoScale({
|
||||
contentType,
|
||||
file,
|
||||
});
|
||||
let limitKb = 1000000;
|
||||
const blobType =
|
||||
file.type === 'image/gif' ? 'gif' : contentType.split('/')[0];
|
||||
|
||||
switch (blobType) {
|
||||
case 'image':
|
||||
limitKb = 6000;
|
||||
break;
|
||||
case 'gif':
|
||||
limitKb = 25000;
|
||||
break;
|
||||
case 'audio':
|
||||
limitKb = 100000;
|
||||
break;
|
||||
case 'video':
|
||||
limitKb = 100000;
|
||||
break;
|
||||
default:
|
||||
limitKb = 100000;
|
||||
break;
|
||||
}
|
||||
if ((blob.size / 1024).toFixed(4) >= limitKb) {
|
||||
const units = ['kB', 'MB', 'GB'];
|
||||
let u = -1;
|
||||
let limit = limitKb * 1000;
|
||||
do {
|
||||
limit /= 1000;
|
||||
u += 1;
|
||||
} while (limit >= 1000 && u < units.length - 1);
|
||||
this.showFileSizeError({ limit, units, u });
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
window.log.error(
|
||||
'Error ensuring that image is properly sized:',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
|
||||
this.showLoadFailure();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (Signal.Util.GoogleChrome.isImageTypeSupported(contentType)) {
|
||||
await renderImagePreview();
|
||||
} else if (Signal.Util.GoogleChrome.isVideoTypeSupported(contentType)) {
|
||||
await renderVideoPreview();
|
||||
} else {
|
||||
this.addAttachment({
|
||||
file,
|
||||
size: file.size,
|
||||
contentType,
|
||||
fileName,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
window.log.error(
|
||||
`Was unable to generate thumbnail for file type ${contentType}`,
|
||||
e && e.stack ? e.stack : e
|
||||
);
|
||||
this.addAttachment({
|
||||
file,
|
||||
size: file.size,
|
||||
contentType,
|
||||
fileName,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
autoScale(attachment) {
|
||||
const { contentType, file } = attachment;
|
||||
if (
|
||||
contentType.split('/')[0] !== 'image' ||
|
||||
contentType === 'image/tiff'
|
||||
) {
|
||||
// nothing to do
|
||||
return Promise.resolve(file);
|
||||
return Promise.resolve(attachment);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
@ -132,13 +453,13 @@
|
|||
img.naturalHeight <= maxHeight &&
|
||||
file.size <= maxSize
|
||||
) {
|
||||
resolve(file);
|
||||
resolve(attachment);
|
||||
return;
|
||||
}
|
||||
|
||||
const gifMaxSize = 25000 * 1024;
|
||||
if (file.type === 'image/gif' && file.size <= gifMaxSize) {
|
||||
resolve(file);
|
||||
resolve(attachment);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -170,285 +491,47 @@
|
|||
}
|
||||
} while (i > 0 && blob.size > maxSize);
|
||||
|
||||
resolve(blob);
|
||||
resolve({
|
||||
...attachment,
|
||||
file: blob,
|
||||
});
|
||||
};
|
||||
img.src = url;
|
||||
});
|
||||
},
|
||||
|
||||
async previewImages() {
|
||||
this.clearForm();
|
||||
const file = this.file || this.$input.prop('files')[0];
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
const { name } = file;
|
||||
if (window.Signal.Util.isFileDangerous(name)) {
|
||||
this.deleteFiles();
|
||||
|
||||
const toast = new Whisper.DangerousFileTypeToast();
|
||||
toast.$el.insertAfter(this.$el);
|
||||
toast.render();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const contentType = file.type;
|
||||
|
||||
const renderVideoPreview = async () => {
|
||||
// we use the variable on this here to ensure cleanup if we're interrupted
|
||||
this.previewObjectUrl = URL.createObjectURL(file);
|
||||
const type = 'image/png';
|
||||
const thumbnail = await VisualAttachment.makeVideoThumbnail({
|
||||
size: 100,
|
||||
videoObjectUrl: this.previewObjectUrl,
|
||||
contentType: type,
|
||||
logger: window.log,
|
||||
});
|
||||
URL.revokeObjectURL(this.previewObjectUrl);
|
||||
|
||||
const data = await VisualAttachment.blobToArrayBuffer(thumbnail);
|
||||
this.previewObjectUrl = Signal.Util.arrayBufferToObjectURL({
|
||||
data,
|
||||
type,
|
||||
});
|
||||
this.addThumb(this.previewObjectUrl, { addPlayIcon: true });
|
||||
};
|
||||
|
||||
const renderImagePreview = async () => {
|
||||
if (!MIME.isJPEG(file.type)) {
|
||||
this.previewObjectUrl = URL.createObjectURL(file);
|
||||
if (!this.previewObjectUrl) {
|
||||
throw new Error('Failed to create object url for image!');
|
||||
}
|
||||
this.addThumb(this.previewObjectUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
const dataUrl = await window.autoOrientImage(file);
|
||||
this.addThumb(dataUrl);
|
||||
};
|
||||
|
||||
try {
|
||||
if (Signal.Util.GoogleChrome.isImageTypeSupported(contentType)) {
|
||||
await renderImagePreview();
|
||||
} else if (Signal.Util.GoogleChrome.isVideoTypeSupported(contentType)) {
|
||||
await renderVideoPreview();
|
||||
} else if (MIME.isAudio(contentType)) {
|
||||
this.addThumb('images/audio.svg');
|
||||
} else {
|
||||
this.addThumb('images/file.svg');
|
||||
}
|
||||
} catch (e) {
|
||||
window.log.error(
|
||||
`Was unable to generate thumbnail for file type ${contentType}`,
|
||||
e && e.stack ? e.stack : e
|
||||
);
|
||||
this.addThumb('images/file.svg');
|
||||
}
|
||||
|
||||
try {
|
||||
const blob = await this.autoScale(file);
|
||||
let limitKb = 1000000;
|
||||
const blobType =
|
||||
file.type === 'image/gif' ? 'gif' : contentType.split('/')[0];
|
||||
|
||||
switch (blobType) {
|
||||
case 'image':
|
||||
limitKb = 6000;
|
||||
break;
|
||||
case 'gif':
|
||||
limitKb = 25000;
|
||||
break;
|
||||
case 'audio':
|
||||
limitKb = 100000;
|
||||
break;
|
||||
case 'video':
|
||||
limitKb = 100000;
|
||||
break;
|
||||
default:
|
||||
limitKb = 100000;
|
||||
break;
|
||||
}
|
||||
if ((blob.size / 1024).toFixed(4) >= limitKb) {
|
||||
const units = ['kB', 'MB', 'GB'];
|
||||
let u = -1;
|
||||
let limit = limitKb * 1000;
|
||||
do {
|
||||
limit /= 1000;
|
||||
u += 1;
|
||||
} while (limit >= 1000 && u < units.length - 1);
|
||||
const toast = new Whisper.FileSizeToast({
|
||||
model: { limit, units: units[u] },
|
||||
});
|
||||
toast.$el.insertAfter(this.$el);
|
||||
toast.render();
|
||||
this.deleteFiles();
|
||||
}
|
||||
} catch (error) {
|
||||
window.log.error(
|
||||
'Error ensuring that image is properly sized:',
|
||||
error && error.message ? error.message : error
|
||||
);
|
||||
|
||||
this.unableToLoadAttachment();
|
||||
}
|
||||
},
|
||||
|
||||
hasFiles() {
|
||||
const files = this.file ? [this.file] : this.$input.prop('files');
|
||||
return files && files.length && files.length > 0;
|
||||
},
|
||||
|
||||
getFiles() {
|
||||
const files = this.file
|
||||
? [this.file]
|
||||
: Array.from(this.$input.prop('files'));
|
||||
const promise = Promise.all(files.map(file => this.getFile(file)));
|
||||
this.clearForm();
|
||||
return promise;
|
||||
},
|
||||
|
||||
getFile(rawFile) {
|
||||
const file = rawFile || this.file || this.$input.prop('files')[0];
|
||||
if (!file) {
|
||||
async getFile(attachment) {
|
||||
if (!attachment) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const attachmentFlags = this.isVoiceNote
|
||||
const attachmentFlags = attachment.isVoiceNote
|
||||
? textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE
|
||||
: null;
|
||||
|
||||
const setFlags = flags => attachment => {
|
||||
const newAttachment = Object.assign({}, attachment);
|
||||
if (flags) {
|
||||
newAttachment.flags = flags;
|
||||
}
|
||||
return newAttachment;
|
||||
const scaled = await this.autoScale(attachment);
|
||||
const fileRead = await this.readFile(scaled);
|
||||
return {
|
||||
...fileRead,
|
||||
url: undefined,
|
||||
videoUrl: undefined,
|
||||
flags: attachmentFlags || null,
|
||||
};
|
||||
|
||||
// NOTE: Temporarily allow `then` until we convert the entire file
|
||||
// to `async` / `await`:
|
||||
// eslint-disable-next-line more/no-then
|
||||
return this.autoScale(file)
|
||||
.then(this.readFile)
|
||||
.then(setFlags(attachmentFlags));
|
||||
},
|
||||
|
||||
async getThumbnail() {
|
||||
// Scale and crop an image to 256px square
|
||||
const size = 256;
|
||||
const file = this.file || this.$input.prop('files')[0];
|
||||
if (
|
||||
file === undefined ||
|
||||
file.type.split('/')[0] !== 'image' ||
|
||||
file.type === 'image/gif'
|
||||
) {
|
||||
// nothing to do
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const objectUrl = URL.createObjectURL(file);
|
||||
|
||||
const arrayBuffer = await VisualAttachment.makeImageThumbnail({
|
||||
size,
|
||||
objectUrl,
|
||||
logger: window.log,
|
||||
});
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
|
||||
return this.readFile(arrayBuffer);
|
||||
},
|
||||
|
||||
// File -> Promise Attachment
|
||||
readFile(file) {
|
||||
readFile(attachment) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const FR = new FileReader();
|
||||
FR.onload = e => {
|
||||
resolve({
|
||||
...attachment,
|
||||
data: e.target.result,
|
||||
contentType: file.type,
|
||||
fileName: file.name,
|
||||
size: file.size,
|
||||
});
|
||||
};
|
||||
FR.onerror = reject;
|
||||
FR.onabort = reject;
|
||||
FR.readAsArrayBuffer(file);
|
||||
FR.readAsArrayBuffer(attachment.file);
|
||||
});
|
||||
},
|
||||
|
||||
clearForm() {
|
||||
if (this.previewObjectUrl) {
|
||||
URL.revokeObjectURL(this.previewObjectUrl);
|
||||
this.previewObjectUrl = null;
|
||||
}
|
||||
|
||||
this.thumb.remove();
|
||||
this.$('.avatar').show();
|
||||
this.$el.trigger('force-resize');
|
||||
},
|
||||
|
||||
deleteFiles(e) {
|
||||
if (e) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
this.clearForm();
|
||||
this.$input
|
||||
.wrap('<form>')
|
||||
.parent('form')
|
||||
.trigger('reset');
|
||||
this.$input.unwrap();
|
||||
this.file = null;
|
||||
this.$input.trigger('change');
|
||||
this.isVoiceNote = false;
|
||||
},
|
||||
|
||||
openDropped(e) {
|
||||
if (e.originalEvent.dataTransfer.types[0] !== 'Files') {
|
||||
return;
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
this.file = e.originalEvent.dataTransfer.files[0];
|
||||
this.previewImages();
|
||||
this.$el.removeClass('dropoff');
|
||||
},
|
||||
|
||||
showArea(e) {
|
||||
if (e.originalEvent.dataTransfer.types[0] !== 'Files') {
|
||||
return;
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.$el.addClass('dropoff');
|
||||
},
|
||||
|
||||
hideArea(e) {
|
||||
if (e.originalEvent.dataTransfer.types[0] !== 'Files') {
|
||||
return;
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.$el.removeClass('dropoff');
|
||||
},
|
||||
onPaste(e) {
|
||||
const { items } = e.originalEvent.clipboardData;
|
||||
let imgBlob = null;
|
||||
for (let i = 0; i < items.length; i += 1) {
|
||||
if (items[i].type.split('/')[0] === 'image') {
|
||||
imgBlob = items[i].getAsFile();
|
||||
}
|
||||
}
|
||||
if (imgBlob !== null) {
|
||||
this.file = imgBlob;
|
||||
this.previewImages();
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -51,8 +51,8 @@
|
|||
blue: '#336ba3',
|
||||
teal: '#067589',
|
||||
green: '#3b7845',
|
||||
light_green: '#895d66',
|
||||
blue_grey: '#607d8b',
|
||||
light_green: '#1c8260',
|
||||
blue_grey: '#895d66',
|
||||
grey: '#6b6b78',
|
||||
};
|
||||
})();
|
||||
|
|
|
@ -70,6 +70,15 @@
|
|||
resetScrollPosition() {
|
||||
this.$el.scrollTop(this.scrollPosition - this.$el.outerHeight());
|
||||
},
|
||||
restoreBottomOffset() {
|
||||
if (_.isNumber(this.bottomOffset)) {
|
||||
// + 10 is necessary to account for padding
|
||||
const height = this.$el.height() + 10;
|
||||
|
||||
const topOfBottomScreen = this.el.scrollHeight - height;
|
||||
this.$el.scrollTop(topOfBottomScreen - this.bottomOffset);
|
||||
}
|
||||
},
|
||||
scrollToBottomIfNeeded() {
|
||||
// This is counter-intuitive. Our current bottomOffset is reflective of what
|
||||
// we last measured, not necessarily the current state. And this is called
|
||||
|
|
|
@ -12,20 +12,43 @@
|
|||
window.Whisper.ReactWrapperView = Backbone.View.extend({
|
||||
className: 'react-wrapper',
|
||||
initialize(options) {
|
||||
const { Component, props, onClose } = options;
|
||||
const {
|
||||
Component,
|
||||
props,
|
||||
onClose,
|
||||
tagName,
|
||||
className,
|
||||
onInitialRender,
|
||||
elCallback,
|
||||
} = options;
|
||||
this.render();
|
||||
if (elCallback) {
|
||||
elCallback(this.el);
|
||||
}
|
||||
|
||||
this.tagName = options.tagName;
|
||||
this.className = options.className;
|
||||
this.tagName = tagName;
|
||||
this.className = className;
|
||||
this.Component = Component;
|
||||
this.onClose = onClose;
|
||||
this.onInitialRender = onInitialRender;
|
||||
|
||||
this.update(props);
|
||||
|
||||
this.hasRendered = false;
|
||||
},
|
||||
update(props) {
|
||||
const updatedProps = this.augmentProps(props);
|
||||
const reactElement = React.createElement(this.Component, updatedProps);
|
||||
ReactDOM.render(reactElement, this.el);
|
||||
ReactDOM.render(reactElement, this.el, () => {
|
||||
if (this.hasRendered) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hasRendered = true;
|
||||
if (this.onInitialRender) {
|
||||
this.onInitialRender();
|
||||
}
|
||||
});
|
||||
},
|
||||
augmentProps(props) {
|
||||
return Object.assign({}, props, {
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
libsignal,
|
||||
mnemonic,
|
||||
btoa,
|
||||
Signal,
|
||||
getString,
|
||||
Event,
|
||||
dcodeIO,
|
||||
|
@ -51,6 +52,65 @@
|
|||
requestSMSVerification(number) {
|
||||
// return this.server.requestVerificationSMS(number);
|
||||
},
|
||||
async encryptDeviceName(name, providedIdentityKey) {
|
||||
const identityKey =
|
||||
providedIdentityKey ||
|
||||
(await textsecure.storage.protocol.getIdentityKeyPair());
|
||||
if (!identityKey) {
|
||||
throw new Error(
|
||||
'Identity key was not provided and is not in database!'
|
||||
);
|
||||
}
|
||||
const encrypted = await Signal.Crypto.encryptDeviceName(
|
||||
name,
|
||||
identityKey.pubKey
|
||||
);
|
||||
|
||||
const proto = new textsecure.protobuf.DeviceName();
|
||||
proto.ephemeralPublic = encrypted.ephemeralPublic;
|
||||
proto.syntheticIv = encrypted.syntheticIv;
|
||||
proto.ciphertext = encrypted.ciphertext;
|
||||
|
||||
const arrayBuffer = proto.encode().toArrayBuffer();
|
||||
return Signal.Crypto.arrayBufferToBase64(arrayBuffer);
|
||||
},
|
||||
async decryptDeviceName(base64) {
|
||||
const identityKey = await textsecure.storage.protocol.getIdentityKeyPair();
|
||||
|
||||
const arrayBuffer = Signal.Crypto.base64ToArrayBuffer(base64);
|
||||
const proto = textsecure.protobuf.DeviceName.decode(arrayBuffer);
|
||||
const encrypted = {
|
||||
ephemeralPublic: proto.ephemeralPublic.toArrayBuffer(),
|
||||
syntheticIv: proto.syntheticIv.toArrayBuffer(),
|
||||
ciphertext: proto.ciphertext.toArrayBuffer(),
|
||||
};
|
||||
|
||||
const name = await Signal.Crypto.decryptDeviceName(
|
||||
encrypted,
|
||||
identityKey.privKey
|
||||
);
|
||||
|
||||
return name;
|
||||
},
|
||||
async maybeUpdateDeviceName() {
|
||||
const isNameEncrypted = textsecure.storage.user.getDeviceNameEncrypted();
|
||||
if (isNameEncrypted) {
|
||||
return;
|
||||
}
|
||||
const deviceName = await textsecure.storage.user.getDeviceName();
|
||||
const base64 = await this.encryptDeviceName(deviceName);
|
||||
|
||||
await this.server.updateDeviceName(base64);
|
||||
},
|
||||
async deviceNameIsEncrypted() {
|
||||
await textsecure.storage.user.setDeviceNameEncrypted();
|
||||
},
|
||||
async maybeDeleteSignalingKey() {
|
||||
const key = await textsecure.storage.user.getSignalingKey();
|
||||
if (key) {
|
||||
await this.server.removeSignalingKey();
|
||||
}
|
||||
},
|
||||
registerSingleDevice(mnemonic, mnemonicLanguage, profileName) {
|
||||
const createAccount = this.createAccount.bind(this);
|
||||
const clearSessionsAndPreKeys = this.clearSessionsAndPreKeys.bind(this);
|
||||
|
@ -362,11 +422,11 @@
|
|||
await textsecure.storage.user.setNumberAndDeviceId(pubKeyString, 1);
|
||||
});
|
||||
},
|
||||
clearSessionsAndPreKeys() {
|
||||
async clearSessionsAndPreKeys() {
|
||||
const store = textsecure.storage.protocol;
|
||||
|
||||
window.log.info('clearing all sessions, prekeys, and signed prekeys');
|
||||
return Promise.all([
|
||||
await Promise.all([
|
||||
store.clearPreKeyStore(),
|
||||
store.clearContactPreKeysStore(),
|
||||
store.clearSignedPreKeysStore(),
|
||||
|
|
|
@ -201,7 +201,6 @@ MessageReceiver.prototype.extend({
|
|||
// We do the message decryption here, instead of in the ordered pending queue,
|
||||
// to avoid exposing the time it took us to process messages through the time-to-ack.
|
||||
|
||||
// TODO: handle different types of requests.
|
||||
if (request.path !== '/api/v1/message') {
|
||||
window.log.info('got request', request.verb, request.path);
|
||||
request.respond(200, 'OK');
|
||||
|
@ -213,7 +212,6 @@ MessageReceiver.prototype.extend({
|
|||
}
|
||||
|
||||
const promise = Promise.resolve(request.body.toArrayBuffer()) // textsecure.crypto
|
||||
// .decryptWebsocketMessage(request.body, this.signalingKey)
|
||||
.then(plaintext => {
|
||||
const envelope = textsecure.protobuf.Envelope.decode(plaintext);
|
||||
// After this point, decoding errors are not the server's
|
||||
|
|
|
@ -43,7 +43,7 @@ function OutgoingMessage(
|
|||
this.failoverNumbers = [];
|
||||
this.unidentifiedDeliveries = [];
|
||||
|
||||
const { numberInfo, senderCertificate, online, messageType } = options;
|
||||
const { numberInfo, senderCertificate, online, messageType } = options || {};
|
||||
this.numberInfo = numberInfo;
|
||||
this.senderCertificate = senderCertificate;
|
||||
this.online = online;
|
||||
|
|
|
@ -36,6 +36,9 @@
|
|||
loadProtoBufs('SubProtocol.proto');
|
||||
loadProtoBufs('DeviceMessages.proto');
|
||||
|
||||
// Just for encrypting device names
|
||||
loadProtoBufs('DeviceName.proto');
|
||||
|
||||
// Metadata-specific protos
|
||||
loadProtoBufs('UnidentifiedDelivery.proto');
|
||||
})();
|
||||
|
|
|
@ -183,15 +183,26 @@ MessageSender.prototype = {
|
|||
proto.id = id;
|
||||
proto.contentType = attachment.contentType;
|
||||
proto.digest = result.digest;
|
||||
if (attachment.fileName) {
|
||||
proto.fileName = attachment.fileName;
|
||||
}
|
||||
|
||||
if (attachment.size) {
|
||||
proto.size = attachment.size;
|
||||
}
|
||||
if (attachment.fileName) {
|
||||
proto.fileName = attachment.fileName;
|
||||
}
|
||||
if (attachment.flags) {
|
||||
proto.flags = attachment.flags;
|
||||
}
|
||||
if (attachment.width) {
|
||||
proto.width = attachment.width;
|
||||
}
|
||||
if (attachment.height) {
|
||||
proto.height = attachment.height;
|
||||
}
|
||||
if (attachment.caption) {
|
||||
proto.caption = attachment.caption;
|
||||
}
|
||||
|
||||
return proto;
|
||||
})
|
||||
);
|
||||
|
|
|
@ -31,5 +31,17 @@
|
|||
getDeviceName() {
|
||||
return textsecure.storage.get('device_name');
|
||||
},
|
||||
|
||||
setDeviceNameEncrypted() {
|
||||
return textsecure.storage.put('deviceNameEncrypted', true);
|
||||
},
|
||||
|
||||
getDeviceNameEncrypted() {
|
||||
return textsecure.storage.get('deviceNameEncrypted');
|
||||
},
|
||||
|
||||
getSignalingKey() {
|
||||
return textsecure.storage.get('signaling_key');
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
/* global libsignal */
|
||||
|
||||
describe('AccountManager', () => {
|
||||
let accountManager;
|
||||
|
||||
|
@ -10,9 +12,14 @@ describe('AccountManager', () => {
|
|||
let signedPreKeys;
|
||||
const DAY = 1000 * 60 * 60 * 24;
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
const identityKey = await libsignal.KeyHelper.generateIdentityKeyPair();
|
||||
|
||||
originalProtocolStorage = window.textsecure.storage.protocol;
|
||||
window.textsecure.storage.protocol = {
|
||||
getIdentityKeyPair() {
|
||||
return identityKey;
|
||||
},
|
||||
loadSignedPreKeys() {
|
||||
return Promise.resolve(signedPreKeys);
|
||||
},
|
||||
|
@ -22,6 +29,17 @@ describe('AccountManager', () => {
|
|||
window.textsecure.storage.protocol = originalProtocolStorage;
|
||||
});
|
||||
|
||||
describe('encrypted device name', () => {
|
||||
it('roundtrips', async () => {
|
||||
const deviceName = 'v2.5.0 on Ubunto 20.04';
|
||||
const encrypted = await accountManager.encryptDeviceName(deviceName);
|
||||
assert.strictEqual(typeof encrypted, 'string');
|
||||
const decrypted = await accountManager.decryptDeviceName(encrypted);
|
||||
|
||||
assert.strictEqual(decrypted, deviceName);
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps three confirmed keys even if over a week old', () => {
|
||||
const now = Date.now();
|
||||
signedPreKeys = [
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
const Request = function Request(options) {
|
||||
this.verb = options.verb || options.type;
|
||||
this.path = options.path || options.url;
|
||||
this.headers = options.headers;
|
||||
this.body = options.body || options.data;
|
||||
this.success = options.success;
|
||||
this.error = options.error;
|
||||
|
@ -50,6 +51,7 @@
|
|||
this.verb = request.verb;
|
||||
this.path = request.path;
|
||||
this.body = request.body;
|
||||
this.headers = request.headers;
|
||||
|
||||
this.respond = (status, message) => {
|
||||
socket.send(
|
||||
|
@ -77,6 +79,7 @@
|
|||
verb: request.verb,
|
||||
path: request.path,
|
||||
body: request.body,
|
||||
headers: request.headers,
|
||||
id: request.id,
|
||||
},
|
||||
})
|
||||
|
@ -105,6 +108,7 @@
|
|||
verb: message.request.verb,
|
||||
path: message.request.path,
|
||||
body: message.request.body,
|
||||
headers: message.request.headers,
|
||||
id: message.request.id,
|
||||
socket,
|
||||
})
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"productName": "Loki Messenger",
|
||||
"description": "Private messaging from your desktop",
|
||||
"repository": "https://github.com/sloki-project/loki-messenger.git",
|
||||
"version": "1.19.0",
|
||||
"version": "1.20.0",
|
||||
"license": "GPL-3.0",
|
||||
"author": {
|
||||
"name": "Open Whisper Systems",
|
||||
|
@ -46,9 +46,8 @@
|
|||
"pow-metrics": "node metrics_app.js localhost 9000"
|
||||
},
|
||||
"dependencies": {
|
||||
"@journeyapps/sqlcipher": "https://github.com/scottnonnenberg-signal/node-sqlcipher.git#ed4f4d179ac010c6347b291cbd4c2ebe5c773741",
|
||||
"@journeyapps/sqlcipher": "https://github.com/scottnonnenberg-signal/node-sqlcipher.git#36149a4b03ccf11ec18b9205e1bfd9056015cf07",
|
||||
"@sindresorhus/is": "0.8.0",
|
||||
"archiver": "2.1.1",
|
||||
"backbone": "1.3.3",
|
||||
"blob-util": "1.3.0",
|
||||
"blueimp-canvas-to-blob": "3.14.0",
|
||||
|
@ -95,6 +94,7 @@
|
|||
"rimraf": "2.6.2",
|
||||
"semver": "5.4.1",
|
||||
"spellchecker": "3.4.4",
|
||||
"tar": "4.4.8",
|
||||
"testcheck": "1.0.0-rc.2",
|
||||
"tmp": "0.0.33",
|
||||
"to-arraybuffer": "1.0.1",
|
||||
|
@ -121,7 +121,7 @@
|
|||
"axios": "0.18.0",
|
||||
"bower": "1.8.2",
|
||||
"chai": "4.1.2",
|
||||
"electron": "3.0.9",
|
||||
"electron": "3.0.14",
|
||||
"electron-builder": "20.13.5",
|
||||
"electron-icon-maker": "0.0.3",
|
||||
"eslint": "4.14.0",
|
||||
|
|
17
preload.js
17
preload.js
|
@ -350,3 +350,20 @@ window.Signal.Logs = require('./js/modules/logs');
|
|||
// We pull this in last, because the native module involved appears to be sensitive to
|
||||
// /tmp mounted as noexec on Linux.
|
||||
require('./js/spell_check');
|
||||
|
||||
if (config.environment === 'test') {
|
||||
const isTravis = 'TRAVIS' in process.env && 'CI' in process.env;
|
||||
const isWindows = process.platform === 'win32';
|
||||
/* eslint-disable global-require, import/no-extraneous-dependencies */
|
||||
window.test = {
|
||||
glob: require('glob'),
|
||||
fse: require('fs-extra'),
|
||||
tmp: require('tmp'),
|
||||
path: require('path'),
|
||||
basePath: __dirname,
|
||||
attachmentsPath: window.Signal.Migrations.attachmentsPath,
|
||||
isTravis,
|
||||
isWindows,
|
||||
};
|
||||
/* eslint-enable global-require, import/no-extraneous-dependencies */
|
||||
}
|
||||
|
|
7
protos/DeviceName.proto
Normal file
7
protos/DeviceName.proto
Normal file
|
@ -0,0 +1,7 @@
|
|||
package signalservice;
|
||||
|
||||
message DeviceName {
|
||||
optional bytes ephemeralPublic = 1;
|
||||
optional bytes syntheticIv = 2;
|
||||
optional bytes ciphertext = 3;
|
||||
}
|
|
@ -19,27 +19,29 @@ package signalservice;
|
|||
option java_package = "org.whispersystems.websocket.messages.protobuf";
|
||||
|
||||
message WebSocketRequestMessage {
|
||||
optional string verb = 1;
|
||||
optional string path = 2;
|
||||
optional bytes body = 3;
|
||||
optional uint64 id = 4;
|
||||
optional string verb = 1;
|
||||
optional string path = 2;
|
||||
optional bytes body = 3;
|
||||
repeated string headers = 5;
|
||||
optional uint64 id = 4;
|
||||
}
|
||||
|
||||
message WebSocketResponseMessage {
|
||||
optional uint64 id = 1;
|
||||
optional uint32 status = 2;
|
||||
optional string message = 3;
|
||||
optional bytes body = 4;
|
||||
optional uint64 id = 1;
|
||||
optional uint32 status = 2;
|
||||
optional string message = 3;
|
||||
repeated string headers = 5;
|
||||
optional bytes body = 4;
|
||||
}
|
||||
|
||||
message WebSocketMessage {
|
||||
enum Type {
|
||||
UNKNOWN = 0;
|
||||
REQUEST = 1;
|
||||
RESPONSE = 2;
|
||||
}
|
||||
enum Type {
|
||||
UNKNOWN = 0;
|
||||
REQUEST = 1;
|
||||
RESPONSE = 2;
|
||||
}
|
||||
|
||||
optional Type type = 1;
|
||||
optional WebSocketRequestMessage request = 2;
|
||||
optional WebSocketResponseMessage response = 3;
|
||||
optional Type type = 1;
|
||||
optional WebSocketRequestMessage request = 2;
|
||||
optional WebSocketResponseMessage response = 3;
|
||||
}
|
||||
|
|
|
@ -184,15 +184,10 @@
|
|||
// things in the composition area. A margin on an inner div won't be included in that
|
||||
// height calculation.
|
||||
.bottom-bar .quote-wrapper {
|
||||
margin-right: 5px;
|
||||
margin-bottom: 6px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.send .quote-wrapper {
|
||||
margin-left: 37px;
|
||||
margin-right: 73px;
|
||||
margin-bottom: 5px;
|
||||
margin-top: 3px;
|
||||
margin-bottom: -5px;
|
||||
}
|
||||
|
||||
.bottom-bar {
|
||||
|
@ -206,6 +201,7 @@
|
|||
}
|
||||
|
||||
form.send {
|
||||
margin-bottom: 0px;
|
||||
background: $color-white;
|
||||
|
||||
&.video-attachment {
|
||||
|
@ -282,6 +278,8 @@
|
|||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
background-color: $color-loki-light-gray;
|
||||
margin-top: 3px;
|
||||
margin-bottom: 6px;
|
||||
color: $color-light-90;
|
||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
outline: 0;
|
||||
|
|
|
@ -88,8 +88,10 @@ button.emoji {
|
|||
opacity: 0.5;
|
||||
border: none;
|
||||
background: transparent;
|
||||
margin-top: 3px;
|
||||
|
||||
&:before {
|
||||
margin-top: 4px;
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: $button-height;
|
||||
|
@ -114,7 +116,6 @@ button.emoji {
|
|||
|
||||
.emoji-panel-container {
|
||||
height: 0px;
|
||||
margin-bottom: 3px;
|
||||
|
||||
.ep-emojies {
|
||||
background-color: $color-white;
|
||||
|
|
|
@ -108,8 +108,10 @@ a {
|
|||
opacity: 0.5;
|
||||
border: none;
|
||||
background: transparent;
|
||||
margin-top: 2px;
|
||||
|
||||
&:before {
|
||||
margin-top: 4px;
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: $button-height;
|
||||
|
@ -411,15 +413,6 @@ $loading-height: 16px;
|
|||
}
|
||||
}
|
||||
|
||||
input[type='text'],
|
||||
input[type='search'],
|
||||
textarea {
|
||||
&:active,
|
||||
&:focus {
|
||||
outline: 1px solid $blue;
|
||||
}
|
||||
}
|
||||
|
||||
.expiredAlert {
|
||||
background: #f3f3a7;
|
||||
padding: 10px;
|
||||
|
|
|
@ -128,6 +128,14 @@
|
|||
color: $color-light-60;
|
||||
}
|
||||
|
||||
.module-typing-animation__dot {
|
||||
background-color: $color-gray-60;
|
||||
}
|
||||
|
||||
.module-typing-animation__dot--light {
|
||||
background-color: $color-gray-60;
|
||||
}
|
||||
|
||||
&.dark-theme {
|
||||
// _modules
|
||||
|
||||
|
|
|
@ -748,6 +748,7 @@
|
|||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
color: $color-gray-90;
|
||||
text-align: start;
|
||||
|
||||
a {
|
||||
color: $color-gray-90;
|
||||
|
@ -803,6 +804,7 @@
|
|||
flex: initial;
|
||||
min-width: 54px;
|
||||
width: 54px;
|
||||
max-height: 54px;
|
||||
position: relative;
|
||||
|
||||
img {
|
||||
|
@ -2194,6 +2196,7 @@
|
|||
position: relative;
|
||||
display: inline-block;
|
||||
margin: 1px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.module-image__caption-icon {
|
||||
|
@ -2202,6 +2205,14 @@
|
|||
left: 6px;
|
||||
}
|
||||
|
||||
.module-image__with-click-handler {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.module-image--soft-corners {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.module-image--curved-top-left {
|
||||
border-top-left-radius: 16px;
|
||||
}
|
||||
|
@ -2304,6 +2315,17 @@
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.module-image__close-button {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
z-index: 2;
|
||||
background-image: url('../images/x-shadow-16.svg');
|
||||
}
|
||||
|
||||
// Module: Image Grid
|
||||
|
||||
.module-image-grid {
|
||||
|
@ -2417,6 +2439,272 @@
|
|||
flex-grow: 1;
|
||||
}
|
||||
|
||||
// Module: Attachments
|
||||
|
||||
.module-attachments {
|
||||
border-top: 1px solid $color-black-015;
|
||||
}
|
||||
|
||||
.module-attachments__header {
|
||||
height: 24px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.module-attachments__close-button {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 16px;
|
||||
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
||||
z-index: 2;
|
||||
@include color-svg('../images/x-16.svg', $color-black);
|
||||
}
|
||||
|
||||
.module-attachments__rail {
|
||||
margin-top: 12px;
|
||||
margin-left: 16px;
|
||||
padding-right: 16px;
|
||||
overflow-x: scroll;
|
||||
max-height: 142px;
|
||||
white-space: nowrap;
|
||||
overflow-y: hidden;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
// Module: Staged Generic Attachment
|
||||
|
||||
.module-staged-generic-attachment {
|
||||
height: 120px;
|
||||
width: 120px;
|
||||
margin: 1px;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
border-radius: 4px;
|
||||
box-shadow: inset 0px 0px 0px 1px $color-black-015;
|
||||
background-color: $color-gray-05;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.module-staged-generic-attachment__close-button {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
z-index: 2;
|
||||
@include color-svg('../images/x-16.svg', $color-black);
|
||||
}
|
||||
|
||||
.module-staged-generic-attachment__icon {
|
||||
margin-top: 30px;
|
||||
|
||||
background: url('../images/file-gradient.svg') no-repeat center;
|
||||
height: 44px;
|
||||
width: 56px;
|
||||
margin-left: 32px;
|
||||
margin-right: 32px;
|
||||
margin-bottom: -4px;
|
||||
|
||||
// So we can center the extension text inside this icon
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.module-staged-generic-attachment__icon__extension {
|
||||
font-size: 10px;
|
||||
line-height: 13px;
|
||||
letter-spacing: 0.1px;
|
||||
text-transform: uppercase;
|
||||
|
||||
// Along with flow layout in parent item, centers text
|
||||
text-align: center;
|
||||
width: 25px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
// We don't have much room for text here, cut it off without ellipse
|
||||
overflow-x: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: clip;
|
||||
|
||||
color: $color-gray-90;
|
||||
}
|
||||
|
||||
.module-staged-generic-attachment__filename {
|
||||
margin: 7px;
|
||||
margin-top: 5px;
|
||||
text-align: center;
|
||||
|
||||
font-family: Roboto;
|
||||
font-size: 14px;
|
||||
|
||||
overflow: hidden;
|
||||
height: 2.4em;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
// Module: Caption Editor
|
||||
|
||||
.module-caption-editor {
|
||||
background-color: $color-black;
|
||||
z-index: 20;
|
||||
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.module-caption-editor__close-button {
|
||||
z-index: 21;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
|
||||
top: 12px;
|
||||
right: 16px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
z-index: 2;
|
||||
@include color-svg('../images/x-16.svg', $color-white);
|
||||
}
|
||||
|
||||
.module-caption-editor__media-container {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
background-color: $color-black;
|
||||
text-align: center;
|
||||
margin: 50px;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.module-caption-editor__image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
.module-caption-editor__video {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
.module-caption-editor__placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
.module-caption-editor__bottom-bar {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
height: 52px;
|
||||
padding: 8px;
|
||||
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: middle;
|
||||
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.module-caption-editor__input-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.module-caption-editor__caption-input {
|
||||
height: 36px;
|
||||
width: 40em;
|
||||
|
||||
font-size: 14px;
|
||||
color: $color-white;
|
||||
|
||||
border: 1px solid $color-white;
|
||||
border-radius: 18px;
|
||||
background-color: $color-black;
|
||||
padding: 9px;
|
||||
padding-left: 12px;
|
||||
padding-right: 65px;
|
||||
|
||||
&::placeholder {
|
||||
color: $color-white-07;
|
||||
}
|
||||
&:focus {
|
||||
border: 1px solid $color-signal-blue;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.module-caption-editor__save-button {
|
||||
position: absolute;
|
||||
background-color: $color-signal-blue;
|
||||
color: $color-white;
|
||||
cursor: pointer;
|
||||
|
||||
height: 28px;
|
||||
border-radius: 15px;
|
||||
|
||||
padding: 5px;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
|
||||
right: 4px;
|
||||
top: 4px;
|
||||
}
|
||||
|
||||
// Module: Staged Placeholder Attachment
|
||||
|
||||
.module-staged-placeholder-attachment {
|
||||
margin: 1px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid $color-gray-25;
|
||||
height: 120px;
|
||||
width: 120px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background: $color-gray-05;
|
||||
}
|
||||
}
|
||||
|
||||
.module-staged-placeholder-attachment__plus-icon {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
|
||||
@include color-svg('../images/plus-36.svg', $color-gray-45);
|
||||
}
|
||||
|
||||
// Third-party module: react-contextmenu
|
||||
|
||||
.react-contextmenu {
|
||||
|
@ -2503,6 +2791,11 @@
|
|||
display: none;
|
||||
}
|
||||
|
||||
// To limit messages with things forcing them wider, like long attachment names
|
||||
.module-message {
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
/* Spec: container > 438px and container < 593px*/
|
||||
@media (min-width: 800px) and (max-width: 925px) {
|
||||
.module-message {
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
background: transparent;
|
||||
padding: 0;
|
||||
border: none;
|
||||
margin-top: 2px;
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
|
@ -16,6 +17,7 @@
|
|||
}
|
||||
|
||||
&:before {
|
||||
margin-top: 4px;
|
||||
content: '';
|
||||
display: inline-block;
|
||||
height: 24px;
|
||||
|
@ -36,6 +38,7 @@
|
|||
opacity: 0.5;
|
||||
text-align: center;
|
||||
padding: 0;
|
||||
margin-top: 5px;
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
|
|
|
@ -265,50 +265,6 @@ body.dark-theme {
|
|||
}
|
||||
}
|
||||
|
||||
.dropoff {
|
||||
outline: solid 1px $blue;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
color: $color-white;
|
||||
background-color: $grey;
|
||||
}
|
||||
|
||||
.group-info-input {
|
||||
background: $color-white;
|
||||
|
||||
.thumbnail:after {
|
||||
border-bottom: 10px solid $grey;
|
||||
border-left: 10px solid transparent;
|
||||
}
|
||||
|
||||
input.name {
|
||||
border: solid 1px #ccc;
|
||||
}
|
||||
}
|
||||
|
||||
.group-member-list,
|
||||
.new-group-update {
|
||||
.members .contact {
|
||||
border-bottom: 1px solid $color-dark-60;
|
||||
}
|
||||
}
|
||||
|
||||
.banner {
|
||||
// what's the right color?
|
||||
background-color: $blue_l;
|
||||
color: black;
|
||||
box-shadow: 0px 3px 5px 0px rgba(0, 0, 0, 0.2);
|
||||
|
||||
.warning {
|
||||
@include color-svg('../images/warning.svg', black);
|
||||
}
|
||||
|
||||
.dismiss {
|
||||
@include color-svg('../images/x.svg', black);
|
||||
}
|
||||
}
|
||||
|
||||
.contact-details {
|
||||
.number {
|
||||
color: $color-dark-30;
|
||||
|
@ -353,15 +309,6 @@ body.dark-theme {
|
|||
}
|
||||
}
|
||||
|
||||
input[type='text'],
|
||||
input[type='search'],
|
||||
textarea {
|
||||
&:active,
|
||||
&:focus {
|
||||
outline: 1px solid $blue;
|
||||
}
|
||||
}
|
||||
|
||||
.expiredAlert {
|
||||
background: #f3f3a7;
|
||||
|
||||
|
@ -749,14 +696,9 @@ body.dark-theme {
|
|||
}
|
||||
|
||||
.module-message__generic-attachment__icon {
|
||||
// TODO: this will eventually be a different image
|
||||
// background: url('../images/file-gradient.svg') no-repeat center;
|
||||
}
|
||||
|
||||
.module-message__generic-attachment__icon__extension {
|
||||
// TODO: probably need color
|
||||
}
|
||||
|
||||
.module-message__generic-attachment__file-name {
|
||||
color: $color-dark-05;
|
||||
}
|
||||
|
@ -1051,10 +993,6 @@ body.dark-theme {
|
|||
@include color-svg('../images/movie.svg', $color-signal-blue);
|
||||
}
|
||||
|
||||
.module-quote__generic-file__icon {
|
||||
// TODO: this will eventually be a different icon
|
||||
// background: url('../images/file-gradient.svg');
|
||||
}
|
||||
.module-quote__generic-file__text {
|
||||
color: $color-dark-05;
|
||||
}
|
||||
|
@ -1415,6 +1353,10 @@ body.dark-theme {
|
|||
color: $color-dark-05;
|
||||
}
|
||||
|
||||
// Module: Image
|
||||
|
||||
// Module: Image Grid
|
||||
|
||||
// Module: Typing Animation
|
||||
|
||||
.module-typing-animation__dot {
|
||||
|
@ -1425,6 +1367,50 @@ body.dark-theme {
|
|||
background-color: $color-white;
|
||||
}
|
||||
|
||||
// Module: Attachments
|
||||
|
||||
.module-attachments {
|
||||
border-top: 1px solid $color-gray-75;
|
||||
}
|
||||
|
||||
.module-attachments__close-button {
|
||||
@include color-svg('../images/x.svg', $color-gray-45);
|
||||
}
|
||||
|
||||
// Module: Staged Generic Attachment
|
||||
|
||||
.module-staged-generic-attachment {
|
||||
box-shadow: inset 0px 0px 0px 1px $color-gray-45;
|
||||
background-color: $color-gray-75;
|
||||
color: $color-dark-05;
|
||||
}
|
||||
|
||||
.module-staged-generic-attachment__close-button {
|
||||
@include color-svg('../images/x.svg', $color-gray-45);
|
||||
}
|
||||
|
||||
.module-staged-generic-attachment__icon {
|
||||
background: url('../images/file-gradient.svg') no-repeat center;
|
||||
}
|
||||
|
||||
.module-staged-generic-attachment__icon__extension {
|
||||
color: $color-gray-90;
|
||||
}
|
||||
|
||||
// Module: Staged Placeholder Attachment
|
||||
|
||||
.module-staged-placeholder-attachment {
|
||||
border: 1px solid $color-gray-60;
|
||||
|
||||
&:hover {
|
||||
background: $color-gray-75;
|
||||
}
|
||||
}
|
||||
|
||||
.module-staged-placeholder-attachment__plus-icon {
|
||||
@include color-svg('../images/plus-36.svg', $color-gray-60);
|
||||
}
|
||||
|
||||
// Third-party module: react-contextmenu
|
||||
|
||||
.react-contextmenu {
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
/* global Signal: false */
|
||||
/* global Whisper: false */
|
||||
/* global assert: false */
|
||||
/* global textsecure: false */
|
||||
/* global _: false */
|
||||
/* global Signal, Whisper, assert, textsecure, _, libsignal */
|
||||
|
||||
/* eslint-disable no-unreachable, no-console */
|
||||
/* eslint-disable no-console */
|
||||
|
||||
'use strict';
|
||||
|
||||
|
@ -240,55 +236,61 @@ describe('Backup', () => {
|
|||
});
|
||||
|
||||
describe('end-to-end', () => {
|
||||
it('exports then imports to produce the same data we started with', async () => {
|
||||
return;
|
||||
it('exports then imports to produce the same data we started with', async function thisNeeded() {
|
||||
this.timeout(6000);
|
||||
|
||||
const {
|
||||
attachmentsPath,
|
||||
fse,
|
||||
glob,
|
||||
path,
|
||||
tmp,
|
||||
isTravis,
|
||||
isWindows,
|
||||
} = window.test;
|
||||
|
||||
// Skip this test on travis windows
|
||||
// because it always fails due to lstat permission error.
|
||||
// Don't know how to fix it so this is a temp work around.
|
||||
if (isTravis && isWindows) {
|
||||
console.log(
|
||||
'Skipping exports then imports to produce the same data we started'
|
||||
);
|
||||
this.skip();
|
||||
return;
|
||||
}
|
||||
|
||||
const { attachmentsPath, fse, glob, path, tmp } = window.test;
|
||||
const {
|
||||
upgradeMessageSchema,
|
||||
loadAttachmentData,
|
||||
} = window.Signal.Migrations;
|
||||
|
||||
const key = new Uint8Array([
|
||||
1,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
11,
|
||||
23,
|
||||
34,
|
||||
1,
|
||||
34,
|
||||
3,
|
||||
5,
|
||||
45,
|
||||
45,
|
||||
1,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
11,
|
||||
23,
|
||||
34,
|
||||
1,
|
||||
34,
|
||||
3,
|
||||
5,
|
||||
45,
|
||||
45,
|
||||
]);
|
||||
const staticKeyPair = await libsignal.KeyHelper.generateIdentityKeyPair();
|
||||
const attachmentsPattern = path.join(attachmentsPath, '**');
|
||||
|
||||
const OUR_NUMBER = '+12025550000';
|
||||
const CONTACT_ONE_NUMBER = '+12025550001';
|
||||
const CONTACT_TWO_NUMBER = '+12025550002';
|
||||
|
||||
const toArrayBuffer = nodeBuffer =>
|
||||
nodeBuffer.buffer.slice(
|
||||
nodeBuffer.byteOffset,
|
||||
nodeBuffer.byteOffset + nodeBuffer.byteLength
|
||||
);
|
||||
|
||||
const getFixture = target => toArrayBuffer(fse.readFileSync(target));
|
||||
|
||||
const FIXTURES = {
|
||||
gif: getFixture('fixtures/giphy-7GFfijngKbeNy.gif'),
|
||||
mp4: getFixture('fixtures/pixabay-Soap-Bubble-7141.mp4'),
|
||||
jpg: getFixture('fixtures/koushik-chowdavarapu-105425-unsplash.jpg'),
|
||||
mp3: getFixture('fixtures/incompetech-com-Agnus-Dei-X.mp3'),
|
||||
txt: getFixture('fixtures/lorem-ipsum.txt'),
|
||||
png: getFixture(
|
||||
'fixtures/freepngs-2cd43b_bed7d1327e88454487397574d87b64dc_mv2.png'
|
||||
),
|
||||
};
|
||||
|
||||
async function wrappedLoadAttachment(attachment) {
|
||||
return _.omit(await loadAttachmentData(attachment), ['path']);
|
||||
}
|
||||
|
@ -376,16 +378,30 @@ describe('Backup', () => {
|
|||
})
|
||||
),
|
||||
attachments: await Promise.all(
|
||||
(message.attachments || []).map(attachment =>
|
||||
wrappedLoadAttachment(attachment)
|
||||
)
|
||||
(message.attachments || []).map(async attachment => {
|
||||
await wrappedLoadAttachment(attachment);
|
||||
|
||||
if (attachment.thumbnail) {
|
||||
await wrappedLoadAttachment(attachment.thumbnail);
|
||||
}
|
||||
|
||||
if (attachment.screenshot) {
|
||||
await wrappedLoadAttachment(attachment.screenshot);
|
||||
}
|
||||
|
||||
return attachment;
|
||||
})
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
let backupDir;
|
||||
try {
|
||||
const ATTACHMENT_COUNT = 3;
|
||||
// Seven total:
|
||||
// - Five from image/video attachments
|
||||
// - One from embedded contact avatar
|
||||
// - Another from embedded quoted attachment thumbnail
|
||||
const ATTACHMENT_COUNT = 7;
|
||||
const MESSAGE_COUNT = 1;
|
||||
const CONVERSATION_COUNT = 1;
|
||||
|
||||
|
@ -397,47 +413,20 @@ describe('Backup', () => {
|
|||
timestamp: 1524185933350,
|
||||
errors: [],
|
||||
attachments: [
|
||||
// Note: generates two more files: screenshot and thumbnail
|
||||
{
|
||||
contentType: 'image/gif',
|
||||
fileName: 'sad_cat.gif',
|
||||
data: new Uint8Array([
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
]).buffer,
|
||||
contentType: 'video/mp4',
|
||||
fileName: 'video.mp4',
|
||||
data: FIXTURES.mp4,
|
||||
},
|
||||
// Note: generates one more file: thumbnail
|
||||
{
|
||||
contentType: 'image/png',
|
||||
fileName: 'landscape.png',
|
||||
data: FIXTURES.png,
|
||||
},
|
||||
],
|
||||
hasAttachments: 1,
|
||||
hasFileAttachments: undefined,
|
||||
hasVisualMediaAttachments: 1,
|
||||
quote: {
|
||||
text: "Isn't it cute?",
|
||||
|
@ -450,43 +439,10 @@ describe('Backup', () => {
|
|||
},
|
||||
{
|
||||
contentType: 'image/gif',
|
||||
fileName: 'happy_cat.gif',
|
||||
fileName: 'avatar.gif',
|
||||
thumbnail: {
|
||||
contentType: 'image/png',
|
||||
data: new Uint8Array([
|
||||
2,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
]).buffer,
|
||||
data: FIXTURES.gif,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -506,40 +462,7 @@ describe('Backup', () => {
|
|||
isProfile: false,
|
||||
avatar: {
|
||||
contentType: 'image/png',
|
||||
data: new Uint8Array([
|
||||
3,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
]).buffer,
|
||||
data: FIXTURES.png,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -552,107 +475,30 @@ describe('Backup', () => {
|
|||
console.log('Backup test: Create models, save to db/disk');
|
||||
const message = await upgradeMessageSchema(messageWithAttachments);
|
||||
console.log({ message });
|
||||
const messageModel = new Whisper.Message(message);
|
||||
const id = await window.Signal.Data.saveMessage(
|
||||
messageModel.attributes,
|
||||
{
|
||||
Message: Whisper.Message,
|
||||
}
|
||||
);
|
||||
messageModel.set({ id });
|
||||
await window.Signal.Data.saveMessage(message, {
|
||||
Message: Whisper.Message,
|
||||
});
|
||||
|
||||
const conversation = {
|
||||
active_at: 1524185933350,
|
||||
color: 'orange',
|
||||
expireTimer: 0,
|
||||
id: CONTACT_ONE_NUMBER,
|
||||
lastMessage: 'Heyo!',
|
||||
name: 'Someone Somewhere',
|
||||
profileAvatar: {
|
||||
contentType: 'image/jpeg',
|
||||
data: new Uint8Array([
|
||||
4,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
]).buffer,
|
||||
data: FIXTURES.jpeg,
|
||||
size: 64,
|
||||
},
|
||||
profileKey: new Uint8Array([
|
||||
5,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
]).buffer,
|
||||
profileKey: 'BASE64KEY',
|
||||
profileName: 'Someone! 🤔',
|
||||
profileSharing: true,
|
||||
timestamp: 1524185933350,
|
||||
tokens: [
|
||||
'someone somewhere',
|
||||
'someone',
|
||||
'somewhere',
|
||||
'2025550001',
|
||||
'12025550001',
|
||||
],
|
||||
type: 'private',
|
||||
unreadCount: 0,
|
||||
verified: 0,
|
||||
sealedSender: 0,
|
||||
version: 2,
|
||||
};
|
||||
console.log({ conversation });
|
||||
await window.Signal.Data.saveConversation(conversation, {
|
||||
|
@ -669,11 +515,13 @@ describe('Backup', () => {
|
|||
console.log('Backup test: Export!');
|
||||
backupDir = tmp.dirSync().name;
|
||||
console.log({ backupDir });
|
||||
await Signal.Backup.exportToDirectory(backupDir, { key });
|
||||
await Signal.Backup.exportToDirectory(backupDir, {
|
||||
key: staticKeyPair.pubKey,
|
||||
});
|
||||
|
||||
console.log('Backup test: Ensure that messages.zip exists');
|
||||
const zipPath = path.join(backupDir, 'messages.zip');
|
||||
const messageZipExists = fse.existsSync(zipPath);
|
||||
console.log('Backup test: Ensure that messages.tar.gz exists');
|
||||
const archivePath = path.join(backupDir, 'messages.tar.gz');
|
||||
const messageZipExists = fse.existsSync(archivePath);
|
||||
assert.strictEqual(true, messageZipExists);
|
||||
|
||||
console.log(
|
||||
|
@ -688,43 +536,9 @@ describe('Backup', () => {
|
|||
await clearAllData();
|
||||
|
||||
console.log('Backup test: Import!');
|
||||
await Signal.Backup.importFromDirectory(backupDir, { key });
|
||||
|
||||
console.log('Backup test: ensure that all attachments were imported');
|
||||
const recreatedAttachmentFiles = removeDirs(
|
||||
glob.sync(attachmentsPattern)
|
||||
);
|
||||
console.log({ recreatedAttachmentFiles });
|
||||
assert.strictEqual(ATTACHMENT_COUNT, recreatedAttachmentFiles.length);
|
||||
assert.deepEqual(attachmentFiles, recreatedAttachmentFiles);
|
||||
|
||||
console.log('Backup test: Check messages');
|
||||
const messageCollection = await window.Signal.Data.getAllMessages({
|
||||
MessageCollection: Whisper.MessageCollection,
|
||||
await Signal.Backup.importFromDirectory(backupDir, {
|
||||
key: staticKeyPair.privKey,
|
||||
});
|
||||
assert.strictEqual(messageCollection.length, MESSAGE_COUNT);
|
||||
const messageFromDB = removeId(messageCollection.at(0).attributes);
|
||||
const expectedMessage = omitUndefinedKeys(message);
|
||||
console.log({ messageFromDB, expectedMessage });
|
||||
assert.deepEqual(messageFromDB, expectedMessage);
|
||||
|
||||
console.log(
|
||||
'Backup test: Check that all attachments were successfully imported'
|
||||
);
|
||||
const messageWithAttachmentsFromDB = await loadAllFilesFromDisk(
|
||||
messageFromDB
|
||||
);
|
||||
const expectedMessageWithAttachments = omitUndefinedKeys(
|
||||
messageWithAttachments
|
||||
);
|
||||
console.log({
|
||||
messageWithAttachmentsFromDB,
|
||||
expectedMessageWithAttachments,
|
||||
});
|
||||
assert.deepEqual(
|
||||
_.omit(messageWithAttachmentsFromDB, ['schemaVersion']),
|
||||
expectedMessageWithAttachments
|
||||
);
|
||||
|
||||
console.log('Backup test: Check conversations');
|
||||
const conversationCollection = await window.Signal.Data.getAllConversations(
|
||||
|
@ -734,11 +548,55 @@ describe('Backup', () => {
|
|||
);
|
||||
assert.strictEqual(conversationCollection.length, CONVERSATION_COUNT);
|
||||
|
||||
// We need to ommit any custom fields we have added
|
||||
const ommited = [
|
||||
'profileAvatar',
|
||||
'swarmNodes',
|
||||
'friendRequestStatus',
|
||||
'unlockTimestamp',
|
||||
'sessionResetStatus',
|
||||
];
|
||||
const conversationFromDB = conversationCollection.at(0).attributes;
|
||||
console.log({ conversationFromDB, conversation });
|
||||
assert.deepEqual(
|
||||
conversationFromDB,
|
||||
_.omit(conversation, ['profileAvatar'])
|
||||
_.omit(conversationFromDB, ommited),
|
||||
_.omit(conversation, ommited)
|
||||
);
|
||||
|
||||
console.log('Backup test: Check messages');
|
||||
const messageCollection = await window.Signal.Data.getAllMessages({
|
||||
MessageCollection: Whisper.MessageCollection,
|
||||
});
|
||||
assert.strictEqual(messageCollection.length, MESSAGE_COUNT);
|
||||
const messageFromDB = removeId(messageCollection.at(0).attributes);
|
||||
const expectedMessage = messageFromDB;
|
||||
console.log({ messageFromDB, expectedMessage });
|
||||
assert.deepEqual(messageFromDB, expectedMessage);
|
||||
|
||||
console.log('Backup test: ensure that all attachments were imported');
|
||||
const recreatedAttachmentFiles = removeDirs(
|
||||
glob.sync(attachmentsPattern)
|
||||
);
|
||||
console.log({ recreatedAttachmentFiles });
|
||||
assert.strictEqual(ATTACHMENT_COUNT, recreatedAttachmentFiles.length);
|
||||
assert.deepEqual(attachmentFiles, recreatedAttachmentFiles);
|
||||
|
||||
console.log(
|
||||
'Backup test: Check that all attachments were successfully imported'
|
||||
);
|
||||
const messageWithAttachmentsFromDB = await loadAllFilesFromDisk(
|
||||
messageFromDB
|
||||
);
|
||||
const expectedMessageWithAttachments = await loadAllFilesFromDisk(
|
||||
omitUndefinedKeys(message)
|
||||
);
|
||||
console.log({
|
||||
messageWithAttachmentsFromDB,
|
||||
expectedMessageWithAttachments,
|
||||
});
|
||||
assert.deepEqual(
|
||||
_.omit(messageWithAttachmentsFromDB, ['sent']),
|
||||
expectedMessageWithAttachments
|
||||
);
|
||||
|
||||
console.log('Backup test: Clear all data');
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* global Signal, textsecure */
|
||||
/* global Signal, textsecure, libsignal */
|
||||
|
||||
'use strict';
|
||||
|
||||
|
@ -44,7 +44,7 @@ describe('Crypto', () => {
|
|||
|
||||
const encrypted = await Signal.Crypto.encryptSymmetric(key, plaintext);
|
||||
const uintArray = new Uint8Array(encrypted);
|
||||
uintArray[2] = 9;
|
||||
uintArray[2] += 2;
|
||||
|
||||
try {
|
||||
await Signal.Crypto.decryptSymmetric(key, uintArray.buffer);
|
||||
|
@ -69,7 +69,7 @@ describe('Crypto', () => {
|
|||
|
||||
const encrypted = await Signal.Crypto.encryptSymmetric(key, plaintext);
|
||||
const uintArray = new Uint8Array(encrypted);
|
||||
uintArray[uintArray.length - 3] = 9;
|
||||
uintArray[uintArray.length - 3] += 2;
|
||||
|
||||
try {
|
||||
await Signal.Crypto.decryptSymmetric(key, uintArray.buffer);
|
||||
|
@ -94,7 +94,7 @@ describe('Crypto', () => {
|
|||
|
||||
const encrypted = await Signal.Crypto.encryptSymmetric(key, plaintext);
|
||||
const uintArray = new Uint8Array(encrypted);
|
||||
uintArray[35] = 9;
|
||||
uintArray[35] += 9;
|
||||
|
||||
try {
|
||||
await Signal.Crypto.decryptSymmetric(key, uintArray.buffer);
|
||||
|
@ -109,4 +109,67 @@ describe('Crypto', () => {
|
|||
throw new Error('Expected error to be thrown');
|
||||
});
|
||||
});
|
||||
|
||||
describe('encrypted device name', () => {
|
||||
it('roundtrips', async () => {
|
||||
const deviceName = 'v1.19.0 on Windows 10';
|
||||
const identityKey = await libsignal.KeyHelper.generateIdentityKeyPair();
|
||||
|
||||
const encrypted = await Signal.Crypto.encryptDeviceName(
|
||||
deviceName,
|
||||
identityKey.pubKey
|
||||
);
|
||||
const decrypted = await Signal.Crypto.decryptDeviceName(
|
||||
encrypted,
|
||||
identityKey.privKey
|
||||
);
|
||||
|
||||
assert.strictEqual(decrypted, deviceName);
|
||||
});
|
||||
|
||||
it('fails if iv is changed', async () => {
|
||||
const deviceName = 'v1.19.0 on Windows 10';
|
||||
const identityKey = await libsignal.KeyHelper.generateIdentityKeyPair();
|
||||
|
||||
const encrypted = await Signal.Crypto.encryptDeviceName(
|
||||
deviceName,
|
||||
identityKey.pubKey
|
||||
);
|
||||
encrypted.syntheticIv = Signal.Crypto.getRandomBytes(16);
|
||||
try {
|
||||
await Signal.Crypto.decryptDeviceName(encrypted, identityKey.privKey);
|
||||
} catch (error) {
|
||||
assert.strictEqual(
|
||||
error.message,
|
||||
'decryptDeviceName: synthetic IV did not match'
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('attachment encryption', () => {
|
||||
it('roundtrips', async () => {
|
||||
const staticKeyPair = await libsignal.KeyHelper.generateIdentityKeyPair();
|
||||
const message = 'this is my message';
|
||||
const plaintext = Signal.Crypto.bytesFromString(message);
|
||||
const path =
|
||||
'fa/facdf99c22945b1c9393345599a276f4b36ad7ccdc8c2467f5441b742c2d11fa';
|
||||
|
||||
const encrypted = await Signal.Crypto.encryptAttachment(
|
||||
staticKeyPair.pubKey.slice(1),
|
||||
path,
|
||||
plaintext
|
||||
);
|
||||
const decrypted = await Signal.Crypto.decryptAttachment(
|
||||
staticKeyPair.privKey,
|
||||
path,
|
||||
encrypted
|
||||
);
|
||||
|
||||
const equal = Signal.Crypto.constantTimeEqual(plaintext, decrypted);
|
||||
if (!equal) {
|
||||
throw new Error('The output and input did not match!');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -148,7 +148,9 @@ InMemorySignalProtocolStore.prototype = {
|
|||
};
|
||||
|
||||
describe('SecretSessionCipher', () => {
|
||||
it('successfully roundtrips', async () => {
|
||||
it('successfully roundtrips', async function thisNeeded() {
|
||||
this.timeout(4000);
|
||||
|
||||
const aliceStore = new InMemorySignalProtocolStore();
|
||||
const bobStore = new InMemorySignalProtocolStore();
|
||||
|
||||
|
@ -187,7 +189,9 @@ describe('SecretSessionCipher', () => {
|
|||
assert.strictEqual(decryptResult.sender.toString(), '+14151111111.1');
|
||||
});
|
||||
|
||||
it('fails when untrusted', async () => {
|
||||
it('fails when untrusted', async function thisNeeded() {
|
||||
this.timeout(4000);
|
||||
|
||||
const aliceStore = new InMemorySignalProtocolStore();
|
||||
const bobStore = new InMemorySignalProtocolStore();
|
||||
|
||||
|
@ -226,7 +230,9 @@ describe('SecretSessionCipher', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('fails when expired', async () => {
|
||||
it('fails when expired', async function thisNeeded() {
|
||||
this.timeout(4000);
|
||||
|
||||
const aliceStore = new InMemorySignalProtocolStore();
|
||||
const bobStore = new InMemorySignalProtocolStore();
|
||||
|
||||
|
@ -264,7 +270,9 @@ describe('SecretSessionCipher', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('fails when wrong identity', async () => {
|
||||
it('fails when wrong identity', async function thisNeeded() {
|
||||
this.timeout(4000);
|
||||
|
||||
const aliceStore = new InMemorySignalProtocolStore();
|
||||
const bobStore = new InMemorySignalProtocolStore();
|
||||
|
||||
|
|
76
ts/components/CaptionEditor.md
Normal file
76
ts/components/CaptionEditor.md
Normal file
|
@ -0,0 +1,76 @@
|
|||
## Image
|
||||
|
||||
```js
|
||||
let caption = null;
|
||||
|
||||
<div style={{ position: 'relative', width: '100%', height: 500 }}>
|
||||
<CaptionEditor
|
||||
url={util.gifObjectUrl}
|
||||
attachment={{
|
||||
contentType: 'image/jpeg',
|
||||
}}
|
||||
onSave={caption => console.log('onSave', caption)}
|
||||
close={() => console.log('close')}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</div>;
|
||||
```
|
||||
|
||||
## Image with caption
|
||||
|
||||
```js
|
||||
let caption =
|
||||
"This is the user-provided caption. We show it overlaid on the image. If it's really long, then it wraps, but it doesn't get too close to the edges of the image.";
|
||||
|
||||
<div style={{ position: 'relative', width: '100%', height: 500 }}>
|
||||
<CaptionEditor
|
||||
url="https://placekitten.com/800/600"
|
||||
attachment={{
|
||||
contentType: 'image/jpeg',
|
||||
}}
|
||||
caption={caption}
|
||||
contentType="image/jpeg"
|
||||
onSave={caption => console.log('onSave', caption)}
|
||||
close={() => console.log('close')}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</div>;
|
||||
```
|
||||
|
||||
## Video
|
||||
|
||||
```js
|
||||
let caption = null;
|
||||
|
||||
<div style={{ position: 'relative', width: '100%', height: 500 }}>
|
||||
<CaptionEditor
|
||||
url="fixtures/pixabay-Soap-Bubble-7141.mp4"
|
||||
attachment={{
|
||||
contentType: 'video/mp4',
|
||||
}}
|
||||
onSave={caption => console.log('onSave', caption)}
|
||||
close={() => console.log('close')}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</div>;
|
||||
```
|
||||
|
||||
## Video with caption
|
||||
|
||||
```js
|
||||
let caption =
|
||||
"This is the user-provided caption. We show it overlaid on the image. If it's really long, then it wraps, but it doesn't get too close to the edges of the image.";
|
||||
|
||||
<div style={{ position: 'relative', width: '100%', height: 500 }}>
|
||||
<CaptionEditor
|
||||
url="fixtures/pixabay-Soap-Bubble-7141.mp4"
|
||||
attachment={{
|
||||
contentType: 'video/mp4',
|
||||
}}
|
||||
caption={caption}
|
||||
onSave={caption => console.log('onSave', caption)}
|
||||
close={() => console.log('close')}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</div>;
|
||||
```
|
168
ts/components/CaptionEditor.tsx
Normal file
168
ts/components/CaptionEditor.tsx
Normal file
|
@ -0,0 +1,168 @@
|
|||
// tslint:disable:react-a11y-anchors
|
||||
|
||||
import React from 'react';
|
||||
import * as GoogleChrome from '../util/GoogleChrome';
|
||||
|
||||
import { AttachmentType } from './conversation/types';
|
||||
|
||||
import { Localizer } from '../types/Util';
|
||||
|
||||
interface Props {
|
||||
attachment: AttachmentType;
|
||||
i18n: Localizer;
|
||||
url: string;
|
||||
caption?: string;
|
||||
onSave?: (caption: string) => void;
|
||||
close?: () => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
caption: string;
|
||||
}
|
||||
|
||||
export class CaptionEditor extends React.Component<Props, State> {
|
||||
private handleKeyUpBound: (
|
||||
event: React.KeyboardEvent<HTMLInputElement>
|
||||
) => void;
|
||||
private setFocusBound: () => void;
|
||||
// TypeScript doesn't like our React.Ref typing here, so we omit it
|
||||
private captureRefBound: () => void;
|
||||
private onChangeBound: () => void;
|
||||
private onSaveBound: () => void;
|
||||
private inputRef: React.Ref<HTMLInputElement> | null;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
const { caption } = props;
|
||||
this.state = {
|
||||
caption: caption || '',
|
||||
};
|
||||
|
||||
this.handleKeyUpBound = this.handleKeyUp.bind(this);
|
||||
this.setFocusBound = this.setFocus.bind(this);
|
||||
this.captureRefBound = this.captureRef.bind(this);
|
||||
this.onChangeBound = this.onChange.bind(this);
|
||||
this.onSaveBound = this.onSave.bind(this);
|
||||
this.inputRef = null;
|
||||
}
|
||||
|
||||
public handleKeyUp(event: React.KeyboardEvent<HTMLInputElement>) {
|
||||
const { close, onSave } = this.props;
|
||||
|
||||
if (close && event.key === 'Escape') {
|
||||
close();
|
||||
}
|
||||
|
||||
if (onSave && event.key === 'Enter') {
|
||||
const { caption } = this.state;
|
||||
onSave(caption);
|
||||
}
|
||||
}
|
||||
|
||||
public setFocus() {
|
||||
if (this.inputRef) {
|
||||
// @ts-ignore
|
||||
this.inputRef.focus();
|
||||
}
|
||||
}
|
||||
|
||||
public captureRef(ref: React.Ref<HTMLInputElement>) {
|
||||
this.inputRef = ref;
|
||||
|
||||
// Forcing focus after a delay due to some focus contention with ConversationView
|
||||
setTimeout(() => {
|
||||
this.setFocus();
|
||||
}, 200);
|
||||
}
|
||||
|
||||
public onSave() {
|
||||
const { onSave } = this.props;
|
||||
const { caption } = this.state;
|
||||
|
||||
if (onSave) {
|
||||
onSave(caption);
|
||||
}
|
||||
}
|
||||
|
||||
public onChange(event: React.FormEvent<HTMLInputElement>) {
|
||||
// @ts-ignore
|
||||
const { value } = event.target;
|
||||
|
||||
this.setState({
|
||||
caption: value,
|
||||
});
|
||||
}
|
||||
|
||||
public renderObject() {
|
||||
const { url, i18n, attachment } = this.props;
|
||||
const { contentType } = attachment || { contentType: null };
|
||||
|
||||
const isImageTypeSupported = GoogleChrome.isImageTypeSupported(contentType);
|
||||
if (isImageTypeSupported) {
|
||||
return (
|
||||
<img
|
||||
className="module-caption-editor__image"
|
||||
alt={i18n('imageAttachmentAlt')}
|
||||
src={url}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const isVideoTypeSupported = GoogleChrome.isVideoTypeSupported(contentType);
|
||||
if (isVideoTypeSupported) {
|
||||
return (
|
||||
<video className="module-caption-editor__video" controls={true}>
|
||||
<source src={url} />
|
||||
</video>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className="module-caption-editor__placeholder" />;
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { i18n, close } = this.props;
|
||||
const { caption } = this.state;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="dialog"
|
||||
onClick={this.setFocusBound}
|
||||
className="module-caption-editor"
|
||||
>
|
||||
<div
|
||||
role="button"
|
||||
onClick={close}
|
||||
className="module-caption-editor__close-button"
|
||||
/>
|
||||
<div className="module-caption-editor__media-container">
|
||||
{this.renderObject()}
|
||||
</div>
|
||||
<div className="module-caption-editor__bottom-bar">
|
||||
<div className="module-caption-editor__input-container">
|
||||
<input
|
||||
type="text"
|
||||
ref={this.captureRefBound}
|
||||
value={caption}
|
||||
maxLength={200}
|
||||
placeholder={i18n('addACaption')}
|
||||
className="module-caption-editor__caption-input"
|
||||
onKeyUp={close ? this.handleKeyUpBound : undefined}
|
||||
onChange={this.onChangeBound}
|
||||
/>
|
||||
{caption ? (
|
||||
<div
|
||||
role="button"
|
||||
onClick={this.onSaveBound}
|
||||
className="module-caption-editor__save-button"
|
||||
>
|
||||
{i18n('save')}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
118
ts/components/conversation/AttachmentList.md
Normal file
118
ts/components/conversation/AttachmentList.md
Normal file
|
@ -0,0 +1,118 @@
|
|||
### One image
|
||||
|
||||
```jsx
|
||||
const attachments = [
|
||||
{
|
||||
url: util.gifObjectUrl,
|
||||
contentType: 'image/gif',
|
||||
width: 320,
|
||||
height: 240,
|
||||
},
|
||||
];
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<AttachmentList
|
||||
attachments={attachments}
|
||||
onClose={() => console.log('onClose')}
|
||||
onClickAttachment={attachment => {
|
||||
console.log('onClickAttachment', attachment);
|
||||
}}
|
||||
onCloseAttachment={attachment => {
|
||||
console.log('onCloseAttachment', attachment);
|
||||
}}
|
||||
onAddAttachment={() => console.log('onAddAttachment')}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</util.ConversationContext>;
|
||||
```
|
||||
|
||||
### Four images
|
||||
|
||||
```jsx
|
||||
const attachments = [
|
||||
{
|
||||
url: util.gifObjectUrl,
|
||||
contentType: 'image/png',
|
||||
width: 320,
|
||||
height: 240,
|
||||
},
|
||||
{
|
||||
url: util.pngObjectUrl,
|
||||
contentType: 'image/png',
|
||||
width: 800,
|
||||
height: 1200,
|
||||
},
|
||||
{
|
||||
url: util.landscapeObjectUrl,
|
||||
contentType: 'image/png',
|
||||
width: 4496,
|
||||
height: 3000,
|
||||
},
|
||||
{
|
||||
url: util.landscapeGreenObjectUrl,
|
||||
contentType: 'image/png',
|
||||
width: 1000,
|
||||
height: 50,
|
||||
},
|
||||
];
|
||||
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<AttachmentList
|
||||
attachments={attachments}
|
||||
onClose={() => console.log('onClose')}
|
||||
onClickAttachment={attachment => {
|
||||
console.log('onClickAttachment', attachment);
|
||||
}}
|
||||
onCloseAttachment={attachment => {
|
||||
console.log('onCloseAttachment', attachment);
|
||||
}}
|
||||
onAddAttachment={() => console.log('onAddAttachment')}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</util.ConversationContext>;
|
||||
```
|
||||
|
||||
### A mix of attachment types
|
||||
|
||||
```jsx
|
||||
const attachments = [
|
||||
{
|
||||
url: util.gifObjectUrl,
|
||||
contentType: 'image/gif',
|
||||
width: 320,
|
||||
height: 240,
|
||||
},
|
||||
{
|
||||
contentType: 'text/plain',
|
||||
fileName: 'manifesto.txt',
|
||||
},
|
||||
{
|
||||
url: util.pngObjectUrl,
|
||||
contentType: 'image/png',
|
||||
width: 800,
|
||||
height: 1200,
|
||||
},
|
||||
];
|
||||
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<AttachmentList
|
||||
attachments={attachments}
|
||||
onClose={() => console.log('onClose')}
|
||||
onClickAttachment={attachment => {
|
||||
console.log('onClickAttachment', attachment);
|
||||
}}
|
||||
onCloseAttachment={attachment => {
|
||||
console.log('onCloseAttachment', attachment);
|
||||
}}
|
||||
onAddAttachment={() => console.log('onAddAttachment')}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</util.ConversationContext>;
|
||||
```
|
||||
|
||||
### No attachments provided
|
||||
|
||||
Nothing is shown if attachment list is empty.
|
||||
|
||||
```jsx
|
||||
<AttachmentList attachments={[]} i18n={util.i18n} />
|
||||
```
|
117
ts/components/conversation/AttachmentList.tsx
Normal file
117
ts/components/conversation/AttachmentList.tsx
Normal file
|
@ -0,0 +1,117 @@
|
|||
import React from 'react';
|
||||
|
||||
import {
|
||||
isImageTypeSupported,
|
||||
isVideoTypeSupported,
|
||||
} from '../../util/GoogleChrome';
|
||||
import { AttachmentType } from './types';
|
||||
import { Image } from './Image';
|
||||
import { areAllAttachmentsVisual } from './ImageGrid';
|
||||
import { StagedGenericAttachment } from './StagedGenericAttachment';
|
||||
import { StagedPlaceholderAttachment } from './StagedPlaceholderAttachment';
|
||||
import { Localizer } from '../../types/Util';
|
||||
|
||||
interface Props {
|
||||
attachments: Array<AttachmentType>;
|
||||
i18n: Localizer;
|
||||
// onError: () => void;
|
||||
onClickAttachment: (attachment: AttachmentType) => void;
|
||||
onCloseAttachment: (attachment: AttachmentType) => void;
|
||||
onAddAttachment: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const IMAGE_WIDTH = 120;
|
||||
const IMAGE_HEIGHT = 120;
|
||||
|
||||
export class AttachmentList extends React.Component<Props> {
|
||||
// tslint:disable-next-line max-func-body-length */
|
||||
public render() {
|
||||
const {
|
||||
attachments,
|
||||
i18n,
|
||||
onAddAttachment,
|
||||
onClickAttachment,
|
||||
onCloseAttachment,
|
||||
onClose,
|
||||
} = this.props;
|
||||
|
||||
if (!attachments.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const allVisualAttachments = areAllAttachmentsVisual(attachments);
|
||||
|
||||
return (
|
||||
<div className="module-attachments">
|
||||
{attachments.length > 1 ? (
|
||||
<div className="module-attachments__header">
|
||||
<div
|
||||
role="button"
|
||||
onClick={onClose}
|
||||
className="module-attachments__close-button"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="module-attachments__rail">
|
||||
{(attachments || []).map((attachment, index) => {
|
||||
const { contentType } = attachment;
|
||||
if (
|
||||
isImageTypeSupported(contentType) ||
|
||||
isVideoTypeSupported(contentType)
|
||||
) {
|
||||
return (
|
||||
<Image
|
||||
key={getUrl(attachment) || attachment.fileName || index}
|
||||
alt={i18n('stagedImageAttachment', [
|
||||
getUrl(attachment) || attachment.fileName,
|
||||
])}
|
||||
i18n={i18n}
|
||||
attachment={attachment}
|
||||
softCorners={true}
|
||||
playIconOverlay={isVideoAttachment(attachment)}
|
||||
height={IMAGE_HEIGHT}
|
||||
width={IMAGE_WIDTH}
|
||||
url={getUrl(attachment)}
|
||||
closeButton={true}
|
||||
onClick={
|
||||
attachments.length > 1 ? onClickAttachment : undefined
|
||||
}
|
||||
onClickClose={onCloseAttachment}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StagedGenericAttachment
|
||||
key={getUrl(attachment) || attachment.fileName || index}
|
||||
attachment={attachment}
|
||||
i18n={i18n}
|
||||
onClose={onCloseAttachment}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{allVisualAttachments ? (
|
||||
<StagedPlaceholderAttachment onClick={onAddAttachment} />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function isVideoAttachment(attachment?: AttachmentType) {
|
||||
return (
|
||||
attachment &&
|
||||
attachment.contentType &&
|
||||
isVideoTypeSupported(attachment.contentType)
|
||||
);
|
||||
}
|
||||
|
||||
function getUrl(attachment: AttachmentType) {
|
||||
if (attachment.screenshot) {
|
||||
return attachment.screenshot.url;
|
||||
}
|
||||
|
||||
return attachment.url;
|
||||
}
|
|
@ -8,6 +8,7 @@ import {
|
|||
getRegex,
|
||||
getReplacementData,
|
||||
getTitle,
|
||||
SizeClassType,
|
||||
} from '../../util/emoji';
|
||||
|
||||
import { Localizer, RenderTextCallback } from '../../types/Util';
|
||||
|
@ -20,7 +21,7 @@ function getImageTag({
|
|||
i18n,
|
||||
}: {
|
||||
match: any;
|
||||
sizeClass: string | undefined;
|
||||
sizeClass?: SizeClassType;
|
||||
key: string | number;
|
||||
i18n: Localizer;
|
||||
}) {
|
||||
|
@ -51,7 +52,7 @@ function getImageTag({
|
|||
interface Props {
|
||||
text: string;
|
||||
/** A class name to be added to the generated emoji images */
|
||||
sizeClass?: '' | 'small' | 'medium' | 'large' | 'jumbo';
|
||||
sizeClass?: SizeClassType;
|
||||
/** Allows you to customize now non-newlines are rendered. Simplest is just a <span>. */
|
||||
renderNonEmoji?: RenderTextCallback;
|
||||
i18n: Localizer;
|
||||
|
|
|
@ -77,18 +77,21 @@
|
|||
width="199"
|
||||
attachment={{ caption: 'dogs playing' }}
|
||||
url={util.pngObjectUrl}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
<Image
|
||||
height="149"
|
||||
width="149"
|
||||
attachment={{ caption: 'dogs playing' }}
|
||||
url={util.pngObjectUrl}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
<Image
|
||||
height="99"
|
||||
width="99"
|
||||
attachment={{ caption: 'dogs playing' }}
|
||||
url={util.pngObjectUrl}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</div>
|
||||
<hr />
|
||||
|
@ -100,6 +103,7 @@
|
|||
darkOverlay
|
||||
overlayText="+3"
|
||||
url={util.pngObjectUrl}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
<Image
|
||||
height="149"
|
||||
|
@ -108,6 +112,7 @@
|
|||
darkOverlay
|
||||
overlayText="+3"
|
||||
url={util.pngObjectUrl}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
<Image
|
||||
height="99"
|
||||
|
@ -116,6 +121,82 @@
|
|||
darkOverlay
|
||||
overlayText="+3"
|
||||
url={util.pngObjectUrl}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### With top-right X and soft corners
|
||||
|
||||
```jsx
|
||||
<div>
|
||||
<div>
|
||||
<Image
|
||||
height="200"
|
||||
width="199"
|
||||
closeButton={true}
|
||||
onClick={() => console.log('onClick')}
|
||||
onClickClose={attachment => console.log('onClickClose', attachment)}
|
||||
softCorners={true}
|
||||
url={util.gifObjectUrl}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
<Image
|
||||
height="149"
|
||||
width="149"
|
||||
closeButton={true}
|
||||
onClick={() => console.log('onClick')}
|
||||
onClickClose={attachment => console.log('onClickClose', attachment)}
|
||||
softCorners={true}
|
||||
url={util.gifObjectUrl}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
<Image
|
||||
height="99"
|
||||
width="99"
|
||||
closeButton={true}
|
||||
onClick={() => console.log('onClick')}
|
||||
onClickClose={attachment => console.log('onClickClose', attachment)}
|
||||
softCorners={true}
|
||||
url={util.gifObjectUrl}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
<Image
|
||||
height="200"
|
||||
width="199"
|
||||
closeButton={true}
|
||||
attachment={{ caption: 'dogs playing' }}
|
||||
onClick={() => console.log('onClick')}
|
||||
onClickClose={attachment => console.log('onClickClose', attachment)}
|
||||
softCorners={true}
|
||||
url={util.gifObjectUrl}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
<Image
|
||||
height="149"
|
||||
width="149"
|
||||
closeButton={true}
|
||||
attachment={{ caption: 'dogs playing' }}
|
||||
onClick={() => console.log('onClick')}
|
||||
onClickClose={attachment => console.log('onClickClose', attachment)}
|
||||
softCorners={true}
|
||||
url={util.gifObjectUrl}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
<Image
|
||||
height="99"
|
||||
width="99"
|
||||
closeButton={true}
|
||||
attachment={{ caption: 'dogs playing' }}
|
||||
onClick={() => console.log('onClick')}
|
||||
onClickClose={attachment => console.log('onClickClose', attachment)}
|
||||
softCorners={true}
|
||||
url={util.gifObjectUrl}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -15,24 +15,29 @@ interface Props {
|
|||
overlayText?: string;
|
||||
|
||||
bottomOverlay?: boolean;
|
||||
closeButton?: boolean;
|
||||
curveBottomLeft?: boolean;
|
||||
curveBottomRight?: boolean;
|
||||
curveTopLeft?: boolean;
|
||||
curveTopRight?: boolean;
|
||||
darkOverlay?: boolean;
|
||||
playIconOverlay?: boolean;
|
||||
softCorners?: boolean;
|
||||
|
||||
i18n: Localizer;
|
||||
onClick?: (attachment: AttachmentType) => void;
|
||||
onClickClose?: (attachment: AttachmentType) => void;
|
||||
onError?: () => void;
|
||||
}
|
||||
|
||||
export class Image extends React.Component<Props> {
|
||||
// tslint:disable-next-line max-func-body-length cyclomatic-complexity
|
||||
public render() {
|
||||
const {
|
||||
alt,
|
||||
attachment,
|
||||
bottomOverlay,
|
||||
closeButton,
|
||||
curveBottomLeft,
|
||||
curveBottomRight,
|
||||
curveTopLeft,
|
||||
|
@ -41,9 +46,11 @@ export class Image extends React.Component<Props> {
|
|||
height,
|
||||
i18n,
|
||||
onClick,
|
||||
onClickClose,
|
||||
onError,
|
||||
overlayText,
|
||||
playIconOverlay,
|
||||
softCorners,
|
||||
url,
|
||||
width,
|
||||
} = this.props;
|
||||
|
@ -52,18 +59,20 @@ export class Image extends React.Component<Props> {
|
|||
|
||||
return (
|
||||
<div
|
||||
role={onClick ? 'button' : undefined}
|
||||
onClick={() => {
|
||||
if (onClick) {
|
||||
onClick(attachment);
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
className={classNames(
|
||||
'module-image',
|
||||
onClick ? 'module-image__with-click-handler' : null,
|
||||
curveBottomLeft ? 'module-image--curved-bottom-left' : null,
|
||||
curveBottomRight ? 'module-image--curved-bottom-right' : null,
|
||||
curveTopLeft ? 'module-image--curved-top-left' : null,
|
||||
curveTopRight ? 'module-image--curved-top-right' : null
|
||||
curveTopRight ? 'module-image--curved-top-right' : null,
|
||||
softCorners ? 'module-image--soft-corners' : null
|
||||
)}
|
||||
>
|
||||
<img
|
||||
|
@ -88,9 +97,22 @@ export class Image extends React.Component<Props> {
|
|||
curveTopRight ? 'module-image--curved-top-right' : null,
|
||||
curveBottomLeft ? 'module-image--curved-bottom-left' : null,
|
||||
curveBottomRight ? 'module-image--curved-bottom-right' : null,
|
||||
softCorners ? 'module-image--soft-corners' : null,
|
||||
darkOverlay ? 'module-image__border-overlay--dark' : null
|
||||
)}
|
||||
/>
|
||||
{closeButton ? (
|
||||
<div
|
||||
role="button"
|
||||
onClick={(e: React.MouseEvent<{}>) => {
|
||||
e.stopPropagation();
|
||||
if (onClickClose) {
|
||||
onClickClose(attachment);
|
||||
}
|
||||
}}
|
||||
className="module-image__close-button"
|
||||
/>
|
||||
) : null}
|
||||
{bottomOverlay ? (
|
||||
<div
|
||||
className={classNames(
|
||||
|
|
|
@ -352,3 +352,35 @@ const attachments = [
|
|||
</div>
|
||||
</div>;
|
||||
```
|
||||
|
||||
### Mixing attachment types
|
||||
|
||||
```
|
||||
const attachments = [
|
||||
{
|
||||
url: util.pngObjectUrl,
|
||||
contentType: 'image/png',
|
||||
width: 320,
|
||||
height: 240,
|
||||
},
|
||||
{
|
||||
contentType: 'text/plain',
|
||||
},
|
||||
{
|
||||
url: util.pngObjectUrl,
|
||||
contentType: 'image/png',
|
||||
width: 320,
|
||||
height: 240,
|
||||
},
|
||||
];
|
||||
|
||||
<div>
|
||||
<div>
|
||||
<ImageGrid attachments={attachments} i18n={util.i18n} />
|
||||
</div>
|
||||
<hr />
|
||||
<div>
|
||||
<ImageGrid withContentAbove withContentBelow attachments={attachments} i18n={util.i18n} />
|
||||
</div>
|
||||
</div>;
|
||||
```
|
||||
|
|
|
@ -24,7 +24,7 @@ interface Props {
|
|||
const MAX_WIDTH = 300;
|
||||
const MAX_HEIGHT = MAX_WIDTH * 1.5;
|
||||
const MIN_WIDTH = 200;
|
||||
const MIN_HEIGHT = 25;
|
||||
const MIN_HEIGHT = 50;
|
||||
|
||||
export class ImageGrid extends React.Component<Props> {
|
||||
// tslint:disable-next-line max-func-body-length */
|
||||
|
@ -50,7 +50,7 @@ export class ImageGrid extends React.Component<Props> {
|
|||
return null;
|
||||
}
|
||||
|
||||
if (attachments.length === 1) {
|
||||
if (attachments.length === 1 || !areAllAttachmentsVisual(attachments)) {
|
||||
const { height, width } = getImageDimensions(attachments[0]);
|
||||
|
||||
return (
|
||||
|
@ -93,7 +93,7 @@ export class ImageGrid extends React.Component<Props> {
|
|||
playIconOverlay={isVideoAttachment(attachments[0])}
|
||||
height={149}
|
||||
width={149}
|
||||
url={getUrl(attachments[0])}
|
||||
url={getThumbnailUrl(attachments[0])}
|
||||
onClick={onClickAttachment}
|
||||
onError={onError}
|
||||
/>
|
||||
|
@ -107,7 +107,7 @@ export class ImageGrid extends React.Component<Props> {
|
|||
height={149}
|
||||
width={149}
|
||||
attachment={attachments[1]}
|
||||
url={getUrl(attachments[1])}
|
||||
url={getThumbnailUrl(attachments[1])}
|
||||
onClick={onClickAttachment}
|
||||
onError={onError}
|
||||
/>
|
||||
|
@ -141,7 +141,7 @@ export class ImageGrid extends React.Component<Props> {
|
|||
width={99}
|
||||
attachment={attachments[1]}
|
||||
playIconOverlay={isVideoAttachment(attachments[1])}
|
||||
url={getUrl(attachments[1])}
|
||||
url={getThumbnailUrl(attachments[1])}
|
||||
onClick={onClickAttachment}
|
||||
onError={onError}
|
||||
/>
|
||||
|
@ -154,7 +154,7 @@ export class ImageGrid extends React.Component<Props> {
|
|||
width={99}
|
||||
attachment={attachments[2]}
|
||||
playIconOverlay={isVideoAttachment(attachments[2])}
|
||||
url={getUrl(attachments[2])}
|
||||
url={getThumbnailUrl(attachments[2])}
|
||||
onClick={onClickAttachment}
|
||||
onError={onError}
|
||||
/>
|
||||
|
@ -176,7 +176,7 @@ export class ImageGrid extends React.Component<Props> {
|
|||
playIconOverlay={isVideoAttachment(attachments[0])}
|
||||
height={149}
|
||||
width={149}
|
||||
url={getUrl(attachments[0])}
|
||||
url={getThumbnailUrl(attachments[0])}
|
||||
onClick={onClickAttachment}
|
||||
onError={onError}
|
||||
/>
|
||||
|
@ -188,7 +188,7 @@ export class ImageGrid extends React.Component<Props> {
|
|||
height={149}
|
||||
width={149}
|
||||
attachment={attachments[1]}
|
||||
url={getUrl(attachments[1])}
|
||||
url={getThumbnailUrl(attachments[1])}
|
||||
onClick={onClickAttachment}
|
||||
onError={onError}
|
||||
/>
|
||||
|
@ -203,7 +203,7 @@ export class ImageGrid extends React.Component<Props> {
|
|||
height={149}
|
||||
width={149}
|
||||
attachment={attachments[2]}
|
||||
url={getUrl(attachments[2])}
|
||||
url={getThumbnailUrl(attachments[2])}
|
||||
onClick={onClickAttachment}
|
||||
onError={onError}
|
||||
/>
|
||||
|
@ -216,7 +216,7 @@ export class ImageGrid extends React.Component<Props> {
|
|||
height={149}
|
||||
width={149}
|
||||
attachment={attachments[3]}
|
||||
url={getUrl(attachments[3])}
|
||||
url={getThumbnailUrl(attachments[3])}
|
||||
onClick={onClickAttachment}
|
||||
onError={onError}
|
||||
/>
|
||||
|
@ -238,7 +238,7 @@ export class ImageGrid extends React.Component<Props> {
|
|||
playIconOverlay={isVideoAttachment(attachments[0])}
|
||||
height={149}
|
||||
width={149}
|
||||
url={getUrl(attachments[0])}
|
||||
url={getThumbnailUrl(attachments[0])}
|
||||
onClick={onClickAttachment}
|
||||
onError={onError}
|
||||
/>
|
||||
|
@ -250,7 +250,7 @@ export class ImageGrid extends React.Component<Props> {
|
|||
height={149}
|
||||
width={149}
|
||||
attachment={attachments[1]}
|
||||
url={getUrl(attachments[1])}
|
||||
url={getThumbnailUrl(attachments[1])}
|
||||
onClick={onClickAttachment}
|
||||
onError={onError}
|
||||
/>
|
||||
|
@ -265,7 +265,7 @@ export class ImageGrid extends React.Component<Props> {
|
|||
height={99}
|
||||
width={99}
|
||||
attachment={attachments[2]}
|
||||
url={getUrl(attachments[2])}
|
||||
url={getThumbnailUrl(attachments[2])}
|
||||
onClick={onClickAttachment}
|
||||
onError={onError}
|
||||
/>
|
||||
|
@ -277,7 +277,7 @@ export class ImageGrid extends React.Component<Props> {
|
|||
height={99}
|
||||
width={98}
|
||||
attachment={attachments[3]}
|
||||
url={getUrl(attachments[3])}
|
||||
url={getThumbnailUrl(attachments[3])}
|
||||
onClick={onClickAttachment}
|
||||
onError={onError}
|
||||
/>
|
||||
|
@ -296,7 +296,7 @@ export class ImageGrid extends React.Component<Props> {
|
|||
: undefined
|
||||
}
|
||||
attachment={attachments[4]}
|
||||
url={getUrl(attachments[4])}
|
||||
url={getThumbnailUrl(attachments[4])}
|
||||
onClick={onClickAttachment}
|
||||
onError={onError}
|
||||
/>
|
||||
|
@ -307,6 +307,14 @@ export class ImageGrid extends React.Component<Props> {
|
|||
}
|
||||
}
|
||||
|
||||
function getThumbnailUrl(attachment: AttachmentType) {
|
||||
if (attachment.thumbnail) {
|
||||
return attachment.thumbnail.url;
|
||||
}
|
||||
|
||||
return getUrl(attachment);
|
||||
}
|
||||
|
||||
function getUrl(attachment: AttachmentType) {
|
||||
if (attachment.screenshot) {
|
||||
return attachment.screenshot.url;
|
||||
|
@ -324,6 +332,13 @@ export function isImage(attachments?: Array<AttachmentType>) {
|
|||
);
|
||||
}
|
||||
|
||||
export function isImageAttachment(attachment: AttachmentType) {
|
||||
return (
|
||||
attachment &&
|
||||
attachment.contentType &&
|
||||
isImageTypeSupported(attachment.contentType)
|
||||
);
|
||||
}
|
||||
export function hasImage(attachments?: Array<AttachmentType>) {
|
||||
return attachments && attachments[0] && attachments[0].url;
|
||||
}
|
||||
|
@ -374,6 +389,24 @@ function getImageDimensions(attachment: AttachmentType): DimensionsType {
|
|||
};
|
||||
}
|
||||
|
||||
export function areAllAttachmentsVisual(
|
||||
attachments?: Array<AttachmentType>
|
||||
): boolean {
|
||||
if (!attachments) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const max = attachments.length;
|
||||
for (let i = 0; i < max; i += 1) {
|
||||
const attachment = attachments[i];
|
||||
if (!isImageAttachment(attachment) && !isVideoAttachment(attachment)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function getGridDimensions(
|
||||
attachments?: Array<AttachmentType>
|
||||
): null | DimensionsType {
|
||||
|
|
|
@ -79,52 +79,6 @@ interface State {
|
|||
imageBroken: boolean;
|
||||
}
|
||||
|
||||
function isAudio(attachments?: Array<AttachmentType>) {
|
||||
return (
|
||||
attachments &&
|
||||
attachments[0] &&
|
||||
attachments[0].contentType &&
|
||||
MIME.isAudio(attachments[0].contentType)
|
||||
);
|
||||
}
|
||||
|
||||
function canDisplayImage(attachments?: Array<AttachmentType>) {
|
||||
const { height, width } =
|
||||
attachments && attachments[0] ? attachments[0] : { height: 0, width: 0 };
|
||||
|
||||
return (
|
||||
height &&
|
||||
height > 0 &&
|
||||
height <= 4096 &&
|
||||
width &&
|
||||
width > 0 &&
|
||||
width <= 4096
|
||||
);
|
||||
}
|
||||
|
||||
function getExtension({
|
||||
fileName,
|
||||
contentType,
|
||||
}: {
|
||||
fileName: string;
|
||||
contentType: MIME.MIMEType;
|
||||
}): string | null {
|
||||
if (fileName && fileName.indexOf('.') >= 0) {
|
||||
const lastPeriod = fileName.lastIndexOf('.');
|
||||
const extension = fileName.slice(lastPeriod + 1);
|
||||
if (extension.length) {
|
||||
return extension;
|
||||
}
|
||||
}
|
||||
|
||||
const slash = contentType.indexOf('/');
|
||||
if (slash >= 0) {
|
||||
return contentType.slice(slash + 1);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const EXPIRATION_CHECK_MINIMUM = 2000;
|
||||
const EXPIRED_DELAY = 600;
|
||||
|
||||
|
@ -847,3 +801,49 @@ export class Message extends React.Component<Props, State> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function getExtension({
|
||||
fileName,
|
||||
contentType,
|
||||
}: {
|
||||
fileName: string;
|
||||
contentType: MIME.MIMEType;
|
||||
}): string | null {
|
||||
if (fileName && fileName.indexOf('.') >= 0) {
|
||||
const lastPeriod = fileName.lastIndexOf('.');
|
||||
const extension = fileName.slice(lastPeriod + 1);
|
||||
if (extension.length) {
|
||||
return extension;
|
||||
}
|
||||
}
|
||||
|
||||
const slash = contentType.indexOf('/');
|
||||
if (slash >= 0) {
|
||||
return contentType.slice(slash + 1);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function isAudio(attachments?: Array<AttachmentType>) {
|
||||
return (
|
||||
attachments &&
|
||||
attachments[0] &&
|
||||
attachments[0].contentType &&
|
||||
MIME.isAudio(attachments[0].contentType)
|
||||
);
|
||||
}
|
||||
|
||||
function canDisplayImage(attachments?: Array<AttachmentType>) {
|
||||
const { height, width } =
|
||||
attachments && attachments[0] ? attachments[0] : { height: 0, width: 0 };
|
||||
|
||||
return (
|
||||
height &&
|
||||
height > 0 &&
|
||||
height <= 4096 &&
|
||||
width &&
|
||||
width > 0 &&
|
||||
width <= 4096
|
||||
);
|
||||
}
|
||||
|
|
|
@ -44,3 +44,9 @@
|
|||
```jsx
|
||||
<MessageBody text="http://somewhere.com" disableLinks i18n={util.i18n} />
|
||||
```
|
||||
|
||||
### Emoji in link
|
||||
|
||||
```jsx
|
||||
<MessageBody text="http://somewhere.com?s=🔥\nCool, huh?" i18n={util.i18n} />
|
||||
```
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
|
||||
import { getSizeClass } from '../../util/emoji';
|
||||
import { getSizeClass, SizeClassType } from '../../util/emoji';
|
||||
import { Emojify } from './Emojify';
|
||||
import { AddNewLines } from './AddNewLines';
|
||||
import { Linkify } from './Linkify';
|
||||
|
@ -21,8 +21,26 @@ const renderNewLines: RenderTextCallback = ({
|
|||
key,
|
||||
}) => <AddNewLines key={key} text={textWithNewLines} />;
|
||||
|
||||
const renderLinks: RenderTextCallback = ({ text: textWithLinks, key }) => (
|
||||
<Linkify key={key} text={textWithLinks} renderNonLink={renderNewLines} />
|
||||
const renderEmoji = ({
|
||||
i18n,
|
||||
text,
|
||||
key,
|
||||
sizeClass,
|
||||
renderNonEmoji,
|
||||
}: {
|
||||
i18n: Localizer;
|
||||
text: string;
|
||||
key: number;
|
||||
sizeClass?: SizeClassType;
|
||||
renderNonEmoji: RenderTextCallback;
|
||||
}) => (
|
||||
<Emojify
|
||||
i18n={i18n}
|
||||
key={key}
|
||||
text={text}
|
||||
sizeClass={sizeClass}
|
||||
renderNonEmoji={renderNonEmoji}
|
||||
/>
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -34,14 +52,30 @@ const renderLinks: RenderTextCallback = ({ text: textWithLinks, key }) => (
|
|||
export class MessageBody extends React.Component<Props> {
|
||||
public render() {
|
||||
const { text, disableJumbomoji, disableLinks, i18n } = this.props;
|
||||
const sizeClass = disableJumbomoji ? '' : getSizeClass(text);
|
||||
const sizeClass = disableJumbomoji ? undefined : getSizeClass(text);
|
||||
|
||||
if (disableLinks) {
|
||||
return renderEmoji({
|
||||
i18n,
|
||||
text,
|
||||
sizeClass,
|
||||
key: 0,
|
||||
renderNonEmoji: renderNewLines,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Emojify
|
||||
<Linkify
|
||||
text={text}
|
||||
sizeClass={sizeClass}
|
||||
renderNonEmoji={disableLinks ? renderNewLines : renderLinks}
|
||||
i18n={i18n}
|
||||
renderNonLink={({ key, text: nonLinkText }) => {
|
||||
return renderEmoji({
|
||||
i18n,
|
||||
text: nonLinkText,
|
||||
sizeClass,
|
||||
key,
|
||||
renderNonEmoji: renderNewLines,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
50
ts/components/conversation/StagedGenericAttachment.md
Normal file
50
ts/components/conversation/StagedGenericAttachment.md
Normal file
|
@ -0,0 +1,50 @@
|
|||
Text file
|
||||
|
||||
```js
|
||||
const attachment = {
|
||||
contentType: 'text/plain',
|
||||
fileName: 'manifesto.txt',
|
||||
};
|
||||
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<StagedGenericAttachment
|
||||
attachment={attachment}
|
||||
i18n={util.i18n}
|
||||
onClose={attachment => console.log('onClose', attachment)}
|
||||
/>
|
||||
</util.ConversationContext>;
|
||||
```
|
||||
|
||||
File with long name
|
||||
|
||||
```js
|
||||
const attachment = {
|
||||
contentType: 'text/plain',
|
||||
fileName: 'this-is-my-very-important-manifesto-you-must-read-it.txt',
|
||||
};
|
||||
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<StagedGenericAttachment
|
||||
attachment={attachment}
|
||||
i18n={util.i18n}
|
||||
onClose={attachment => console.log('onClose', attachment)}
|
||||
/>
|
||||
</util.ConversationContext>;
|
||||
```
|
||||
|
||||
File with long extension
|
||||
|
||||
```js
|
||||
const attachment = {
|
||||
contentType: 'text/plain',
|
||||
fileName: 'manifesto.reallylongtxt',
|
||||
};
|
||||
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<StagedGenericAttachment
|
||||
attachment={attachment}
|
||||
i18n={util.i18n}
|
||||
onClose={attachment => console.log('onClose', attachment)}
|
||||
/>
|
||||
</util.ConversationContext>;
|
||||
```
|
44
ts/components/conversation/StagedGenericAttachment.tsx
Normal file
44
ts/components/conversation/StagedGenericAttachment.tsx
Normal file
|
@ -0,0 +1,44 @@
|
|||
import React from 'react';
|
||||
|
||||
import { getExtension } from './Message';
|
||||
|
||||
import { Localizer } from '../../types/Util';
|
||||
import { AttachmentType } from './types';
|
||||
|
||||
interface Props {
|
||||
attachment: AttachmentType;
|
||||
onClose: (attachment: AttachmentType) => void;
|
||||
i18n: Localizer;
|
||||
}
|
||||
|
||||
export class StagedGenericAttachment extends React.Component<Props> {
|
||||
public render() {
|
||||
const { attachment, onClose } = this.props;
|
||||
const { fileName, contentType } = attachment;
|
||||
const extension = getExtension({ contentType, fileName });
|
||||
|
||||
return (
|
||||
<div className="module-staged-generic-attachment">
|
||||
<div
|
||||
className="module-staged-generic-attachment__close-button"
|
||||
role="button"
|
||||
onClick={() => {
|
||||
if (onClose) {
|
||||
onClose(attachment);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="module-staged-generic-attachment__icon">
|
||||
{extension ? (
|
||||
<div className="module-staged-generic-attachment__icon__extension">
|
||||
{extension}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="module-staged-generic-attachment__filename">
|
||||
{fileName}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
10
ts/components/conversation/StagedPlaceholderAttachment.md
Normal file
10
ts/components/conversation/StagedPlaceholderAttachment.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
```js
|
||||
const attachment = {
|
||||
contentType: 'text/plain',
|
||||
fileName: 'manifesto.txt',
|
||||
};
|
||||
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<StagedPlaceholderAttachment onClick={attachment => console.log('onClick')} />
|
||||
</util.ConversationContext>;
|
||||
```
|
21
ts/components/conversation/StagedPlaceholderAttachment.tsx
Normal file
21
ts/components/conversation/StagedPlaceholderAttachment.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export class StagedPlaceholderAttachment extends React.Component<Props> {
|
||||
public render() {
|
||||
const { onClick } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="module-staged-placeholder-attachment"
|
||||
role="button"
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="module-staged-placeholder-attachment__plus-icon" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
### In message bubble
|
||||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<li>
|
||||
<TypingBubble conversationType="direct" i18n={util.i18n} />
|
||||
</li>
|
||||
|
@ -14,7 +14,7 @@
|
|||
### In message bubble, group conversation
|
||||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<li>
|
||||
<TypingBubble color="red" conversationType="group" i18n={util.i18n} />
|
||||
</li>
|
||||
|
|
|
@ -10,6 +10,8 @@ instance.include_title = true;
|
|||
instance.replace_mode = 'img';
|
||||
instance.supports_css = false; // needed to avoid spans with background-image
|
||||
|
||||
export type SizeClassType = '' | 'small' | 'medium' | 'large' | 'jumbo';
|
||||
|
||||
export function getRegex(): RegExp {
|
||||
return instance.rx_unified;
|
||||
}
|
||||
|
@ -56,7 +58,7 @@ function hasNormalCharacters(str: string) {
|
|||
return noEmoji.length > 0;
|
||||
}
|
||||
|
||||
export function getSizeClass(str: string) {
|
||||
export function getSizeClass(str: string): SizeClassType {
|
||||
if (hasNormalCharacters(str)) {
|
||||
return '';
|
||||
}
|
||||
|
|
|
@ -244,7 +244,7 @@
|
|||
"rule": "jQuery-wrap(",
|
||||
"path": "js/background.js",
|
||||
"line": " wrap(",
|
||||
"lineNumber": 727,
|
||||
"lineNumber": 740,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2018-10-18T22:23:00.485Z"
|
||||
},
|
||||
|
@ -252,7 +252,7 @@
|
|||
"rule": "jQuery-wrap(",
|
||||
"path": "js/background.js",
|
||||
"line": " await wrap(",
|
||||
"lineNumber": 1257,
|
||||
"lineNumber": 1270,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2018-10-26T22:43:23.229Z"
|
||||
},
|
||||
|
@ -319,7 +319,7 @@
|
|||
"rule": "jQuery-wrap(",
|
||||
"path": "js/modules/crypto.js",
|
||||
"line": " return dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64');",
|
||||
"lineNumber": 271,
|
||||
"lineNumber": 38,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2018-10-05T23:12:28.961Z"
|
||||
},
|
||||
|
@ -327,7 +327,7 @@
|
|||
"rule": "jQuery-wrap(",
|
||||
"path": "js/modules/crypto.js",
|
||||
"line": " return dcodeIO.ByteBuffer.wrap(base64string, 'base64').toArrayBuffer();",
|
||||
"lineNumber": 274,
|
||||
"lineNumber": 41,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2018-10-05T23:12:28.961Z"
|
||||
},
|
||||
|
@ -335,7 +335,7 @@
|
|||
"rule": "jQuery-wrap(",
|
||||
"path": "js/modules/crypto.js",
|
||||
"line": " return dcodeIO.ByteBuffer.wrap(key, 'binary').toArrayBuffer();",
|
||||
"lineNumber": 278,
|
||||
"lineNumber": 45,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2018-10-05T23:12:28.961Z"
|
||||
},
|
||||
|
@ -343,7 +343,7 @@
|
|||
"rule": "jQuery-wrap(",
|
||||
"path": "js/modules/crypto.js",
|
||||
"line": " return dcodeIO.ByteBuffer.wrap(string, 'utf8').toArrayBuffer();",
|
||||
"lineNumber": 282,
|
||||
"lineNumber": 49,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2018-10-05T23:12:28.961Z"
|
||||
},
|
||||
|
@ -351,7 +351,7 @@
|
|||
"rule": "jQuery-wrap(",
|
||||
"path": "js/modules/crypto.js",
|
||||
"line": " return dcodeIO.ByteBuffer.wrap(buffer).toString('utf8');",
|
||||
"lineNumber": 285,
|
||||
"lineNumber": 52,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2018-10-05T23:12:28.961Z"
|
||||
},
|
||||
|
@ -655,474 +655,6 @@
|
|||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " template: $('#conversation').html(),",
|
||||
"lineNumber": 73,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-html(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " template: $('#conversation').html(),",
|
||||
"lineNumber": 73,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-15T00:38:04.183Z",
|
||||
"reasonDetail": "Getting the value, not setting it"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.loadingScreen.$el.prependTo(this.$('.discussion-container'));",
|
||||
"lineNumber": 143,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-11-14T19:09:08.182Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-prependTo(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.loadingScreen.$el.prependTo(this.$('.discussion-container'));",
|
||||
"lineNumber": 143,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-11-14T19:07:46.079Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " el: this.$('form.send'),",
|
||||
"lineNumber": 147,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-11-14T19:07:46.079Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$('.conversation-header').append(this.titleView.el);",
|
||||
"lineNumber": 205,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-append(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$('.conversation-header').append(this.titleView.el);",
|
||||
"lineNumber": 205,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$('.discussion-container').append(this.view.el);",
|
||||
"lineNumber": 211,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-append(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$('.discussion-container').append(this.view.el);",
|
||||
"lineNumber": 211,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$messageField = this.$('.send-message');",
|
||||
"lineNumber": 214,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$('.send-message').focus(this.focusBottomBar.bind(this));",
|
||||
"lineNumber": 232,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$emojiPanelContainer = this.$('.emoji-panel-container');",
|
||||
"lineNumber": 235,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " const container = this.$('.discussion-container');",
|
||||
"lineNumber": 421,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-append(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " container.append(this.banner.el);",
|
||||
"lineNumber": 422,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.typingBubbleView.$el.appendTo(this.$('.typing-container'));",
|
||||
"lineNumber": 459,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-11-14T18:51:15.180Z",
|
||||
"reasonDetail": "$() parameter is a hard-coded string"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-appendTo(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.typingBubbleView.$el.appendTo(this.$('.typing-container'));",
|
||||
"lineNumber": 459,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-11-14T18:51:15.180Z",
|
||||
"reasonDetail": "Both parameters are known elements from the DOM"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$('.send-message').val().length > 0 ||",
|
||||
"lineNumber": 468,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$('.capture-audio').hide();",
|
||||
"lineNumber": 471,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$('.capture-audio').show();",
|
||||
"lineNumber": 473,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " if (this.$('.send-message').val().length > 2000) {",
|
||||
"lineNumber": 477,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$('.android-length-warning').hide();",
|
||||
"lineNumber": 480,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " view.$el.appendTo(this.$('.capture-audio'));",
|
||||
"lineNumber": 500,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-appendTo(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " view.$el.appendTo(this.$('.capture-audio'));",
|
||||
"lineNumber": 500,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$('.send-message').attr('disabled', true);",
|
||||
"lineNumber": 502,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$('.bottom-bar form').submit();",
|
||||
"lineNumber": 509,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$('.send-message').removeAttr('disabled');",
|
||||
"lineNumber": 512,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$('.bottom-bar form').removeClass('active');",
|
||||
"lineNumber": 518,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$('.bottom-bar form').addClass('active');",
|
||||
"lineNumber": 521,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " const container = this.$('.discussion-container');",
|
||||
"lineNumber": 609,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-append(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " container.append(this.scrollDownButton.el);",
|
||||
"lineNumber": 610,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-appendTo(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " toast.$el.appendTo(this.$el);",
|
||||
"lineNumber": 637,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-appendTo(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " toast.$el.appendTo(this.$el);",
|
||||
"lineNumber": 670,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-appendTo(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " toast.$el.appendTo(this.$el);",
|
||||
"lineNumber": 674,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " const el = this.$(`#${databaseId}`);",
|
||||
"lineNumber": 681,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-appendTo(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " toast.$el.appendTo(this.$el);",
|
||||
"lineNumber": 684,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " lastSeenEl.insertBefore(this.$(`#${oldestUnread.get('id')}`));",
|
||||
"lineNumber": 861,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-insertBefore(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " lastSeenEl.insertBefore(this.$(`#${oldestUnread.get('id')}`));",
|
||||
"lineNumber": 861,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$('.bar-container').show();",
|
||||
"lineNumber": 916,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$('.bar-container').hide();",
|
||||
"lineNumber": 928,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " const el = this.$(`#${message.id}`);",
|
||||
"lineNumber": 1025,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-prepend(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$el.prepend(dialog.el);",
|
||||
"lineNumber": 1098,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-appendTo(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " toast.$el.appendTo(this.$el);",
|
||||
"lineNumber": 1121,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-10-11T19:22:47.331Z",
|
||||
"reasonDetail": "Operating on already-existing DOM elements"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-prepend(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$el.prepend(dialog.el);",
|
||||
"lineNumber": 1149,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " view.$el.insertBefore(this.$('.panel').first());",
|
||||
"lineNumber": 1283,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-insertBefore(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " view.$el.insertBefore(this.$('.panel').first());",
|
||||
"lineNumber": 1283,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-prepend(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$el.prepend(dialog.el);",
|
||||
"lineNumber": 1361,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$('.send').prepend(this.quoteView.el);",
|
||||
"lineNumber": 1531,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-prepend(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$('.send').prepend(this.quoteView.el);",
|
||||
"lineNumber": 1531,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-appendTo(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " toast.$el.appendTo(this.$el);",
|
||||
"lineNumber": 1555,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$('.bottom-bar form').submit();",
|
||||
"lineNumber": 1610,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " const $attachmentPreviews = this.$('.attachment-previews');",
|
||||
"lineNumber": 1619,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$('.panel').css('display') === 'none'",
|
||||
"lineNumber": 1650,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/debug_log_view.js",
|
||||
|
@ -1195,105 +727,6 @@
|
|||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/file_input_view.js",
|
||||
"line": " this.$input = this.$('input[type=file]');",
|
||||
"lineNumber": 45,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/file_input_view.js",
|
||||
"line": " this.$('.avatar').hide();",
|
||||
"lineNumber": 88,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/file_input_view.js",
|
||||
"line": " this.$('.attachment-previews').append(this.thumb.render().el);",
|
||||
"lineNumber": 90,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-append(",
|
||||
"path": "js/views/file_input_view.js",
|
||||
"line": " this.$('.attachment-previews').append(this.thumb.render().el);",
|
||||
"lineNumber": 90,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/file_input_view.js",
|
||||
"line": " this.thumb.$('img')[0].onload = () => {",
|
||||
"lineNumber": 98,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/file_input_view.js",
|
||||
"line": " this.thumb.$('img')[0].onerror = () => {",
|
||||
"lineNumber": 101,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-insertAfter(",
|
||||
"path": "js/views/file_input_view.js",
|
||||
"line": " toast.$el.insertAfter(this.$el);",
|
||||
"lineNumber": 108,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-insertAfter(",
|
||||
"path": "js/views/file_input_view.js",
|
||||
"line": " toast.$el.insertAfter(this.$el);",
|
||||
"lineNumber": 190,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-10-11T19:22:47.331Z",
|
||||
"reasonDetail": "Operating on already-existing DOM elements"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-insertAfter(",
|
||||
"path": "js/views/file_input_view.js",
|
||||
"line": " toast.$el.insertAfter(this.$el);",
|
||||
"lineNumber": 284,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/file_input_view.js",
|
||||
"line": " this.$('.avatar').show();",
|
||||
"lineNumber": 388,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-wrap(",
|
||||
"path": "js/views/file_input_view.js",
|
||||
"line": " .wrap('<form>')",
|
||||
"lineNumber": 398,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Hard-coded value"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/group_member_list_view.js",
|
||||
|
@ -1692,7 +1125,7 @@
|
|||
"rule": "jQuery-append(",
|
||||
"path": "js/views/message_list_view.js",
|
||||
"line": " this.$messages.append(view.el);",
|
||||
"lineNumber": 102,
|
||||
"lineNumber": 111,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-11-14T18:51:15.180Z",
|
||||
"reasonDetail": "view.el is a known DOM element"
|
||||
|
@ -1701,7 +1134,7 @@
|
|||
"rule": "jQuery-prepend(",
|
||||
"path": "js/views/message_list_view.js",
|
||||
"line": " this.$messages.prepend(view.el);",
|
||||
"lineNumber": 105,
|
||||
"lineNumber": 114,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-11-14T18:51:15.180Z",
|
||||
"reasonDetail": "view.el is a known DOM element"
|
||||
|
@ -1710,7 +1143,7 @@
|
|||
"rule": "jQuery-$(",
|
||||
"path": "js/views/message_list_view.js",
|
||||
"line": " const next = this.$(`#${this.collection.at(index + 1).id}`);",
|
||||
"lineNumber": 108,
|
||||
"lineNumber": 117,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-11-14T18:51:15.180Z",
|
||||
"reasonDetail": "Message ids are GUIDs, and therefore the resultant string for $() is an id"
|
||||
|
@ -1719,7 +1152,7 @@
|
|||
"rule": "jQuery-insertBefore(",
|
||||
"path": "js/views/message_list_view.js",
|
||||
"line": " view.$el.insertBefore(next);",
|
||||
"lineNumber": 111,
|
||||
"lineNumber": 120,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-11-14T18:51:15.180Z",
|
||||
"reasonDetail": "next is a known DOM element"
|
||||
|
@ -1728,7 +1161,7 @@
|
|||
"rule": "jQuery-insertAfter(",
|
||||
"path": "js/views/message_list_view.js",
|
||||
"line": " view.$el.insertAfter(prev);",
|
||||
"lineNumber": 113,
|
||||
"lineNumber": 122,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-11-14T18:51:15.180Z",
|
||||
"reasonDetail": "prev is a known DOM element"
|
||||
|
@ -1737,7 +1170,7 @@
|
|||
"rule": "jQuery-insertBefore(",
|
||||
"path": "js/views/message_list_view.js",
|
||||
"line": " view.$el.insertBefore(elements[i]);",
|
||||
"lineNumber": 122,
|
||||
"lineNumber": 131,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-11-14T18:51:15.180Z",
|
||||
"reasonDetail": "elements[i] is a known DOM element"
|
||||
|
@ -1746,7 +1179,7 @@
|
|||
"rule": "jQuery-append(",
|
||||
"path": "js/views/message_list_view.js",
|
||||
"line": " this.$messages.append(view.el);",
|
||||
"lineNumber": 127,
|
||||
"lineNumber": 136,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-11-14T18:51:15.180Z",
|
||||
"reasonDetail": "view.el is a known DOM element"
|
||||
|
@ -2352,7 +1785,7 @@
|
|||
"rule": "jQuery-wrap(",
|
||||
"path": "libtextsecure/message_receiver.js",
|
||||
"line": " const buffer = dcodeIO.ByteBuffer.wrap(ciphertext);",
|
||||
"lineNumber": 785,
|
||||
"lineNumber": 794,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2018-09-19T18:13:29.628Z"
|
||||
},
|
||||
|
@ -2360,7 +1793,7 @@
|
|||
"rule": "jQuery-wrap(",
|
||||
"path": "libtextsecure/message_receiver.js",
|
||||
"line": " const buffer = dcodeIO.ByteBuffer.wrap(ciphertext);",
|
||||
"lineNumber": 810,
|
||||
"lineNumber": 819,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2018-09-19T18:13:29.628Z"
|
||||
},
|
||||
|
@ -3174,7 +2607,7 @@
|
|||
"rule": "jQuery-append(",
|
||||
"path": "node_modules/electron/electron.d.ts",
|
||||
"line": " append(menuItem: MenuItem): void;",
|
||||
"lineNumber": 3243,
|
||||
"lineNumber": 3232,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2018-09-19T18:13:29.628Z"
|
||||
},
|
||||
|
@ -3493,7 +2926,7 @@
|
|||
"lineNumber": 4136,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "<optional>"
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-wrap(",
|
||||
|
@ -4083,7 +3516,7 @@
|
|||
"lineNumber": 483,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "<optional>"
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-appendTo(",
|
||||
|
@ -5849,7 +5282,7 @@
|
|||
"lineNumber": 1699,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2018-09-18T19:19:27.699Z",
|
||||
"reasonDetail": "<optional>"
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "DOM-innerHTML",
|
||||
|
|
|
@ -49,6 +49,10 @@ const allSourceFiles = glob.sync(searchPattern, { nodir: true });
|
|||
const results: Array<ExceptionType> = [];
|
||||
|
||||
const excludedFiles = [
|
||||
// High-traffic files in our project
|
||||
'^js/views/conversation_view.js',
|
||||
'^js/views/file_input_view.js',
|
||||
|
||||
// Generated files
|
||||
'^js/components.js',
|
||||
'^js/libtextsecure.js',
|
||||
|
|
61
yarn.lock
61
yarn.lock
|
@ -22,9 +22,9 @@
|
|||
"7zip-bin-mac" "~1.0.1"
|
||||
"7zip-bin-win" "~2.2.0"
|
||||
|
||||
"@journeyapps/sqlcipher@https://github.com/scottnonnenberg-signal/node-sqlcipher.git#ed4f4d179ac010c6347b291cbd4c2ebe5c773741":
|
||||
"@journeyapps/sqlcipher@https://github.com/scottnonnenberg-signal/node-sqlcipher.git#36149a4b03ccf11ec18b9205e1bfd9056015cf07":
|
||||
version "3.2.1"
|
||||
resolved "https://github.com/scottnonnenberg-signal/node-sqlcipher.git#ed4f4d179ac010c6347b291cbd4c2ebe5c773741"
|
||||
resolved "https://github.com/scottnonnenberg-signal/node-sqlcipher.git#36149a4b03ccf11ec18b9205e1bfd9056015cf07"
|
||||
dependencies:
|
||||
nan "^2.10.0"
|
||||
node-pre-gyp "^0.10.0"
|
||||
|
@ -391,20 +391,6 @@ archiver-utils@^1.3.0:
|
|||
normalize-path "^2.0.0"
|
||||
readable-stream "^2.0.0"
|
||||
|
||||
archiver@2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/archiver/-/archiver-2.1.1.tgz#ff662b4a78201494a3ee544d3a33fe7496509ebc"
|
||||
integrity sha1-/2YrSnggFJSj7lRNOjP+dJZQnrw=
|
||||
dependencies:
|
||||
archiver-utils "^1.3.0"
|
||||
async "^2.0.0"
|
||||
buffer-crc32 "^0.2.1"
|
||||
glob "^7.0.0"
|
||||
lodash "^4.8.0"
|
||||
readable-stream "^2.0.0"
|
||||
tar-stream "^1.5.0"
|
||||
zip-stream "^1.2.0"
|
||||
|
||||
archiver@~2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/archiver/-/archiver-2.1.0.tgz#d2df2e8d5773a82c1dcce925ccc41450ea999afd"
|
||||
|
@ -1371,6 +1357,11 @@ chownr@^1.0.1:
|
|||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181"
|
||||
|
||||
chownr@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494"
|
||||
integrity sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==
|
||||
|
||||
chrome-trace-event@^0.1.1:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-0.1.2.tgz#90f36885d5345a50621332f0717b595883d5d982"
|
||||
|
@ -2534,10 +2525,10 @@ electron-updater@2.21.10:
|
|||
semver "^5.5.0"
|
||||
source-map-support "^0.5.5"
|
||||
|
||||
electron@3.0.9:
|
||||
version "3.0.9"
|
||||
resolved "https://registry.yarnpkg.com/electron/-/electron-3.0.9.tgz#79bd25dfd5496918a00d579e702fb83082f1a036"
|
||||
integrity sha512-OoSoeUWo9PzbArgrwS1yTfTRSlpXmIgrFGWUuUZCjKAk4DGR70elHDNeRnnBJ9NTwXXZVifChcfx73Ah3GnlVQ==
|
||||
electron@3.0.14:
|
||||
version "3.0.14"
|
||||
resolved "https://registry.yarnpkg.com/electron/-/electron-3.0.14.tgz#d54c51de3651c0fe48a6a6e9aef1ca98e5ea5796"
|
||||
integrity sha512-1fG9bE0LzL5QXeEq2MC0dHdVO0pbZOnNlVAIyOyJaCFAu/TjLhxQfWj38bFUEojzuVlaR87tZz0iy2qlVZj3sw==
|
||||
dependencies:
|
||||
"@types/node" "^8.0.24"
|
||||
electron-download "^4.1.0"
|
||||
|
@ -3112,6 +3103,7 @@ file-sync-cmp@^0.1.0:
|
|||
file-type@^3.1.0:
|
||||
version "3.9.0"
|
||||
resolved "https://registry.yarnpkg.com/file-type/-/file-type-3.9.0.tgz#257a078384d1db8087bc449d107d52a52672b9e9"
|
||||
integrity sha1-JXoHg4TR24CHvESdEH1SpSZyuek=
|
||||
|
||||
file-uri-to-path@1:
|
||||
version "1.0.0"
|
||||
|
@ -5515,12 +5507,27 @@ minipass@^2.2.1, minipass@^2.3.3:
|
|||
safe-buffer "^5.1.2"
|
||||
yallist "^3.0.0"
|
||||
|
||||
minipass@^2.3.4:
|
||||
version "2.3.5"
|
||||
resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.5.tgz#cacebe492022497f656b0f0f51e2682a9ed2d848"
|
||||
integrity sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==
|
||||
dependencies:
|
||||
safe-buffer "^5.1.2"
|
||||
yallist "^3.0.0"
|
||||
|
||||
minizlib@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.1.0.tgz#11e13658ce46bc3a70a267aac58359d1e0c29ceb"
|
||||
dependencies:
|
||||
minipass "^2.2.1"
|
||||
|
||||
minizlib@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.1.1.tgz#6734acc045a46e61d596a43bb9d9cd326e19cc42"
|
||||
integrity sha512-TrfjCjk4jLhcJyGMYymBH6oTXcWjYbUAXTHDbtnWHjZC25h0cdajHuPE1zxb4DVmu8crfh+HwH/WMuyLG0nHBg==
|
||||
dependencies:
|
||||
minipass "^2.2.1"
|
||||
|
||||
mississippi@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-2.0.0.tgz#3442a508fafc28500486feea99409676e4ee5a6f"
|
||||
|
@ -8355,6 +8362,7 @@ string-width@^2.1.0, string-width@^2.1.1:
|
|||
string_decoder@^1.0.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
|
||||
integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
|
||||
dependencies:
|
||||
safe-buffer "~5.1.0"
|
||||
|
||||
|
@ -8522,6 +8530,19 @@ tar-stream@^1.5.0:
|
|||
readable-stream "^2.0.0"
|
||||
xtend "^4.0.0"
|
||||
|
||||
tar@4.4.8:
|
||||
version "4.4.8"
|
||||
resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.8.tgz#b19eec3fde2a96e64666df9fdb40c5ca1bc3747d"
|
||||
integrity sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==
|
||||
dependencies:
|
||||
chownr "^1.1.1"
|
||||
fs-minipass "^1.2.5"
|
||||
minipass "^2.3.4"
|
||||
minizlib "^1.1.1"
|
||||
mkdirp "^0.5.0"
|
||||
safe-buffer "^5.1.2"
|
||||
yallist "^3.0.2"
|
||||
|
||||
tar@^2.0.0:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1"
|
||||
|
|
Loading…
Reference in a new issue