Merge commit signal/master into signal-1.20
This commit is contained in:
commit
1a15ec9c15
|
@ -1,6 +1,6 @@
|
||||||
<!--
|
<!--
|
||||||
Please fill out this template with all the information you have. We can't do much without
|
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
|
Please note that this tracker is only for bugs and feature requests. Please try these
|
||||||
locations if you have a question or comment:
|
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. -->
|
<!-- Give an overall summary of the issue. -->
|
||||||
|
|
||||||
### Steps to reproduce
|
### Steps to Reproduce
|
||||||
|
|
||||||
<!-- Using bullet points, list the steps that reproduce the bug. -->
|
<!-- 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
|
2. step two
|
||||||
3. step three
|
3. step three
|
||||||
|
|
||||||
Actual result:
|
Actual Result:
|
||||||
|
|
||||||
<!-- Describe the details of the buggy behaviour. -->
|
<!-- Describe the details of the buggy behaviour. -->
|
||||||
|
|
||||||
Expected result:
|
Expected Result:
|
||||||
|
|
||||||
<!-- Describe in detail what the correct behavior should be. -->
|
<!-- 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.
|
You can drag and drop images into this text box.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
### Platform info
|
### Platform Info
|
||||||
|
|
||||||
Loki Messenger version:
|
Loki Messenger version:
|
||||||
|
|
||||||
|
@ -58,16 +58,16 @@ Operating System:
|
||||||
|
|
||||||
<!-- Instructions for finding your OS version are here: http://whatsmyos.com/ -->
|
<!-- Instructions for finding your OS version are here: http://whatsmyos.com/ -->
|
||||||
|
|
||||||
Linked device version:
|
Linked Device Version:
|
||||||
|
|
||||||
<!-- Android: Settings -> Advanced, iOS: Settings -> General -> About -->
|
<!-- 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.
|
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:
|
In most cases, a log from your other devices is also useful:
|
||||||
Android: https://support.signal.org/hc/en-us/articles/212535838
|
Android: https://support.signal.org/hc/en-us/articles/360007318591#android_debug
|
||||||
iOS: https://support.signal.org/hc/en-us/articles/229676507
|
iOS: https://support.signal.org/hc/en-us/articles/360007318591#ios_debug
|
||||||
-->
|
-->
|
||||||
|
|
|
@ -267,8 +267,8 @@ module.exports = grunt => {
|
||||||
grunt.registerTask('getExpireTime', () => {
|
grunt.registerTask('getExpireTime', () => {
|
||||||
grunt.task.requires('gitinfo');
|
grunt.task.requires('gitinfo');
|
||||||
const gitinfo = grunt.config.get('gitinfo');
|
const gitinfo = grunt.config.get('gitinfo');
|
||||||
const commited = gitinfo.local.branch.current.lastCommitTime;
|
const committed = gitinfo.local.branch.current.lastCommitTime;
|
||||||
const time = Date.parse(commited) + 1000 * 60 * 60 * 24 * 90;
|
const time = Date.parse(committed) + 1000 * 60 * 60 * 24 * 90;
|
||||||
grunt.file.write(
|
grunt.file.write(
|
||||||
'config/local-production.json',
|
'config/local-production.json',
|
||||||
`${JSON.stringify({ buildExpiration: time })}\n`
|
`${JSON.stringify({ buildExpiration: time })}\n`
|
||||||
|
@ -307,7 +307,7 @@ module.exports = grunt => {
|
||||||
app.client
|
app.client
|
||||||
.execute(getMochaResults)
|
.execute(getMochaResults)
|
||||||
.then(data => Boolean(data.value)),
|
.then(data => Boolean(data.value)),
|
||||||
10000,
|
25000,
|
||||||
'Expected to find window.mochaResults set!'
|
'Expected to find window.mochaResults set!'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
# Loki Messenger
|
# 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).
|
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
|
## Summary
|
||||||
|
|
|
@ -172,6 +172,10 @@
|
||||||
"message": "Choose folder",
|
"message": "Choose folder",
|
||||||
"description": "Button to allow the user to find a folder on disk"
|
"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": {
|
"loadDataHeader": {
|
||||||
"message": "Load your data",
|
"message": "Load your data",
|
||||||
"description": "Header shown on the first screen in the data import process"
|
"description": "Header shown on the first screen in the data import process"
|
||||||
|
@ -542,15 +546,37 @@
|
||||||
"message": "Voice Message",
|
"message": "Voice Message",
|
||||||
"description": "Name for a voice message attachment"
|
"description": "Name for a voice message attachment"
|
||||||
},
|
},
|
||||||
"unsupportedFileType": {
|
|
||||||
"message": "Unsupported file type",
|
|
||||||
"description": "Displayed for outgoing unsupported attachment"
|
|
||||||
},
|
|
||||||
"dangerousFileType": {
|
"dangerousFileType": {
|
||||||
"message": "Attachment type not allowed for security reasons",
|
"message": "Attachment type not allowed for security reasons",
|
||||||
"description":
|
"description":
|
||||||
"Shown in toast when user attempts to send .exe file, for example"
|
"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": {
|
"fileSizeWarning": {
|
||||||
"message": "Sorry, the selected file exceeds message size restrictions."
|
"message": "Sorry, the selected file exceeds message size restrictions."
|
||||||
},
|
},
|
||||||
|
@ -732,6 +758,12 @@
|
||||||
"description":
|
"description":
|
||||||
"Shown in toast if user clicks on quote references messages not loaded in view, but in database"
|
"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": {
|
"you": {
|
||||||
"message": "You",
|
"message": "You",
|
||||||
"description":
|
"description":
|
||||||
|
@ -936,6 +968,16 @@
|
||||||
"description":
|
"description":
|
||||||
"Used for the icon layered on top of an image in message bubbles"
|
"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": {
|
"fileIconAlt": {
|
||||||
"message": "File icon",
|
"message": "File icon",
|
||||||
"description":
|
"description":
|
||||||
|
|
141
app/sql.js
141
app/sql.js
|
@ -24,6 +24,7 @@ module.exports = {
|
||||||
createOrUpdateGroup,
|
createOrUpdateGroup,
|
||||||
getGroupById,
|
getGroupById,
|
||||||
getAllGroupIds,
|
getAllGroupIds,
|
||||||
|
getAllGroups,
|
||||||
bulkAddGroups,
|
bulkAddGroups,
|
||||||
removeGroupById,
|
removeGroupById,
|
||||||
removeAllGroups,
|
removeAllGroups,
|
||||||
|
@ -92,7 +93,10 @@ module.exports = {
|
||||||
getAllConversationIds,
|
getAllConversationIds,
|
||||||
getAllPrivateConversations,
|
getAllPrivateConversations,
|
||||||
getAllGroupsInvolvingId,
|
getAllGroupsInvolvingId,
|
||||||
|
|
||||||
searchConversations,
|
searchConversations,
|
||||||
|
searchMessages,
|
||||||
|
searchMessagesInConversation,
|
||||||
|
|
||||||
getMessageCount,
|
getMessageCount,
|
||||||
saveMessage,
|
saveMessage,
|
||||||
|
@ -540,6 +544,69 @@ async function updateToSchemaVersion7(currentVersion, instance) {
|
||||||
console.log('updateToSchemaVersion7: success!');
|
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 = [
|
const SCHEMA_VERSIONS = [
|
||||||
updateToSchemaVersion1,
|
updateToSchemaVersion1,
|
||||||
updateToSchemaVersion2,
|
updateToSchemaVersion2,
|
||||||
|
@ -548,6 +615,7 @@ const SCHEMA_VERSIONS = [
|
||||||
() => null, // version 5 was dropped
|
() => null, // version 5 was dropped
|
||||||
updateToSchemaVersion6,
|
updateToSchemaVersion6,
|
||||||
updateToSchemaVersion7,
|
updateToSchemaVersion7,
|
||||||
|
updateToSchemaVersion8,
|
||||||
];
|
];
|
||||||
|
|
||||||
async function updateSchema(instance) {
|
async function updateSchema(instance) {
|
||||||
|
@ -598,7 +666,7 @@ async function initialize({ configDir, key }) {
|
||||||
const promisified = promisify(sqlInstance);
|
const promisified = promisify(sqlInstance);
|
||||||
|
|
||||||
// promisified.on('trace', async statement => {
|
// promisified.on('trace', async statement => {
|
||||||
// if (!db) {
|
// if (!db || statement.startsWith('--')) {
|
||||||
// console._log(statement);
|
// console._log(statement);
|
||||||
// return;
|
// return;
|
||||||
// }
|
// }
|
||||||
|
@ -669,6 +737,10 @@ async function getAllGroupIds() {
|
||||||
const rows = await db.all('SELECT id FROM groups ORDER BY id ASC;');
|
const rows = await db.all('SELECT id FROM groups ORDER BY id ASC;');
|
||||||
return map(rows, row => row.id);
|
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) {
|
async function bulkAddGroups(array) {
|
||||||
return bulkAdd(GROUPS_TABLE, array);
|
return bulkAdd(GROUPS_TABLE, array);
|
||||||
}
|
}
|
||||||
|
@ -1232,9 +1304,11 @@ async function getAllGroupsInvolvingId(id) {
|
||||||
async function searchConversations(query) {
|
async function searchConversations(query) {
|
||||||
const rows = await db.all(
|
const rows = await db.all(
|
||||||
`SELECT json FROM conversations WHERE
|
`SELECT json FROM conversations WHERE
|
||||||
id LIKE $id OR
|
(
|
||||||
name LIKE $name OR
|
id LIKE $id OR
|
||||||
profileName LIKE $profileName
|
name LIKE $name OR
|
||||||
|
profileName LIKE $profileName
|
||||||
|
)
|
||||||
ORDER BY id ASC;`,
|
ORDER BY id ASC;`,
|
||||||
{
|
{
|
||||||
$id: `%${query}%`,
|
$id: `%${query}%`,
|
||||||
|
@ -1246,6 +1320,58 @@ async function searchConversations(query) {
|
||||||
return map(rows, row => jsonToObject(row.json));
|
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() {
|
async function getMessageCount() {
|
||||||
const row = await db.get('SELECT count(*) from messages;');
|
const row = await db.get('SELECT count(*) from messages;');
|
||||||
|
|
||||||
|
@ -1258,6 +1384,7 @@ async function getMessageCount() {
|
||||||
|
|
||||||
async function saveMessage(data, { forceSave } = {}) {
|
async function saveMessage(data, { forceSave } = {}) {
|
||||||
const {
|
const {
|
||||||
|
body,
|
||||||
conversationId,
|
conversationId,
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line camelcase
|
||||||
expires_at,
|
expires_at,
|
||||||
|
@ -1283,6 +1410,7 @@ async function saveMessage(data, { forceSave } = {}) {
|
||||||
$id: id,
|
$id: id,
|
||||||
$json: objectToJSON(data),
|
$json: objectToJSON(data),
|
||||||
|
|
||||||
|
$body: body,
|
||||||
$conversationId: conversationId,
|
$conversationId: conversationId,
|
||||||
$expirationStartTimestamp: expirationStartTimestamp,
|
$expirationStartTimestamp: expirationStartTimestamp,
|
||||||
$expires_at: expires_at,
|
$expires_at: expires_at,
|
||||||
|
@ -1296,7 +1424,7 @@ async function saveMessage(data, { forceSave } = {}) {
|
||||||
$sent_at: sent_at,
|
$sent_at: sent_at,
|
||||||
$source: source,
|
$source: source,
|
||||||
$sourceDevice: sourceDevice,
|
$sourceDevice: sourceDevice,
|
||||||
$type: type,
|
$type: type || '',
|
||||||
$unread: unread,
|
$unread: unread,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1304,6 +1432,7 @@ async function saveMessage(data, { forceSave } = {}) {
|
||||||
await db.run(
|
await db.run(
|
||||||
`UPDATE messages SET
|
`UPDATE messages SET
|
||||||
json = $json,
|
json = $json,
|
||||||
|
body = $body,
|
||||||
conversationId = $conversationId,
|
conversationId = $conversationId,
|
||||||
expirationStartTimestamp = $expirationStartTimestamp,
|
expirationStartTimestamp = $expirationStartTimestamp,
|
||||||
expires_at = $expires_at,
|
expires_at = $expires_at,
|
||||||
|
@ -1337,6 +1466,7 @@ async function saveMessage(data, { forceSave } = {}) {
|
||||||
id,
|
id,
|
||||||
json,
|
json,
|
||||||
|
|
||||||
|
body,
|
||||||
conversationId,
|
conversationId,
|
||||||
expirationStartTimestamp,
|
expirationStartTimestamp,
|
||||||
expires_at,
|
expires_at,
|
||||||
|
@ -1356,6 +1486,7 @@ async function saveMessage(data, { forceSave } = {}) {
|
||||||
$id,
|
$id,
|
||||||
$json,
|
$json,
|
||||||
|
|
||||||
|
$body,
|
||||||
$conversationId,
|
$conversationId,
|
||||||
$expirationStartTimestamp,
|
$expirationStartTimestamp,
|
||||||
$expires_at,
|
$expires_at,
|
||||||
|
|
|
@ -17,6 +17,17 @@ function createTrayIcon(getMainWindow, messages) {
|
||||||
|
|
||||||
tray = new Tray(iconNoNewMessages);
|
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 = () => {
|
tray.toggleWindowVisibility = () => {
|
||||||
const mainWindow = getMainWindow();
|
const mainWindow = getMainWindow();
|
||||||
if (mainWindow) {
|
if (mainWindow) {
|
||||||
|
@ -25,17 +36,24 @@ function createTrayIcon(getMainWindow, messages) {
|
||||||
} else {
|
} else {
|
||||||
mainWindow.show();
|
mainWindow.show();
|
||||||
|
|
||||||
// On some versions of GNOME the window may not be on top when restored.
|
tray.forceOnTop(mainWindow);
|
||||||
// This trick should fix it.
|
|
||||||
// Thanks to: https://github.com/Enrico204/Whatsapp-Desktop/commit/6b0dc86b64e481b455f8fce9b4d797e86d000dc1
|
|
||||||
mainWindow.setAlwaysOnTop(true);
|
|
||||||
mainWindow.focus();
|
|
||||||
mainWindow.setAlwaysOnTop(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tray.updateContextMenu();
|
tray.updateContextMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
tray.showWindow = () => {
|
||||||
|
const mainWindow = getMainWindow();
|
||||||
|
if (mainWindow) {
|
||||||
|
if (!mainWindow.isVisible()) {
|
||||||
|
mainWindow.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
tray.forceOnTop(mainWindow);
|
||||||
|
}
|
||||||
|
tray.updateContextMenu();
|
||||||
|
};
|
||||||
|
|
||||||
tray.updateContextMenu = () => {
|
tray.updateContextMenu = () => {
|
||||||
const mainWindow = getMainWindow();
|
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.setToolTip(messages.trayTooltip.message);
|
||||||
tray.updateContextMenu();
|
tray.updateContextMenu();
|
||||||
|
|
|
@ -143,9 +143,9 @@
|
||||||
|
|
||||||
<div class='bottom-bar' id='footer'>
|
<div class='bottom-bar' id='footer'>
|
||||||
<div class='emoji-panel-container'></div>
|
<div class='emoji-panel-container'></div>
|
||||||
|
<div class='attachment-list'></div>
|
||||||
<div class='compose'>
|
<div class='compose'>
|
||||||
<form class='send clearfix'>
|
<form class='send clearfix file-input'>
|
||||||
<div class='attachment-previews'></div>
|
|
||||||
<div class='flex'>
|
<div class='flex'>
|
||||||
<button class='emoji' {{#disable-inputs}} disabled="disabled" {{/disable-inputs}}></button>
|
<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>
|
<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>
|
||||||
<div class='choose-file hide'>
|
<div class='choose-file hide'>
|
||||||
<button class='paperclip thumbnail' {{#disable-inputs}} disabled="disabled" {{/disable-inputs}}></button>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</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/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/scroll_down_button_view.js'></script>
|
||||||
<script type='text/javascript' src='js/views/toast_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/file_input_view.js'></script>
|
||||||
<script type='text/javascript' src='js/views/list_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>
|
<script type='text/javascript' src='js/views/conversation_list_item_view.js'></script>
|
||||||
|
|
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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'
|
textsecure.storage.user.getDeviceId() != '1'
|
||||||
) {
|
) {
|
||||||
window.getSyncRequest();
|
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';
|
// const udSupportKey = 'hasRegisterSupportForUnauthenticatedDelivery';
|
||||||
|
|
|
@ -2063,6 +2063,21 @@
|
||||||
return this.id;
|
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() {
|
isPrivate() {
|
||||||
return this.get('type') === 'private';
|
return this.get('type') === 'private';
|
||||||
},
|
},
|
||||||
|
@ -2099,7 +2114,7 @@
|
||||||
const symbol = this.isValid() ? '#' : '!';
|
const symbol = this.isValid() ? '#' : '!';
|
||||||
return {
|
return {
|
||||||
color,
|
color,
|
||||||
content: title ? title.trim()[0] : symbol,
|
content: this.getInitials(title) || symbol,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return { url: 'images/group_default.png', color };
|
return { url: 'images/group_default.png', color };
|
||||||
|
|
|
@ -1266,9 +1266,12 @@
|
||||||
conversation,
|
conversation,
|
||||||
message
|
message
|
||||||
);
|
);
|
||||||
receipts.forEach(() =>
|
receipts.forEach(receipt =>
|
||||||
message.set({
|
message.set({
|
||||||
delivered: (message.get('delivered') || 0) + 1,
|
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 browser */
|
||||||
/* eslint-env node */
|
/* 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 fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
const { map, fromPairs } = require('lodash');
|
const { map, fromPairs } = require('lodash');
|
||||||
|
const tar = require('tar');
|
||||||
const tmp = require('tmp');
|
const tmp = require('tmp');
|
||||||
const pify = require('pify');
|
const pify = require('pify');
|
||||||
const archiver = require('archiver');
|
|
||||||
const rimraf = require('rimraf');
|
const rimraf = require('rimraf');
|
||||||
const electronRemote = require('electron').remote;
|
const electronRemote = require('electron').remote;
|
||||||
|
|
||||||
const Attachment = require('./types/attachment');
|
|
||||||
const crypto = require('./crypto');
|
const crypto = require('./crypto');
|
||||||
|
|
||||||
const decompress = () => null;
|
|
||||||
const { dialog, BrowserWindow } = electronRemote;
|
const { dialog, BrowserWindow } = electronRemote;
|
||||||
|
|
||||||
module.exports = {
|
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');
|
const writer = await createFileAndWriter(parent, 'db.json');
|
||||||
return exportContactsAndGroups(db, writer);
|
return exportContactsAndGroups(writer);
|
||||||
}
|
}
|
||||||
|
|
||||||
function exportContactsAndGroups(db, fileWriter) {
|
function writeArray(stream, array) {
|
||||||
return new Promise((resolve, reject) => {
|
stream.write('[');
|
||||||
let storeNames = db.objectStoreNames;
|
|
||||||
storeNames = _.without(
|
|
||||||
storeNames,
|
|
||||||
'messages',
|
|
||||||
'items',
|
|
||||||
'signedPreKeys',
|
|
||||||
'preKeys',
|
|
||||||
'identityKeys',
|
|
||||||
'sessions',
|
|
||||||
'unprocessed'
|
|
||||||
);
|
|
||||||
|
|
||||||
const exportedStoreNames = [];
|
for (let i = 0, max = array.length; i < max; i += 1) {
|
||||||
if (storeNames.length === 0) {
|
if (i > 0) {
|
||||||
throw new Error('No stores to export');
|
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 => {
|
stream.write(JSON.stringify(stringify(cleaned)));
|
||||||
// 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');
|
|
||||||
};
|
|
||||||
|
|
||||||
const store = transaction.objectStore(storeName);
|
stream.write(']');
|
||||||
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}": [`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const cursor = event.target.result;
|
function getPlainJS(collection) {
|
||||||
if (cursor) {
|
return collection.map(model => model.attributes);
|
||||||
if (count > 0) {
|
}
|
||||||
stream.write(',');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Preventing base64'd images from reaching the disk, making db.json too big
|
async function exportContactsAndGroups(fileWriter) {
|
||||||
const item = _.omit(cursor.value, ['avatar', 'profileAvatar']);
|
const stream = createOutputStream(fileWriter);
|
||||||
|
|
||||||
const jsonString = JSON.stringify(stringify(item));
|
stream.write('{');
|
||||||
stream.write(jsonString);
|
|
||||||
cursor.continue();
|
|
||||||
count += 1;
|
|
||||||
} else {
|
|
||||||
// no more
|
|
||||||
stream.write(']');
|
|
||||||
window.log.info('Exported', count, 'items from store', storeName);
|
|
||||||
|
|
||||||
exportedStoreNames.push(storeName);
|
stream.write('"conversations": ');
|
||||||
if (exportedStoreNames.length < storeNames.length) {
|
const conversations = await window.Signal.Data.getAllConversations({
|
||||||
stream.write(',');
|
ConversationCollection: Whisper.ConversationCollection,
|
||||||
} else {
|
|
||||||
window.log.info('Exported all stores');
|
|
||||||
stream.write('}');
|
|
||||||
|
|
||||||
await stream.close();
|
|
||||||
window.log.info('Finished writing all stores to disk');
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
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) {
|
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) {
|
function readFileAsArrayBuffer(targetPath) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// omitting the encoding to get a buffer back
|
// omitting the encoding to get a buffer back
|
||||||
|
@ -422,9 +383,7 @@ function readFileAsArrayBuffer(targetPath) {
|
||||||
return reject(error);
|
return reject(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buffer instances are also Uint8Array instances
|
return resolve(toArrayBuffer(buffer));
|
||||||
// https://nodejs.org/docs/latest/api/buffer.html#buffer_buffers_and_typedarray
|
|
||||||
return resolve(buffer.buffer);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -468,7 +427,7 @@ function _getAnonymousAttachmentFileName(message, index) {
|
||||||
return `${message.id}-${index}`;
|
return `${message.id}-${index}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readAttachment(dir, attachment, name, options) {
|
async function readEncryptedAttachment(dir, attachment, name, options) {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
const { key } = options;
|
const { key } = options;
|
||||||
|
|
||||||
|
@ -485,26 +444,29 @@ async function readAttachment(dir, attachment, name, options) {
|
||||||
const isEncrypted = !_.isUndefined(key);
|
const isEncrypted = !_.isUndefined(key);
|
||||||
|
|
||||||
if (isEncrypted) {
|
if (isEncrypted) {
|
||||||
attachment.data = await crypto.decryptSymmetric(key, data);
|
attachment.data = await crypto.decryptAttachment(
|
||||||
|
key,
|
||||||
|
attachment.path,
|
||||||
|
data
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
attachment.data = data;
|
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 { dir, message, index, key, newKey } = options;
|
||||||
const filename = `${_getAnonymousAttachmentFileName(
|
const filename = `${_getAnonymousAttachmentFileName(
|
||||||
message,
|
message,
|
||||||
index
|
index
|
||||||
)}-thumbnail`;
|
)}-quote-thumbnail`;
|
||||||
const target = path.join(dir, filename);
|
const target = path.join(dir, filename);
|
||||||
const { thumbnail } = attachment;
|
|
||||||
|
|
||||||
if (!thumbnail || !thumbnail.data) {
|
await writeEncryptedAttachment(target, attachment.thumbnail.path, {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await writeEncryptedAttachment(target, thumbnail.data, {
|
|
||||||
key,
|
key,
|
||||||
newKey,
|
newKey,
|
||||||
filename,
|
filename,
|
||||||
|
@ -512,25 +474,13 @@ async function writeThumbnail(attachment, options) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function writeThumbnails(rawQuotedAttachments, options) {
|
async function writeQuoteThumbnails(quotedAttachments, options) {
|
||||||
const { name } = 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 {
|
try {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
_.map(attachments, (attachment, index) =>
|
_.map(quotedAttachments, (attachment, index) =>
|
||||||
writeThumbnail(
|
writeQuoteThumbnail(
|
||||||
attachment,
|
attachment,
|
||||||
Object.assign({}, options, {
|
Object.assign({}, options, {
|
||||||
index,
|
index,
|
||||||
|
@ -550,26 +500,57 @@ async function writeThumbnails(rawQuotedAttachments, options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function writeAttachment(attachment, 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 { dir, message, index, key, newKey } = options;
|
||||||
const filename = _getAnonymousAttachmentFileName(message, index);
|
const filename = _getAnonymousAttachmentFileName(message, index);
|
||||||
const target = path.join(dir, filename);
|
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,
|
key,
|
||||||
newKey,
|
newKey,
|
||||||
filename,
|
filename,
|
||||||
dir,
|
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 { name } = options;
|
||||||
|
|
||||||
const { loadAttachmentData } = Signal.Migrations;
|
|
||||||
const attachments = await Promise.all(rawAttachments.map(loadAttachmentData));
|
|
||||||
const promises = _.map(attachments, (attachment, index) =>
|
const promises = _.map(attachments, (attachment, index) =>
|
||||||
writeAttachment(
|
writeAttachment(
|
||||||
attachment,
|
attachment,
|
||||||
|
@ -591,17 +572,18 @@ async function writeAttachments(rawAttachments, options) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function writeAvatar(avatar, options) {
|
async function writeAvatar(contact, options) {
|
||||||
const { dir, message, index, key, newKey } = options;
|
const { avatar } = contact || {};
|
||||||
const name = _getAnonymousAttachmentFileName(message, index);
|
if (!avatar || !avatar.avatar || !avatar.avatar.path) {
|
||||||
const filename = `${name}-contact-avatar`;
|
|
||||||
|
|
||||||
const target = path.join(dir, filename);
|
|
||||||
if (!avatar || !avatar.path) {
|
|
||||||
return;
|
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,
|
key,
|
||||||
newKey,
|
newKey,
|
||||||
filename,
|
filename,
|
||||||
|
@ -612,23 +594,9 @@ async function writeAvatar(avatar, options) {
|
||||||
async function writeContactAvatars(contact, options) {
|
async function writeContactAvatars(contact, options) {
|
||||||
const { name } = 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 {
|
try {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
_.map(await Promise.all(promises), (item, index) =>
|
_.map(contact, (item, index) =>
|
||||||
writeAvatar(
|
writeAvatar(
|
||||||
item,
|
item,
|
||||||
Object.assign({}, options, {
|
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;
|
const { key, newKey, filename, dir } = options;
|
||||||
|
|
||||||
if (fs.existsSync(target)) {
|
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 writer = await createFileAndWriter(dir, filename);
|
||||||
const stream = createOutputStream(writer);
|
const stream = createOutputStream(writer);
|
||||||
|
@ -673,9 +643,9 @@ function _sanitizeFileName(filename) {
|
||||||
return filename.toString().replace(/[^a-z0-9.,+()'#\- ]/gi, '_');
|
return filename.toString().replace(/[^a-z0-9.,+()'#\- ]/gi, '_');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function exportConversation(db, conversation, options) {
|
async function exportConversation(conversation, options = {}) {
|
||||||
options = options || {};
|
|
||||||
const { name, dir, attachmentsDir, key, newKey } = options;
|
const { name, dir, attachmentsDir, key, newKey } = options;
|
||||||
|
|
||||||
if (!name) {
|
if (!name) {
|
||||||
throw new Error('Need a name!');
|
throw new Error('Need a name!');
|
||||||
}
|
}
|
||||||
|
@ -691,143 +661,111 @@ async function exportConversation(db, conversation, options) {
|
||||||
|
|
||||||
window.log.info('exporting conversation', name);
|
window.log.info('exporting conversation', name);
|
||||||
const writer = await createFileAndWriter(dir, 'messages.json');
|
const writer = await createFileAndWriter(dir, 'messages.json');
|
||||||
|
const stream = createOutputStream(writer);
|
||||||
|
stream.write('{"messages":[');
|
||||||
|
|
||||||
return new Promise(async (resolve, reject) => {
|
const CHUNK_SIZE = 50;
|
||||||
// TODO: need to iterate through message ids, export using window.Signal.Data
|
let count = 0;
|
||||||
const transaction = db.transaction('messages', 'readwrite');
|
let complete = false;
|
||||||
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 store = transaction.objectStore('messages');
|
// We're looping from the most recent to the oldest
|
||||||
const index = store.index('conversation');
|
let lastReceivedAt = Number.MAX_VALUE;
|
||||||
const range = window.IDBKeyRange.bound(
|
|
||||||
[conversation.id, 0],
|
|
||||||
[conversation.id, Number.MAX_VALUE]
|
|
||||||
);
|
|
||||||
|
|
||||||
let promiseChain = Promise.resolve();
|
while (!complete) {
|
||||||
let count = 0;
|
// eslint-disable-next-line no-await-in-loop
|
||||||
const request = index.openCursor(range);
|
const collection = await window.Signal.Data.getMessagesByConversation(
|
||||||
|
conversation.id,
|
||||||
const stream = createOutputStream(writer);
|
{
|
||||||
stream.write('{"messages":[');
|
limit: CHUNK_SIZE,
|
||||||
|
receivedAt: lastReceivedAt,
|
||||||
request.onerror = () => {
|
MessageCollection: Whisper.MessageCollection,
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
};
|
);
|
||||||
});
|
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:
|
// Goals for directory names:
|
||||||
|
@ -857,74 +795,40 @@ function _getConversationLoggingName(conversation) {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
function exportConversations(db, options) {
|
async function exportConversations(options) {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
const { messagesDir, attachmentsDir, key, newKey } = options;
|
const { messagesDir, attachmentsDir, key, newKey } = options;
|
||||||
|
|
||||||
if (!messagesDir) {
|
if (!messagesDir) {
|
||||||
return Promise.reject(new Error('Need a messages directory!'));
|
throw new Error('Need a messages directory!');
|
||||||
}
|
}
|
||||||
if (!attachmentsDir) {
|
if (!attachmentsDir) {
|
||||||
return Promise.reject(new Error('Need an attachments directory!'));
|
throw new Error('Need an attachments directory!');
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
const collection = await window.Signal.Data.getAllConversations({
|
||||||
const transaction = db.transaction('conversations', 'readwrite');
|
ConversationCollection: Whisper.ConversationCollection,
|
||||||
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 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 = {}) {
|
function getDirectory(options = {}) {
|
||||||
|
@ -968,9 +872,30 @@ async function loadAttachments(dir, getName, options) {
|
||||||
const { message } = options;
|
const { message } = options;
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
_.map(message.attachments, (attachment, index) => {
|
_.map(message.attachments, async (attachment, index) => {
|
||||||
const name = getName(message, index, attachment);
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = `${getName(message, index)}-thumbnail`;
|
const name = `${getName(message, index)}-quote-thumbnail`;
|
||||||
return readAttachment(dir, thumbnail, name, options);
|
return readEncryptedAttachment(dir, thumbnail, name, options);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -996,7 +921,7 @@ async function loadAttachments(dir, getName, options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = `${getName(message, index)}-contact-avatar`;
|
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();
|
return getDirectory();
|
||||||
}
|
}
|
||||||
|
|
||||||
function createZip(zipDir, targetDir) {
|
async function compressArchive(file, targetDir) {
|
||||||
return new Promise((resolve, reject) => {
|
const items = fs.readdirSync(targetDir);
|
||||||
const target = path.join(zipDir, 'messages.zip');
|
return tar.c(
|
||||||
const output = fs.createWriteStream(target);
|
{
|
||||||
const archive = archiver('zip', {
|
gzip: true,
|
||||||
|
file,
|
||||||
cwd: targetDir,
|
cwd: targetDir,
|
||||||
});
|
},
|
||||||
|
items
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
output.on('close', () => {
|
async function decompressArchive(file, targetDir) {
|
||||||
resolve(target);
|
return tar.x({
|
||||||
});
|
file,
|
||||||
|
cwd: targetDir,
|
||||||
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();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1211,6 +1127,13 @@ function writeFile(targetPath, contents) {
|
||||||
return pify(fs.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) {
|
async function encryptFile(sourcePath, targetPath, options) {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
|
|
||||||
|
@ -1220,8 +1143,8 @@ async function encryptFile(sourcePath, targetPath, options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const plaintext = await readFileAsArrayBuffer(sourcePath);
|
const plaintext = await readFileAsArrayBuffer(sourcePath);
|
||||||
const ciphertext = await crypto.encryptSymmetric(key, plaintext);
|
const ciphertext = await crypto.encryptFile(key, UNIQUE_ID, plaintext);
|
||||||
return writeFile(targetPath, ciphertext);
|
return writeFile(targetPath, Buffer.from(ciphertext));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function decryptFile(sourcePath, targetPath, options) {
|
async function decryptFile(sourcePath, targetPath, options) {
|
||||||
|
@ -1233,7 +1156,7 @@ async function decryptFile(sourcePath, targetPath, options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ciphertext = await readFileAsArrayBuffer(sourcePath);
|
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));
|
return writeFile(targetPath, Buffer.from(plaintext));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1246,9 +1169,9 @@ function deleteAll(pattern) {
|
||||||
return pify(rimraf)(pattern);
|
return pify(rimraf)(pattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function exportToDirectory(directory, options) {
|
const ARCHIVE_NAME = 'messages.tar.gz';
|
||||||
throw new Error('Encrypted export/import is disabled');
|
|
||||||
|
|
||||||
|
async function exportToDirectory(directory, options) {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
|
|
||||||
if (!options.key) {
|
if (!options.key) {
|
||||||
|
@ -1261,20 +1184,19 @@ async function exportToDirectory(directory, options) {
|
||||||
stagingDir = await createTempDir();
|
stagingDir = await createTempDir();
|
||||||
encryptionDir = await createTempDir();
|
encryptionDir = await createTempDir();
|
||||||
|
|
||||||
const db = await Whisper.Database.open();
|
|
||||||
const attachmentsDir = await createDirectory(directory, 'attachments');
|
const attachmentsDir = await createDirectory(directory, 'attachments');
|
||||||
|
|
||||||
await exportContactAndGroupsToFile(db, stagingDir);
|
await exportContactAndGroupsToFile(stagingDir);
|
||||||
await exportConversations(
|
await exportConversations(
|
||||||
db,
|
|
||||||
Object.assign({}, options, {
|
Object.assign({}, options, {
|
||||||
messagesDir: stagingDir,
|
messagesDir: stagingDir,
|
||||||
attachmentsDir,
|
attachmentsDir,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const zip = await createZip(encryptionDir, stagingDir);
|
const archivePath = path.join(directory, ARCHIVE_NAME);
|
||||||
await encryptFile(zip, path.join(directory, 'messages.zip'), options);
|
await compressArchive(archivePath, stagingDir);
|
||||||
|
await encryptFile(archivePath, path.join(directory, ARCHIVE_NAME), options);
|
||||||
|
|
||||||
window.log.info('done backing up!');
|
window.log.info('done backing up!');
|
||||||
return directory;
|
return directory;
|
||||||
|
@ -1317,10 +1239,8 @@ async function importFromDirectory(directory, options) {
|
||||||
groupLookup,
|
groupLookup,
|
||||||
});
|
});
|
||||||
|
|
||||||
const zipPath = path.join(directory, 'messages.zip');
|
const archivePath = path.join(directory, ARCHIVE_NAME);
|
||||||
if (fs.existsSync(zipPath)) {
|
if (fs.existsSync(archivePath)) {
|
||||||
throw new Error('Encrypted export/import is disabled');
|
|
||||||
|
|
||||||
// we're in the world of an encrypted, zipped backup
|
// we're in the world of an encrypted, zipped backup
|
||||||
if (!options.key) {
|
if (!options.key) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
@ -1336,9 +1256,9 @@ async function importFromDirectory(directory, options) {
|
||||||
|
|
||||||
const attachmentsDir = path.join(directory, 'attachments');
|
const attachmentsDir = path.join(directory, 'attachments');
|
||||||
|
|
||||||
const decryptedZip = path.join(decryptionDir, 'messages.zip');
|
const decryptedArchivePath = path.join(decryptionDir, ARCHIVE_NAME);
|
||||||
await decryptFile(zipPath, decryptedZip, options);
|
await decryptFile(archivePath, decryptedArchivePath, options);
|
||||||
await decompress(decryptedZip, stagingDir);
|
await decompressArchive(decryptedArchivePath, stagingDir);
|
||||||
|
|
||||||
options = Object.assign({}, options, {
|
options = Object.assign({}, options, {
|
||||||
attachmentsDir,
|
attachmentsDir,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/* eslint-env browser */
|
/* eslint-env browser */
|
||||||
/* global dcodeIO */
|
/* global dcodeIO, libsignal */
|
||||||
|
|
||||||
/* eslint-disable camelcase, no-bitwise */
|
/* eslint-disable camelcase, no-bitwise */
|
||||||
|
|
||||||
|
@ -10,9 +10,15 @@ module.exports = {
|
||||||
concatenateBytes,
|
concatenateBytes,
|
||||||
constantTimeEqual,
|
constantTimeEqual,
|
||||||
decryptAesCtr,
|
decryptAesCtr,
|
||||||
|
decryptDeviceName,
|
||||||
|
decryptAttachment,
|
||||||
|
decryptFile,
|
||||||
decryptSymmetric,
|
decryptSymmetric,
|
||||||
deriveAccessKey,
|
deriveAccessKey,
|
||||||
encryptAesCtr,
|
encryptAesCtr,
|
||||||
|
encryptDeviceName,
|
||||||
|
encryptAttachment,
|
||||||
|
encryptFile,
|
||||||
encryptSymmetric,
|
encryptSymmetric,
|
||||||
fromEncodedBinaryToArrayBuffer,
|
fromEncodedBinaryToArrayBuffer,
|
||||||
getAccessKeyVerifier,
|
getAccessKeyVerifier,
|
||||||
|
@ -28,8 +34,117 @@ module.exports = {
|
||||||
verifyAccessKey,
|
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
|
// 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) {
|
async function deriveAccessKey(profileKey) {
|
||||||
const iv = getZeroes(12);
|
const iv = getZeroes(12);
|
||||||
const plaintext = getZeroes(16);
|
const plaintext = getZeroes(16);
|
||||||
|
@ -267,24 +382,6 @@ function trimBytes(buffer, length) {
|
||||||
return _getFirstBytes(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) {
|
function getViewOfArrayBuffer(buffer, start, finish) {
|
||||||
const source = new Uint8Array(buffer);
|
const source = new Uint8Array(buffer);
|
||||||
const result = source.slice(start, finish);
|
const result = source.slice(start, finish);
|
||||||
|
|
|
@ -52,6 +52,7 @@ module.exports = {
|
||||||
createOrUpdateGroup,
|
createOrUpdateGroup,
|
||||||
getGroupById,
|
getGroupById,
|
||||||
getAllGroupIds,
|
getAllGroupIds,
|
||||||
|
getAllGroups,
|
||||||
bulkAddGroups,
|
bulkAddGroups,
|
||||||
removeGroupById,
|
removeGroupById,
|
||||||
removeAllGroups,
|
removeAllGroups,
|
||||||
|
@ -428,6 +429,10 @@ async function getAllGroupIds() {
|
||||||
const ids = await channels.getAllGroupIds();
|
const ids = await channels.getAllGroupIds();
|
||||||
return ids;
|
return ids;
|
||||||
}
|
}
|
||||||
|
async function getAllGroups() {
|
||||||
|
const groups = await channels.getAllGroups();
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
async function bulkAddGroups(array) {
|
async function bulkAddGroups(array) {
|
||||||
await channels.bulkAddGroups(array);
|
await channels.bulkAddGroups(array);
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,10 @@ const Metadata = require('./metadata/SecretSessionCipher');
|
||||||
const RefreshSenderCertificate = require('./refresh_sender_certificate');
|
const RefreshSenderCertificate = require('./refresh_sender_certificate');
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
|
const {
|
||||||
|
AttachmentList,
|
||||||
|
} = require('../../ts/components/conversation/AttachmentList');
|
||||||
|
const { CaptionEditor } = require('../../ts/components/CaptionEditor');
|
||||||
const {
|
const {
|
||||||
ContactDetail,
|
ContactDetail,
|
||||||
} = require('../../ts/components/conversation/ContactDetail');
|
} = require('../../ts/components/conversation/ContactDetail');
|
||||||
|
@ -136,6 +140,7 @@ function initializeMigrations({
|
||||||
loadAttachmentData,
|
loadAttachmentData,
|
||||||
loadQuoteData,
|
loadQuoteData,
|
||||||
loadMessage: MessageType.createAttachmentLoader(loadAttachmentData),
|
loadMessage: MessageType.createAttachmentLoader(loadAttachmentData),
|
||||||
|
readAttachmentData,
|
||||||
run,
|
run,
|
||||||
upgradeMessageSchema: (message, options = {}) => {
|
upgradeMessageSchema: (message, options = {}) => {
|
||||||
const { maxVersion } = options;
|
const { maxVersion } = options;
|
||||||
|
@ -174,6 +179,8 @@ exports.setup = (options = {}) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const Components = {
|
const Components = {
|
||||||
|
AttachmentList,
|
||||||
|
CaptionEditor,
|
||||||
ContactDetail,
|
ContactDetail,
|
||||||
ContactListItem,
|
ContactListItem,
|
||||||
ContactName,
|
ContactName,
|
||||||
|
|
|
@ -545,8 +545,6 @@ exports.createAttachmentDataWriter = ({
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: need to handle attachment thumbnails and video screenshots
|
|
||||||
|
|
||||||
const messageWithoutAttachmentData = Object.assign(
|
const messageWithoutAttachmentData = Object.assign(
|
||||||
{},
|
{},
|
||||||
await writeThumbnails(message, { logger }),
|
await writeThumbnails(message, { logger }),
|
||||||
|
@ -555,7 +553,23 @@ exports.createAttachmentDataWriter = ({
|
||||||
attachments: await Promise.all(
|
attachments: await Promise.all(
|
||||||
(attachments || []).map(async attachment => {
|
(attachments || []).map(async attachment => {
|
||||||
await writeExistingAttachmentData(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 = {
|
const URL_CALLS = {
|
||||||
accounts: 'v1/accounts',
|
accounts: 'v1/accounts',
|
||||||
|
updateDeviceName: 'v1/accounts/name',
|
||||||
|
removeSignalingKey: 'v1/accounts/signaling_key',
|
||||||
attachment: 'v1/attachments',
|
attachment: 'v1/attachments',
|
||||||
deliveryCert: 'v1/certificate/delivery',
|
deliveryCert: 'v1/certificate/delivery',
|
||||||
supportUnauthenticatedDelivery: 'v1/devices/unauthenticated_delivery',
|
supportUnauthenticatedDelivery: 'v1/devices/unauthenticated_delivery',
|
||||||
|
@ -393,6 +395,8 @@ function initialize({ url, cdnUrl, certificateAuthority, proxyUrl }) {
|
||||||
sendMessages,
|
sendMessages,
|
||||||
sendMessagesUnauth,
|
sendMessagesUnauth,
|
||||||
setSignedPreKey,
|
setSignedPreKey,
|
||||||
|
updateDeviceName,
|
||||||
|
removeSignalingKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
function _ajax(param) {
|
function _ajax(param) {
|
||||||
|
@ -523,14 +527,12 @@ function initialize({ url, cdnUrl, certificateAuthority, proxyUrl }) {
|
||||||
number,
|
number,
|
||||||
code,
|
code,
|
||||||
newPassword,
|
newPassword,
|
||||||
signalingKey,
|
|
||||||
registrationId,
|
registrationId,
|
||||||
deviceName,
|
deviceName,
|
||||||
options = {}
|
options = {}
|
||||||
) {
|
) {
|
||||||
const { accessKey } = options;
|
const { accessKey } = options;
|
||||||
const jsonData = {
|
const jsonData = {
|
||||||
signalingKey: _btoa(_getString(signalingKey)),
|
|
||||||
supportsSms: false,
|
supportsSms: false,
|
||||||
fetchesMessages: true,
|
fetchesMessages: true,
|
||||||
registrationId,
|
registrationId,
|
||||||
|
@ -575,6 +577,23 @@ function initialize({ url, cdnUrl, certificateAuthority, proxyUrl }) {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateDeviceName(deviceName) {
|
||||||
|
return _ajax({
|
||||||
|
call: 'updateDeviceName',
|
||||||
|
httpType: 'PUT',
|
||||||
|
jsonData: {
|
||||||
|
deviceName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeSignalingKey() {
|
||||||
|
return _ajax({
|
||||||
|
call: 'removeSignalingKey',
|
||||||
|
httpType: 'DELETE',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getDevices() {
|
function getDevices() {
|
||||||
return _ajax({
|
return _ajax({
|
||||||
call: 'devices',
|
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') };
|
return { toastMessage: i18n('messageFoundButNotLoaded') };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
Whisper.VoiceNoteMustBeOnlyAttachmentToast = Whisper.ToastView.extend({
|
||||||
|
render_attributes() {
|
||||||
|
return { toastMessage: i18n('voiceNoteMustBeOnlyAttachment') };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
Whisper.ConversationLoadingScreen = Whisper.View.extend({
|
Whisper.ConversationLoadingScreen = Whisper.View.extend({
|
||||||
templateName: 'conversation-loading-screen',
|
templateName: 'conversation-loading-screen',
|
||||||
|
@ -150,8 +155,16 @@
|
||||||
|
|
||||||
this.window = options.window;
|
this.window = options.window;
|
||||||
this.fileInput = new Whisper.FileInputView({
|
this.fileInput = new Whisper.FileInputView({
|
||||||
el: this.$('form.send'),
|
el: this.$('.attachment-list'),
|
||||||
window: this.window,
|
});
|
||||||
|
this.listenTo(
|
||||||
|
this.fileInput,
|
||||||
|
'choose-attachment',
|
||||||
|
this.onChooseAttachment
|
||||||
|
);
|
||||||
|
this.listenTo(this.fileInput, 'staged-attachments-changed', () => {
|
||||||
|
this.view.resetScrollPosition();
|
||||||
|
this.toggleMicrophone();
|
||||||
});
|
});
|
||||||
|
|
||||||
const getHeaderProps = () => {
|
const getHeaderProps = () => {
|
||||||
|
@ -185,7 +198,7 @@
|
||||||
onDeleteMessages: () => this.destroyMessages(),
|
onDeleteMessages: () => this.destroyMessages(),
|
||||||
onResetSession: () => this.endSession(),
|
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.
|
// need a manual update call.
|
||||||
onShowSafetyNumber: () => {
|
onShowSafetyNumber: () => {
|
||||||
this.showSafetyNumber();
|
this.showSafetyNumber();
|
||||||
|
@ -290,15 +303,49 @@
|
||||||
'farFromBottom .message-list': 'addScrollDownButton',
|
'farFromBottom .message-list': 'addScrollDownButton',
|
||||||
'lazyScroll .message-list': 'onLazyScroll',
|
'lazyScroll .message-list': 'onLazyScroll',
|
||||||
'force-resize': 'forceUpdateMessageFieldSize',
|
'force-resize': 'forceUpdateMessageFieldSize',
|
||||||
dragover: 'sendToFileInput',
|
|
||||||
drop: 'sendToFileInput',
|
'click button.paperclip': 'onChooseAttachment',
|
||||||
dragleave: 'sendToFileInput',
|
'change input.file-input': 'onChoseAttachment',
|
||||||
|
|
||||||
|
dragover: 'onDragOver',
|
||||||
|
dragleave: 'onDragLeave',
|
||||||
|
drop: 'onDrop',
|
||||||
|
paste: 'onPaste',
|
||||||
},
|
},
|
||||||
sendToFileInput(e) {
|
|
||||||
if (e.originalEvent.dataTransfer.types[0] !== 'Files') {
|
onChooseAttachment(e) {
|
||||||
return;
|
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() {
|
onPrune() {
|
||||||
|
@ -546,6 +593,13 @@
|
||||||
captureAudio(e) {
|
captureAudio(e) {
|
||||||
e.preventDefault();
|
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
|
// Note - clicking anywhere will close the audio capture panel, due to
|
||||||
// the onClick handler in InboxView, which calls its closeRecording method.
|
// the onClick handler in InboxView, which calls its closeRecording method.
|
||||||
|
|
||||||
|
@ -566,9 +620,11 @@
|
||||||
this.$('.microphone').hide();
|
this.$('.microphone').hide();
|
||||||
},
|
},
|
||||||
handleAudioCapture(blob) {
|
handleAudioCapture(blob) {
|
||||||
this.fileInput.file = blob;
|
this.fileInput.addAttachment({
|
||||||
this.fileInput.isVoiceNote = true;
|
contentType: blob.type,
|
||||||
this.fileInput.previewImages();
|
file: blob,
|
||||||
|
isVoiceNote: true,
|
||||||
|
});
|
||||||
this.$('.bottom-bar form').submit();
|
this.$('.bottom-bar form').submit();
|
||||||
},
|
},
|
||||||
endCaptureAudio() {
|
endCaptureAudio() {
|
||||||
|
@ -1229,6 +1285,7 @@
|
||||||
const props = {
|
const props = {
|
||||||
objectURL: getAbsoluteAttachmentPath(path),
|
objectURL: getAbsoluteAttachmentPath(path),
|
||||||
contentType,
|
contentType,
|
||||||
|
caption: attachment.caption,
|
||||||
onSave: () => this.downloadAttachment({ attachment, message }),
|
onSave: () => this.downloadAttachment({ attachment, message }),
|
||||||
};
|
};
|
||||||
this.lightboxView = new Whisper.ReactWrapperView({
|
this.lightboxView = new Whisper.ReactWrapperView({
|
||||||
|
@ -1496,7 +1553,6 @@
|
||||||
if (event.key !== 'Escape') {
|
if (event.key !== 'Escape') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.closeEmojiPanel();
|
this.closeEmojiPanel();
|
||||||
},
|
},
|
||||||
openEmojiPanel() {
|
openEmojiPanel() {
|
||||||
|
@ -1504,6 +1560,7 @@
|
||||||
this.emojiPanel = new EmojiPanel(this.$emojiPanelContainer[0], {
|
this.emojiPanel = new EmojiPanel(this.$emojiPanelContainer[0], {
|
||||||
onClick: this.insertEmoji.bind(this),
|
onClick: this.insertEmoji.bind(this),
|
||||||
});
|
});
|
||||||
|
this.view.resetScrollPosition();
|
||||||
this.updateMessageFieldSize({});
|
this.updateMessageFieldSize({});
|
||||||
},
|
},
|
||||||
closeEmojiPanel() {
|
closeEmojiPanel() {
|
||||||
|
@ -1513,6 +1570,7 @@
|
||||||
|
|
||||||
this.$emojiPanelContainer.empty().outerHeight(0);
|
this.$emojiPanelContainer.empty().outerHeight(0);
|
||||||
this.emojiPanel = null;
|
this.emojiPanel = null;
|
||||||
|
this.view.resetScrollPosition();
|
||||||
this.updateMessageFieldSize({});
|
this.updateMessageFieldSize({});
|
||||||
},
|
},
|
||||||
insertEmoji(e) {
|
insertEmoji(e) {
|
||||||
|
@ -1560,6 +1618,7 @@
|
||||||
this.quoteView = null;
|
this.quoteView = null;
|
||||||
}
|
}
|
||||||
if (!this.quotedMessage) {
|
if (!this.quotedMessage) {
|
||||||
|
this.view.restoreBottomOffset();
|
||||||
this.updateMessageFieldSize({});
|
this.updateMessageFieldSize({});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1583,16 +1642,18 @@
|
||||||
this.quoteView = new Whisper.ReactWrapperView({
|
this.quoteView = new Whisper.ReactWrapperView({
|
||||||
className: 'quote-wrapper',
|
className: 'quote-wrapper',
|
||||||
Component: window.Signal.Components.Quote,
|
Component: window.Signal.Components.Quote,
|
||||||
|
elCallback: el => this.$('.send').prepend(el),
|
||||||
props: Object.assign({}, props, {
|
props: Object.assign({}, props, {
|
||||||
withContentAbove: true,
|
withContentAbove: true,
|
||||||
onClose: () => {
|
onClose: () => {
|
||||||
this.setQuoteMessage(null);
|
this.setQuoteMessage(null);
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
onInitialRender: () => {
|
||||||
|
this.view.restoreBottomOffset();
|
||||||
|
this.updateMessageFieldSize({});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
this.$('.send').prepend(this.quoteView.el);
|
|
||||||
this.updateMessageFieldSize({});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async sendMessage(e) {
|
async sendMessage(e) {
|
||||||
|
@ -1647,7 +1708,7 @@
|
||||||
this.setQuoteMessage(null);
|
this.setQuoteMessage(null);
|
||||||
this.focusMessageFieldAndClearDisabled();
|
this.focusMessageFieldAndClearDisabled();
|
||||||
this.forceUpdateMessageFieldSize(e);
|
this.forceUpdateMessageFieldSize(e);
|
||||||
this.fileInput.deleteFiles();
|
this.fileInput.clearAttachments();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.log.error(
|
window.log.error(
|
||||||
'Error pulling attached files before send',
|
'Error pulling attached files before send',
|
||||||
|
|
|
@ -30,91 +30,412 @@
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
Whisper.UnsupportedFileTypeToast = Whisper.ToastView.extend({
|
|
||||||
template: i18n('unsupportedFileType'),
|
|
||||||
});
|
|
||||||
|
|
||||||
Whisper.DangerousFileTypeToast = Whisper.ToastView.extend({
|
Whisper.DangerousFileTypeToast = Whisper.ToastView.extend({
|
||||||
template: i18n('dangerousFileType'),
|
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({
|
Whisper.FileInputView = Backbone.View.extend({
|
||||||
tagName: 'span',
|
tagName: 'span',
|
||||||
className: 'file-input',
|
className: 'file-input',
|
||||||
initialize(options) {
|
initialize() {
|
||||||
this.$input = this.$('input[type=file]');
|
this.attachments = [];
|
||||||
this.$input.click(e => {
|
|
||||||
e.stopPropagation();
|
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: {
|
remove() {
|
||||||
'change .choose-file': 'previewImages',
|
if (this.attachmentListView) {
|
||||||
'click .close': 'deleteFiles',
|
this.attachmentListView.remove();
|
||||||
'click .choose-file': 'open',
|
}
|
||||||
drop: 'openDropped',
|
if (this.captionEditorView) {
|
||||||
dragover: 'showArea',
|
this.captionEditorView.remove();
|
||||||
dragleave: 'hideArea',
|
}
|
||||||
paste: 'onPaste',
|
|
||||||
|
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();
|
e.preventDefault();
|
||||||
// hack
|
this.$el.addClass('dropoff');
|
||||||
if (this.window && this.window.chrome && this.window.chrome.fileSystem) {
|
},
|
||||||
this.window.chrome.fileSystem.chooseEntry(
|
|
||||||
{ type: 'openFile' },
|
onDragLeave(e) {
|
||||||
entry => {
|
if (e.originalEvent.dataTransfer.types[0] !== 'Files') {
|
||||||
if (!entry) {
|
return;
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
entry.file(file => {
|
e.stopPropagation();
|
||||||
this.file = file;
|
e.preventDefault();
|
||||||
this.previewImages();
|
this.$el.removeClass('dropoff');
|
||||||
});
|
},
|
||||||
}
|
|
||||||
);
|
async onDrop(e) {
|
||||||
} else {
|
if (e.originalEvent.dataTransfer.types[0] !== 'Files') {
|
||||||
this.$input.click();
|
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 = {}) {
|
// Public interface
|
||||||
_.defaults(options, { addPlayIcon: false });
|
|
||||||
this.$('.avatar').hide();
|
|
||||||
this.thumb.src = src;
|
|
||||||
this.$('.attachment-previews').append(this.thumb.render().el);
|
|
||||||
|
|
||||||
if (options.addPlayIcon) {
|
hasFiles() {
|
||||||
this.$el.addClass('video-attachment');
|
return this.attachments.length > 0;
|
||||||
} else {
|
|
||||||
this.$el.removeClass('video-attachment');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.thumb.$('img')[0].onload = () => {
|
|
||||||
this.$el.trigger('force-resize');
|
|
||||||
};
|
|
||||||
this.thumb.$('img')[0].onerror = () => {
|
|
||||||
this.unableToLoadAttachment();
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
|
|
||||||
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();
|
const toast = new Whisper.UnableToLoadToast();
|
||||||
toast.$el.insertAfter(this.$el);
|
toast.$el.insertAfter(this.$el);
|
||||||
toast.render();
|
toast.render();
|
||||||
|
|
||||||
this.deleteFiles();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
autoScale(file) {
|
showDangerousError() {
|
||||||
if (file.type.split('/')[0] !== 'image' || file.type === 'image/tiff') {
|
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
|
// nothing to do
|
||||||
return Promise.resolve(file);
|
return Promise.resolve(attachment);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
@ -132,13 +453,13 @@
|
||||||
img.naturalHeight <= maxHeight &&
|
img.naturalHeight <= maxHeight &&
|
||||||
file.size <= maxSize
|
file.size <= maxSize
|
||||||
) {
|
) {
|
||||||
resolve(file);
|
resolve(attachment);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const gifMaxSize = 25000 * 1024;
|
const gifMaxSize = 25000 * 1024;
|
||||||
if (file.type === 'image/gif' && file.size <= gifMaxSize) {
|
if (file.type === 'image/gif' && file.size <= gifMaxSize) {
|
||||||
resolve(file);
|
resolve(attachment);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -170,285 +491,47 @@
|
||||||
}
|
}
|
||||||
} while (i > 0 && blob.size > maxSize);
|
} while (i > 0 && blob.size > maxSize);
|
||||||
|
|
||||||
resolve(blob);
|
resolve({
|
||||||
|
...attachment,
|
||||||
|
file: blob,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
img.src = url;
|
img.src = url;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async previewImages() {
|
async getFile(attachment) {
|
||||||
this.clearForm();
|
if (!attachment) {
|
||||||
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) {
|
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
const attachmentFlags = this.isVoiceNote
|
const attachmentFlags = attachment.isVoiceNote
|
||||||
? textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE
|
? textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const setFlags = flags => attachment => {
|
const scaled = await this.autoScale(attachment);
|
||||||
const newAttachment = Object.assign({}, attachment);
|
const fileRead = await this.readFile(scaled);
|
||||||
if (flags) {
|
return {
|
||||||
newAttachment.flags = flags;
|
...fileRead,
|
||||||
}
|
url: undefined,
|
||||||
return newAttachment;
|
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() {
|
readFile(attachment) {
|
||||||
// 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) {
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const FR = new FileReader();
|
const FR = new FileReader();
|
||||||
FR.onload = e => {
|
FR.onload = e => {
|
||||||
resolve({
|
resolve({
|
||||||
|
...attachment,
|
||||||
data: e.target.result,
|
data: e.target.result,
|
||||||
contentType: file.type,
|
|
||||||
fileName: file.name,
|
|
||||||
size: file.size,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
FR.onerror = reject;
|
FR.onerror = reject;
|
||||||
FR.onabort = 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',
|
blue: '#336ba3',
|
||||||
teal: '#067589',
|
teal: '#067589',
|
||||||
green: '#3b7845',
|
green: '#3b7845',
|
||||||
light_green: '#895d66',
|
light_green: '#1c8260',
|
||||||
blue_grey: '#607d8b',
|
blue_grey: '#895d66',
|
||||||
grey: '#6b6b78',
|
grey: '#6b6b78',
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -70,6 +70,15 @@
|
||||||
resetScrollPosition() {
|
resetScrollPosition() {
|
||||||
this.$el.scrollTop(this.scrollPosition - this.$el.outerHeight());
|
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() {
|
scrollToBottomIfNeeded() {
|
||||||
// This is counter-intuitive. Our current bottomOffset is reflective of what
|
// This is counter-intuitive. Our current bottomOffset is reflective of what
|
||||||
// we last measured, not necessarily the current state. And this is called
|
// we last measured, not necessarily the current state. And this is called
|
||||||
|
|
|
@ -12,20 +12,43 @@
|
||||||
window.Whisper.ReactWrapperView = Backbone.View.extend({
|
window.Whisper.ReactWrapperView = Backbone.View.extend({
|
||||||
className: 'react-wrapper',
|
className: 'react-wrapper',
|
||||||
initialize(options) {
|
initialize(options) {
|
||||||
const { Component, props, onClose } = options;
|
const {
|
||||||
|
Component,
|
||||||
|
props,
|
||||||
|
onClose,
|
||||||
|
tagName,
|
||||||
|
className,
|
||||||
|
onInitialRender,
|
||||||
|
elCallback,
|
||||||
|
} = options;
|
||||||
this.render();
|
this.render();
|
||||||
|
if (elCallback) {
|
||||||
|
elCallback(this.el);
|
||||||
|
}
|
||||||
|
|
||||||
this.tagName = options.tagName;
|
this.tagName = tagName;
|
||||||
this.className = options.className;
|
this.className = className;
|
||||||
this.Component = Component;
|
this.Component = Component;
|
||||||
this.onClose = onClose;
|
this.onClose = onClose;
|
||||||
|
this.onInitialRender = onInitialRender;
|
||||||
|
|
||||||
this.update(props);
|
this.update(props);
|
||||||
|
|
||||||
|
this.hasRendered = false;
|
||||||
},
|
},
|
||||||
update(props) {
|
update(props) {
|
||||||
const updatedProps = this.augmentProps(props);
|
const updatedProps = this.augmentProps(props);
|
||||||
const reactElement = React.createElement(this.Component, updatedProps);
|
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) {
|
augmentProps(props) {
|
||||||
return Object.assign({}, props, {
|
return Object.assign({}, props, {
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
libsignal,
|
libsignal,
|
||||||
mnemonic,
|
mnemonic,
|
||||||
btoa,
|
btoa,
|
||||||
|
Signal,
|
||||||
getString,
|
getString,
|
||||||
Event,
|
Event,
|
||||||
dcodeIO,
|
dcodeIO,
|
||||||
|
@ -51,6 +52,65 @@
|
||||||
requestSMSVerification(number) {
|
requestSMSVerification(number) {
|
||||||
// return this.server.requestVerificationSMS(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) {
|
registerSingleDevice(mnemonic, mnemonicLanguage, profileName) {
|
||||||
const createAccount = this.createAccount.bind(this);
|
const createAccount = this.createAccount.bind(this);
|
||||||
const clearSessionsAndPreKeys = this.clearSessionsAndPreKeys.bind(this);
|
const clearSessionsAndPreKeys = this.clearSessionsAndPreKeys.bind(this);
|
||||||
|
@ -362,11 +422,11 @@
|
||||||
await textsecure.storage.user.setNumberAndDeviceId(pubKeyString, 1);
|
await textsecure.storage.user.setNumberAndDeviceId(pubKeyString, 1);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
clearSessionsAndPreKeys() {
|
async clearSessionsAndPreKeys() {
|
||||||
const store = textsecure.storage.protocol;
|
const store = textsecure.storage.protocol;
|
||||||
|
|
||||||
window.log.info('clearing all sessions, prekeys, and signed prekeys');
|
window.log.info('clearing all sessions, prekeys, and signed prekeys');
|
||||||
return Promise.all([
|
await Promise.all([
|
||||||
store.clearPreKeyStore(),
|
store.clearPreKeyStore(),
|
||||||
store.clearContactPreKeysStore(),
|
store.clearContactPreKeysStore(),
|
||||||
store.clearSignedPreKeysStore(),
|
store.clearSignedPreKeysStore(),
|
||||||
|
|
|
@ -180,7 +180,6 @@ MessageReceiver.prototype.extend({
|
||||||
// We do the message decryption here, instead of in the ordered pending queue,
|
// 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.
|
// 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') {
|
if (request.path !== '/api/v1/message') {
|
||||||
window.log.info('got request', request.verb, request.path);
|
window.log.info('got request', request.verb, request.path);
|
||||||
request.respond(200, 'OK');
|
request.respond(200, 'OK');
|
||||||
|
@ -192,7 +191,6 @@ MessageReceiver.prototype.extend({
|
||||||
}
|
}
|
||||||
|
|
||||||
const promise = Promise.resolve(request.body.toArrayBuffer()) // textsecure.crypto
|
const promise = Promise.resolve(request.body.toArrayBuffer()) // textsecure.crypto
|
||||||
// .decryptWebsocketMessage(request.body, this.signalingKey)
|
|
||||||
.then(plaintext => {
|
.then(plaintext => {
|
||||||
const envelope = textsecure.protobuf.Envelope.decode(plaintext);
|
const envelope = textsecure.protobuf.Envelope.decode(plaintext);
|
||||||
// After this point, decoding errors are not the server's
|
// After this point, decoding errors are not the server's
|
||||||
|
|
|
@ -43,7 +43,7 @@ function OutgoingMessage(
|
||||||
this.failoverNumbers = [];
|
this.failoverNumbers = [];
|
||||||
this.unidentifiedDeliveries = [];
|
this.unidentifiedDeliveries = [];
|
||||||
|
|
||||||
const { numberInfo, senderCertificate, online, messageType } = options;
|
const { numberInfo, senderCertificate, online, messageType } = options || {};
|
||||||
this.numberInfo = numberInfo;
|
this.numberInfo = numberInfo;
|
||||||
this.senderCertificate = senderCertificate;
|
this.senderCertificate = senderCertificate;
|
||||||
this.online = online;
|
this.online = online;
|
||||||
|
|
|
@ -36,6 +36,9 @@
|
||||||
loadProtoBufs('SubProtocol.proto');
|
loadProtoBufs('SubProtocol.proto');
|
||||||
loadProtoBufs('DeviceMessages.proto');
|
loadProtoBufs('DeviceMessages.proto');
|
||||||
|
|
||||||
|
// Just for encrypting device names
|
||||||
|
loadProtoBufs('DeviceName.proto');
|
||||||
|
|
||||||
// Metadata-specific protos
|
// Metadata-specific protos
|
||||||
loadProtoBufs('UnidentifiedDelivery.proto');
|
loadProtoBufs('UnidentifiedDelivery.proto');
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -183,15 +183,26 @@ MessageSender.prototype = {
|
||||||
proto.id = id;
|
proto.id = id;
|
||||||
proto.contentType = attachment.contentType;
|
proto.contentType = attachment.contentType;
|
||||||
proto.digest = result.digest;
|
proto.digest = result.digest;
|
||||||
if (attachment.fileName) {
|
|
||||||
proto.fileName = attachment.fileName;
|
|
||||||
}
|
|
||||||
if (attachment.size) {
|
if (attachment.size) {
|
||||||
proto.size = attachment.size;
|
proto.size = attachment.size;
|
||||||
}
|
}
|
||||||
|
if (attachment.fileName) {
|
||||||
|
proto.fileName = attachment.fileName;
|
||||||
|
}
|
||||||
if (attachment.flags) {
|
if (attachment.flags) {
|
||||||
proto.flags = 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;
|
return proto;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
@ -31,5 +31,17 @@
|
||||||
getDeviceName() {
|
getDeviceName() {
|
||||||
return textsecure.storage.get('device_name');
|
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', () => {
|
describe('AccountManager', () => {
|
||||||
let accountManager;
|
let accountManager;
|
||||||
|
|
||||||
|
@ -10,9 +12,14 @@ describe('AccountManager', () => {
|
||||||
let signedPreKeys;
|
let signedPreKeys;
|
||||||
const DAY = 1000 * 60 * 60 * 24;
|
const DAY = 1000 * 60 * 60 * 24;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
|
const identityKey = await libsignal.KeyHelper.generateIdentityKeyPair();
|
||||||
|
|
||||||
originalProtocolStorage = window.textsecure.storage.protocol;
|
originalProtocolStorage = window.textsecure.storage.protocol;
|
||||||
window.textsecure.storage.protocol = {
|
window.textsecure.storage.protocol = {
|
||||||
|
getIdentityKeyPair() {
|
||||||
|
return identityKey;
|
||||||
|
},
|
||||||
loadSignedPreKeys() {
|
loadSignedPreKeys() {
|
||||||
return Promise.resolve(signedPreKeys);
|
return Promise.resolve(signedPreKeys);
|
||||||
},
|
},
|
||||||
|
@ -22,6 +29,17 @@ describe('AccountManager', () => {
|
||||||
window.textsecure.storage.protocol = originalProtocolStorage;
|
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', () => {
|
it('keeps three confirmed keys even if over a week old', () => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
signedPreKeys = [
|
signedPreKeys = [
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
const Request = function Request(options) {
|
const Request = function Request(options) {
|
||||||
this.verb = options.verb || options.type;
|
this.verb = options.verb || options.type;
|
||||||
this.path = options.path || options.url;
|
this.path = options.path || options.url;
|
||||||
|
this.headers = options.headers;
|
||||||
this.body = options.body || options.data;
|
this.body = options.body || options.data;
|
||||||
this.success = options.success;
|
this.success = options.success;
|
||||||
this.error = options.error;
|
this.error = options.error;
|
||||||
|
@ -50,6 +51,7 @@
|
||||||
this.verb = request.verb;
|
this.verb = request.verb;
|
||||||
this.path = request.path;
|
this.path = request.path;
|
||||||
this.body = request.body;
|
this.body = request.body;
|
||||||
|
this.headers = request.headers;
|
||||||
|
|
||||||
this.respond = (status, message) => {
|
this.respond = (status, message) => {
|
||||||
socket.send(
|
socket.send(
|
||||||
|
@ -77,6 +79,7 @@
|
||||||
verb: request.verb,
|
verb: request.verb,
|
||||||
path: request.path,
|
path: request.path,
|
||||||
body: request.body,
|
body: request.body,
|
||||||
|
headers: request.headers,
|
||||||
id: request.id,
|
id: request.id,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -105,6 +108,7 @@
|
||||||
verb: message.request.verb,
|
verb: message.request.verb,
|
||||||
path: message.request.path,
|
path: message.request.path,
|
||||||
body: message.request.body,
|
body: message.request.body,
|
||||||
|
headers: message.request.headers,
|
||||||
id: message.request.id,
|
id: message.request.id,
|
||||||
socket,
|
socket,
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"productName": "Loki Messenger",
|
"productName": "Loki Messenger",
|
||||||
"description": "Private messaging from your desktop",
|
"description": "Private messaging from your desktop",
|
||||||
"repository": "https://github.com/sloki-project/loki-messenger.git",
|
"repository": "https://github.com/sloki-project/loki-messenger.git",
|
||||||
"version": "1.19.0",
|
"version": "1.20.0",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Open Whisper Systems",
|
"name": "Open Whisper Systems",
|
||||||
|
@ -46,9 +46,8 @@
|
||||||
"pow-metrics": "node metrics_app.js localhost 9000"
|
"pow-metrics": "node metrics_app.js localhost 9000"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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",
|
"@sindresorhus/is": "0.8.0",
|
||||||
"archiver": "2.1.1",
|
|
||||||
"backbone": "1.3.3",
|
"backbone": "1.3.3",
|
||||||
"blob-util": "1.3.0",
|
"blob-util": "1.3.0",
|
||||||
"blueimp-canvas-to-blob": "3.14.0",
|
"blueimp-canvas-to-blob": "3.14.0",
|
||||||
|
@ -95,6 +94,7 @@
|
||||||
"rimraf": "2.6.2",
|
"rimraf": "2.6.2",
|
||||||
"semver": "5.4.1",
|
"semver": "5.4.1",
|
||||||
"spellchecker": "3.4.4",
|
"spellchecker": "3.4.4",
|
||||||
|
"tar": "4.4.8",
|
||||||
"testcheck": "1.0.0-rc.2",
|
"testcheck": "1.0.0-rc.2",
|
||||||
"tmp": "0.0.33",
|
"tmp": "0.0.33",
|
||||||
"to-arraybuffer": "1.0.1",
|
"to-arraybuffer": "1.0.1",
|
||||||
|
@ -120,7 +120,7 @@
|
||||||
"asar": "0.14.0",
|
"asar": "0.14.0",
|
||||||
"bower": "1.8.2",
|
"bower": "1.8.2",
|
||||||
"chai": "4.1.2",
|
"chai": "4.1.2",
|
||||||
"electron": "3.0.9",
|
"electron": "3.0.14",
|
||||||
"electron-builder": "20.13.5",
|
"electron-builder": "20.13.5",
|
||||||
"electron-icon-maker": "0.0.3",
|
"electron-icon-maker": "0.0.3",
|
||||||
"eslint": "4.14.0",
|
"eslint": "4.14.0",
|
||||||
|
|
13
preload.js
13
preload.js
|
@ -346,3 +346,16 @@ window.Signal.Logs = require('./js/modules/logs');
|
||||||
// We pull this in last, because the native module involved appears to be sensitive to
|
// We pull this in last, because the native module involved appears to be sensitive to
|
||||||
// /tmp mounted as noexec on Linux.
|
// /tmp mounted as noexec on Linux.
|
||||||
require('./js/spell_check');
|
require('./js/spell_check');
|
||||||
|
|
||||||
|
if (config.environment === 'test') {
|
||||||
|
/* 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,
|
||||||
|
};
|
||||||
|
/* eslint-enable global-require, import/no-extraneous-dependencies */
|
||||||
|
}
|
||||||
|
|
|
@ -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";
|
option java_package = "org.whispersystems.websocket.messages.protobuf";
|
||||||
|
|
||||||
message WebSocketRequestMessage {
|
message WebSocketRequestMessage {
|
||||||
optional string verb = 1;
|
optional string verb = 1;
|
||||||
optional string path = 2;
|
optional string path = 2;
|
||||||
optional bytes body = 3;
|
optional bytes body = 3;
|
||||||
optional uint64 id = 4;
|
repeated string headers = 5;
|
||||||
|
optional uint64 id = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message WebSocketResponseMessage {
|
message WebSocketResponseMessage {
|
||||||
optional uint64 id = 1;
|
optional uint64 id = 1;
|
||||||
optional uint32 status = 2;
|
optional uint32 status = 2;
|
||||||
optional string message = 3;
|
optional string message = 3;
|
||||||
optional bytes body = 4;
|
repeated string headers = 5;
|
||||||
|
optional bytes body = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message WebSocketMessage {
|
message WebSocketMessage {
|
||||||
enum Type {
|
enum Type {
|
||||||
UNKNOWN = 0;
|
UNKNOWN = 0;
|
||||||
REQUEST = 1;
|
REQUEST = 1;
|
||||||
RESPONSE = 2;
|
RESPONSE = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
optional Type type = 1;
|
optional Type type = 1;
|
||||||
optional WebSocketRequestMessage request = 2;
|
optional WebSocketRequestMessage request = 2;
|
||||||
optional WebSocketResponseMessage response = 3;
|
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
|
// things in the composition area. A margin on an inner div won't be included in that
|
||||||
// height calculation.
|
// height calculation.
|
||||||
.bottom-bar .quote-wrapper {
|
.bottom-bar .quote-wrapper {
|
||||||
margin-right: 5px;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
margin-top: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.send .quote-wrapper {
|
|
||||||
margin-left: 37px;
|
margin-left: 37px;
|
||||||
margin-right: 73px;
|
margin-right: 73px;
|
||||||
margin-bottom: 5px;
|
margin-top: 3px;
|
||||||
|
margin-bottom: -5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottom-bar {
|
.bottom-bar {
|
||||||
|
@ -206,6 +201,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
form.send {
|
form.send {
|
||||||
|
margin-bottom: 0px;
|
||||||
background: $color-white;
|
background: $color-white;
|
||||||
|
|
||||||
&.video-attachment {
|
&.video-attachment {
|
||||||
|
@ -282,6 +278,8 @@
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: $color-loki-light-gray;
|
background-color: $color-loki-light-gray;
|
||||||
|
margin-top: 3px;
|
||||||
|
margin-bottom: 6px;
|
||||||
color: $color-light-90;
|
color: $color-light-90;
|
||||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||||
outline: 0;
|
outline: 0;
|
||||||
|
|
|
@ -88,8 +88,10 @@ button.emoji {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
margin-top: 3px;
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
|
margin-top: 4px;
|
||||||
content: '';
|
content: '';
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: $button-height;
|
width: $button-height;
|
||||||
|
@ -114,7 +116,6 @@ button.emoji {
|
||||||
|
|
||||||
.emoji-panel-container {
|
.emoji-panel-container {
|
||||||
height: 0px;
|
height: 0px;
|
||||||
margin-bottom: 3px;
|
|
||||||
|
|
||||||
.ep-emojies {
|
.ep-emojies {
|
||||||
background-color: $color-white;
|
background-color: $color-white;
|
||||||
|
|
|
@ -108,8 +108,10 @@ a {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
margin-top: 2px;
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
|
margin-top: 4px;
|
||||||
content: '';
|
content: '';
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: $button-height;
|
width: $button-height;
|
||||||
|
@ -411,15 +413,6 @@ $loading-height: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type='text'],
|
|
||||||
input[type='search'],
|
|
||||||
textarea {
|
|
||||||
&:active,
|
|
||||||
&:focus {
|
|
||||||
outline: 1px solid $blue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.expiredAlert {
|
.expiredAlert {
|
||||||
background: #f3f3a7;
|
background: #f3f3a7;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
|
|
@ -128,6 +128,14 @@
|
||||||
color: $color-light-60;
|
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 {
|
&.dark-theme {
|
||||||
// _modules
|
// _modules
|
||||||
|
|
||||||
|
|
|
@ -748,6 +748,7 @@
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
color: $color-gray-90;
|
color: $color-gray-90;
|
||||||
|
text-align: start;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: $color-gray-90;
|
color: $color-gray-90;
|
||||||
|
@ -803,6 +804,7 @@
|
||||||
flex: initial;
|
flex: initial;
|
||||||
min-width: 54px;
|
min-width: 54px;
|
||||||
width: 54px;
|
width: 54px;
|
||||||
|
max-height: 54px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
|
@ -2194,6 +2196,7 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 1px;
|
margin: 1px;
|
||||||
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-image__caption-icon {
|
.module-image__caption-icon {
|
||||||
|
@ -2202,6 +2205,14 @@
|
||||||
left: 6px;
|
left: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module-image__with-click-handler {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-image--soft-corners {
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.module-image--curved-top-left {
|
.module-image--curved-top-left {
|
||||||
border-top-left-radius: 16px;
|
border-top-left-radius: 16px;
|
||||||
}
|
}
|
||||||
|
@ -2304,6 +2315,17 @@
|
||||||
text-align: center;
|
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
|
||||||
|
|
||||||
.module-image-grid {
|
.module-image-grid {
|
||||||
|
@ -2417,6 +2439,272 @@
|
||||||
flex-grow: 1;
|
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
|
// Third-party module: react-contextmenu
|
||||||
|
|
||||||
.react-contextmenu {
|
.react-contextmenu {
|
||||||
|
@ -2503,6 +2791,11 @@
|
||||||
display: none;
|
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*/
|
/* Spec: container > 438px and container < 593px*/
|
||||||
@media (min-width: 800px) and (max-width: 925px) {
|
@media (min-width: 800px) and (max-width: 925px) {
|
||||||
.module-message {
|
.module-message {
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
background: transparent;
|
background: transparent;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: none;
|
border: none;
|
||||||
|
margin-top: 2px;
|
||||||
|
|
||||||
&:focus,
|
&:focus,
|
||||||
&:hover {
|
&:hover {
|
||||||
|
@ -16,6 +17,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
|
margin-top: 4px;
|
||||||
content: '';
|
content: '';
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
|
@ -36,6 +38,7 @@
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
margin-top: 5px;
|
||||||
|
|
||||||
&:focus,
|
&:focus,
|
||||||
&:hover {
|
&: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 {
|
.contact-details {
|
||||||
.number {
|
.number {
|
||||||
color: $color-dark-30;
|
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 {
|
.expiredAlert {
|
||||||
background: #f3f3a7;
|
background: #f3f3a7;
|
||||||
|
|
||||||
|
@ -749,14 +696,9 @@ body.dark-theme {
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-message__generic-attachment__icon {
|
.module-message__generic-attachment__icon {
|
||||||
// TODO: this will eventually be a different image
|
|
||||||
// background: url('../images/file-gradient.svg') no-repeat center;
|
// 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 {
|
.module-message__generic-attachment__file-name {
|
||||||
color: $color-dark-05;
|
color: $color-dark-05;
|
||||||
}
|
}
|
||||||
|
@ -1051,10 +993,6 @@ body.dark-theme {
|
||||||
@include color-svg('../images/movie.svg', $color-signal-blue);
|
@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 {
|
.module-quote__generic-file__text {
|
||||||
color: $color-dark-05;
|
color: $color-dark-05;
|
||||||
}
|
}
|
||||||
|
@ -1415,6 +1353,10 @@ body.dark-theme {
|
||||||
color: $color-dark-05;
|
color: $color-dark-05;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Module: Image
|
||||||
|
|
||||||
|
// Module: Image Grid
|
||||||
|
|
||||||
// Module: Typing Animation
|
// Module: Typing Animation
|
||||||
|
|
||||||
.module-typing-animation__dot {
|
.module-typing-animation__dot {
|
||||||
|
@ -1425,6 +1367,50 @@ body.dark-theme {
|
||||||
background-color: $color-white;
|
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
|
// Third-party module: react-contextmenu
|
||||||
|
|
||||||
.react-contextmenu {
|
.react-contextmenu {
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
/* global Signal: false */
|
/* global Signal, Whisper, assert, textsecure, _, libsignal */
|
||||||
/* global Whisper: false */
|
|
||||||
/* global assert: false */
|
|
||||||
/* global textsecure: false */
|
|
||||||
/* global _: false */
|
|
||||||
|
|
||||||
/* eslint-disable no-unreachable, no-console */
|
/* eslint-disable no-console */
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
@ -240,8 +236,8 @@ describe('Backup', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('end-to-end', () => {
|
describe('end-to-end', () => {
|
||||||
it('exports then imports to produce the same data we started with', async () => {
|
it('exports then imports to produce the same data we started with', async function thisNeeded() {
|
||||||
return;
|
this.timeout(6000);
|
||||||
|
|
||||||
const { attachmentsPath, fse, glob, path, tmp } = window.test;
|
const { attachmentsPath, fse, glob, path, tmp } = window.test;
|
||||||
const {
|
const {
|
||||||
|
@ -249,46 +245,32 @@ describe('Backup', () => {
|
||||||
loadAttachmentData,
|
loadAttachmentData,
|
||||||
} = window.Signal.Migrations;
|
} = window.Signal.Migrations;
|
||||||
|
|
||||||
const key = new Uint8Array([
|
const staticKeyPair = await libsignal.KeyHelper.generateIdentityKeyPair();
|
||||||
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 attachmentsPattern = path.join(attachmentsPath, '**');
|
const attachmentsPattern = path.join(attachmentsPath, '**');
|
||||||
|
|
||||||
const OUR_NUMBER = '+12025550000';
|
const OUR_NUMBER = '+12025550000';
|
||||||
const CONTACT_ONE_NUMBER = '+12025550001';
|
const CONTACT_ONE_NUMBER = '+12025550001';
|
||||||
const CONTACT_TWO_NUMBER = '+12025550002';
|
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) {
|
async function wrappedLoadAttachment(attachment) {
|
||||||
return _.omit(await loadAttachmentData(attachment), ['path']);
|
return _.omit(await loadAttachmentData(attachment), ['path']);
|
||||||
}
|
}
|
||||||
|
@ -376,16 +358,30 @@ describe('Backup', () => {
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
attachments: await Promise.all(
|
attachments: await Promise.all(
|
||||||
(message.attachments || []).map(attachment =>
|
(message.attachments || []).map(async attachment => {
|
||||||
wrappedLoadAttachment(attachment)
|
await wrappedLoadAttachment(attachment);
|
||||||
)
|
|
||||||
|
if (attachment.thumbnail) {
|
||||||
|
await wrappedLoadAttachment(attachment.thumbnail);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attachment.screenshot) {
|
||||||
|
await wrappedLoadAttachment(attachment.screenshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
return attachment;
|
||||||
|
})
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let backupDir;
|
let backupDir;
|
||||||
try {
|
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 MESSAGE_COUNT = 1;
|
||||||
const CONVERSATION_COUNT = 1;
|
const CONVERSATION_COUNT = 1;
|
||||||
|
|
||||||
|
@ -397,47 +393,20 @@ describe('Backup', () => {
|
||||||
timestamp: 1524185933350,
|
timestamp: 1524185933350,
|
||||||
errors: [],
|
errors: [],
|
||||||
attachments: [
|
attachments: [
|
||||||
|
// Note: generates two more files: screenshot and thumbnail
|
||||||
{
|
{
|
||||||
contentType: 'image/gif',
|
contentType: 'video/mp4',
|
||||||
fileName: 'sad_cat.gif',
|
fileName: 'video.mp4',
|
||||||
data: new Uint8Array([
|
data: FIXTURES.mp4,
|
||||||
1,
|
},
|
||||||
2,
|
// Note: generates one more file: thumbnail
|
||||||
3,
|
{
|
||||||
4,
|
contentType: 'image/png',
|
||||||
5,
|
fileName: 'landscape.png',
|
||||||
6,
|
data: FIXTURES.png,
|
||||||
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,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
hasAttachments: 1,
|
hasAttachments: 1,
|
||||||
hasFileAttachments: undefined,
|
|
||||||
hasVisualMediaAttachments: 1,
|
hasVisualMediaAttachments: 1,
|
||||||
quote: {
|
quote: {
|
||||||
text: "Isn't it cute?",
|
text: "Isn't it cute?",
|
||||||
|
@ -450,43 +419,10 @@ describe('Backup', () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
contentType: 'image/gif',
|
contentType: 'image/gif',
|
||||||
fileName: 'happy_cat.gif',
|
fileName: 'avatar.gif',
|
||||||
thumbnail: {
|
thumbnail: {
|
||||||
contentType: 'image/png',
|
contentType: 'image/png',
|
||||||
data: new Uint8Array([
|
data: FIXTURES.gif,
|
||||||
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,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -506,40 +442,7 @@ describe('Backup', () => {
|
||||||
isProfile: false,
|
isProfile: false,
|
||||||
avatar: {
|
avatar: {
|
||||||
contentType: 'image/png',
|
contentType: 'image/png',
|
||||||
data: new Uint8Array([
|
data: FIXTURES.png,
|
||||||
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,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -552,107 +455,30 @@ describe('Backup', () => {
|
||||||
console.log('Backup test: Create models, save to db/disk');
|
console.log('Backup test: Create models, save to db/disk');
|
||||||
const message = await upgradeMessageSchema(messageWithAttachments);
|
const message = await upgradeMessageSchema(messageWithAttachments);
|
||||||
console.log({ message });
|
console.log({ message });
|
||||||
const messageModel = new Whisper.Message(message);
|
await window.Signal.Data.saveMessage(message, {
|
||||||
const id = await window.Signal.Data.saveMessage(
|
Message: Whisper.Message,
|
||||||
messageModel.attributes,
|
});
|
||||||
{
|
|
||||||
Message: Whisper.Message,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
messageModel.set({ id });
|
|
||||||
|
|
||||||
const conversation = {
|
const conversation = {
|
||||||
active_at: 1524185933350,
|
active_at: 1524185933350,
|
||||||
color: 'orange',
|
color: 'orange',
|
||||||
expireTimer: 0,
|
expireTimer: 0,
|
||||||
id: CONTACT_ONE_NUMBER,
|
id: CONTACT_ONE_NUMBER,
|
||||||
lastMessage: 'Heyo!',
|
|
||||||
name: 'Someone Somewhere',
|
name: 'Someone Somewhere',
|
||||||
profileAvatar: {
|
profileAvatar: {
|
||||||
contentType: 'image/jpeg',
|
contentType: 'image/jpeg',
|
||||||
data: new Uint8Array([
|
data: FIXTURES.jpeg,
|
||||||
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,
|
|
||||||
size: 64,
|
size: 64,
|
||||||
},
|
},
|
||||||
profileKey: new Uint8Array([
|
profileKey: 'BASE64KEY',
|
||||||
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,
|
|
||||||
profileName: 'Someone! 🤔',
|
profileName: 'Someone! 🤔',
|
||||||
profileSharing: true,
|
profileSharing: true,
|
||||||
timestamp: 1524185933350,
|
timestamp: 1524185933350,
|
||||||
tokens: [
|
|
||||||
'someone somewhere',
|
|
||||||
'someone',
|
|
||||||
'somewhere',
|
|
||||||
'2025550001',
|
|
||||||
'12025550001',
|
|
||||||
],
|
|
||||||
type: 'private',
|
type: 'private',
|
||||||
unreadCount: 0,
|
unreadCount: 0,
|
||||||
verified: 0,
|
verified: 0,
|
||||||
|
sealedSender: 0,
|
||||||
|
version: 2,
|
||||||
};
|
};
|
||||||
console.log({ conversation });
|
console.log({ conversation });
|
||||||
await window.Signal.Data.saveConversation(conversation, {
|
await window.Signal.Data.saveConversation(conversation, {
|
||||||
|
@ -669,11 +495,13 @@ describe('Backup', () => {
|
||||||
console.log('Backup test: Export!');
|
console.log('Backup test: Export!');
|
||||||
backupDir = tmp.dirSync().name;
|
backupDir = tmp.dirSync().name;
|
||||||
console.log({ backupDir });
|
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');
|
console.log('Backup test: Ensure that messages.tar.gz exists');
|
||||||
const zipPath = path.join(backupDir, 'messages.zip');
|
const archivePath = path.join(backupDir, 'messages.tar.gz');
|
||||||
const messageZipExists = fse.existsSync(zipPath);
|
const messageZipExists = fse.existsSync(archivePath);
|
||||||
assert.strictEqual(true, messageZipExists);
|
assert.strictEqual(true, messageZipExists);
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
|
@ -688,43 +516,9 @@ describe('Backup', () => {
|
||||||
await clearAllData();
|
await clearAllData();
|
||||||
|
|
||||||
console.log('Backup test: Import!');
|
console.log('Backup test: Import!');
|
||||||
await Signal.Backup.importFromDirectory(backupDir, { key });
|
await Signal.Backup.importFromDirectory(backupDir, {
|
||||||
|
key: staticKeyPair.privKey,
|
||||||
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,
|
|
||||||
});
|
});
|
||||||
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');
|
console.log('Backup test: Check conversations');
|
||||||
const conversationCollection = await window.Signal.Data.getAllConversations(
|
const conversationCollection = await window.Signal.Data.getAllConversations(
|
||||||
|
@ -734,11 +528,55 @@ describe('Backup', () => {
|
||||||
);
|
);
|
||||||
assert.strictEqual(conversationCollection.length, CONVERSATION_COUNT);
|
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;
|
const conversationFromDB = conversationCollection.at(0).attributes;
|
||||||
console.log({ conversationFromDB, conversation });
|
console.log({ conversationFromDB, conversation });
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
conversationFromDB,
|
_.omit(conversationFromDB, ommited),
|
||||||
_.omit(conversation, ['profileAvatar'])
|
_.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');
|
console.log('Backup test: Clear all data');
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/* global Signal, textsecure */
|
/* global Signal, textsecure, libsignal */
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ describe('Crypto', () => {
|
||||||
|
|
||||||
const encrypted = await Signal.Crypto.encryptSymmetric(key, plaintext);
|
const encrypted = await Signal.Crypto.encryptSymmetric(key, plaintext);
|
||||||
const uintArray = new Uint8Array(encrypted);
|
const uintArray = new Uint8Array(encrypted);
|
||||||
uintArray[2] = 9;
|
uintArray[2] += 2;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Signal.Crypto.decryptSymmetric(key, uintArray.buffer);
|
await Signal.Crypto.decryptSymmetric(key, uintArray.buffer);
|
||||||
|
@ -69,7 +69,7 @@ describe('Crypto', () => {
|
||||||
|
|
||||||
const encrypted = await Signal.Crypto.encryptSymmetric(key, plaintext);
|
const encrypted = await Signal.Crypto.encryptSymmetric(key, plaintext);
|
||||||
const uintArray = new Uint8Array(encrypted);
|
const uintArray = new Uint8Array(encrypted);
|
||||||
uintArray[uintArray.length - 3] = 9;
|
uintArray[uintArray.length - 3] += 2;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Signal.Crypto.decryptSymmetric(key, uintArray.buffer);
|
await Signal.Crypto.decryptSymmetric(key, uintArray.buffer);
|
||||||
|
@ -94,7 +94,7 @@ describe('Crypto', () => {
|
||||||
|
|
||||||
const encrypted = await Signal.Crypto.encryptSymmetric(key, plaintext);
|
const encrypted = await Signal.Crypto.encryptSymmetric(key, plaintext);
|
||||||
const uintArray = new Uint8Array(encrypted);
|
const uintArray = new Uint8Array(encrypted);
|
||||||
uintArray[35] = 9;
|
uintArray[35] += 9;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Signal.Crypto.decryptSymmetric(key, uintArray.buffer);
|
await Signal.Crypto.decryptSymmetric(key, uintArray.buffer);
|
||||||
|
@ -109,4 +109,67 @@ describe('Crypto', () => {
|
||||||
throw new Error('Expected error to be thrown');
|
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', () => {
|
describe('SecretSessionCipher', () => {
|
||||||
it('successfully roundtrips', async () => {
|
it('successfully roundtrips', async function thisNeeded() {
|
||||||
|
this.timeout(4000);
|
||||||
|
|
||||||
const aliceStore = new InMemorySignalProtocolStore();
|
const aliceStore = new InMemorySignalProtocolStore();
|
||||||
const bobStore = new InMemorySignalProtocolStore();
|
const bobStore = new InMemorySignalProtocolStore();
|
||||||
|
|
||||||
|
@ -187,7 +189,9 @@ describe('SecretSessionCipher', () => {
|
||||||
assert.strictEqual(decryptResult.sender.toString(), '+14151111111.1');
|
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 aliceStore = new InMemorySignalProtocolStore();
|
||||||
const bobStore = 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 aliceStore = new InMemorySignalProtocolStore();
|
||||||
const bobStore = 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 aliceStore = new InMemorySignalProtocolStore();
|
||||||
const bobStore = new InMemorySignalProtocolStore();
|
const bobStore = new InMemorySignalProtocolStore();
|
||||||
|
|
||||||
|
|
|
@ -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>;
|
||||||
|
```
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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} />
|
||||||
|
```
|
|
@ -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,
|
getRegex,
|
||||||
getReplacementData,
|
getReplacementData,
|
||||||
getTitle,
|
getTitle,
|
||||||
|
SizeClassType,
|
||||||
} from '../../util/emoji';
|
} from '../../util/emoji';
|
||||||
|
|
||||||
import { Localizer, RenderTextCallback } from '../../types/Util';
|
import { Localizer, RenderTextCallback } from '../../types/Util';
|
||||||
|
@ -20,7 +21,7 @@ function getImageTag({
|
||||||
i18n,
|
i18n,
|
||||||
}: {
|
}: {
|
||||||
match: any;
|
match: any;
|
||||||
sizeClass: string | undefined;
|
sizeClass?: SizeClassType;
|
||||||
key: string | number;
|
key: string | number;
|
||||||
i18n: Localizer;
|
i18n: Localizer;
|
||||||
}) {
|
}) {
|
||||||
|
@ -51,7 +52,7 @@ function getImageTag({
|
||||||
interface Props {
|
interface Props {
|
||||||
text: string;
|
text: string;
|
||||||
/** A class name to be added to the generated emoji images */
|
/** 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>. */
|
/** Allows you to customize now non-newlines are rendered. Simplest is just a <span>. */
|
||||||
renderNonEmoji?: RenderTextCallback;
|
renderNonEmoji?: RenderTextCallback;
|
||||||
i18n: Localizer;
|
i18n: Localizer;
|
||||||
|
|
|
@ -77,18 +77,21 @@
|
||||||
width="199"
|
width="199"
|
||||||
attachment={{ caption: 'dogs playing' }}
|
attachment={{ caption: 'dogs playing' }}
|
||||||
url={util.pngObjectUrl}
|
url={util.pngObjectUrl}
|
||||||
|
i18n={util.i18n}
|
||||||
/>
|
/>
|
||||||
<Image
|
<Image
|
||||||
height="149"
|
height="149"
|
||||||
width="149"
|
width="149"
|
||||||
attachment={{ caption: 'dogs playing' }}
|
attachment={{ caption: 'dogs playing' }}
|
||||||
url={util.pngObjectUrl}
|
url={util.pngObjectUrl}
|
||||||
|
i18n={util.i18n}
|
||||||
/>
|
/>
|
||||||
<Image
|
<Image
|
||||||
height="99"
|
height="99"
|
||||||
width="99"
|
width="99"
|
||||||
attachment={{ caption: 'dogs playing' }}
|
attachment={{ caption: 'dogs playing' }}
|
||||||
url={util.pngObjectUrl}
|
url={util.pngObjectUrl}
|
||||||
|
i18n={util.i18n}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
|
@ -100,6 +103,7 @@
|
||||||
darkOverlay
|
darkOverlay
|
||||||
overlayText="+3"
|
overlayText="+3"
|
||||||
url={util.pngObjectUrl}
|
url={util.pngObjectUrl}
|
||||||
|
i18n={util.i18n}
|
||||||
/>
|
/>
|
||||||
<Image
|
<Image
|
||||||
height="149"
|
height="149"
|
||||||
|
@ -108,6 +112,7 @@
|
||||||
darkOverlay
|
darkOverlay
|
||||||
overlayText="+3"
|
overlayText="+3"
|
||||||
url={util.pngObjectUrl}
|
url={util.pngObjectUrl}
|
||||||
|
i18n={util.i18n}
|
||||||
/>
|
/>
|
||||||
<Image
|
<Image
|
||||||
height="99"
|
height="99"
|
||||||
|
@ -116,6 +121,82 @@
|
||||||
darkOverlay
|
darkOverlay
|
||||||
overlayText="+3"
|
overlayText="+3"
|
||||||
url={util.pngObjectUrl}
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -15,24 +15,29 @@ interface Props {
|
||||||
overlayText?: string;
|
overlayText?: string;
|
||||||
|
|
||||||
bottomOverlay?: boolean;
|
bottomOverlay?: boolean;
|
||||||
|
closeButton?: boolean;
|
||||||
curveBottomLeft?: boolean;
|
curveBottomLeft?: boolean;
|
||||||
curveBottomRight?: boolean;
|
curveBottomRight?: boolean;
|
||||||
curveTopLeft?: boolean;
|
curveTopLeft?: boolean;
|
||||||
curveTopRight?: boolean;
|
curveTopRight?: boolean;
|
||||||
darkOverlay?: boolean;
|
darkOverlay?: boolean;
|
||||||
playIconOverlay?: boolean;
|
playIconOverlay?: boolean;
|
||||||
|
softCorners?: boolean;
|
||||||
|
|
||||||
i18n: Localizer;
|
i18n: Localizer;
|
||||||
onClick?: (attachment: AttachmentType) => void;
|
onClick?: (attachment: AttachmentType) => void;
|
||||||
|
onClickClose?: (attachment: AttachmentType) => void;
|
||||||
onError?: () => void;
|
onError?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Image extends React.Component<Props> {
|
export class Image extends React.Component<Props> {
|
||||||
|
// tslint:disable-next-line max-func-body-length cyclomatic-complexity
|
||||||
public render() {
|
public render() {
|
||||||
const {
|
const {
|
||||||
alt,
|
alt,
|
||||||
attachment,
|
attachment,
|
||||||
bottomOverlay,
|
bottomOverlay,
|
||||||
|
closeButton,
|
||||||
curveBottomLeft,
|
curveBottomLeft,
|
||||||
curveBottomRight,
|
curveBottomRight,
|
||||||
curveTopLeft,
|
curveTopLeft,
|
||||||
|
@ -41,9 +46,11 @@ export class Image extends React.Component<Props> {
|
||||||
height,
|
height,
|
||||||
i18n,
|
i18n,
|
||||||
onClick,
|
onClick,
|
||||||
|
onClickClose,
|
||||||
onError,
|
onError,
|
||||||
overlayText,
|
overlayText,
|
||||||
playIconOverlay,
|
playIconOverlay,
|
||||||
|
softCorners,
|
||||||
url,
|
url,
|
||||||
width,
|
width,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
@ -52,18 +59,20 @@ export class Image extends React.Component<Props> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
role={onClick ? 'button' : undefined}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (onClick) {
|
if (onClick) {
|
||||||
onClick(attachment);
|
onClick(attachment);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
role="button"
|
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-image',
|
'module-image',
|
||||||
|
onClick ? 'module-image__with-click-handler' : null,
|
||||||
curveBottomLeft ? 'module-image--curved-bottom-left' : null,
|
curveBottomLeft ? 'module-image--curved-bottom-left' : null,
|
||||||
curveBottomRight ? 'module-image--curved-bottom-right' : null,
|
curveBottomRight ? 'module-image--curved-bottom-right' : null,
|
||||||
curveTopLeft ? 'module-image--curved-top-left' : 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
|
<img
|
||||||
|
@ -88,9 +97,22 @@ export class Image extends React.Component<Props> {
|
||||||
curveTopRight ? 'module-image--curved-top-right' : null,
|
curveTopRight ? 'module-image--curved-top-right' : null,
|
||||||
curveBottomLeft ? 'module-image--curved-bottom-left' : null,
|
curveBottomLeft ? 'module-image--curved-bottom-left' : null,
|
||||||
curveBottomRight ? 'module-image--curved-bottom-right' : null,
|
curveBottomRight ? 'module-image--curved-bottom-right' : null,
|
||||||
|
softCorners ? 'module-image--soft-corners' : null,
|
||||||
darkOverlay ? 'module-image__border-overlay--dark' : 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 ? (
|
{bottomOverlay ? (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
|
|
|
@ -352,3 +352,35 @@ const attachments = [
|
||||||
</div>
|
</div>
|
||||||
</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_WIDTH = 300;
|
||||||
const MAX_HEIGHT = MAX_WIDTH * 1.5;
|
const MAX_HEIGHT = MAX_WIDTH * 1.5;
|
||||||
const MIN_WIDTH = 200;
|
const MIN_WIDTH = 200;
|
||||||
const MIN_HEIGHT = 25;
|
const MIN_HEIGHT = 50;
|
||||||
|
|
||||||
export class ImageGrid extends React.Component<Props> {
|
export class ImageGrid extends React.Component<Props> {
|
||||||
// tslint:disable-next-line max-func-body-length */
|
// tslint:disable-next-line max-func-body-length */
|
||||||
|
@ -50,7 +50,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attachments.length === 1) {
|
if (attachments.length === 1 || !areAllAttachmentsVisual(attachments)) {
|
||||||
const { height, width } = getImageDimensions(attachments[0]);
|
const { height, width } = getImageDimensions(attachments[0]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -93,7 +93,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
playIconOverlay={isVideoAttachment(attachments[0])}
|
playIconOverlay={isVideoAttachment(attachments[0])}
|
||||||
height={149}
|
height={149}
|
||||||
width={149}
|
width={149}
|
||||||
url={getUrl(attachments[0])}
|
url={getThumbnailUrl(attachments[0])}
|
||||||
onClick={onClickAttachment}
|
onClick={onClickAttachment}
|
||||||
onError={onError}
|
onError={onError}
|
||||||
/>
|
/>
|
||||||
|
@ -107,7 +107,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
height={149}
|
height={149}
|
||||||
width={149}
|
width={149}
|
||||||
attachment={attachments[1]}
|
attachment={attachments[1]}
|
||||||
url={getUrl(attachments[1])}
|
url={getThumbnailUrl(attachments[1])}
|
||||||
onClick={onClickAttachment}
|
onClick={onClickAttachment}
|
||||||
onError={onError}
|
onError={onError}
|
||||||
/>
|
/>
|
||||||
|
@ -141,7 +141,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
width={99}
|
width={99}
|
||||||
attachment={attachments[1]}
|
attachment={attachments[1]}
|
||||||
playIconOverlay={isVideoAttachment(attachments[1])}
|
playIconOverlay={isVideoAttachment(attachments[1])}
|
||||||
url={getUrl(attachments[1])}
|
url={getThumbnailUrl(attachments[1])}
|
||||||
onClick={onClickAttachment}
|
onClick={onClickAttachment}
|
||||||
onError={onError}
|
onError={onError}
|
||||||
/>
|
/>
|
||||||
|
@ -154,7 +154,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
width={99}
|
width={99}
|
||||||
attachment={attachments[2]}
|
attachment={attachments[2]}
|
||||||
playIconOverlay={isVideoAttachment(attachments[2])}
|
playIconOverlay={isVideoAttachment(attachments[2])}
|
||||||
url={getUrl(attachments[2])}
|
url={getThumbnailUrl(attachments[2])}
|
||||||
onClick={onClickAttachment}
|
onClick={onClickAttachment}
|
||||||
onError={onError}
|
onError={onError}
|
||||||
/>
|
/>
|
||||||
|
@ -176,7 +176,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
playIconOverlay={isVideoAttachment(attachments[0])}
|
playIconOverlay={isVideoAttachment(attachments[0])}
|
||||||
height={149}
|
height={149}
|
||||||
width={149}
|
width={149}
|
||||||
url={getUrl(attachments[0])}
|
url={getThumbnailUrl(attachments[0])}
|
||||||
onClick={onClickAttachment}
|
onClick={onClickAttachment}
|
||||||
onError={onError}
|
onError={onError}
|
||||||
/>
|
/>
|
||||||
|
@ -188,7 +188,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
height={149}
|
height={149}
|
||||||
width={149}
|
width={149}
|
||||||
attachment={attachments[1]}
|
attachment={attachments[1]}
|
||||||
url={getUrl(attachments[1])}
|
url={getThumbnailUrl(attachments[1])}
|
||||||
onClick={onClickAttachment}
|
onClick={onClickAttachment}
|
||||||
onError={onError}
|
onError={onError}
|
||||||
/>
|
/>
|
||||||
|
@ -203,7 +203,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
height={149}
|
height={149}
|
||||||
width={149}
|
width={149}
|
||||||
attachment={attachments[2]}
|
attachment={attachments[2]}
|
||||||
url={getUrl(attachments[2])}
|
url={getThumbnailUrl(attachments[2])}
|
||||||
onClick={onClickAttachment}
|
onClick={onClickAttachment}
|
||||||
onError={onError}
|
onError={onError}
|
||||||
/>
|
/>
|
||||||
|
@ -216,7 +216,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
height={149}
|
height={149}
|
||||||
width={149}
|
width={149}
|
||||||
attachment={attachments[3]}
|
attachment={attachments[3]}
|
||||||
url={getUrl(attachments[3])}
|
url={getThumbnailUrl(attachments[3])}
|
||||||
onClick={onClickAttachment}
|
onClick={onClickAttachment}
|
||||||
onError={onError}
|
onError={onError}
|
||||||
/>
|
/>
|
||||||
|
@ -238,7 +238,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
playIconOverlay={isVideoAttachment(attachments[0])}
|
playIconOverlay={isVideoAttachment(attachments[0])}
|
||||||
height={149}
|
height={149}
|
||||||
width={149}
|
width={149}
|
||||||
url={getUrl(attachments[0])}
|
url={getThumbnailUrl(attachments[0])}
|
||||||
onClick={onClickAttachment}
|
onClick={onClickAttachment}
|
||||||
onError={onError}
|
onError={onError}
|
||||||
/>
|
/>
|
||||||
|
@ -250,7 +250,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
height={149}
|
height={149}
|
||||||
width={149}
|
width={149}
|
||||||
attachment={attachments[1]}
|
attachment={attachments[1]}
|
||||||
url={getUrl(attachments[1])}
|
url={getThumbnailUrl(attachments[1])}
|
||||||
onClick={onClickAttachment}
|
onClick={onClickAttachment}
|
||||||
onError={onError}
|
onError={onError}
|
||||||
/>
|
/>
|
||||||
|
@ -265,7 +265,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
height={99}
|
height={99}
|
||||||
width={99}
|
width={99}
|
||||||
attachment={attachments[2]}
|
attachment={attachments[2]}
|
||||||
url={getUrl(attachments[2])}
|
url={getThumbnailUrl(attachments[2])}
|
||||||
onClick={onClickAttachment}
|
onClick={onClickAttachment}
|
||||||
onError={onError}
|
onError={onError}
|
||||||
/>
|
/>
|
||||||
|
@ -277,7 +277,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
height={99}
|
height={99}
|
||||||
width={98}
|
width={98}
|
||||||
attachment={attachments[3]}
|
attachment={attachments[3]}
|
||||||
url={getUrl(attachments[3])}
|
url={getThumbnailUrl(attachments[3])}
|
||||||
onClick={onClickAttachment}
|
onClick={onClickAttachment}
|
||||||
onError={onError}
|
onError={onError}
|
||||||
/>
|
/>
|
||||||
|
@ -296,7 +296,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
attachment={attachments[4]}
|
attachment={attachments[4]}
|
||||||
url={getUrl(attachments[4])}
|
url={getThumbnailUrl(attachments[4])}
|
||||||
onClick={onClickAttachment}
|
onClick={onClickAttachment}
|
||||||
onError={onError}
|
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) {
|
function getUrl(attachment: AttachmentType) {
|
||||||
if (attachment.screenshot) {
|
if (attachment.screenshot) {
|
||||||
return attachment.screenshot.url;
|
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>) {
|
export function hasImage(attachments?: Array<AttachmentType>) {
|
||||||
return attachments && attachments[0] && attachments[0].url;
|
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(
|
export function getGridDimensions(
|
||||||
attachments?: Array<AttachmentType>
|
attachments?: Array<AttachmentType>
|
||||||
): null | DimensionsType {
|
): null | DimensionsType {
|
||||||
|
|
|
@ -79,52 +79,6 @@ interface State {
|
||||||
imageBroken: boolean;
|
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 EXPIRATION_CHECK_MINIMUM = 2000;
|
||||||
const EXPIRED_DELAY = 600;
|
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
|
```jsx
|
||||||
<MessageBody text="http://somewhere.com" disableLinks i18n={util.i18n} />
|
<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 React from 'react';
|
||||||
|
|
||||||
import { getSizeClass } from '../../util/emoji';
|
import { getSizeClass, SizeClassType } from '../../util/emoji';
|
||||||
import { Emojify } from './Emojify';
|
import { Emojify } from './Emojify';
|
||||||
import { AddNewLines } from './AddNewLines';
|
import { AddNewLines } from './AddNewLines';
|
||||||
import { Linkify } from './Linkify';
|
import { Linkify } from './Linkify';
|
||||||
|
@ -21,8 +21,26 @@ const renderNewLines: RenderTextCallback = ({
|
||||||
key,
|
key,
|
||||||
}) => <AddNewLines key={key} text={textWithNewLines} />;
|
}) => <AddNewLines key={key} text={textWithNewLines} />;
|
||||||
|
|
||||||
const renderLinks: RenderTextCallback = ({ text: textWithLinks, key }) => (
|
const renderEmoji = ({
|
||||||
<Linkify key={key} text={textWithLinks} renderNonLink={renderNewLines} />
|
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> {
|
export class MessageBody extends React.Component<Props> {
|
||||||
public render() {
|
public render() {
|
||||||
const { text, disableJumbomoji, disableLinks, i18n } = this.props;
|
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 (
|
return (
|
||||||
<Emojify
|
<Linkify
|
||||||
text={text}
|
text={text}
|
||||||
sizeClass={sizeClass}
|
renderNonLink={({ key, text: nonLinkText }) => {
|
||||||
renderNonEmoji={disableLinks ? renderNewLines : renderLinks}
|
return renderEmoji({
|
||||||
i18n={i18n}
|
i18n,
|
||||||
|
text: nonLinkText,
|
||||||
|
sizeClass,
|
||||||
|
key,
|
||||||
|
renderNonEmoji: renderNewLines,
|
||||||
|
});
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>;
|
||||||
|
```
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>;
|
||||||
|
```
|
|
@ -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
|
### In message bubble
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
<util.ConversationContext theme={util.theme}>
|
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||||
<li>
|
<li>
|
||||||
<TypingBubble conversationType="direct" i18n={util.i18n} />
|
<TypingBubble conversationType="direct" i18n={util.i18n} />
|
||||||
</li>
|
</li>
|
||||||
|
@ -14,7 +14,7 @@
|
||||||
### In message bubble, group conversation
|
### In message bubble, group conversation
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
<util.ConversationContext theme={util.theme}>
|
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||||
<li>
|
<li>
|
||||||
<TypingBubble color="red" conversationType="group" i18n={util.i18n} />
|
<TypingBubble color="red" conversationType="group" i18n={util.i18n} />
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -10,6 +10,8 @@ instance.include_title = true;
|
||||||
instance.replace_mode = 'img';
|
instance.replace_mode = 'img';
|
||||||
instance.supports_css = false; // needed to avoid spans with background-image
|
instance.supports_css = false; // needed to avoid spans with background-image
|
||||||
|
|
||||||
|
export type SizeClassType = '' | 'small' | 'medium' | 'large' | 'jumbo';
|
||||||
|
|
||||||
export function getRegex(): RegExp {
|
export function getRegex(): RegExp {
|
||||||
return instance.rx_unified;
|
return instance.rx_unified;
|
||||||
}
|
}
|
||||||
|
@ -56,7 +58,7 @@ function hasNormalCharacters(str: string) {
|
||||||
return noEmoji.length > 0;
|
return noEmoji.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSizeClass(str: string) {
|
export function getSizeClass(str: string): SizeClassType {
|
||||||
if (hasNormalCharacters(str)) {
|
if (hasNormalCharacters(str)) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
|
@ -244,7 +244,7 @@
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "js/background.js",
|
"path": "js/background.js",
|
||||||
"line": " wrap(",
|
"line": " wrap(",
|
||||||
"lineNumber": 727,
|
"lineNumber": 740,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-10-18T22:23:00.485Z"
|
"updated": "2018-10-18T22:23:00.485Z"
|
||||||
},
|
},
|
||||||
|
@ -252,7 +252,7 @@
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "js/background.js",
|
"path": "js/background.js",
|
||||||
"line": " await wrap(",
|
"line": " await wrap(",
|
||||||
"lineNumber": 1257,
|
"lineNumber": 1270,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-10-26T22:43:23.229Z"
|
"updated": "2018-10-26T22:43:23.229Z"
|
||||||
},
|
},
|
||||||
|
@ -319,7 +319,7 @@
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "js/modules/crypto.js",
|
"path": "js/modules/crypto.js",
|
||||||
"line": " return dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64');",
|
"line": " return dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64');",
|
||||||
"lineNumber": 271,
|
"lineNumber": 38,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-10-05T23:12:28.961Z"
|
"updated": "2018-10-05T23:12:28.961Z"
|
||||||
},
|
},
|
||||||
|
@ -327,7 +327,7 @@
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "js/modules/crypto.js",
|
"path": "js/modules/crypto.js",
|
||||||
"line": " return dcodeIO.ByteBuffer.wrap(base64string, 'base64').toArrayBuffer();",
|
"line": " return dcodeIO.ByteBuffer.wrap(base64string, 'base64').toArrayBuffer();",
|
||||||
"lineNumber": 274,
|
"lineNumber": 41,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-10-05T23:12:28.961Z"
|
"updated": "2018-10-05T23:12:28.961Z"
|
||||||
},
|
},
|
||||||
|
@ -335,7 +335,7 @@
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "js/modules/crypto.js",
|
"path": "js/modules/crypto.js",
|
||||||
"line": " return dcodeIO.ByteBuffer.wrap(key, 'binary').toArrayBuffer();",
|
"line": " return dcodeIO.ByteBuffer.wrap(key, 'binary').toArrayBuffer();",
|
||||||
"lineNumber": 278,
|
"lineNumber": 45,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-10-05T23:12:28.961Z"
|
"updated": "2018-10-05T23:12:28.961Z"
|
||||||
},
|
},
|
||||||
|
@ -343,7 +343,7 @@
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "js/modules/crypto.js",
|
"path": "js/modules/crypto.js",
|
||||||
"line": " return dcodeIO.ByteBuffer.wrap(string, 'utf8').toArrayBuffer();",
|
"line": " return dcodeIO.ByteBuffer.wrap(string, 'utf8').toArrayBuffer();",
|
||||||
"lineNumber": 282,
|
"lineNumber": 49,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-10-05T23:12:28.961Z"
|
"updated": "2018-10-05T23:12:28.961Z"
|
||||||
},
|
},
|
||||||
|
@ -351,7 +351,7 @@
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "js/modules/crypto.js",
|
"path": "js/modules/crypto.js",
|
||||||
"line": " return dcodeIO.ByteBuffer.wrap(buffer).toString('utf8');",
|
"line": " return dcodeIO.ByteBuffer.wrap(buffer).toString('utf8');",
|
||||||
"lineNumber": 285,
|
"lineNumber": 52,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-10-05T23:12:28.961Z"
|
"updated": "2018-10-05T23:12:28.961Z"
|
||||||
},
|
},
|
||||||
|
@ -655,474 +655,6 @@
|
||||||
"updated": "2018-09-19T18:13:29.628Z",
|
"updated": "2018-09-19T18:13:29.628Z",
|
||||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
"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-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/debug_log_view.js",
|
"path": "js/views/debug_log_view.js",
|
||||||
|
@ -1195,105 +727,6 @@
|
||||||
"updated": "2018-09-19T21:59:32.770Z",
|
"updated": "2018-09-19T21:59:32.770Z",
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
"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-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/group_member_list_view.js",
|
"path": "js/views/group_member_list_view.js",
|
||||||
|
@ -1692,7 +1125,7 @@
|
||||||
"rule": "jQuery-append(",
|
"rule": "jQuery-append(",
|
||||||
"path": "js/views/message_list_view.js",
|
"path": "js/views/message_list_view.js",
|
||||||
"line": " this.$messages.append(view.el);",
|
"line": " this.$messages.append(view.el);",
|
||||||
"lineNumber": 102,
|
"lineNumber": 111,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2018-11-14T18:51:15.180Z",
|
"updated": "2018-11-14T18:51:15.180Z",
|
||||||
"reasonDetail": "view.el is a known DOM element"
|
"reasonDetail": "view.el is a known DOM element"
|
||||||
|
@ -1701,7 +1134,7 @@
|
||||||
"rule": "jQuery-prepend(",
|
"rule": "jQuery-prepend(",
|
||||||
"path": "js/views/message_list_view.js",
|
"path": "js/views/message_list_view.js",
|
||||||
"line": " this.$messages.prepend(view.el);",
|
"line": " this.$messages.prepend(view.el);",
|
||||||
"lineNumber": 105,
|
"lineNumber": 114,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2018-11-14T18:51:15.180Z",
|
"updated": "2018-11-14T18:51:15.180Z",
|
||||||
"reasonDetail": "view.el is a known DOM element"
|
"reasonDetail": "view.el is a known DOM element"
|
||||||
|
@ -1710,7 +1143,7 @@
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/message_list_view.js",
|
"path": "js/views/message_list_view.js",
|
||||||
"line": " const next = this.$(`#${this.collection.at(index + 1).id}`);",
|
"line": " const next = this.$(`#${this.collection.at(index + 1).id}`);",
|
||||||
"lineNumber": 108,
|
"lineNumber": 117,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2018-11-14T18:51:15.180Z",
|
"updated": "2018-11-14T18:51:15.180Z",
|
||||||
"reasonDetail": "Message ids are GUIDs, and therefore the resultant string for $() is an id"
|
"reasonDetail": "Message ids are GUIDs, and therefore the resultant string for $() is an id"
|
||||||
|
@ -1719,7 +1152,7 @@
|
||||||
"rule": "jQuery-insertBefore(",
|
"rule": "jQuery-insertBefore(",
|
||||||
"path": "js/views/message_list_view.js",
|
"path": "js/views/message_list_view.js",
|
||||||
"line": " view.$el.insertBefore(next);",
|
"line": " view.$el.insertBefore(next);",
|
||||||
"lineNumber": 111,
|
"lineNumber": 120,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2018-11-14T18:51:15.180Z",
|
"updated": "2018-11-14T18:51:15.180Z",
|
||||||
"reasonDetail": "next is a known DOM element"
|
"reasonDetail": "next is a known DOM element"
|
||||||
|
@ -1728,7 +1161,7 @@
|
||||||
"rule": "jQuery-insertAfter(",
|
"rule": "jQuery-insertAfter(",
|
||||||
"path": "js/views/message_list_view.js",
|
"path": "js/views/message_list_view.js",
|
||||||
"line": " view.$el.insertAfter(prev);",
|
"line": " view.$el.insertAfter(prev);",
|
||||||
"lineNumber": 113,
|
"lineNumber": 122,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2018-11-14T18:51:15.180Z",
|
"updated": "2018-11-14T18:51:15.180Z",
|
||||||
"reasonDetail": "prev is a known DOM element"
|
"reasonDetail": "prev is a known DOM element"
|
||||||
|
@ -1737,7 +1170,7 @@
|
||||||
"rule": "jQuery-insertBefore(",
|
"rule": "jQuery-insertBefore(",
|
||||||
"path": "js/views/message_list_view.js",
|
"path": "js/views/message_list_view.js",
|
||||||
"line": " view.$el.insertBefore(elements[i]);",
|
"line": " view.$el.insertBefore(elements[i]);",
|
||||||
"lineNumber": 122,
|
"lineNumber": 131,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2018-11-14T18:51:15.180Z",
|
"updated": "2018-11-14T18:51:15.180Z",
|
||||||
"reasonDetail": "elements[i] is a known DOM element"
|
"reasonDetail": "elements[i] is a known DOM element"
|
||||||
|
@ -1746,7 +1179,7 @@
|
||||||
"rule": "jQuery-append(",
|
"rule": "jQuery-append(",
|
||||||
"path": "js/views/message_list_view.js",
|
"path": "js/views/message_list_view.js",
|
||||||
"line": " this.$messages.append(view.el);",
|
"line": " this.$messages.append(view.el);",
|
||||||
"lineNumber": 127,
|
"lineNumber": 136,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2018-11-14T18:51:15.180Z",
|
"updated": "2018-11-14T18:51:15.180Z",
|
||||||
"reasonDetail": "view.el is a known DOM element"
|
"reasonDetail": "view.el is a known DOM element"
|
||||||
|
@ -2352,7 +1785,7 @@
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "libtextsecure/message_receiver.js",
|
"path": "libtextsecure/message_receiver.js",
|
||||||
"line": " const buffer = dcodeIO.ByteBuffer.wrap(ciphertext);",
|
"line": " const buffer = dcodeIO.ByteBuffer.wrap(ciphertext);",
|
||||||
"lineNumber": 785,
|
"lineNumber": 794,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
"updated": "2018-09-19T18:13:29.628Z"
|
||||||
},
|
},
|
||||||
|
@ -2360,7 +1793,7 @@
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "libtextsecure/message_receiver.js",
|
"path": "libtextsecure/message_receiver.js",
|
||||||
"line": " const buffer = dcodeIO.ByteBuffer.wrap(ciphertext);",
|
"line": " const buffer = dcodeIO.ByteBuffer.wrap(ciphertext);",
|
||||||
"lineNumber": 810,
|
"lineNumber": 819,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
"updated": "2018-09-19T18:13:29.628Z"
|
||||||
},
|
},
|
||||||
|
@ -3174,7 +2607,7 @@
|
||||||
"rule": "jQuery-append(",
|
"rule": "jQuery-append(",
|
||||||
"path": "node_modules/electron/electron.d.ts",
|
"path": "node_modules/electron/electron.d.ts",
|
||||||
"line": " append(menuItem: MenuItem): void;",
|
"line": " append(menuItem: MenuItem): void;",
|
||||||
"lineNumber": 3243,
|
"lineNumber": 3232,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
"updated": "2018-09-19T18:13:29.628Z"
|
||||||
},
|
},
|
||||||
|
@ -3493,7 +2926,7 @@
|
||||||
"lineNumber": 4136,
|
"lineNumber": 4136,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-19T18:13:29.628Z",
|
"updated": "2018-09-19T18:13:29.628Z",
|
||||||
"reasonDetail": "<optional>"
|
"reasonDetail": "Protected from arbitrary input"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
|
@ -4083,7 +3516,7 @@
|
||||||
"lineNumber": 483,
|
"lineNumber": 483,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2018-09-19T21:59:32.770Z",
|
"updated": "2018-09-19T21:59:32.770Z",
|
||||||
"reasonDetail": "<optional>"
|
"reasonDetail": "Protected from arbitrary input"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-appendTo(",
|
"rule": "jQuery-appendTo(",
|
||||||
|
@ -5849,7 +5282,7 @@
|
||||||
"lineNumber": 1699,
|
"lineNumber": 1699,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-18T19:19:27.699Z",
|
"updated": "2018-09-18T19:19:27.699Z",
|
||||||
"reasonDetail": "<optional>"
|
"reasonDetail": "Protected from arbitrary input"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "DOM-innerHTML",
|
"rule": "DOM-innerHTML",
|
||||||
|
|
|
@ -49,6 +49,10 @@ const allSourceFiles = glob.sync(searchPattern, { nodir: true });
|
||||||
const results: Array<ExceptionType> = [];
|
const results: Array<ExceptionType> = [];
|
||||||
|
|
||||||
const excludedFiles = [
|
const excludedFiles = [
|
||||||
|
// High-traffic files in our project
|
||||||
|
'^js/views/conversation_view.js',
|
||||||
|
'^js/views/file_input_view.js',
|
||||||
|
|
||||||
// Generated files
|
// Generated files
|
||||||
'^js/components.js',
|
'^js/components.js',
|
||||||
'^js/libtextsecure.js',
|
'^js/libtextsecure.js',
|
||||||
|
|
61
yarn.lock
61
yarn.lock
|
@ -22,9 +22,9 @@
|
||||||
"7zip-bin-mac" "~1.0.1"
|
"7zip-bin-mac" "~1.0.1"
|
||||||
"7zip-bin-win" "~2.2.0"
|
"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"
|
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:
|
dependencies:
|
||||||
nan "^2.10.0"
|
nan "^2.10.0"
|
||||||
node-pre-gyp "^0.10.0"
|
node-pre-gyp "^0.10.0"
|
||||||
|
@ -391,20 +391,6 @@ archiver-utils@^1.3.0:
|
||||||
normalize-path "^2.0.0"
|
normalize-path "^2.0.0"
|
||||||
readable-stream "^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:
|
archiver@~2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/archiver/-/archiver-2.1.0.tgz#d2df2e8d5773a82c1dcce925ccc41450ea999afd"
|
resolved "https://registry.yarnpkg.com/archiver/-/archiver-2.1.0.tgz#d2df2e8d5773a82c1dcce925ccc41450ea999afd"
|
||||||
|
@ -1363,6 +1349,11 @@ chownr@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181"
|
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:
|
chrome-trace-event@^0.1.1:
|
||||||
version "0.1.2"
|
version "0.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-0.1.2.tgz#90f36885d5345a50621332f0717b595883d5d982"
|
resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-0.1.2.tgz#90f36885d5345a50621332f0717b595883d5d982"
|
||||||
|
@ -2526,10 +2517,10 @@ electron-updater@2.21.10:
|
||||||
semver "^5.5.0"
|
semver "^5.5.0"
|
||||||
source-map-support "^0.5.5"
|
source-map-support "^0.5.5"
|
||||||
|
|
||||||
electron@3.0.9:
|
electron@3.0.14:
|
||||||
version "3.0.9"
|
version "3.0.14"
|
||||||
resolved "https://registry.yarnpkg.com/electron/-/electron-3.0.9.tgz#79bd25dfd5496918a00d579e702fb83082f1a036"
|
resolved "https://registry.yarnpkg.com/electron/-/electron-3.0.14.tgz#d54c51de3651c0fe48a6a6e9aef1ca98e5ea5796"
|
||||||
integrity sha512-OoSoeUWo9PzbArgrwS1yTfTRSlpXmIgrFGWUuUZCjKAk4DGR70elHDNeRnnBJ9NTwXXZVifChcfx73Ah3GnlVQ==
|
integrity sha512-1fG9bE0LzL5QXeEq2MC0dHdVO0pbZOnNlVAIyOyJaCFAu/TjLhxQfWj38bFUEojzuVlaR87tZz0iy2qlVZj3sw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "^8.0.24"
|
"@types/node" "^8.0.24"
|
||||||
electron-download "^4.1.0"
|
electron-download "^4.1.0"
|
||||||
|
@ -3104,6 +3095,7 @@ file-sync-cmp@^0.1.0:
|
||||||
file-type@^3.1.0:
|
file-type@^3.1.0:
|
||||||
version "3.9.0"
|
version "3.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/file-type/-/file-type-3.9.0.tgz#257a078384d1db8087bc449d107d52a52672b9e9"
|
resolved "https://registry.yarnpkg.com/file-type/-/file-type-3.9.0.tgz#257a078384d1db8087bc449d107d52a52672b9e9"
|
||||||
|
integrity sha1-JXoHg4TR24CHvESdEH1SpSZyuek=
|
||||||
|
|
||||||
file-uri-to-path@1:
|
file-uri-to-path@1:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
|
@ -5500,12 +5492,27 @@ minipass@^2.2.1, minipass@^2.3.3:
|
||||||
safe-buffer "^5.1.2"
|
safe-buffer "^5.1.2"
|
||||||
yallist "^3.0.0"
|
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:
|
minizlib@^1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.1.0.tgz#11e13658ce46bc3a70a267aac58359d1e0c29ceb"
|
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.1.0.tgz#11e13658ce46bc3a70a267aac58359d1e0c29ceb"
|
||||||
dependencies:
|
dependencies:
|
||||||
minipass "^2.2.1"
|
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:
|
mississippi@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-2.0.0.tgz#3442a508fafc28500486feea99409676e4ee5a6f"
|
resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-2.0.0.tgz#3442a508fafc28500486feea99409676e4ee5a6f"
|
||||||
|
@ -8340,6 +8347,7 @@ string-width@^2.1.0, string-width@^2.1.1:
|
||||||
string_decoder@^1.0.0:
|
string_decoder@^1.0.0:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
|
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
|
||||||
|
integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
|
||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer "~5.1.0"
|
safe-buffer "~5.1.0"
|
||||||
|
|
||||||
|
@ -8507,6 +8515,19 @@ tar-stream@^1.5.0:
|
||||||
readable-stream "^2.0.0"
|
readable-stream "^2.0.0"
|
||||||
xtend "^4.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:
|
tar@^2.0.0:
|
||||||
version "2.2.1"
|
version "2.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1"
|
resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1"
|
||||||
|
|
Loading…
Reference in New Issue