mirror of
https://github.com/oxen-io/session-desktop.git
synced 2023-12-14 02:12:57 +01:00
commit
5e72521b21
81 changed files with 13526 additions and 7907 deletions
|
@ -26,3 +26,8 @@ test/blanket_mocha.js
|
|||
|
||||
# TypeScript generated files
|
||||
ts/**/*.js
|
||||
|
||||
# Libloki specific files
|
||||
libloki/test/components.js
|
||||
libloki/mnemonic.js
|
||||
libloki/sc_reduce32.js
|
||||
|
|
|
@ -39,6 +39,9 @@ module.exports = {
|
|||
// consistently place operators at end of line except ternaries
|
||||
'operator-linebreak': 'error',
|
||||
|
||||
// Use LF to stay consistent
|
||||
'linebreak-style': ['error', 'unix'],
|
||||
|
||||
quotes: [
|
||||
'error',
|
||||
'single',
|
||||
|
|
10
.gitattributes
vendored
Normal file
10
.gitattributes
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
* text=auto
|
||||
*.js text eol=lf
|
||||
*.ts text eol=lf
|
||||
*.tsx text eol=lf
|
||||
*.scss text eol=lf
|
||||
*.json text eol=lf
|
||||
*.css text eol=lf
|
||||
*.html text eol=lf
|
||||
*.yaml text eol=lf
|
||||
*.yml text eol=lf
|
|
@ -19,6 +19,10 @@ ts/protobuf/*.js
|
|||
stylesheets/manifest.css
|
||||
ts/util/lint/exceptions.json
|
||||
|
||||
# Libloki specific files
|
||||
libloki/test/test.js
|
||||
libloki/test/components.js
|
||||
|
||||
# Third-party files
|
||||
node_modules/**
|
||||
components/**
|
||||
|
|
|
@ -9,6 +9,7 @@ install:
|
|||
- travis_wait 30 yarn install --frozen-lockfile --network-timeout 1000000
|
||||
script:
|
||||
- yarn generate
|
||||
- yarn lint-windows
|
||||
- yarn test
|
||||
env:
|
||||
global:
|
||||
|
@ -35,3 +36,9 @@ matrix:
|
|||
cache: false
|
||||
env:
|
||||
- YARN_GPG=no
|
||||
before_install:
|
||||
- cd ../..
|
||||
- mv $TRAVIS_REPO_SLUG _old
|
||||
- git config --global core.autocrlf false
|
||||
- git clone --depth=50 _old $TRAVIS_REPO_SLUG
|
||||
- cd $TRAVIS_REPO_SLUG
|
||||
|
|
|
@ -181,7 +181,7 @@ module.exports = grunt => {
|
|||
tasks: ['sass'],
|
||||
},
|
||||
transpile: {
|
||||
files: ['./ts/**/*.ts','./ts/**/*.tsx'],
|
||||
files: ['./ts/**/*.ts', './ts/**/*.tsx'],
|
||||
tasks: ['exec:transpile'],
|
||||
},
|
||||
},
|
||||
|
@ -491,7 +491,11 @@ module.exports = grunt => {
|
|||
'locale-patch',
|
||||
]);
|
||||
grunt.registerTask('dev', ['default', 'watch']);
|
||||
grunt.registerTask('test', ['unit-tests', 'lib-unit-tests', 'loki-unit-tests']);
|
||||
grunt.registerTask('test', [
|
||||
'unit-tests',
|
||||
'lib-unit-tests',
|
||||
'loki-unit-tests',
|
||||
]);
|
||||
grunt.registerTask('date', ['gitinfo', 'getExpireTime']);
|
||||
grunt.registerTask('default', [
|
||||
'exec:build-protobuf',
|
||||
|
|
|
@ -1084,11 +1084,13 @@
|
|||
"description": "Title of the Message TTL setting"
|
||||
},
|
||||
"messageTTLSettingDescription": {
|
||||
"message": "Time to live (how long the recipient will have to collect their messages)",
|
||||
"message":
|
||||
"Time to live (how long the recipient will have to collect their messages)",
|
||||
"description": "Description of the time to live setting"
|
||||
},
|
||||
"messageTTLSettingWarning": {
|
||||
"message": "Warning! Lowering the TTL could result in messages being lost if the recipient doesn't collect them in time!",
|
||||
"message":
|
||||
"Warning! Lowering the TTL could result in messages being lost if the recipient doesn't collect them in time!",
|
||||
"description": "Warning for the time to live setting"
|
||||
},
|
||||
"notificationSettingsDialog": {
|
||||
|
@ -1629,23 +1631,28 @@
|
|||
},
|
||||
"friendRequestPending": {
|
||||
"message": "Friend request",
|
||||
"description": "Shown in the conversation history when the user sends or recieves a friend request"
|
||||
"description":
|
||||
"Shown in the conversation history when the user sends or recieves a friend request"
|
||||
},
|
||||
"friendRequestAccepted": {
|
||||
"message": "Friend request accepted",
|
||||
"description": "Shown in the conversation history when the user accepts a friend request"
|
||||
"description":
|
||||
"Shown in the conversation history when the user accepts a friend request"
|
||||
},
|
||||
"friendRequestDeclined": {
|
||||
"message": "Friend request declined",
|
||||
"description": "Shown in the conversation history when the user declines a friend request"
|
||||
"description":
|
||||
"Shown in the conversation history when the user declines a friend request"
|
||||
},
|
||||
"friendRequestExpired": {
|
||||
"message": "Friend request expired",
|
||||
"description": "Shown in the conversation history when the users friend request expires"
|
||||
"description":
|
||||
"Shown in the conversation history when the users friend request expires"
|
||||
},
|
||||
"friendRequestNotificationTitle": {
|
||||
"message": "Friend request",
|
||||
"description": "Shown in a notification title when receiving a friend request"
|
||||
"description":
|
||||
"Shown in a notification title when receiving a friend request"
|
||||
},
|
||||
"friendRequestNotificationMessage": {
|
||||
"message": "$name$ sent you a friend request",
|
||||
|
@ -1660,7 +1667,8 @@
|
|||
},
|
||||
"friendRequestAcceptedNotificationTitle": {
|
||||
"message": "Friend request accepted",
|
||||
"description": "Shown in a notification title when friend request was accepted by the other user"
|
||||
"description":
|
||||
"Shown in a notification title when friend request was accepted by the other user"
|
||||
},
|
||||
"friendRequestAcceptedNotificationMessage": {
|
||||
"message": "$name$ accepted your friend request",
|
||||
|
@ -1681,7 +1689,8 @@
|
|||
},
|
||||
"settingsUnblockHeader": {
|
||||
"message": "Blocked Users",
|
||||
"description": "Shown in the settings page as the heading for the blocked user settings"
|
||||
"description":
|
||||
"Shown in the settings page as the heading for the blocked user settings"
|
||||
},
|
||||
"editProfileTitle": {
|
||||
"message": "Change your own display name",
|
||||
|
@ -1694,7 +1703,8 @@
|
|||
|
||||
"copyPublicKey": {
|
||||
"message": "Copy public key",
|
||||
"description": "Button action that the user can click to copy their public keys"
|
||||
"description":
|
||||
"Button action that the user can click to copy their public keys"
|
||||
},
|
||||
"copiedPublicKey": {
|
||||
"message": "Copied public key",
|
||||
|
@ -1702,21 +1712,25 @@
|
|||
},
|
||||
"editDisplayName": {
|
||||
"message": "Edit display name",
|
||||
"description": "Button action that the user can click to edit their display name"
|
||||
"description":
|
||||
"Button action that the user can click to edit their display name"
|
||||
},
|
||||
"showSeed": {
|
||||
"message": "Show seed",
|
||||
"description": "Button action that the user can click to view their unique seed"
|
||||
"description":
|
||||
"Button action that the user can click to view their unique seed"
|
||||
},
|
||||
|
||||
"copiedMnemonic": {
|
||||
"message": "Copied seed to clipboard",
|
||||
"description": "A toast message telling the user that the mnemonic seed was copied"
|
||||
"description":
|
||||
"A toast message telling the user that the mnemonic seed was copied"
|
||||
},
|
||||
|
||||
"passwordViewTitle": {
|
||||
"message": "Type in your password",
|
||||
"description": "The title shown when user needs to type in a password to unlock the messenger"
|
||||
"description":
|
||||
"The title shown when user needs to type in a password to unlock the messenger"
|
||||
},
|
||||
"unlock": {
|
||||
"message": "Unlock"
|
||||
|
@ -1765,15 +1779,18 @@
|
|||
},
|
||||
"passwordLengthError": {
|
||||
"message": "Password must be between 6 and 50 characters long",
|
||||
"description": "Error string shown to the user when password doesn't meet length criteria"
|
||||
"description":
|
||||
"Error string shown to the user when password doesn't meet length criteria"
|
||||
},
|
||||
"passwordTypeError": {
|
||||
"message": "Password must be a string",
|
||||
"description": "Error string shown to the user when password is not a string"
|
||||
"description":
|
||||
"Error string shown to the user when password is not a string"
|
||||
},
|
||||
"passwordCharacterError": {
|
||||
"message": "Password must only contain letters, numbers and symbols",
|
||||
"description": "Error string shown to the user when password contains an invalid character"
|
||||
"description":
|
||||
"Error string shown to the user when password contains an invalid character"
|
||||
},
|
||||
"change": {
|
||||
"message": "Change"
|
||||
|
@ -1784,5 +1801,4 @@
|
|||
"remove": {
|
||||
"message": "Remove"
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -6,8 +6,9 @@ const ERRORS = {
|
|||
CHARACTER: 'Password must only contain letters, numbers and symbols',
|
||||
};
|
||||
|
||||
const generateHash = (phrase) => phrase && sha512(phrase.trim());
|
||||
const matchesHash = (phrase, hash) => phrase && sha512(phrase.trim()) === hash.trim();
|
||||
const generateHash = phrase => phrase && sha512(phrase.trim());
|
||||
const matchesHash = (phrase, hash) =>
|
||||
phrase && sha512(phrase.trim()) === hash.trim();
|
||||
|
||||
const validatePassword = (phrase, i18n) => {
|
||||
if (!phrase || typeof phrase !== 'string') {
|
||||
|
@ -20,13 +21,13 @@ const validatePassword = (phrase, i18n) => {
|
|||
}
|
||||
|
||||
// Restrict characters to letters, numbers and symbols
|
||||
const characterRegex = /^[a-zA-Z0-9-!()._`~@#$%^&*+=[\]{}|<>,;: ]+$/
|
||||
const characterRegex = /^[a-zA-Z0-9-!()._`~@#$%^&*+=[\]{}|<>,;: ]+$/;
|
||||
if (!characterRegex.test(trimmed)) {
|
||||
return i18n ? i18n('passwordCharacterError') : ERRORS.CHARACTER;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
generateHash,
|
||||
|
|
|
@ -15,8 +15,7 @@ const hasImage = pubKey => fs.existsSync(getImagePath(pubKey));
|
|||
const getImagePath = pubKey => `${PATH}/${pubKey}.png`;
|
||||
const getOrCreateImagePath = pubKey => {
|
||||
// If the image doesn't exist then create it
|
||||
if (!hasImage(pubKey))
|
||||
return generateImage(pubKey);
|
||||
if (!hasImage(pubKey)) return generateImage(pubKey);
|
||||
|
||||
return getImagePath(pubKey);
|
||||
};
|
||||
|
@ -25,10 +24,11 @@ const removeImage = pubKey => {
|
|||
if (hasImage(pubKey)) {
|
||||
fs.unlinkSync(getImagePath(pubKey));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const removeImagesNotInArray = pubKeyArray => {
|
||||
fs.readdirSync(PATH)
|
||||
fs
|
||||
.readdirSync(PATH)
|
||||
// Get all files that end with png
|
||||
.filter(file => file.includes('.png'))
|
||||
// Strip the extension
|
||||
|
@ -37,7 +37,7 @@ const removeImagesNotInArray = pubKeyArray => {
|
|||
.filter(i => !pubKeyArray.includes(i))
|
||||
// Remove them
|
||||
.forEach(i => removeImage(i));
|
||||
}
|
||||
};
|
||||
|
||||
const generateImage = pubKey => {
|
||||
const imagePath = getImagePath(pubKey);
|
||||
|
@ -52,8 +52,8 @@ const generateImage = pubKey => {
|
|||
background: [0, 0, 0, 0],
|
||||
}).toString();
|
||||
fs.writeFileSync(imagePath, png, 'base64');
|
||||
return imagePath
|
||||
}
|
||||
return imagePath;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
generateImage,
|
||||
|
|
84
app/sql.js
84
app/sql.js
|
@ -724,9 +724,12 @@ async function getPreKeyById(id) {
|
|||
return getById(PRE_KEYS_TABLE, id);
|
||||
}
|
||||
async function getPreKeyByRecipient(recipient) {
|
||||
const row = await db.get(`SELECT * FROM ${PRE_KEYS_TABLE} WHERE recipient = $recipient;`, {
|
||||
$recipient: recipient,
|
||||
});
|
||||
const row = await db.get(
|
||||
`SELECT * FROM ${PRE_KEYS_TABLE} WHERE recipient = $recipient;`,
|
||||
{
|
||||
$recipient: recipient,
|
||||
}
|
||||
);
|
||||
|
||||
if (!row) {
|
||||
return null;
|
||||
|
@ -769,9 +772,12 @@ async function getContactPreKeyById(id) {
|
|||
return getById(CONTACT_PRE_KEYS_TABLE, id);
|
||||
}
|
||||
async function getContactPreKeyByIdentityKey(key) {
|
||||
const row = await db.get(`SELECT * FROM ${CONTACT_PRE_KEYS_TABLE} WHERE identityKeyString = $identityKeyString ORDER BY keyId DESC LIMIT 1;`, {
|
||||
$identityKeyString: key,
|
||||
});
|
||||
const row = await db.get(
|
||||
`SELECT * FROM ${CONTACT_PRE_KEYS_TABLE} WHERE identityKeyString = $identityKeyString ORDER BY keyId DESC LIMIT 1;`,
|
||||
{
|
||||
$identityKeyString: key,
|
||||
}
|
||||
);
|
||||
|
||||
if (!row) {
|
||||
return null;
|
||||
|
@ -792,9 +798,12 @@ async function bulkAddContactPreKeys(array) {
|
|||
return bulkAdd(CONTACT_PRE_KEYS_TABLE, array);
|
||||
}
|
||||
async function removeContactPreKeyByIdentityKey(key) {
|
||||
await db.run(`DELETE FROM ${CONTACT_PRE_KEYS_TABLE} WHERE identityKeyString = $identityKeyString;`, {
|
||||
$identityKeyString: key,
|
||||
});
|
||||
await db.run(
|
||||
`DELETE FROM ${CONTACT_PRE_KEYS_TABLE} WHERE identityKeyString = $identityKeyString;`,
|
||||
{
|
||||
$identityKeyString: key,
|
||||
}
|
||||
);
|
||||
}
|
||||
async function removeAllContactPreKeys() {
|
||||
return removeAllFromTable(CONTACT_PRE_KEYS_TABLE);
|
||||
|
@ -825,9 +834,12 @@ async function getContactSignedPreKeyById(id) {
|
|||
return getById(CONTACT_SIGNED_PRE_KEYS_TABLE, id);
|
||||
}
|
||||
async function getContactSignedPreKeyByIdentityKey(key) {
|
||||
const row = await db.get(`SELECT * FROM ${CONTACT_SIGNED_PRE_KEYS_TABLE} WHERE identityKeyString = $identityKeyString;`, {
|
||||
$identityKeyString: key,
|
||||
});
|
||||
const row = await db.get(
|
||||
`SELECT * FROM ${CONTACT_SIGNED_PRE_KEYS_TABLE} WHERE identityKeyString = $identityKeyString;`,
|
||||
{
|
||||
$identityKeyString: key,
|
||||
}
|
||||
);
|
||||
|
||||
if (!row) {
|
||||
return null;
|
||||
|
@ -847,9 +859,12 @@ async function bulkAddContactSignedPreKeys(array) {
|
|||
return bulkAdd(CONTACT_SIGNED_PRE_KEYS_TABLE, array);
|
||||
}
|
||||
async function removeContactSignedPreKeyByIdentityKey(key) {
|
||||
await db.run(`DELETE FROM ${CONTACT_SIGNED_PRE_KEYS_TABLE} WHERE identityKeyString = $identityKeyString;`, {
|
||||
$identityKeyString: key,
|
||||
});
|
||||
await db.run(
|
||||
`DELETE FROM ${CONTACT_SIGNED_PRE_KEYS_TABLE} WHERE identityKeyString = $identityKeyString;`,
|
||||
{
|
||||
$identityKeyString: key,
|
||||
}
|
||||
);
|
||||
}
|
||||
async function removeAllContactSignedPreKeys() {
|
||||
return removeAllFromTable(CONTACT_SIGNED_PRE_KEYS_TABLE);
|
||||
|
@ -1049,8 +1064,16 @@ async function getConversationCount() {
|
|||
}
|
||||
|
||||
async function saveConversation(data) {
|
||||
// eslint-disable-next-line camelcase
|
||||
const { id, active_at, type, members, name, friendRequestStatus, profileName } = data;
|
||||
const {
|
||||
id,
|
||||
// eslint-disable-next-line camelcase
|
||||
active_at,
|
||||
type,
|
||||
members,
|
||||
name,
|
||||
friendRequestStatus,
|
||||
profileName,
|
||||
} = data;
|
||||
|
||||
await db.run(
|
||||
`INSERT INTO conversations (
|
||||
|
@ -1105,8 +1128,16 @@ async function saveConversations(arrayOfConversations) {
|
|||
}
|
||||
|
||||
async function updateConversation(data) {
|
||||
// eslint-disable-next-line camelcase
|
||||
const { id, active_at, type, members, name, friendRequestStatus, profileName } = data;
|
||||
const {
|
||||
id,
|
||||
// eslint-disable-next-line camelcase
|
||||
active_at,
|
||||
type,
|
||||
members,
|
||||
name,
|
||||
friendRequestStatus,
|
||||
profileName,
|
||||
} = data;
|
||||
|
||||
await db.run(
|
||||
`UPDATE conversations SET
|
||||
|
@ -1366,10 +1397,7 @@ async function saveSeenMessageHashes(arrayOfHashes) {
|
|||
}
|
||||
|
||||
async function saveSeenMessageHash(data) {
|
||||
const {
|
||||
expiresAt,
|
||||
hash,
|
||||
} = data;
|
||||
const { expiresAt, hash } = data;
|
||||
await db.run(
|
||||
`INSERT INTO seenMessages (
|
||||
expiresAt,
|
||||
|
@ -1377,7 +1405,8 @@ async function saveSeenMessageHash(data) {
|
|||
) values (
|
||||
$expiresAt,
|
||||
$hash
|
||||
);`, {
|
||||
);`,
|
||||
{
|
||||
$expiresAt: expiresAt,
|
||||
$hash: hash,
|
||||
}
|
||||
|
@ -1489,7 +1518,8 @@ async function getMessagesByConversation(
|
|||
conversationId,
|
||||
{ limit = 100, receivedAt = Number.MAX_VALUE, type = '%' } = {}
|
||||
) {
|
||||
const rows = await db.all(`
|
||||
const rows = await db.all(
|
||||
`
|
||||
SELECT json FROM messages WHERE
|
||||
conversationId = $conversationId AND
|
||||
received_at < $received_at AND
|
||||
|
@ -1522,7 +1552,9 @@ async function getMessagesBySentAt(sentAt) {
|
|||
|
||||
async function getSeenMessagesByHashList(hashes) {
|
||||
const rows = await db.all(
|
||||
`SELECT * FROM seenMessages WHERE hash IN ( ${hashes.map(() => '?').join(', ')} );`,
|
||||
`SELECT * FROM seenMessages WHERE hash IN ( ${hashes
|
||||
.map(() => '?')
|
||||
.join(', ')} );`,
|
||||
hashes
|
||||
);
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
*/
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(async function () {
|
||||
(async function() {
|
||||
'use strict';
|
||||
|
||||
// Globally disable drag and drop
|
||||
|
@ -240,12 +240,11 @@
|
|||
setMessageTTL: value => {
|
||||
// Make sure the ttl is between a given range and is valid
|
||||
const intValue = parseInt(value, 10);
|
||||
const ttl = Number.isNaN(intValue) ? 24 : intValue;
|
||||
const ttl = Number.isNaN(intValue) ? 24 : intValue;
|
||||
storage.put('message-ttl', ttl);
|
||||
},
|
||||
|
||||
getReadReceiptSetting: () =>
|
||||
storage.get('read-receipt-setting'),
|
||||
getReadReceiptSetting: () => storage.get('read-receipt-setting'),
|
||||
setReadReceiptSetting: value =>
|
||||
storage.put('read-receipt-setting', value),
|
||||
getNotificationSetting: () =>
|
||||
|
@ -331,12 +330,19 @@
|
|||
|
||||
window.log.info('Cleanup: starting...');
|
||||
const results = await Promise.all([
|
||||
window.Signal.Data.getOutgoingWithoutExpiresAt({ MessageCollection: Whisper.MessageCollection }),
|
||||
window.Signal.Data.getAllUnsentMessages({ MessageCollection: Whisper.MessageCollection }),
|
||||
window.Signal.Data.getOutgoingWithoutExpiresAt({
|
||||
MessageCollection: Whisper.MessageCollection,
|
||||
}),
|
||||
window.Signal.Data.getAllUnsentMessages({
|
||||
MessageCollection: Whisper.MessageCollection,
|
||||
}),
|
||||
]);
|
||||
|
||||
// Combine the models
|
||||
const messagesForCleanup = results.reduce((array, current) => array.concat(current.toArray()), []);
|
||||
const messagesForCleanup = results.reduce(
|
||||
(array, current) => array.concat(current.toArray()),
|
||||
[]
|
||||
);
|
||||
|
||||
window.log.info(
|
||||
`Cleanup: Found ${messagesForCleanup.length} messages for cleanup`
|
||||
|
@ -350,7 +356,10 @@
|
|||
);
|
||||
|
||||
// Make sure we only target outgoing messages
|
||||
if (message.isFriendRequest() && message.get('direction') === 'incoming') {
|
||||
if (
|
||||
message.isFriendRequest() &&
|
||||
message.get('direction') === 'incoming'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -392,7 +401,7 @@
|
|||
let isMigrationWithIndexComplete = false;
|
||||
window.log.info(
|
||||
`Starting background data migration. Target version: ${
|
||||
Message.CURRENT_SCHEMA_VERSION
|
||||
Message.CURRENT_SCHEMA_VERSION
|
||||
}`
|
||||
);
|
||||
idleDetector.on('idle', async () => {
|
||||
|
@ -589,7 +598,7 @@
|
|||
title: window.i18n('editProfileTitle'),
|
||||
message: window.i18n('editProfileDisplayNameWarning'),
|
||||
nickname: displayName,
|
||||
onOk: async (newName) => {
|
||||
onOk: async newName => {
|
||||
await storage.setProfileName(newName);
|
||||
|
||||
// Update the conversation if we have it
|
||||
|
@ -599,7 +608,7 @@
|
|||
conversation.setProfile(newProfile);
|
||||
}
|
||||
},
|
||||
})
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1346,7 +1355,7 @@
|
|||
type: 'friend-request',
|
||||
friendStatus: 'pending',
|
||||
direction: 'incoming',
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const message = new Whisper.Message(messageData);
|
||||
|
@ -1371,7 +1380,7 @@
|
|||
} catch (error) {
|
||||
window.log.error(
|
||||
`Failed to send delivery receipt to ${data.source} for message ${
|
||||
data.timestamp
|
||||
data.timestamp
|
||||
}:`,
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
|
|
|
@ -21,7 +21,9 @@
|
|||
window.log.info('BlockedNumberController: starting initial fetch');
|
||||
|
||||
if (!storage) {
|
||||
throw new Error('BlockedNumberController: Could not load blocked numbers');
|
||||
throw new Error(
|
||||
'BlockedNumberController: Could not load blocked numbers'
|
||||
);
|
||||
}
|
||||
|
||||
// Add the numbers to the collection
|
||||
|
@ -40,8 +42,7 @@
|
|||
storage.addBlockedNumber(number);
|
||||
|
||||
// Make sure we don't add duplicates
|
||||
if (blockedNumbers.getModel(number))
|
||||
return;
|
||||
if (blockedNumbers.getModel(number)) return;
|
||||
|
||||
blockedNumbers.add({ number });
|
||||
},
|
||||
|
|
|
@ -80,9 +80,16 @@
|
|||
|
||||
const contactCollection = new (Backbone.Collection.extend({
|
||||
initialize() {
|
||||
this.on('change:timestamp change:name change:number change:profileName', this.sort);
|
||||
this.on(
|
||||
'change:timestamp change:name change:number change:profileName',
|
||||
this.sort
|
||||
);
|
||||
|
||||
this.listenTo(conversations, 'add change:active_at change:friendRequestStatus', this.addActive);
|
||||
this.listenTo(
|
||||
conversations,
|
||||
'add change:active_at change:friendRequestStatus',
|
||||
this.addActive
|
||||
);
|
||||
this.listenTo(conversations, 'reset', () => this.reset([]));
|
||||
|
||||
this.collator = new Intl.Collator();
|
||||
|
@ -193,7 +200,9 @@
|
|||
return conversation;
|
||||
};
|
||||
|
||||
conversation.initialPromise = create().then(() => conversation.updateProfileAvatar());
|
||||
conversation.initialPromise = create().then(() =>
|
||||
conversation.updateProfileAvatar()
|
||||
);
|
||||
|
||||
return conversation;
|
||||
},
|
||||
|
@ -265,7 +274,9 @@
|
|||
await Promise.all(promises);
|
||||
|
||||
// Remove any unused images
|
||||
window.profileImages.removeImagesNotInArray(conversations.map(c => c.id));
|
||||
window.profileImages.removeImagesNotInArray(
|
||||
conversations.map(c => c.id)
|
||||
);
|
||||
|
||||
window.log.info('ConversationController: done with initial fetch');
|
||||
} catch (error) {
|
||||
|
|
|
@ -54,7 +54,7 @@ if (window.console) {
|
|||
console._log = console.log;
|
||||
console.log = log;
|
||||
console._trace = console.trace;
|
||||
console._debug = console.debug
|
||||
console._debug = console.debug;
|
||||
console._info = console.info;
|
||||
console._warn = console.warn;
|
||||
console._error = console.error;
|
||||
|
@ -101,7 +101,7 @@ function fetch() {
|
|||
}
|
||||
|
||||
const publish = debuglogs.upload;
|
||||
const development = (window.getEnvironment() !== 'production');
|
||||
const development = window.getEnvironment() !== 'production';
|
||||
|
||||
// A modern logging interface for the browser
|
||||
|
||||
|
|
|
@ -89,5 +89,4 @@
|
|||
return this.models.find(m => m.get('number') === number);
|
||||
},
|
||||
});
|
||||
|
||||
})();
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
/* eslint-disable more/no-then */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
@ -35,6 +35,7 @@
|
|||
upgradeMessageSchema,
|
||||
loadAttachmentData,
|
||||
getAbsoluteAttachmentPath,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
writeNewAttachmentData,
|
||||
deleteAttachmentData,
|
||||
} = window.Signal.Migrations;
|
||||
|
@ -323,7 +324,9 @@
|
|||
if (this.id !== pubKey) return;
|
||||
|
||||
// Go through our messages and find the one that we need to update
|
||||
const messages = this.messageCollection.models.filter(m => m.get('sent_at') === timestamp);
|
||||
const messages = this.messageCollection.models.filter(
|
||||
m => m.get('sent_at') === timestamp
|
||||
);
|
||||
await Promise.all(messages.map(m => m.setCalculatingPoW()));
|
||||
},
|
||||
|
||||
|
@ -372,8 +375,7 @@
|
|||
// Get the pending friend requests that match the direction
|
||||
// If no direction is supplied then return all pending friend requests
|
||||
return messages.models.filter(m => {
|
||||
if (m.get('friendStatus') !== 'pending')
|
||||
return false;
|
||||
if (m.get('friendStatus') !== 'pending') return false;
|
||||
return direction === null || m.get('direction') === direction;
|
||||
});
|
||||
},
|
||||
|
@ -462,7 +464,7 @@
|
|||
if (!this.isPrivate()) {
|
||||
throw new Error(
|
||||
'You cannot verify a group conversation. ' +
|
||||
'You must verify individual contacts.'
|
||||
'You must verify individual contacts.'
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -547,18 +549,27 @@
|
|||
},
|
||||
isPendingFriendRequest() {
|
||||
const status = this.get('friendRequestStatus');
|
||||
return status === FriendRequestStatusEnum.requestSent ||
|
||||
return (
|
||||
status === FriendRequestStatusEnum.requestSent ||
|
||||
status === FriendRequestStatusEnum.requestReceived ||
|
||||
status === FriendRequestStatusEnum.pendingSend;
|
||||
status === FriendRequestStatusEnum.pendingSend
|
||||
);
|
||||
},
|
||||
hasSentFriendRequest() {
|
||||
return this.get('friendRequestStatus') === FriendRequestStatusEnum.requestSent;
|
||||
return (
|
||||
this.get('friendRequestStatus') === FriendRequestStatusEnum.requestSent
|
||||
);
|
||||
},
|
||||
hasReceivedFriendRequest() {
|
||||
return this.get('friendRequestStatus') === FriendRequestStatusEnum.requestReceived;
|
||||
return (
|
||||
this.get('friendRequestStatus') ===
|
||||
FriendRequestStatusEnum.requestReceived
|
||||
);
|
||||
},
|
||||
isFriend() {
|
||||
return this.get('friendRequestStatus') === FriendRequestStatusEnum.friends;
|
||||
return (
|
||||
this.get('friendRequestStatus') === FriendRequestStatusEnum.friends
|
||||
);
|
||||
},
|
||||
updateTextInputState() {
|
||||
switch (this.get('friendRequestStatus')) {
|
||||
|
@ -582,8 +593,7 @@
|
|||
},
|
||||
async setFriendRequestStatus(newStatus) {
|
||||
// Ensure that the new status is a valid FriendStatusEnum value
|
||||
if (!(newStatus in Object.values(FriendRequestStatusEnum)))
|
||||
return;
|
||||
if (!(newStatus in Object.values(FriendRequestStatusEnum))) return;
|
||||
if (this.get('friendRequestStatus') !== newStatus) {
|
||||
this.set({ friendRequestStatus: newStatus });
|
||||
await window.Signal.Data.updateConversation(this.id, this.attributes, {
|
||||
|
@ -610,7 +620,9 @@
|
|||
);
|
||||
},
|
||||
async resetPendingSend() {
|
||||
if (this.get('friendRequestStatus') === FriendRequestStatusEnum.pendingSend) {
|
||||
if (
|
||||
this.get('friendRequestStatus') === FriendRequestStatusEnum.pendingSend
|
||||
) {
|
||||
await this.setFriendRequestStatus(FriendRequestStatusEnum.none);
|
||||
}
|
||||
},
|
||||
|
@ -645,8 +657,7 @@
|
|||
},
|
||||
async onFriendRequestTimeout() {
|
||||
// Unset the timer
|
||||
if (this.unlockTimer)
|
||||
clearTimeout(this.unlockTimer);
|
||||
if (this.unlockTimer) clearTimeout(this.unlockTimer);
|
||||
|
||||
this.unlockTimer = null;
|
||||
|
||||
|
@ -745,7 +756,7 @@
|
|||
if (!this.isPrivate()) {
|
||||
throw new Error(
|
||||
'You cannot set a group conversation as trusted. ' +
|
||||
'You must set individual contacts as trusted.'
|
||||
'You must set individual contacts as trusted.'
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1055,9 +1066,9 @@
|
|||
fileName: fileName || null,
|
||||
thumbnail: thumbnail
|
||||
? {
|
||||
...(await loadAttachmentData(thumbnail)),
|
||||
objectUrl: getAbsoluteAttachmentPath(thumbnail.path),
|
||||
}
|
||||
...(await loadAttachmentData(thumbnail)),
|
||||
objectUrl: getAbsoluteAttachmentPath(thumbnail.path),
|
||||
}
|
||||
: null,
|
||||
};
|
||||
})
|
||||
|
@ -1067,8 +1078,7 @@
|
|||
|
||||
async sendMessage(body, attachments, quote) {
|
||||
// Input should be blocked if there is a pending friend request
|
||||
if (this.isPendingFriendRequest())
|
||||
return;
|
||||
if (this.isPendingFriendRequest()) return;
|
||||
|
||||
this.clearTypingTimers();
|
||||
|
||||
|
@ -1108,7 +1118,9 @@
|
|||
});
|
||||
} else {
|
||||
// Check if we have sent a friend request
|
||||
const outgoingRequests = await this.getPendingFriendRequests('outgoing');
|
||||
const outgoingRequests = await this.getPendingFriendRequests(
|
||||
'outgoing'
|
||||
);
|
||||
if (outgoingRequests.length > 0) {
|
||||
// Check if the requests have errored, if so then remove them
|
||||
// and send the new request if possible
|
||||
|
@ -1128,7 +1140,9 @@
|
|||
// because one of them was sent successfully
|
||||
if (friendRequestSent) return null;
|
||||
}
|
||||
await this.setFriendRequestStatus(FriendRequestStatusEnum.pendingSend);
|
||||
await this.setFriendRequestStatus(
|
||||
FriendRequestStatusEnum.pendingSend
|
||||
);
|
||||
|
||||
// Send the friend request!
|
||||
messageWithSchema = await upgradeMessageSchema({
|
||||
|
@ -1213,7 +1227,7 @@
|
|||
options
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
return true;
|
||||
|
@ -1375,8 +1389,8 @@
|
|||
accessKey && sealedSender === SEALED_SENDER.ENABLED
|
||||
? accessKey
|
||||
: window.Signal.Crypto.arrayBufferToBase64(
|
||||
window.Signal.Crypto.getRandomBytes(16)
|
||||
),
|
||||
window.Signal.Crypto.getRandomBytes(16)
|
||||
),
|
||||
},
|
||||
};
|
||||
},
|
||||
|
@ -1533,8 +1547,7 @@
|
|||
},
|
||||
async setSessionResetStatus(newStatus) {
|
||||
// Ensure that the new status is a valid SessionResetEnum value
|
||||
if (!(newStatus in Object.values(SessionResetEnum)))
|
||||
return;
|
||||
if (!(newStatus in Object.values(SessionResetEnum))) return;
|
||||
if (this.get('sessionResetStatus') !== newStatus) {
|
||||
this.set({ sessionResetStatus: newStatus });
|
||||
await window.Signal.Data.updateConversation(this.id, this.attributes, {
|
||||
|
@ -1553,7 +1566,9 @@
|
|||
},
|
||||
|
||||
isSessionResetReceived() {
|
||||
return this.get('sessionResetStatus') === SessionResetEnum.request_received;
|
||||
return (
|
||||
this.get('sessionResetStatus') === SessionResetEnum.request_received
|
||||
);
|
||||
},
|
||||
|
||||
isSessionResetOngoing() {
|
||||
|
@ -1585,7 +1600,10 @@
|
|||
// send empty message to confirm that we have adopted the new session
|
||||
await window.libloki.api.sendEmptyMessage(this.id);
|
||||
}
|
||||
await this.createAndStoreEndSessionMessage({ type: 'incoming', endSessionType: 'done' });
|
||||
await this.createAndStoreEndSessionMessage({
|
||||
type: 'incoming',
|
||||
endSessionType: 'done',
|
||||
});
|
||||
await this.setSessionResetStatus(SessionResetEnum.none);
|
||||
},
|
||||
|
||||
|
@ -1594,13 +1612,22 @@
|
|||
// Only create a new message if *we* initiated the session reset.
|
||||
// On the receiver side, the actual message containing the END_SESSION flag
|
||||
// will ensure the "session reset" message will be added to their conversation.
|
||||
if (this.get('sessionResetStatus') !== SessionResetEnum.request_received) {
|
||||
if (
|
||||
this.get('sessionResetStatus') !== SessionResetEnum.request_received
|
||||
) {
|
||||
await this.onSessionResetInitiated();
|
||||
const message = await this.createAndStoreEndSessionMessage({ type: 'outgoing', endSessionType: 'ongoing' });
|
||||
const message = await this.createAndStoreEndSessionMessage({
|
||||
type: 'outgoing',
|
||||
endSessionType: 'ongoing',
|
||||
});
|
||||
const options = this.getSendOptions();
|
||||
await message.send(
|
||||
this.wrapSend(
|
||||
textsecure.messaging.resetSession(this.id, message.get('sent_at'), options)
|
||||
textsecure.messaging.resetSession(
|
||||
this.id,
|
||||
message.get('sent_at'),
|
||||
options
|
||||
)
|
||||
)
|
||||
);
|
||||
if (message.hasErrors()) {
|
||||
|
@ -1700,7 +1727,7 @@
|
|||
} else {
|
||||
window.log.warn(
|
||||
'Marked a message as read in the database, but ' +
|
||||
'it was not in messageCollection.'
|
||||
'it was not in messageCollection.'
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -2063,7 +2090,8 @@
|
|||
const color = this.getColor();
|
||||
const avatar = this.get('avatar') || this.get('profileAvatar');
|
||||
|
||||
const url = avatar && avatar.path ? getAbsoluteAttachmentPath(avatar.path) : avatar;
|
||||
const url =
|
||||
avatar && avatar.path ? getAbsoluteAttachmentPath(avatar.path) : avatar;
|
||||
|
||||
if (url) {
|
||||
return { url, color };
|
||||
|
@ -2091,7 +2119,7 @@
|
|||
notify(message) {
|
||||
if (message.isFriendRequest()) {
|
||||
if (this.hasSentFriendRequest())
|
||||
return this.notifyFriendRequest(message.get('source'), 'accepted')
|
||||
return this.notifyFriendRequest(message.get('source'), 'accepted');
|
||||
return this.notifyFriendRequest(message.get('source'), 'requested');
|
||||
}
|
||||
if (!message.isIncoming()) return Promise.resolve();
|
||||
|
@ -2127,8 +2155,7 @@
|
|||
// Notification for friend request received
|
||||
async notifyFriendRequest(source, type) {
|
||||
// Data validation
|
||||
if (!source)
|
||||
throw new Error('Invalid source');
|
||||
if (!source) throw new Error('Invalid source');
|
||||
if (!['accepted', 'requested'].includes(type))
|
||||
throw new Error('Type must be accepted or requested.');
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
/* eslint-disable more/no-then */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
@ -202,8 +202,7 @@
|
|||
getNotificationText() {
|
||||
const description = this.getDescription();
|
||||
if (description) {
|
||||
if (this.isFriendRequest())
|
||||
return `Friend Request: ${description}`;
|
||||
if (this.isFriendRequest()) return `Friend Request: ${description}`;
|
||||
return description;
|
||||
}
|
||||
if (this.get('attachments').length > 0) {
|
||||
|
@ -364,7 +363,7 @@
|
|||
onBlockUser,
|
||||
onUnblockUser,
|
||||
onRetrySend,
|
||||
}
|
||||
};
|
||||
},
|
||||
findContact(phoneNumber) {
|
||||
return ConversationController.get(phoneNumber);
|
||||
|
@ -444,7 +443,8 @@
|
|||
|
||||
// Handle friend request statuses
|
||||
const isFriendRequest = this.isFriendRequest();
|
||||
const isOutgoingFriendRequest = isFriendRequest && this.get('direction') === 'outgoing';
|
||||
const isOutgoingFriendRequest =
|
||||
isFriendRequest && this.get('direction') === 'outgoing';
|
||||
const isOutgoing = this.isOutgoing() || isOutgoingFriendRequest;
|
||||
|
||||
// Only return the status on outgoing messages
|
||||
|
@ -562,8 +562,8 @@
|
|||
contact.number && contact.number[0] && contact.number[0].value;
|
||||
const onSendMessage = firstNumber
|
||||
? () => {
|
||||
this.trigger('open-conversation', firstNumber);
|
||||
}
|
||||
this.trigger('open-conversation', firstNumber);
|
||||
}
|
||||
: null;
|
||||
const onClick = async () => {
|
||||
// First let's be sure that the signal account check is complete.
|
||||
|
@ -602,8 +602,8 @@
|
|||
!path && !objectUrl
|
||||
? null
|
||||
: Object.assign({}, attachment.thumbnail || {}, {
|
||||
objectUrl: path || objectUrl,
|
||||
});
|
||||
objectUrl: path || objectUrl,
|
||||
});
|
||||
|
||||
return Object.assign({}, attachment, {
|
||||
isVoiceMessage: Signal.Types.Attachment.isVoiceMessage(attachment),
|
||||
|
@ -670,15 +670,15 @@
|
|||
url: getAbsoluteAttachmentPath(path),
|
||||
screenshot: screenshot
|
||||
? {
|
||||
...screenshot,
|
||||
url: getAbsoluteAttachmentPath(screenshot.path),
|
||||
}
|
||||
...screenshot,
|
||||
url: getAbsoluteAttachmentPath(screenshot.path),
|
||||
}
|
||||
: null,
|
||||
thumbnail: thumbnail
|
||||
? {
|
||||
...thumbnail,
|
||||
url: getAbsoluteAttachmentPath(thumbnail.path),
|
||||
}
|
||||
...thumbnail,
|
||||
url: getAbsoluteAttachmentPath(thumbnail.path),
|
||||
}
|
||||
: null,
|
||||
};
|
||||
},
|
||||
|
@ -1143,7 +1143,7 @@
|
|||
errors = errors.concat(this.get('errors') || []);
|
||||
|
||||
if (this.isEndSession) {
|
||||
this.set({ endSessionType: 'failed'});
|
||||
this.set({ endSessionType: 'failed' });
|
||||
}
|
||||
|
||||
this.set({ errors });
|
||||
|
@ -1407,7 +1407,9 @@
|
|||
autoAccept = true;
|
||||
message.set({ friendStatus: 'accepted' });
|
||||
await conversation.onFriendRequestAccepted();
|
||||
window.libloki.api.sendFriendRequestAccepted(message.get('source'));
|
||||
window.libloki.api.sendFriendRequestAccepted(
|
||||
message.get('source')
|
||||
);
|
||||
} else if (!conversation.isFriend()) {
|
||||
await conversation.onFriendRequestReceived();
|
||||
}
|
||||
|
@ -1445,7 +1447,7 @@
|
|||
if (previousUnread !== message.get('unread')) {
|
||||
window.log.warn(
|
||||
'Caught race condition on new message read state! ' +
|
||||
'Manually starting timers.'
|
||||
'Manually starting timers.'
|
||||
);
|
||||
// We call markRead() even though the message is already
|
||||
// marked read because we need to start expiration
|
||||
|
@ -1464,8 +1466,7 @@
|
|||
// Need to do this here because the conversation has already changed states
|
||||
if (autoAccept)
|
||||
await conversation.notifyFriendRequest(source, 'accepted');
|
||||
else
|
||||
await conversation.notify(message);
|
||||
else await conversation.notify(message);
|
||||
}
|
||||
|
||||
confirm();
|
||||
|
|
|
@ -14,9 +14,9 @@
|
|||
storage.getLocalProfile = () => {
|
||||
const profile = storage.get(PROFILE_ID, null);
|
||||
return profile;
|
||||
}
|
||||
};
|
||||
|
||||
storage.setProfileName = async (newName) => {
|
||||
storage.setProfileName = async newName => {
|
||||
if (typeof newName !== 'string' && newName !== null) {
|
||||
throw Error('Name must be a string!');
|
||||
}
|
||||
|
@ -33,13 +33,13 @@
|
|||
} else {
|
||||
newProfile.name = {
|
||||
displayName: trimmed,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
await storage.saveLocalProfile(newProfile);
|
||||
}
|
||||
};
|
||||
|
||||
storage.saveLocalProfile = async (profile) => {
|
||||
storage.saveLocalProfile = async profile => {
|
||||
const storedProfile = storage.get(PROFILE_ID, null);
|
||||
|
||||
// Only store the profile if we have a different object
|
||||
|
@ -49,10 +49,10 @@
|
|||
|
||||
window.log.info('saving local profile ', profile);
|
||||
await storage.put(PROFILE_ID, profile);
|
||||
}
|
||||
};
|
||||
|
||||
storage.removeLocalProfile = async () => {
|
||||
window.log.info('removing local profile');
|
||||
await storage.remove(PROFILE_ID);
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
|
|
@ -573,7 +573,6 @@ async function removeAllContactSignedPreKeys() {
|
|||
await channels.removeAllContactSignedPreKeys();
|
||||
}
|
||||
|
||||
|
||||
// Items
|
||||
|
||||
const ITEM_KEYS = {
|
||||
|
@ -690,7 +689,7 @@ async function saveConversations(data) {
|
|||
}
|
||||
|
||||
async function getConversationById(id, { Conversation }) {
|
||||
const rawData = await channels.getConversationById(id)
|
||||
const rawData = await channels.getConversationById(id);
|
||||
const data = setifyProperty(rawData, 'swarmNodes');
|
||||
return new Conversation(data);
|
||||
}
|
||||
|
@ -725,8 +724,9 @@ async function _removeConversations(ids) {
|
|||
}
|
||||
|
||||
async function getAllConversations({ ConversationCollection }) {
|
||||
const conversations = (await channels.getAllConversations())
|
||||
.map(c => setifyProperty(c, 'swarmNodes'));
|
||||
const conversations = (await channels.getAllConversations()).map(c =>
|
||||
setifyProperty(c, 'swarmNodes')
|
||||
);
|
||||
|
||||
const collection = new ConversationCollection();
|
||||
collection.add(conversations);
|
||||
|
@ -739,8 +739,9 @@ async function getAllConversationIds() {
|
|||
}
|
||||
|
||||
async function getAllPrivateConversations({ ConversationCollection }) {
|
||||
const conversations = (await channels.getAllPrivateConversations())
|
||||
.map(c => setifyProperty(c, 'swarmNodes'));
|
||||
const conversations = (await channels.getAllPrivateConversations()).map(c =>
|
||||
setifyProperty(c, 'swarmNodes')
|
||||
);
|
||||
|
||||
const collection = new ConversationCollection();
|
||||
collection.add(conversations);
|
||||
|
@ -748,8 +749,9 @@ async function getAllPrivateConversations({ ConversationCollection }) {
|
|||
}
|
||||
|
||||
async function getAllGroupsInvolvingId(id, { ConversationCollection }) {
|
||||
const conversations = (await channels.getAllGroupsInvolvingId(id))
|
||||
.map(c => setifyProperty(c, 'swarmNodes'));
|
||||
const conversations = (await channels.getAllGroupsInvolvingId(id)).map(c =>
|
||||
setifyProperty(c, 'swarmNodes')
|
||||
);
|
||||
|
||||
const collection = new ConversationCollection();
|
||||
collection.add(conversations);
|
||||
|
@ -757,8 +759,9 @@ async function getAllGroupsInvolvingId(id, { ConversationCollection }) {
|
|||
}
|
||||
|
||||
async function searchConversations(query, { ConversationCollection }) {
|
||||
const conversations = (await channels.searchConversations(query))
|
||||
.map(c => setifyProperty(c, 'swarmNodes'));
|
||||
const conversations = (await channels.searchConversations(query)).map(c =>
|
||||
setifyProperty(c, 'swarmNodes')
|
||||
);
|
||||
|
||||
const collection = new ConversationCollection();
|
||||
collection.add(conversations);
|
||||
|
@ -905,9 +908,7 @@ async function getMessagesByConversation(
|
|||
return new MessageCollection(messages);
|
||||
}
|
||||
|
||||
async function getSeenMessagesByHashList(
|
||||
hashes
|
||||
) {
|
||||
async function getSeenMessagesByHashList(hashes) {
|
||||
const seenMessages = await channels.getSeenMessagesByHashList(hashes);
|
||||
return seenMessages;
|
||||
}
|
||||
|
|
|
@ -4,21 +4,18 @@
|
|||
const fetch = require('node-fetch');
|
||||
|
||||
// eslint-disable-next-line
|
||||
const invert = p => new Promise((res, rej) => p.then(rej, res));
|
||||
const invert = p => new Promise((res, rej) => p.then(rej, res));
|
||||
const firstOf = ps => invert(Promise.all(ps.map(invert)));
|
||||
|
||||
// Will be raised (to 3?) when we get more nodes
|
||||
const MINIMUM_SUCCESSFUL_REQUESTS = 2;
|
||||
class LokiMessageAPI {
|
||||
|
||||
constructor({ messageServerPort }) {
|
||||
this.messageServerPort = messageServerPort
|
||||
? `:${messageServerPort}`
|
||||
: '';
|
||||
this.messageServerPort = messageServerPort ? `:${messageServerPort}` : '';
|
||||
}
|
||||
|
||||
async sendMessage(pubKey, data, messageTimeStamp, ttl) {
|
||||
const swarmNodes = await window.LokiSnodeAPI.getSwarmNodesByPubkey(pubKey)
|
||||
const swarmNodes = await window.LokiSnodeAPI.getSwarmNodesByPubkey(pubKey);
|
||||
if (!swarmNodes || swarmNodes.size === 0) {
|
||||
throw Error('No swarm nodes to query!');
|
||||
}
|
||||
|
@ -33,7 +30,14 @@ class LokiMessageAPI {
|
|||
timestamp: messageTimeStamp,
|
||||
});
|
||||
const development = window.getEnvironment() !== 'production';
|
||||
nonce = await callWorker('calcPoW', timestamp, ttl, pubKey, data64, development);
|
||||
nonce = await callWorker(
|
||||
'calcPoW',
|
||||
timestamp,
|
||||
ttl,
|
||||
pubKey,
|
||||
data64,
|
||||
development
|
||||
);
|
||||
} catch (err) {
|
||||
// Something went horribly wrong
|
||||
throw err;
|
||||
|
@ -84,14 +88,19 @@ class LokiMessageAPI {
|
|||
if (response.status >= 0 && response.status < 400) {
|
||||
return result;
|
||||
}
|
||||
log.error(options.type, options.url, response.status, 'Error sending message');
|
||||
log.error(
|
||||
options.type,
|
||||
options.url,
|
||||
response.status,
|
||||
'Error sending message'
|
||||
);
|
||||
throw HTTPError('sendMessage: error response', response.status, result);
|
||||
});
|
||||
try {
|
||||
// TODO: Possibly change this to require more than a single response?
|
||||
const result = await firstOf(requests);
|
||||
return result;
|
||||
} catch(err) {
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
@ -128,7 +137,12 @@ class LokiMessageAPI {
|
|||
} catch (e) {
|
||||
// TODO: Maybe we shouldn't immediately delete?
|
||||
// And differentiate between different connectivity issues
|
||||
log.error(options.type, options.url, 0, `Error retrieving messages from ${nodeUrl}`);
|
||||
log.error(
|
||||
options.type,
|
||||
options.url,
|
||||
0,
|
||||
`Error retrieving messages from ${nodeUrl}`
|
||||
);
|
||||
window.LokiSnodeAPI.unreachableNode(ourKey, nodeUrl);
|
||||
return;
|
||||
}
|
||||
|
@ -155,7 +169,7 @@ class LokiMessageAPI {
|
|||
}
|
||||
// Handle error from snode
|
||||
log.error(options.type, options.url, response.status, 'Error');
|
||||
}
|
||||
};
|
||||
|
||||
while (completedRequests < MINIMUM_SUCCESSFUL_REQUESTS) {
|
||||
const remainingRequests = MINIMUM_SUCCESSFUL_REQUESTS - completedRequests;
|
||||
|
|
|
@ -8,15 +8,12 @@ const dns = require('dns');
|
|||
const MINIMUM_SWARM_NODES = 1;
|
||||
|
||||
class LokiSnodeAPI {
|
||||
|
||||
constructor({ url, swarmServerPort }) {
|
||||
if (!is.string(url)) {
|
||||
throw new Error('WebAPI.initialize: Invalid server url');
|
||||
}
|
||||
this.url = url;
|
||||
this.swarmServerPort = swarmServerPort
|
||||
? `:${swarmServerPort}`
|
||||
: '';
|
||||
this.swarmServerPort = swarmServerPort ? `:${swarmServerPort}` : '';
|
||||
this.swarmsPendingReplenish = {};
|
||||
this.ourSwarmNodes = {};
|
||||
}
|
||||
|
@ -25,7 +22,7 @@ class LokiSnodeAPI {
|
|||
/* resolve random snode */
|
||||
return new Promise((resolve, reject) => {
|
||||
dns.resolveCname(this.url, (err, address) => {
|
||||
if(err) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(address[0]);
|
||||
|
@ -43,9 +40,13 @@ class LokiSnodeAPI {
|
|||
const swarmNodes = conversation.get('swarmNodes');
|
||||
if (swarmNodes.delete(nodeUrl)) {
|
||||
conversation.set({ swarmNodes });
|
||||
await window.Signal.Data.updateConversation(conversation.id, conversation.attributes, {
|
||||
Conversation: Whisper.Conversation,
|
||||
});
|
||||
await window.Signal.Data.updateConversation(
|
||||
conversation.id,
|
||||
conversation.attributes,
|
||||
{
|
||||
Conversation: Whisper.Conversation,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -53,7 +54,7 @@ class LokiSnodeAPI {
|
|||
if (!this.ourSwarmNodes[nodeUrl]) {
|
||||
this.ourSwarmNodes[nodeUrl] = {
|
||||
lastHash: hash,
|
||||
}
|
||||
};
|
||||
} else {
|
||||
this.ourSwarmNodes[nodeUrl].lastHash = hash;
|
||||
}
|
||||
|
@ -69,7 +70,7 @@ class LokiSnodeAPI {
|
|||
const ourKey = window.textsecure.storage.user.getNumber();
|
||||
const nodeAddresses = await window.LokiSnodeAPI.getSwarmNodes(ourKey);
|
||||
if (!nodeAddresses || nodeAddresses.length === 0) {
|
||||
throw Error('Could not load our swarm')
|
||||
throw Error('Could not load our swarm');
|
||||
}
|
||||
|
||||
nodeAddresses.forEach(url => {
|
||||
|
@ -91,8 +92,8 @@ class LokiSnodeAPI {
|
|||
async replenishSwarm(pubKey) {
|
||||
const conversation = window.ConversationController.get(pubKey);
|
||||
if (!(pubKey in this.swarmsPendingReplenish)) {
|
||||
this.swarmsPendingReplenish[pubKey] = new Promise(async (resolve) => {
|
||||
let newSwarmNodes
|
||||
this.swarmsPendingReplenish[pubKey] = new Promise(async resolve => {
|
||||
let newSwarmNodes;
|
||||
try {
|
||||
newSwarmNodes = new Set(await this.getSwarmNodes(pubKey));
|
||||
} catch (e) {
|
||||
|
@ -100,9 +101,13 @@ class LokiSnodeAPI {
|
|||
newSwarmNodes = new Set([]);
|
||||
}
|
||||
conversation.set({ swarmNodes: newSwarmNodes });
|
||||
await window.Signal.Data.updateConversation(conversation.id, conversation.attributes, {
|
||||
Conversation: Whisper.Conversation,
|
||||
});
|
||||
await window.Signal.Data.updateConversation(
|
||||
conversation.id,
|
||||
conversation.attributes,
|
||||
{
|
||||
Conversation: Whisper.Conversation,
|
||||
}
|
||||
);
|
||||
resolve(newSwarmNodes);
|
||||
});
|
||||
}
|
||||
|
@ -129,7 +134,7 @@ class LokiSnodeAPI {
|
|||
params: {
|
||||
pubkey: pubKey,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
const fetchOptions = {
|
||||
method: options.type,
|
||||
|
@ -144,7 +149,12 @@ class LokiSnodeAPI {
|
|||
try {
|
||||
response = await fetch(options.url, fetchOptions);
|
||||
} catch (e) {
|
||||
log.error(options.type, options.url, 0, `Error getting swarm nodes for ${pubKey}`);
|
||||
log.error(
|
||||
options.type,
|
||||
options.url,
|
||||
0,
|
||||
`Error getting swarm nodes for ${pubKey}`
|
||||
);
|
||||
throw HTTPError('fetch error', 0, e.toString());
|
||||
}
|
||||
|
||||
|
@ -163,7 +173,12 @@ class LokiSnodeAPI {
|
|||
if (response.status >= 0 && response.status < 400) {
|
||||
return result.nodes;
|
||||
}
|
||||
log.error(options.type, options.url, response.status, `Error getting swarm nodes for ${pubKey}`);
|
||||
log.error(
|
||||
options.type,
|
||||
options.url,
|
||||
response.status,
|
||||
`Error getting swarm nodes for ${pubKey}`
|
||||
);
|
||||
throw HTTPError('sendMessage: error response', response.status, result);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ class TimedOutError extends Error {
|
|||
if (typeof Error.captureStackTrace === 'function') {
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
} else {
|
||||
this.stack = (new Error(message)).stack;
|
||||
this.stack = new Error(message).stack;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ class WorkerInterface {
|
|||
};
|
||||
}
|
||||
|
||||
_makeJob (fnName) {
|
||||
_makeJob(fnName) {
|
||||
this._jobCounter += 1;
|
||||
const id = this._jobCounter;
|
||||
|
||||
|
@ -59,7 +59,7 @@ class WorkerInterface {
|
|||
};
|
||||
|
||||
return id;
|
||||
};
|
||||
}
|
||||
|
||||
_updateJob(id, data) {
|
||||
const { resolve, reject } = data;
|
||||
|
@ -85,7 +85,7 @@ class WorkerInterface {
|
|||
return reject(error);
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
_removeJob(id) {
|
||||
if (this._DEBUG) {
|
||||
|
@ -97,7 +97,7 @@ class WorkerInterface {
|
|||
|
||||
_getJob(id) {
|
||||
return this._jobs[id];
|
||||
};
|
||||
}
|
||||
|
||||
callWorker(fnName, ...args) {
|
||||
const jobId = this._makeJob(fnName);
|
||||
|
@ -112,11 +112,14 @@ class WorkerInterface {
|
|||
});
|
||||
|
||||
setTimeout(
|
||||
() => reject(new TimedOutError(`Worker job ${jobId} (${fnName}) timed out`)),
|
||||
() =>
|
||||
reject(
|
||||
new TimedOutError(`Worker job ${jobId} (${fnName}) timed out`)
|
||||
),
|
||||
this.timeout
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
|
|
@ -87,14 +87,18 @@
|
|||
let iconUrl;
|
||||
|
||||
// The number of notifications excluding friend request
|
||||
const messagesNotificationCount = this.models.filter(n => !n.get('isFriendRequest')).length;
|
||||
const messagesNotificationCount = this.models.filter(
|
||||
n => !n.get('isFriendRequest')
|
||||
).length;
|
||||
|
||||
// NOTE: i18n has more complex rules for pluralization than just
|
||||
// distinguishing between zero (0) and other (non-zero),
|
||||
// e.g. Russian:
|
||||
// http://docs.translatehouse.org/projects/localization-guide/en/latest/l10n/pluralforms.html
|
||||
const newMessageCountLabel = `${messagesNotificationCount} ${
|
||||
messagesNotificationCount === 1 ? i18n('newMessage') : i18n('newMessages')
|
||||
messagesNotificationCount === 1
|
||||
? i18n('newMessage')
|
||||
: i18n('newMessages')
|
||||
}`;
|
||||
|
||||
const last = this.last().toJSON();
|
||||
|
|
|
@ -1,5 +1,14 @@
|
|||
/* global
|
||||
dcodeIO, Backbone, _, libsignal, textsecure, ConversationController, stringObject, BlockedNumberController */
|
||||
/*
|
||||
global
|
||||
dcodeIO,
|
||||
Backbone,
|
||||
_,
|
||||
libsignal,
|
||||
textsecure,
|
||||
ConversationController,
|
||||
stringObject,
|
||||
BlockedNumberController
|
||||
*/
|
||||
|
||||
/* eslint-disable no-proto */
|
||||
|
||||
|
@ -187,7 +196,10 @@
|
|||
const key = await window.Signal.Data.getPreKeyByRecipient(contactPubKey);
|
||||
|
||||
if (key) {
|
||||
window.log.info('Successfully fetched prekey for recipient:', contactPubKey);
|
||||
window.log.info(
|
||||
'Successfully fetched prekey for recipient:',
|
||||
contactPubKey
|
||||
);
|
||||
return {
|
||||
pubKey: key.publicKey,
|
||||
privKey: key.privateKey,
|
||||
|
|
|
@ -41,6 +41,24 @@ function stringToArrayBufferBase64(string) {
|
|||
function arrayBufferToStringBase64(arrayBuffer) {
|
||||
return dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64');
|
||||
}
|
||||
function calcPoW(timestamp, ttl, pubKey, data, development, nonceTrials = undefined, increment = 1, startNonce = 0) {
|
||||
return pow.calcPoW(timestamp, ttl, pubKey, data, development, nonceTrials, increment, startNonce);
|
||||
function calcPoW(
|
||||
timestamp,
|
||||
ttl,
|
||||
pubKey,
|
||||
data,
|
||||
development,
|
||||
nonceTrials = undefined,
|
||||
increment = 1,
|
||||
startNonce = 0
|
||||
) {
|
||||
return pow.calcPoW(
|
||||
timestamp,
|
||||
ttl,
|
||||
pubKey,
|
||||
data,
|
||||
development,
|
||||
nonceTrials,
|
||||
increment,
|
||||
startNonce
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,87 +8,89 @@
|
|||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
'use strict';
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
Whisper.BlockedNumberView = Whisper.View.extend({
|
||||
templateName: 'blockedUserSettings',
|
||||
className: 'blockedUserSettings',
|
||||
events: {
|
||||
'click .unblock-button': 'onUnblock',
|
||||
},
|
||||
initialize() {
|
||||
storage.onready(() => {
|
||||
BlockedNumberController.refresh();
|
||||
this.collection = getBlockedNumbers();
|
||||
this.listView = new Whisper.BlockedNumberListView({
|
||||
collection: this.collection,
|
||||
});
|
||||
|
||||
this.listView.render();
|
||||
this.blockedUserSettings = this.$('.blocked-user-settings');
|
||||
this.blockedUserSettings.prepend(this.listView.el);
|
||||
Whisper.BlockedNumberView = Whisper.View.extend({
|
||||
templateName: 'blockedUserSettings',
|
||||
className: 'blockedUserSettings',
|
||||
events: {
|
||||
'click .unblock-button': 'onUnblock',
|
||||
},
|
||||
initialize() {
|
||||
storage.onready(() => {
|
||||
BlockedNumberController.refresh();
|
||||
this.collection = getBlockedNumbers();
|
||||
this.listView = new Whisper.BlockedNumberListView({
|
||||
collection: this.collection,
|
||||
});
|
||||
},
|
||||
render_attributes() {
|
||||
return {
|
||||
blockedHeader: i18n('settingsUnblockHeader'),
|
||||
unblockMessage: i18n('unblockUser'),
|
||||
};
|
||||
},
|
||||
onUnblock() {
|
||||
const number = this.$('select option:selected').val();
|
||||
if (!number) return;
|
||||
|
||||
if (BlockedNumberController.isBlocked(number)) {
|
||||
BlockedNumberController.unblock(number);
|
||||
window.onUnblockNumber(number);
|
||||
this.listView.collection.remove(this.listView.collection.where({ number }));
|
||||
}
|
||||
},
|
||||
});
|
||||
this.listView.render();
|
||||
this.blockedUserSettings = this.$('.blocked-user-settings');
|
||||
this.blockedUserSettings.prepend(this.listView.el);
|
||||
});
|
||||
},
|
||||
render_attributes() {
|
||||
return {
|
||||
blockedHeader: i18n('settingsUnblockHeader'),
|
||||
unblockMessage: i18n('unblockUser'),
|
||||
};
|
||||
},
|
||||
onUnblock() {
|
||||
const number = this.$('select option:selected').val();
|
||||
if (!number) return;
|
||||
|
||||
if (BlockedNumberController.isBlocked(number)) {
|
||||
BlockedNumberController.unblock(number);
|
||||
window.onUnblockNumber(number);
|
||||
this.listView.collection.remove(
|
||||
this.listView.collection.where({ number })
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Whisper.BlockedNumberListView = Whisper.View.extend({
|
||||
tagName: 'select',
|
||||
initialize(options) {
|
||||
this.options = options || {};
|
||||
this.listenTo(this.collection, 'add', this.addOne);
|
||||
this.listenTo(this.collection, 'reset', this.addAll);
|
||||
this.listenTo(this.collection, 'remove', this.addAll);
|
||||
},
|
||||
addOne(model) {
|
||||
const number = model.get('number');
|
||||
if (number) {
|
||||
this.$el.append(`<option value="${number}">${this.truncate(number, 25)}</option>`);
|
||||
}
|
||||
},
|
||||
addAll() {
|
||||
this.$el.html('');
|
||||
this.collection.each(this.addOne, this);
|
||||
},
|
||||
truncate(string, limit) {
|
||||
// Make sure an element and number of items to truncate is provided
|
||||
if (!string || !limit) return string;
|
||||
tagName: 'select',
|
||||
initialize(options) {
|
||||
this.options = options || {};
|
||||
this.listenTo(this.collection, 'add', this.addOne);
|
||||
this.listenTo(this.collection, 'reset', this.addAll);
|
||||
this.listenTo(this.collection, 'remove', this.addAll);
|
||||
},
|
||||
addOne(model) {
|
||||
const number = model.get('number');
|
||||
if (number) {
|
||||
this.$el.append(
|
||||
`<option value="${number}">${this.truncate(number, 25)}</option>`
|
||||
);
|
||||
}
|
||||
},
|
||||
addAll() {
|
||||
this.$el.html('');
|
||||
this.collection.each(this.addOne, this);
|
||||
},
|
||||
truncate(string, limit) {
|
||||
// Make sure an element and number of items to truncate is provided
|
||||
if (!string || !limit) return string;
|
||||
|
||||
// Get the inner content of the element
|
||||
let content = string.trim();
|
||||
// Get the inner content of the element
|
||||
let content = string.trim();
|
||||
|
||||
// Convert the content into an array of words
|
||||
// Remove any words above the limit
|
||||
content = content.slice(0, limit);
|
||||
// Convert the content into an array of words
|
||||
// Remove any words above the limit
|
||||
content = content.slice(0, limit);
|
||||
|
||||
// Convert the array of words back into a string
|
||||
// If there's content to add after it, add it
|
||||
if (string.length > limit)
|
||||
content = `${content}...`;
|
||||
// Convert the array of words back into a string
|
||||
// If there's content to add after it, add it
|
||||
if (string.length > limit) content = `${content}...`;
|
||||
|
||||
return content;
|
||||
},
|
||||
render() {
|
||||
this.addAll();
|
||||
return this;
|
||||
},
|
||||
});
|
||||
})();
|
||||
return content;
|
||||
},
|
||||
render() {
|
||||
this.addAll();
|
||||
return this;
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -42,8 +42,7 @@
|
|||
props,
|
||||
});
|
||||
|
||||
const update = () =>
|
||||
this.childView.update(this.getProps());
|
||||
const update = () => this.childView.update(this.getProps());
|
||||
|
||||
this.listenTo(this.model, 'change', update);
|
||||
|
||||
|
@ -54,14 +53,16 @@
|
|||
});
|
||||
|
||||
// list of conversations, showing user/group and last message sent
|
||||
Whisper.ConversationContactListItemView = Whisper.ConversationListItemView.extend({
|
||||
getProps() {
|
||||
// We don't want to show a timestamp or a message
|
||||
const props = this.model.getPropsForListItem();
|
||||
delete props.lastMessage;
|
||||
delete props.lastUpdated;
|
||||
Whisper.ConversationContactListItemView = Whisper.ConversationListItemView.extend(
|
||||
{
|
||||
getProps() {
|
||||
// We don't want to show a timestamp or a message
|
||||
const props = this.model.getPropsForListItem();
|
||||
delete props.lastMessage;
|
||||
delete props.lastUpdated;
|
||||
|
||||
return props;
|
||||
},
|
||||
});
|
||||
return props;
|
||||
},
|
||||
}
|
||||
);
|
||||
})();
|
||||
|
|
|
@ -21,7 +21,10 @@
|
|||
},
|
||||
render_attributes() {
|
||||
// Show the appropriate message based on model validity
|
||||
const message = this.model && this.model.isValid() ? i18n('startConversation') : i18n('invalidNumberError');
|
||||
const message =
|
||||
this.model && this.model.isValid()
|
||||
? i18n('startConversation')
|
||||
: i18n('invalidNumberError');
|
||||
return {
|
||||
number: message,
|
||||
title: this.model.getNumber(),
|
||||
|
@ -70,7 +73,6 @@
|
|||
filterContacts() {
|
||||
const query = this.$input.val().trim();
|
||||
if (query.length) {
|
||||
|
||||
// Update the contact model
|
||||
this.new_contact_view.model.set('id', query);
|
||||
this.new_contact_view.render().$el.hide();
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
*/
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function () {
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
@ -321,8 +321,7 @@
|
|||
},
|
||||
|
||||
onChangePlaceholder(type) {
|
||||
if (!this.$messageField)
|
||||
return;
|
||||
if (!this.$messageField) return;
|
||||
let placeholder;
|
||||
switch (type) {
|
||||
case 'friend-request':
|
||||
|
@ -1623,10 +1622,15 @@
|
|||
}
|
||||
|
||||
const input = this.$messageField;
|
||||
const inputMessage = window.Signal.Emoji.replaceColons(input.val()).trim();
|
||||
const inputMessage = window.Signal.Emoji.replaceColons(
|
||||
input.val()
|
||||
).trim();
|
||||
|
||||
// Limit the message to 2000 characters
|
||||
const message = inputMessage.substring(0, Math.min(2000, inputMessage.length));
|
||||
const message = inputMessage.substring(
|
||||
0,
|
||||
Math.min(2000, inputMessage.length)
|
||||
);
|
||||
|
||||
try {
|
||||
if (!message.length && !this.fileInput.hasFiles()) {
|
||||
|
|
|
@ -150,11 +150,7 @@
|
|||
);
|
||||
|
||||
// Listen to any conversation remove
|
||||
this.listenTo(
|
||||
inboxCollection,
|
||||
'remove',
|
||||
this.closeConversation
|
||||
);
|
||||
this.listenTo(inboxCollection, 'remove', this.closeConversation);
|
||||
|
||||
// Friends
|
||||
const contactCollection = getContactCollection();
|
||||
|
@ -335,8 +331,14 @@
|
|||
},
|
||||
updateInboxSectionUnread() {
|
||||
const $section = this.$('.section-conversations-unread-counter');
|
||||
const models = (this.inboxListView.collection && this.inboxListView.collection.models) || [];
|
||||
const unreadCount = models.reduce((count, m) => count + Math.max(0, m.get('unreadCount')), 0);
|
||||
const models =
|
||||
(this.inboxListView.collection &&
|
||||
this.inboxListView.collection.models) ||
|
||||
[];
|
||||
const unreadCount = models.reduce(
|
||||
(count, m) => count + Math.max(0, m.get('unreadCount')),
|
||||
0
|
||||
);
|
||||
$section.text(unreadCount);
|
||||
if (unreadCount > 0) {
|
||||
$section.show();
|
||||
|
@ -350,7 +352,7 @@
|
|||
getMainHeaderItems() {
|
||||
return [
|
||||
this._mainHeaderItem('copyPublicKey', () => {
|
||||
const ourNumber = textsecure.storage.user.getNumber();
|
||||
const ourNumber = textsecure.storage.user.getNumber();
|
||||
clipboard.writeText(ourNumber);
|
||||
|
||||
this.showToastMessageInGutter(i18n('copiedPublicKey'));
|
||||
|
@ -367,17 +369,18 @@
|
|||
const hasPassword = await Signal.Data.getPasswordHash();
|
||||
const items = this.getMainHeaderItems();
|
||||
|
||||
const showPasswordDialog = (type, resolve) => Whisper.events.trigger('showPasswordDialog', {
|
||||
type,
|
||||
resolve,
|
||||
});
|
||||
const showPasswordDialog = (type, resolve) =>
|
||||
Whisper.events.trigger('showPasswordDialog', {
|
||||
type,
|
||||
resolve,
|
||||
});
|
||||
|
||||
const passwordItem = (textKey, type) => this._mainHeaderItem(
|
||||
textKey,
|
||||
() => showPasswordDialog(type, () => {
|
||||
this.showToastMessageInGutter(i18n(`${textKey}Success`));
|
||||
})
|
||||
);
|
||||
const passwordItem = (textKey, type) =>
|
||||
this._mainHeaderItem(textKey, () =>
|
||||
showPasswordDialog(type, () => {
|
||||
this.showToastMessageInGutter(i18n(`${textKey}Success`));
|
||||
})
|
||||
);
|
||||
|
||||
if (hasPassword) {
|
||||
items.push(
|
||||
|
@ -385,9 +388,7 @@
|
|||
passwordItem('removePassword', 'remove')
|
||||
);
|
||||
} else {
|
||||
items.push(
|
||||
passwordItem('setPassword', 'set')
|
||||
);
|
||||
items.push(passwordItem('setPassword', 'set'));
|
||||
}
|
||||
|
||||
this.mainHeaderView.updateItems(items);
|
||||
|
|
|
@ -2,59 +2,61 @@
|
|||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
'use strict';
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
Whisper.MainHeaderView = Whisper.View.extend({
|
||||
templateName: 'main-header-placeholder',
|
||||
events: {
|
||||
'click .main-header-title-wrapper': 'onClick',
|
||||
'click .edit-name': 'onEditProfile',
|
||||
'click .copy-key': 'onCopyKey',
|
||||
},
|
||||
initialize(options) {
|
||||
this.ourNumber = textsecure.storage.user.getNumber();
|
||||
const me = ConversationController.getOrCreate(this.ourNumber, 'private');
|
||||
Whisper.MainHeaderView = Whisper.View.extend({
|
||||
templateName: 'main-header-placeholder',
|
||||
events: {
|
||||
'click .main-header-title-wrapper': 'onClick',
|
||||
'click .edit-name': 'onEditProfile',
|
||||
'click .copy-key': 'onCopyKey',
|
||||
},
|
||||
initialize(options) {
|
||||
this.ourNumber = textsecure.storage.user.getNumber();
|
||||
const me = ConversationController.getOrCreate(this.ourNumber, 'private');
|
||||
|
||||
this.mainHeaderView = new Whisper.ReactWrapperView({
|
||||
className: 'main-header-wrapper',
|
||||
Component: Signal.Components.MainHeader,
|
||||
props: me.format(),
|
||||
});
|
||||
const update = () => this.mainHeaderView.update(me.format());
|
||||
this.listenTo(me, 'change', update);
|
||||
this.mainHeaderView = new Whisper.ReactWrapperView({
|
||||
className: 'main-header-wrapper',
|
||||
Component: Signal.Components.MainHeader,
|
||||
props: me.format(),
|
||||
});
|
||||
const update = () => this.mainHeaderView.update(me.format());
|
||||
this.listenTo(me, 'change', update);
|
||||
|
||||
this.render();
|
||||
this.$('.main-header-title-wrapper').prepend(this.mainHeaderView.el);
|
||||
this.render();
|
||||
this.$('.main-header-title-wrapper').prepend(this.mainHeaderView.el);
|
||||
|
||||
this.$toggle = this.$('.main-header-content-toggle');
|
||||
this.$content = this.$('.main-header-content-wrapper');
|
||||
this.$content.hide();
|
||||
this.$toggle = this.$('.main-header-content-toggle');
|
||||
this.$content = this.$('.main-header-content-wrapper');
|
||||
this.$content.hide();
|
||||
|
||||
this.updateItems(options.items);
|
||||
},
|
||||
updateItems(items) {
|
||||
this.$content.html('');
|
||||
(items || []).forEach(item => {
|
||||
// Add the item
|
||||
this.$content.append(`<div role='button' id='${item.id}'>${item.text}</div>`);
|
||||
this.updateItems(options.items);
|
||||
},
|
||||
updateItems(items) {
|
||||
this.$content.html('');
|
||||
(items || []).forEach(item => {
|
||||
// Add the item
|
||||
this.$content.append(
|
||||
`<div role='button' id='${item.id}'>${item.text}</div>`
|
||||
);
|
||||
|
||||
// Register its callback
|
||||
if (item.onClick) {
|
||||
this.$(`#${item.id}`).click(item.onClick);
|
||||
}
|
||||
});
|
||||
},
|
||||
render_attributes() {
|
||||
return {
|
||||
items: this.items,
|
||||
};
|
||||
},
|
||||
onClick() {
|
||||
// Toggle section visibility
|
||||
this.$content.slideToggle('fast');
|
||||
this.$toggle.toggleClass('main-header-content-toggle-visible');
|
||||
},
|
||||
});
|
||||
})();
|
||||
// Register its callback
|
||||
if (item.onClick) {
|
||||
this.$(`#${item.id}`).click(item.onClick);
|
||||
}
|
||||
});
|
||||
},
|
||||
render_attributes() {
|
||||
return {
|
||||
items: this.items,
|
||||
};
|
||||
},
|
||||
onClick() {
|
||||
// Toggle section visibility
|
||||
this.$content.slideToggle('fast');
|
||||
this.$toggle.toggleClass('main-header-content-toggle-visible');
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -50,10 +50,13 @@
|
|||
const password = this.$('#password').val();
|
||||
const passwordConfirmation = this.$('#password-confirmation').val();
|
||||
|
||||
const pairValidation = this.validatePasswordPair(password, passwordConfirmation);
|
||||
const pairValidation = this.validatePasswordPair(
|
||||
password,
|
||||
passwordConfirmation
|
||||
);
|
||||
const hashValidation = await this.validatePasswordHash(password);
|
||||
|
||||
return (pairValidation || hashValidation);
|
||||
return pairValidation || hashValidation;
|
||||
},
|
||||
async validatePasswordHash(password) {
|
||||
// Check if the password matches the hash we have stored
|
||||
|
@ -65,15 +68,20 @@
|
|||
},
|
||||
validatePasswordPair(password, passwordConfirmation) {
|
||||
if (!_.isEmpty(password)) {
|
||||
|
||||
// Check if the password is first valid
|
||||
const passwordValidation = passwordUtil.validatePassword(password, i18n);
|
||||
const passwordValidation = passwordUtil.validatePassword(
|
||||
password,
|
||||
i18n
|
||||
);
|
||||
if (passwordValidation) {
|
||||
return passwordValidation;
|
||||
}
|
||||
|
||||
// Check if the confirmation password is the same
|
||||
if (!passwordConfirmation || password.trim() !== passwordConfirmation.trim()) {
|
||||
if (
|
||||
!passwordConfirmation ||
|
||||
password.trim() !== passwordConfirmation.trim()
|
||||
) {
|
||||
return i18n('passwordsDoNotMatch');
|
||||
}
|
||||
}
|
||||
|
@ -161,8 +169,11 @@
|
|||
const oldPassword = this.$('#old-password').val();
|
||||
|
||||
// Validate the old password
|
||||
if (!_.isEmpty(oldPassword) ) {
|
||||
const oldPasswordValidation = passwordUtil.validatePassword(oldPassword, i18n);
|
||||
if (!_.isEmpty(oldPassword)) {
|
||||
const oldPasswordValidation = passwordUtil.validatePassword(
|
||||
oldPassword,
|
||||
i18n
|
||||
);
|
||||
if (oldPasswordValidation) {
|
||||
return oldPasswordValidation;
|
||||
}
|
||||
|
@ -173,7 +184,10 @@
|
|||
const password = this.$('#new-password').val();
|
||||
const passwordConfirmation = this.$('#new-password-confirmation').val();
|
||||
|
||||
const pairValidation = this.validatePasswordPair(password, passwordConfirmation);
|
||||
const pairValidation = this.validatePasswordPair(
|
||||
password,
|
||||
passwordConfirmation
|
||||
);
|
||||
const hashValidation = await this.validatePasswordHash(oldPassword);
|
||||
|
||||
return pairValidation || hashValidation;
|
||||
|
@ -189,7 +203,6 @@
|
|||
});
|
||||
|
||||
Whisper.getPasswordDialogView = (type, resolve, reject) => {
|
||||
|
||||
// This is a differently styled dialog
|
||||
if (type === 'change') {
|
||||
return new ChangePasswordDialogView({
|
||||
|
@ -201,7 +214,8 @@
|
|||
}
|
||||
|
||||
// Set and Remove is basically the same UI
|
||||
const title = type === 'remove' ? i18n('removePassword') : i18n('setPassword');
|
||||
const title =
|
||||
type === 'remove' ? i18n('removePassword') : i18n('setPassword');
|
||||
const okTitle = type === 'remove' ? i18n('remove') : i18n('set');
|
||||
return new PasswordDialogView({
|
||||
title,
|
||||
|
|
|
@ -38,5 +38,4 @@
|
|||
this.$('.error').text(string);
|
||||
},
|
||||
});
|
||||
|
||||
})();
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* global Whisper, $, getAccountManager, textsecure, i18n, passwordUtil, ConversationController */
|
||||
/* global Whisper, $, getAccountManager, textsecure, i18n, passwordUtil */
|
||||
|
||||
/* eslint-disable more/no-then */
|
||||
|
||||
|
@ -102,7 +102,7 @@
|
|||
async onGenerateMnemonic() {
|
||||
const language = this.$('#mnemonic-display-language').val();
|
||||
const mnemonic = await this.accountManager.generateMnemonic(language);
|
||||
this.$('#mnemonic-display').text(mnemonic)
|
||||
this.$('#mnemonic-display').text(mnemonic);
|
||||
},
|
||||
onCopyMnemonic() {
|
||||
window.clipboard.writeText(this.$('#mnemonic-display').text());
|
||||
|
@ -178,8 +178,12 @@
|
|||
$target.toggleClass('section-toggle-visible');
|
||||
|
||||
// Hide the other sections
|
||||
this.$('.section-toggle').not($target).removeClass('section-toggle-visible')
|
||||
this.$('.section-content').not($next).slideUp('fast');
|
||||
this.$('.section-toggle')
|
||||
.not($target)
|
||||
.removeClass('section-toggle-visible');
|
||||
this.$('.section-content')
|
||||
.not($next)
|
||||
.slideUp('fast');
|
||||
},
|
||||
onPasswordChange() {
|
||||
const input = this.$passwordInput.val();
|
||||
|
@ -193,7 +197,9 @@
|
|||
},
|
||||
validatePassword() {
|
||||
const input = this.trim(this.$passwordInput.val());
|
||||
const confirmationInput = this.trim(this.$passwordConfirmationInput.val());
|
||||
const confirmationInput = this.trim(
|
||||
this.$passwordConfirmationInput.val()
|
||||
);
|
||||
|
||||
// If user hasn't set a value then skip
|
||||
if (!input && !confirmationInput) {
|
||||
|
@ -206,7 +212,7 @@
|
|||
}
|
||||
|
||||
if (input !== confirmationInput) {
|
||||
return 'Password don\'t match';
|
||||
return "Password don't match";
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -222,7 +228,6 @@
|
|||
|
||||
this.$passwordInputError.text(passwordValidation);
|
||||
this.$passwordInputError.show();
|
||||
|
||||
} else {
|
||||
this.$passwordInput.removeClass('error-input');
|
||||
this.$passwordConfirmationInput.removeClass('error-input');
|
||||
|
@ -232,7 +237,9 @@
|
|||
|
||||
// Show green box around inputs that match
|
||||
const input = this.trim(this.$passwordInput.val());
|
||||
const confirmationInput = this.trim(this.$passwordConfirmationInput.val());
|
||||
const confirmationInput = this.trim(
|
||||
this.$passwordConfirmationInput.val()
|
||||
);
|
||||
if (input && input === confirmationInput) {
|
||||
this.$passwordInput.addClass('match-input');
|
||||
this.$passwordConfirmationInput.addClass('match-input');
|
||||
|
|
|
@ -36,5 +36,5 @@
|
|||
render_attributes() {
|
||||
return { toastMessage: this.message };
|
||||
},
|
||||
})
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/* global window, textsecure, log */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function () {
|
||||
(function() {
|
||||
window.libloki = window.libloki || {};
|
||||
|
||||
async function sendFriendRequestAccepted(pubKey) {
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
/* global window, libsignal, textsecure, StringView */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function () {
|
||||
(function() {
|
||||
window.libloki = window.libloki || {};
|
||||
|
||||
class FallBackDecryptionError extends Error { }
|
||||
class FallBackDecryptionError extends Error {}
|
||||
|
||||
const IV_LENGTH = 16;
|
||||
|
||||
class FallBackSessionCipher {
|
||||
|
||||
constructor(address) {
|
||||
this.identityKeyString = address.getName();
|
||||
this.pubKey = StringView.hexToArrayBuffer(address.getName());
|
||||
|
@ -18,10 +17,19 @@
|
|||
async encrypt(plaintext) {
|
||||
const myKeyPair = await textsecure.storage.protocol.getIdentityKeyPair();
|
||||
const myPrivateKey = myKeyPair.privKey;
|
||||
const symmetricKey = libsignal.Curve.calculateAgreement(this.pubKey, myPrivateKey);
|
||||
const symmetricKey = libsignal.Curve.calculateAgreement(
|
||||
this.pubKey,
|
||||
myPrivateKey
|
||||
);
|
||||
const iv = libsignal.crypto.getRandomBytes(IV_LENGTH);
|
||||
const ciphertext = await libsignal.crypto.encrypt(symmetricKey, plaintext, iv);
|
||||
const ivAndCiphertext = new Uint8Array(iv.byteLength + ciphertext.byteLength);
|
||||
const ciphertext = await libsignal.crypto.encrypt(
|
||||
symmetricKey,
|
||||
plaintext,
|
||||
iv
|
||||
);
|
||||
const ivAndCiphertext = new Uint8Array(
|
||||
iv.byteLength + ciphertext.byteLength
|
||||
);
|
||||
ivAndCiphertext.set(new Uint8Array(iv));
|
||||
ivAndCiphertext.set(new Uint8Array(ciphertext), iv.byteLength);
|
||||
return {
|
||||
|
@ -36,12 +44,18 @@
|
|||
const cipherText = ivAndCiphertext.slice(IV_LENGTH);
|
||||
const myKeyPair = await textsecure.storage.protocol.getIdentityKeyPair();
|
||||
const myPrivateKey = myKeyPair.privKey;
|
||||
const symmetricKey = libsignal.Curve.calculateAgreement(this.pubKey, myPrivateKey);
|
||||
const symmetricKey = libsignal.Curve.calculateAgreement(
|
||||
this.pubKey,
|
||||
myPrivateKey
|
||||
);
|
||||
try {
|
||||
return await libsignal.crypto.decrypt(symmetricKey, cipherText, iv);
|
||||
}
|
||||
catch (e) {
|
||||
throw new FallBackDecryptionError(`Could not decrypt message from ${this.identityKeyString} using FallBack encryption.`);
|
||||
} catch (e) {
|
||||
throw new FallBackDecryptionError(
|
||||
`Could not decrypt message from ${
|
||||
this.identityKeyString
|
||||
} using FallBack encryption.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ const pow = {
|
|||
newNonce[idx] = sum % 256;
|
||||
increment = Math.floor(sum / 256);
|
||||
idx -= 1;
|
||||
} while(increment > 0 && idx >= 0);
|
||||
} while (increment > 0 && idx >= 0);
|
||||
return newNonce;
|
||||
},
|
||||
|
||||
|
@ -36,10 +36,7 @@ const pow = {
|
|||
n = NONCE_LEN - (idx + 1);
|
||||
// 256 ** n is the value of one bit in arr[idx], modulus to carry over
|
||||
// (bigInt / 256**n) % 256;
|
||||
const denominator = JSBI.exponentiate(
|
||||
JSBI.BigInt('256'),
|
||||
JSBI.BigInt(n)
|
||||
);
|
||||
const denominator = JSBI.exponentiate(JSBI.BigInt('256'), JSBI.BigInt(n));
|
||||
const fraction = JSBI.divide(bigInt, denominator);
|
||||
const uint8Val = JSBI.remainder(fraction, JSBI.BigInt(256));
|
||||
arr[idx] = JSBI.toNumber(uint8Val);
|
||||
|
@ -60,7 +57,16 @@ const pow = {
|
|||
},
|
||||
|
||||
// Return nonce that hashes together with payload lower than the target
|
||||
async calcPoW(timestamp, ttl, pubKey, data, development = false, _nonceTrials = null, increment = 1, startNonce = 0) {
|
||||
async calcPoW(
|
||||
timestamp,
|
||||
ttl,
|
||||
pubKey,
|
||||
data,
|
||||
development = false,
|
||||
_nonceTrials = null,
|
||||
increment = 1,
|
||||
startNonce = 0
|
||||
) {
|
||||
const payload = new Uint8Array(
|
||||
dcodeIO.ByteBuffer.wrap(
|
||||
timestamp.toString() + ttl.toString() + pubKey + data,
|
||||
|
@ -68,7 +74,8 @@ const pow = {
|
|||
).toArrayBuffer()
|
||||
);
|
||||
|
||||
const nonceTrials = _nonceTrials || (development ? DEV_NONCE_TRIALS : PROD_NONCE_TRIALS);
|
||||
const nonceTrials =
|
||||
_nonceTrials || (development ? DEV_NONCE_TRIALS : PROD_NONCE_TRIALS);
|
||||
const target = pow.calcTarget(ttl, payload.length, nonceTrials);
|
||||
|
||||
let nonce = new Uint8Array(NONCE_LEN);
|
||||
|
@ -98,25 +105,16 @@ const pow = {
|
|||
|
||||
calcTarget(ttl, payloadLen, nonceTrials = PROD_NONCE_TRIALS) {
|
||||
// payloadLength + NONCE_LEN
|
||||
const totalLen = JSBI.add(
|
||||
JSBI.BigInt(payloadLen),
|
||||
JSBI.BigInt(NONCE_LEN)
|
||||
);
|
||||
const totalLen = JSBI.add(JSBI.BigInt(payloadLen), JSBI.BigInt(NONCE_LEN));
|
||||
// ttl * totalLen
|
||||
const ttlMult = JSBI.multiply(
|
||||
JSBI.BigInt(ttl),
|
||||
JSBI.BigInt(totalLen)
|
||||
);
|
||||
const ttlMult = JSBI.multiply(JSBI.BigInt(ttl), JSBI.BigInt(totalLen));
|
||||
// 2^16 - 1
|
||||
const two16 = JSBI.subtract(
|
||||
JSBI.exponentiate(JSBI.BigInt(2), JSBI.BigInt(16)), // 2^16
|
||||
JSBI.BigInt(1)
|
||||
);
|
||||
// ttlMult / two16
|
||||
const innerFrac = JSBI.divide(
|
||||
ttlMult,
|
||||
two16
|
||||
);
|
||||
const innerFrac = JSBI.divide(ttlMult, two16);
|
||||
// totalLen + innerFrac
|
||||
const lenPlusInnerFrac = JSBI.add(totalLen, innerFrac);
|
||||
// nonceTrials * lenPlusInnerFrac
|
||||
|
|
19118
libloki/sc_reduce32.js
19118
libloki/sc_reduce32.js
File diff suppressed because it is too large
Load diff
|
@ -1,10 +1,10 @@
|
|||
/* global window */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function () {
|
||||
(function() {
|
||||
window.libloki = window.libloki || {};
|
||||
|
||||
function consolidateLists(lists, threshold, selector = (x) => x){
|
||||
function consolidateLists(lists, threshold, selector = x => x) {
|
||||
if (typeof threshold !== 'number') {
|
||||
throw Error('Provided threshold is not a number');
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/* global window, libsignal, textsecure */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function () {
|
||||
(function() {
|
||||
window.libloki = window.libloki || {};
|
||||
|
||||
async function getPreKeyBundleForContact(pubKey) {
|
||||
|
@ -107,14 +107,16 @@
|
|||
};
|
||||
|
||||
store.loadContactPreKey = async pubKey => {
|
||||
const preKey = await window.Signal.Data.getContactPreKeyByIdentityKey(pubKey);
|
||||
const preKey = await window.Signal.Data.getContactPreKeyByIdentityKey(
|
||||
pubKey
|
||||
);
|
||||
if (preKey) {
|
||||
return {
|
||||
id: preKey.id,
|
||||
keyId: preKey.keyId,
|
||||
publicKey: preKey.publicKey,
|
||||
identityKeyString: preKey.identityKeyString,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
window.log.warn('Failed to fetch contact prekey:', pubKey);
|
||||
|
@ -123,7 +125,10 @@
|
|||
|
||||
store.loadContactPreKeys = async filters => {
|
||||
const { keyId, identityKeyString } = filters;
|
||||
const keys = await window.Signal.Data.getContactPreKeys(keyId, identityKeyString);
|
||||
const keys = await window.Signal.Data.getContactPreKeys(
|
||||
keyId,
|
||||
identityKeyString
|
||||
);
|
||||
if (keys) {
|
||||
return keys.map(preKey => ({
|
||||
id: preKey.id,
|
||||
|
@ -133,10 +138,7 @@
|
|||
}));
|
||||
}
|
||||
|
||||
window.log.warn(
|
||||
'Failed to fetch signed prekey with filters',
|
||||
filters
|
||||
);
|
||||
window.log.warn('Failed to fetch signed prekey with filters', filters);
|
||||
return undefined;
|
||||
};
|
||||
|
||||
|
@ -162,7 +164,9 @@
|
|||
};
|
||||
|
||||
store.loadContactSignedPreKey = async pubKey => {
|
||||
const preKey = await window.Signal.Data.getContactSignedPreKeyByIdentityKey(pubKey);
|
||||
const preKey = await window.Signal.Data.getContactSignedPreKeyByIdentityKey(
|
||||
pubKey
|
||||
);
|
||||
if (preKey) {
|
||||
return {
|
||||
id: preKey.id,
|
||||
|
@ -180,7 +184,10 @@
|
|||
|
||||
store.loadContactSignedPreKeys = async filters => {
|
||||
const { keyId, identityKeyString } = filters;
|
||||
const keys = await window.Signal.Data.getContactSignedPreKeys(keyId, identityKeyString);
|
||||
const keys = await window.Signal.Data.getContactSignedPreKeys(
|
||||
keyId,
|
||||
identityKeyString
|
||||
);
|
||||
if (keys) {
|
||||
return keys.map(preKey => ({
|
||||
id: preKey.id,
|
||||
|
|
|
@ -15,21 +15,21 @@ describe('Crypto', () => {
|
|||
store.put('identityKey', identityKey);
|
||||
const key = libsignal.crypto.getRandomBytes(32);
|
||||
const pubKeyString = StringView.arrayBufferToHex(key);
|
||||
address = new libsignal.SignalProtocolAddress(
|
||||
pubKeyString,
|
||||
1
|
||||
);
|
||||
address = new libsignal.SignalProtocolAddress(pubKeyString, 1);
|
||||
fallbackCipher = new libloki.crypto.FallBackSessionCipher(address);
|
||||
});
|
||||
|
||||
it('should encrypt fallback cipher messages as friend requests', async () => {
|
||||
const buffer = new ArrayBuffer(10);
|
||||
const { type } = await fallbackCipher.encrypt(buffer);
|
||||
assert.strictEqual(type, textsecure.protobuf.Envelope.Type.FRIEND_REQUEST);
|
||||
assert.strictEqual(
|
||||
type,
|
||||
textsecure.protobuf.Envelope.Type.FRIEND_REQUEST
|
||||
);
|
||||
});
|
||||
|
||||
it('should encrypt and then decrypt a message with the same result', async () => {
|
||||
const arr = new Uint8Array([1,2,3,4,5]);
|
||||
const arr = new Uint8Array([1, 2, 3, 4, 5]);
|
||||
const { body } = await fallbackCipher.encrypt(arr.buffer);
|
||||
const result = await fallbackCipher.decrypt(body);
|
||||
assert.deepEqual(result, arr.buffer);
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
/* global dcodeIO, Plotly */
|
||||
let jobId = 0;
|
||||
let currentTrace = 0
|
||||
let currentTrace = 0;
|
||||
let plotlyDiv;
|
||||
const workers = [];
|
||||
async function run(messageLength, numWorkers = 1, nonceTrials = 100, ttl = 72) {
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
const pubKey = '05ec8635a07a13743516c7c9b3412f3e8252efb7fcaf67eb1615ffba62bebc6802';
|
||||
const pubKey =
|
||||
'05ec8635a07a13743516c7c9b3412f3e8252efb7fcaf67eb1615ffba62bebc6802';
|
||||
const message = randomString(messageLength);
|
||||
const messageBuffer = dcodeIO.ByteBuffer.wrap(message, 'utf8').toArrayBuffer();
|
||||
const messageBuffer = dcodeIO.ByteBuffer.wrap(
|
||||
message,
|
||||
'utf8'
|
||||
).toArrayBuffer();
|
||||
const data = dcodeIO.ByteBuffer.wrap(messageBuffer).toString('base64');
|
||||
const promises = [];
|
||||
const t0 = performance.now();
|
||||
|
@ -17,9 +21,20 @@ async function run(messageLength, numWorkers = 1, nonceTrials = 100, ttl = 72) {
|
|||
jobId += 1;
|
||||
const increment = numWorkers;
|
||||
const index = w;
|
||||
worker.postMessage([jobId, 'calcPoW', timestamp, ttl * 60 * 60, pubKey, data, false, nonceTrials, increment, index]);
|
||||
worker.postMessage([
|
||||
jobId,
|
||||
'calcPoW',
|
||||
timestamp,
|
||||
ttl * 60 * 60,
|
||||
pubKey,
|
||||
data,
|
||||
false,
|
||||
nonceTrials,
|
||||
increment,
|
||||
index,
|
||||
]);
|
||||
const p = new Promise(resolve => {
|
||||
worker.onmessage = (nonce) => {
|
||||
worker.onmessage = nonce => {
|
||||
resolve(nonce);
|
||||
};
|
||||
});
|
||||
|
@ -33,9 +48,15 @@ async function run(messageLength, numWorkers = 1, nonceTrials = 100, ttl = 72) {
|
|||
workers.forEach(worker => worker.terminate());
|
||||
}
|
||||
|
||||
async function runPoW({ iteration, nonceTrials, numWorkers, messageLength = 50, ttl = 72 }) {
|
||||
async function runPoW({
|
||||
iteration,
|
||||
nonceTrials,
|
||||
numWorkers,
|
||||
messageLength = 50,
|
||||
ttl = 72,
|
||||
}) {
|
||||
const name = `W:${numWorkers} - NT: ${nonceTrials} - L:${messageLength} - TTL:${ttl}`;
|
||||
Plotly.addTraces(plotlyDiv ,{
|
||||
Plotly.addTraces(plotlyDiv, {
|
||||
y: [],
|
||||
type: 'box',
|
||||
boxpoints: 'all',
|
||||
|
@ -46,84 +67,148 @@ async function runPoW({ iteration, nonceTrials, numWorkers, messageLength = 50,
|
|||
await run(messageLength, numWorkers, nonceTrials, ttl);
|
||||
}
|
||||
currentTrace += 1;
|
||||
|
||||
// eslint-disable-next-line
|
||||
console.log(`done for ${name}`);
|
||||
}
|
||||
|
||||
|
||||
function randomString(length) {
|
||||
let text = '';
|
||||
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
const possible =
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
for (let i = 0; i < length; i += 1)
|
||||
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||
return text;
|
||||
}
|
||||
|
||||
function addPoint(duration) {
|
||||
Plotly.extendTraces(plotlyDiv, {y: [[duration]]}, [currentTrace]);
|
||||
Plotly.extendTraces(plotlyDiv, { y: [[duration]] }, [currentTrace]);
|
||||
}
|
||||
async function startMessageLengthRun() {
|
||||
const iteration0 = parseFloat(document.getElementById('iteration0').value);
|
||||
const nonceTrials0 = parseFloat(document.getElementById('nonceTrials0').value);
|
||||
const nonceTrials0 = parseFloat(
|
||||
document.getElementById('nonceTrials0').value
|
||||
);
|
||||
const numWorkers0 = parseFloat(document.getElementById('numWorkers0').value);
|
||||
const messageLengthStart0 = parseFloat(document.getElementById('messageLengthStart0').value);
|
||||
const messageLengthStop0 = parseFloat(document.getElementById('messageLengthStop0').value);
|
||||
const messageLengthStep0 = parseFloat(document.getElementById('messageLengthStep0').value);
|
||||
const messageLengthStart0 = parseFloat(
|
||||
document.getElementById('messageLengthStart0').value
|
||||
);
|
||||
const messageLengthStop0 = parseFloat(
|
||||
document.getElementById('messageLengthStop0').value
|
||||
);
|
||||
const messageLengthStep0 = parseFloat(
|
||||
document.getElementById('messageLengthStep0').value
|
||||
);
|
||||
const TTL0 = parseFloat(document.getElementById('TTL0').value);
|
||||
for (let l = messageLengthStart0; l < messageLengthStop0; l += messageLengthStep0) {
|
||||
for (
|
||||
let l = messageLengthStart0;
|
||||
l < messageLengthStop0;
|
||||
l += messageLengthStep0
|
||||
) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await runPoW({ iteration: iteration0, nonceTrials: nonceTrials0, numWorkers: numWorkers0, messageLength: l, ttl: TTL0 });
|
||||
await runPoW({
|
||||
iteration: iteration0,
|
||||
nonceTrials: nonceTrials0,
|
||||
numWorkers: numWorkers0,
|
||||
messageLength: l,
|
||||
ttl: TTL0,
|
||||
});
|
||||
}
|
||||
}
|
||||
async function startNumWorkerRun() {
|
||||
const iteration1 = parseFloat(document.getElementById('iteration1').value);
|
||||
const nonceTrials1 = parseFloat(document.getElementById('nonceTrials1').value);
|
||||
const numWorkersStart1 = parseFloat(document.getElementById('numWorkersStart1').value);
|
||||
const numWorkersEnd1 = parseFloat(document.getElementById('numWorkersEnd1').value);
|
||||
const messageLength1 = parseFloat(document.getElementById('messageLength1').value);
|
||||
const nonceTrials1 = parseFloat(
|
||||
document.getElementById('nonceTrials1').value
|
||||
);
|
||||
const numWorkersStart1 = parseFloat(
|
||||
document.getElementById('numWorkersStart1').value
|
||||
);
|
||||
const numWorkersEnd1 = parseFloat(
|
||||
document.getElementById('numWorkersEnd1').value
|
||||
);
|
||||
const messageLength1 = parseFloat(
|
||||
document.getElementById('messageLength1').value
|
||||
);
|
||||
const TTL1 = parseFloat(document.getElementById('TTL1').value);
|
||||
for (let numWorkers = numWorkersStart1; numWorkers <= numWorkersEnd1; numWorkers +=1) {
|
||||
for (
|
||||
let numWorkers = numWorkersStart1;
|
||||
numWorkers <= numWorkersEnd1;
|
||||
numWorkers += 1
|
||||
) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await runPoW({ iteration: iteration1, nonceTrials: nonceTrials1, numWorkers, messageLength: messageLength1, ttl: TTL1 });
|
||||
await runPoW({
|
||||
iteration: iteration1,
|
||||
nonceTrials: nonceTrials1,
|
||||
numWorkers,
|
||||
messageLength: messageLength1,
|
||||
ttl: TTL1,
|
||||
});
|
||||
}
|
||||
}
|
||||
async function startNonceTrialsRun() {
|
||||
const iteration2 = parseFloat(document.getElementById('iteration2').value);
|
||||
const messageLength2 = parseFloat(document.getElementById('messageLength2').value);
|
||||
const messageLength2 = parseFloat(
|
||||
document.getElementById('messageLength2').value
|
||||
);
|
||||
const numWorkers2 = parseFloat(document.getElementById('numWorkers2').value);
|
||||
const nonceTrialsStart2 = parseFloat(document.getElementById('nonceTrialsStart2').value);
|
||||
const nonceTrialsStop2 = parseFloat(document.getElementById('nonceTrialsStop2').value);
|
||||
const nonceTrialsStep2 = parseFloat(document.getElementById('nonceTrialsStep2').value);
|
||||
const nonceTrialsStart2 = parseFloat(
|
||||
document.getElementById('nonceTrialsStart2').value
|
||||
);
|
||||
const nonceTrialsStop2 = parseFloat(
|
||||
document.getElementById('nonceTrialsStop2').value
|
||||
);
|
||||
const nonceTrialsStep2 = parseFloat(
|
||||
document.getElementById('nonceTrialsStep2').value
|
||||
);
|
||||
const TTL2 = parseFloat(document.getElementById('TTL2').value);
|
||||
for (let n = nonceTrialsStart2; n < nonceTrialsStop2; n += nonceTrialsStep2) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await runPoW({ iteration: iteration2, nonceTrials: n, numWorkers: numWorkers2, messageLength: messageLength2, ttl: TTL2 });
|
||||
await runPoW({
|
||||
iteration: iteration2,
|
||||
nonceTrials: n,
|
||||
numWorkers: numWorkers2,
|
||||
messageLength: messageLength2,
|
||||
ttl: TTL2,
|
||||
});
|
||||
}
|
||||
}
|
||||
async function starTTLRun() {
|
||||
const iteration3 = parseFloat(document.getElementById('iteration3').value);
|
||||
const nonceTrials3 = parseFloat(document.getElementById('nonceTrials3').value);
|
||||
const messageLength3 = parseFloat(document.getElementById('messageLength3').value);
|
||||
const nonceTrials3 = parseFloat(
|
||||
document.getElementById('nonceTrials3').value
|
||||
);
|
||||
const messageLength3 = parseFloat(
|
||||
document.getElementById('messageLength3').value
|
||||
);
|
||||
const numWorkers3 = parseFloat(document.getElementById('numWorkers3').value);
|
||||
const TTLStart3 = parseFloat(document.getElementById('TTLStart3').value);
|
||||
const TTLStop3 = parseFloat(document.getElementById('TTLStop3').value);
|
||||
const TTLStep3 = parseFloat(document.getElementById('TTLStep3').value);
|
||||
for (let ttl = TTLStart3; ttl < TTLStop3; ttl += TTLStep3) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await runPoW({ iteration: iteration3, nonceTrials: nonceTrials3, numWorkers: numWorkers3, messageLength: messageLength3, ttl });
|
||||
await runPoW({
|
||||
iteration: iteration3,
|
||||
nonceTrials: nonceTrials3,
|
||||
numWorkers: numWorkers3,
|
||||
messageLength: messageLength3,
|
||||
ttl,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
async function start(index) {
|
||||
const data = [];
|
||||
const layout = {};
|
||||
const options = {
|
||||
responsive: true,
|
||||
};
|
||||
plotlyDiv =`plotly${index}`;
|
||||
plotlyDiv = `plotly${index}`;
|
||||
currentTrace = 0;
|
||||
window.chart = Plotly.newPlot(plotlyDiv, data, layout, options);
|
||||
workers.forEach(worker => worker.terminate());
|
||||
|
||||
switch(index) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
await startMessageLengthRun();
|
||||
break;
|
||||
|
|
|
@ -11,7 +11,7 @@ const {
|
|||
describe('Proof of Work', () => {
|
||||
describe('#incrementNonce', () => {
|
||||
it('should increment a Uint8Array nonce correctly', () => {
|
||||
const arr1Before = new Uint8Array([0,0,0,0,0,0,0,0]);
|
||||
const arr1Before = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
const arr1After = incrementNonce(arr1Before);
|
||||
assert.strictEqual(arr1After[0], 0);
|
||||
assert.strictEqual(arr1After[1], 0);
|
||||
|
@ -24,15 +24,21 @@ describe('Proof of Work', () => {
|
|||
});
|
||||
|
||||
it('should increment a Uint8Array nonce correctly in a loop', () => {
|
||||
let arr = new Uint8Array([0,0,0,0,0,0,0,0]);
|
||||
assert.deepEqual(incrementNonce(arr), new Uint8Array([0,0,0,0,0,0,0,1]));
|
||||
arr = new Uint8Array([0,0,0,0,0,0,0,0]);
|
||||
for(let i = 0; i <= 255; i += 1) {
|
||||
let arr = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
assert.deepEqual(
|
||||
incrementNonce(arr),
|
||||
new Uint8Array([0, 0, 0, 0, 0, 0, 0, 1])
|
||||
);
|
||||
arr = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
for (let i = 0; i <= 255; i += 1) {
|
||||
arr = incrementNonce(arr);
|
||||
}
|
||||
assert.deepEqual(arr, new Uint8Array([0,0,0,0,0,0,1,0]));
|
||||
arr = new Uint8Array([255,255,255,255,255,255,255,255]);
|
||||
assert.deepEqual(incrementNonce(arr), new Uint8Array([0,0,0,0,0,0,0,0]));
|
||||
assert.deepEqual(arr, new Uint8Array([0, 0, 0, 0, 0, 0, 1, 0]));
|
||||
arr = new Uint8Array([255, 255, 255, 255, 255, 255, 255, 255]);
|
||||
assert.deepEqual(
|
||||
incrementNonce(arr),
|
||||
new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -41,12 +47,12 @@ describe('Proof of Work', () => {
|
|||
// These values will need to be updated if we adjust the difficulty settings
|
||||
let payloadLen = 625;
|
||||
const ttl = 86400;
|
||||
let expectedTarget = new Uint8Array([0,4,119,164,35,224,222,64]);
|
||||
let expectedTarget = new Uint8Array([0, 4, 119, 164, 35, 224, 222, 64]);
|
||||
|
||||
let actualTarget = calcTarget(ttl, payloadLen, 10);
|
||||
assert.deepEqual(actualTarget, expectedTarget);
|
||||
payloadLen = 6597;
|
||||
expectedTarget = new Uint8Array([0,0,109,145,174,146,124,3]);
|
||||
expectedTarget = new Uint8Array([0, 0, 109, 145, 174, 146, 124, 3]);
|
||||
actualTarget = calcTarget(ttl, payloadLen, 10);
|
||||
assert.deepEqual(actualTarget, expectedTarget);
|
||||
});
|
||||
|
@ -54,30 +60,30 @@ describe('Proof of Work', () => {
|
|||
|
||||
describe('#greaterThan', () => {
|
||||
it('should correclty compare two Uint8Arrays', () => {
|
||||
let arr1 = new Uint8Array([0,0,0,0,0,0,0,0,0,1]);
|
||||
let arr2 = new Uint8Array([0,0,0,0,0,0,0,0,0,1]);
|
||||
assert.isFalse(greaterThan(arr1, arr2))
|
||||
arr1 = new Uint8Array([0,0,0,0,0,0,0,0,0,2]);
|
||||
arr2 = new Uint8Array([0,0,0,0,0,0,0,0,0,1]);
|
||||
assert.isTrue(greaterThan(arr1, arr2))
|
||||
arr1 = new Uint8Array([255,255,255,255,255,255,255,255,255,255]);
|
||||
arr2 = new Uint8Array([255,255,255,255,255,255,255,255,255,254]);
|
||||
assert.isTrue(greaterThan(arr1, arr2))
|
||||
arr1 = new Uint8Array([254,255,255,255,255,255,255,255,255,255]);
|
||||
arr2 = new Uint8Array([255,255,255,255,255,255,255,255,255,255]);
|
||||
let arr1 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 1]);
|
||||
let arr2 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 1]);
|
||||
assert.isFalse(greaterThan(arr1, arr2));
|
||||
arr1 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 2]);
|
||||
arr2 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 1]);
|
||||
assert.isTrue(greaterThan(arr1, arr2));
|
||||
arr1 = new Uint8Array([255, 255, 255, 255, 255, 255, 255, 255, 255, 255]);
|
||||
arr2 = new Uint8Array([255, 255, 255, 255, 255, 255, 255, 255, 255, 254]);
|
||||
assert.isTrue(greaterThan(arr1, arr2));
|
||||
arr1 = new Uint8Array([254, 255, 255, 255, 255, 255, 255, 255, 255, 255]);
|
||||
arr2 = new Uint8Array([255, 255, 255, 255, 255, 255, 255, 255, 255, 255]);
|
||||
assert.isFalse(greaterThan(arr1, arr2));
|
||||
arr1 = new Uint8Array([0]);
|
||||
arr2 = new Uint8Array([0,0]);
|
||||
assert.isFalse(greaterThan(arr1, arr2))
|
||||
arr2 = new Uint8Array([0, 0]);
|
||||
assert.isFalse(greaterThan(arr1, arr2));
|
||||
});
|
||||
});
|
||||
|
||||
describe('#bufferToBase64', () => {
|
||||
it('should correclty convert a Uint8Array to a base64 string', () => {
|
||||
let arr = new Uint8Array([1,2,3]);
|
||||
let arr = new Uint8Array([1, 2, 3]);
|
||||
let expected = 'AQID';
|
||||
assert.strictEqual(bufferToBase64(arr), expected);
|
||||
arr = new Uint8Array([123,25,3,121,45,87,24,111]);
|
||||
arr = new Uint8Array([123, 25, 3, 121, 45, 87, 24, 111]);
|
||||
expected = 'exkDeS1XGG8=';
|
||||
assert.strictEqual(bufferToBase64(arr), expected);
|
||||
arr = new Uint8Array([]);
|
||||
|
|
|
@ -3,11 +3,17 @@
|
|||
describe('ServiceNodes', () => {
|
||||
describe('#consolidateLists', () => {
|
||||
it('should throw when provided a non-iterable list', () => {
|
||||
assert.throws(() => libloki.serviceNodes.consolidateLists(null, 1), Error);
|
||||
assert.throws(
|
||||
() => libloki.serviceNodes.consolidateLists(null, 1),
|
||||
Error
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw when provided a non-iterable item in the list', () => {
|
||||
assert.throws(() => libloki.serviceNodes.consolidateLists([1, 2, 3], 1), Error);
|
||||
assert.throws(
|
||||
() => libloki.serviceNodes.consolidateLists([1, 2, 3], 1),
|
||||
Error
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw when provided a non-number threshold', () => {
|
||||
|
@ -19,10 +25,10 @@ describe('ServiceNodes', () => {
|
|||
|
||||
it('should throw when provided a non-function selector', () => {
|
||||
[1, 'a', 0xffffffff, { really: 'not a function' }].forEach(x => {
|
||||
assert.throws(() =>
|
||||
libloki.serviceNodes.consolidateLists([], 1, x),
|
||||
assert.throws(
|
||||
() => libloki.serviceNodes.consolidateLists([], 1, x),
|
||||
'Provided selector is not a function'
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -32,64 +38,85 @@ describe('ServiceNodes', () => {
|
|||
});
|
||||
|
||||
it('should return the input when only 1 list is provided', () => {
|
||||
const result = libloki.serviceNodes.consolidateLists([['a', 'b', 'c']], 1);
|
||||
const result = libloki.serviceNodes.consolidateLists(
|
||||
[['a', 'b', 'c']],
|
||||
1
|
||||
);
|
||||
assert.deepEqual(result, ['a', 'b', 'c']);
|
||||
});
|
||||
|
||||
it('should return the union of all lists when threshold is 0', () => {
|
||||
const result = libloki.serviceNodes.consolidateLists([
|
||||
['a', 'b', 'c', 'h'],
|
||||
['d', 'e', 'f', 'g'],
|
||||
['g', 'h'],
|
||||
], 0);
|
||||
const result = libloki.serviceNodes.consolidateLists(
|
||||
[['a', 'b', 'c', 'h'], ['d', 'e', 'f', 'g'], ['g', 'h']],
|
||||
0
|
||||
);
|
||||
assert.deepEqual(result.sort(), ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']);
|
||||
});
|
||||
|
||||
it('should use the selector to identify the elements', () => {
|
||||
const result = libloki.serviceNodes.consolidateLists([
|
||||
[{ id: 1, val: 'a'}, { id: 2, val: 'b'}, { id: 3, val: 'c'}, { id: 8, val: 'h'}],
|
||||
[{ id: 4, val: 'd'}, { id: 5, val: 'e'}, { id: 6, val: 'f'}, { id: 7, val: 'g'}],
|
||||
[{ id: 7, val: 'g'}, { id: 8, val: 'h'}],
|
||||
], 0, x => x.id);
|
||||
const result = libloki.serviceNodes.consolidateLists(
|
||||
[
|
||||
[
|
||||
{ id: 1, val: 'a' },
|
||||
{ id: 2, val: 'b' },
|
||||
{ id: 3, val: 'c' },
|
||||
{ id: 8, val: 'h' },
|
||||
],
|
||||
[
|
||||
{ id: 4, val: 'd' },
|
||||
{ id: 5, val: 'e' },
|
||||
{ id: 6, val: 'f' },
|
||||
{ id: 7, val: 'g' },
|
||||
],
|
||||
[{ id: 7, val: 'g' }, { id: 8, val: 'h' }],
|
||||
],
|
||||
0,
|
||||
x => x.id
|
||||
);
|
||||
const expected = [
|
||||
{ id: 1, val: 'a'},
|
||||
{ id: 2, val: 'b'},
|
||||
{ id: 3, val: 'c'},
|
||||
{ id: 4, val: 'd'},
|
||||
{ id: 5, val: 'e'},
|
||||
{ id: 6, val: 'f'},
|
||||
{ id: 7, val: 'g'},
|
||||
{ id: 8, val: 'h'},
|
||||
{ id: 1, val: 'a' },
|
||||
{ id: 2, val: 'b' },
|
||||
{ id: 3, val: 'c' },
|
||||
{ id: 4, val: 'd' },
|
||||
{ id: 5, val: 'e' },
|
||||
{ id: 6, val: 'f' },
|
||||
{ id: 7, val: 'g' },
|
||||
{ id: 8, val: 'h' },
|
||||
];
|
||||
assert.deepEqual(result.sort((a, b) => a.val > b.val), expected);
|
||||
});
|
||||
|
||||
it('should return the intersection of all lists when threshold is 1', () => {
|
||||
const result = libloki.serviceNodes.consolidateLists([
|
||||
['a', 'b', 'c', 'd'],
|
||||
['a', 'e', 'f', 'g'],
|
||||
['a', 'h'],
|
||||
], 1);
|
||||
const result = libloki.serviceNodes.consolidateLists(
|
||||
[['a', 'b', 'c', 'd'], ['a', 'e', 'f', 'g'], ['a', 'h']],
|
||||
1
|
||||
);
|
||||
assert.deepEqual(result, ['a']);
|
||||
});
|
||||
|
||||
it('should return the elements that have an occurence >= the provided threshold', () => {
|
||||
const result = libloki.serviceNodes.consolidateLists([
|
||||
['a', 'b', 'c', 'd', 'e', 'f', 'g'],
|
||||
['a', 'b', 'c', 'd', 'e', 'f', 'h'],
|
||||
['a', 'b', 'c', 'd', 'e', 'f', 'g'],
|
||||
['a', 'b', 'c', 'd', 'e', 'g', 'h'],
|
||||
], 3/4);
|
||||
const result = libloki.serviceNodes.consolidateLists(
|
||||
[
|
||||
['a', 'b', 'c', 'd', 'e', 'f', 'g'],
|
||||
['a', 'b', 'c', 'd', 'e', 'f', 'h'],
|
||||
['a', 'b', 'c', 'd', 'e', 'f', 'g'],
|
||||
['a', 'b', 'c', 'd', 'e', 'g', 'h'],
|
||||
],
|
||||
3 / 4
|
||||
);
|
||||
assert.deepEqual(result, ['a', 'b', 'c', 'd', 'e', 'f', 'g']);
|
||||
});
|
||||
|
||||
it('should work with sets as well', () => {
|
||||
const result = libloki.serviceNodes.consolidateLists(new Set([
|
||||
new Set(['a', 'b', 'c', 'd', 'e', 'f', 'g']),
|
||||
new Set(['a', 'b', 'c', 'd', 'e', 'f', 'h']),
|
||||
new Set(['a', 'b', 'c', 'd', 'e', 'f', 'g']),
|
||||
new Set(['a', 'b', 'c', 'd', 'e', 'g', 'h']),
|
||||
]), 3/4);
|
||||
const result = libloki.serviceNodes.consolidateLists(
|
||||
new Set([
|
||||
new Set(['a', 'b', 'c', 'd', 'e', 'f', 'g']),
|
||||
new Set(['a', 'b', 'c', 'd', 'e', 'f', 'h']),
|
||||
new Set(['a', 'b', 'c', 'd', 'e', 'f', 'g']),
|
||||
new Set(['a', 'b', 'c', 'd', 'e', 'g', 'h']),
|
||||
]),
|
||||
3 / 4
|
||||
);
|
||||
assert.deepEqual(result, ['a', 'b', 'c', 'd', 'e', 'f', 'g']);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -21,7 +21,9 @@ describe('Storage', () => {
|
|||
const pubKey = libsignal.crypto.getRandomBytes(32);
|
||||
const pubKeyString = StringView.arrayBufferToHex(pubKey);
|
||||
const preKeyIdBefore = textsecure.storage.get('maxPreKeyId', 1);
|
||||
const newBundle = await libloki.storage.getPreKeyBundleForContact(pubKeyString);
|
||||
const newBundle = await libloki.storage.getPreKeyBundleForContact(
|
||||
pubKeyString
|
||||
);
|
||||
const preKeyIdAfter = textsecure.storage.get('maxPreKeyId', 1);
|
||||
assert.strictEqual(preKeyIdAfter, preKeyIdBefore + 1);
|
||||
|
||||
|
@ -34,16 +36,23 @@ describe('Storage', () => {
|
|||
assert.isDefined(newBundle.preKey);
|
||||
assert.isDefined(newBundle.signedKey);
|
||||
assert.isDefined(newBundle.signature);
|
||||
assert.strictEqual(testKeyArray.byteLength, newBundle.signedKey.byteLength);
|
||||
for (let i = 0 ; i !== testKeyArray.byteLength; i += 1)
|
||||
assert.strictEqual(
|
||||
testKeyArray.byteLength,
|
||||
newBundle.signedKey.byteLength
|
||||
);
|
||||
for (let i = 0; i !== testKeyArray.byteLength; i += 1)
|
||||
assert.strictEqual(testKeyArray[i], newBundle.signedKey[i]);
|
||||
});
|
||||
|
||||
it('should return the same prekey bundle after creating a contact', async () => {
|
||||
const pubKey = libsignal.crypto.getRandomBytes(32);
|
||||
const pubKeyString = StringView.arrayBufferToHex(pubKey);
|
||||
const bundle1 = await libloki.storage.getPreKeyBundleForContact(pubKeyString);
|
||||
const bundle2 = await libloki.storage.getPreKeyBundleForContact(pubKeyString);
|
||||
const bundle1 = await libloki.storage.getPreKeyBundleForContact(
|
||||
pubKeyString
|
||||
);
|
||||
const bundle2 = await libloki.storage.getPreKeyBundleForContact(
|
||||
pubKeyString
|
||||
);
|
||||
assert.isDefined(bundle1);
|
||||
assert.isDefined(bundle2);
|
||||
assert.deepEqual(bundle1, bundle2);
|
||||
|
@ -53,7 +62,9 @@ describe('Storage', () => {
|
|||
const pubKey = libsignal.crypto.getRandomBytes(32);
|
||||
const pubKeyString = StringView.arrayBufferToHex(pubKey);
|
||||
const preKeyIdBefore = textsecure.storage.get('maxPreKeyId', 1);
|
||||
const newBundle = await libloki.storage.getPreKeyBundleForContact(pubKeyString);
|
||||
const newBundle = await libloki.storage.getPreKeyBundleForContact(
|
||||
pubKeyString
|
||||
);
|
||||
const preKeyIdAfter = textsecure.storage.get('maxPreKeyId', 1);
|
||||
assert.strictEqual(preKeyIdAfter, preKeyIdBefore + 1);
|
||||
|
||||
|
|
|
@ -15,6 +15,8 @@
|
|||
*/
|
||||
|
||||
/* eslint-disable more/no-then */
|
||||
/* eslint-disable no-unused-vars */
|
||||
/* eslint-disable no-await-in-loop */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
|
@ -70,17 +72,18 @@
|
|||
generateKeypair = libsignal.KeyHelper.generateIdentityKeyPair;
|
||||
}
|
||||
return this.queueTask(() =>
|
||||
generateKeypair().then(async identityKeyPair => {
|
||||
return createAccount(identityKeyPair)
|
||||
.then(() => this.saveMnemonic(mnemonic))
|
||||
.then(clearSessionsAndPreKeys)
|
||||
.then(generateKeys)
|
||||
.then(confirmKeys)
|
||||
.then(() => {
|
||||
const pubKeyString = StringView.arrayBufferToHex(identityKeyPair.pubKey);
|
||||
registrationDone(pubKeyString, profileName);
|
||||
});
|
||||
}
|
||||
generateKeypair().then(async identityKeyPair =>
|
||||
createAccount(identityKeyPair)
|
||||
.then(() => this.saveMnemonic(mnemonic))
|
||||
.then(clearSessionsAndPreKeys)
|
||||
.then(generateKeys)
|
||||
.then(confirmKeys)
|
||||
.then(() => {
|
||||
const pubKeyString = StringView.arrayBufferToHex(
|
||||
identityKeyPair.pubKey
|
||||
);
|
||||
registrationDone(pubKeyString, profileName);
|
||||
})
|
||||
)
|
||||
);
|
||||
},
|
||||
|
@ -138,12 +141,13 @@
|
|||
log.info('Added mock contact');
|
||||
},
|
||||
registerSecondDevice(setProvisioningUrl, confirmNumber, progressCallback) {
|
||||
throw new Error('account_manager: registerSecondDevice has not been implemented!');
|
||||
throw new Error(
|
||||
'account_manager: registerSecondDevice has not been implemented!'
|
||||
);
|
||||
},
|
||||
refreshPreKeys() {
|
||||
// const generateKeys = this.generateKeys.bind(this, 0);
|
||||
// const registerKeys = this.server.registerKeys.bind(this.server);
|
||||
|
||||
// return this.queueTask(() =>
|
||||
// this.server.getMyKeys().then(preKeyCount => {
|
||||
// window.log.info(`prekey count ${preKeyCount}`);
|
||||
|
@ -312,50 +316,51 @@
|
|||
password = password.substring(0, password.length - 2);
|
||||
const registrationId = libsignal.KeyHelper.generateRegistrationId();
|
||||
|
||||
// update our own identity key, which may have changed
|
||||
// if we're relinking after a reinstall on the master device
|
||||
const pubKeyString = StringView.arrayBufferToHex(
|
||||
identityKeyPair.pubKey
|
||||
);
|
||||
// update our own identity key, which may have changed
|
||||
// if we're relinking after a reinstall on the master device
|
||||
const pubKeyString = StringView.arrayBufferToHex(identityKeyPair.pubKey);
|
||||
|
||||
return Promise.resolve().then(async () => {
|
||||
await Promise.all([
|
||||
textsecure.storage.remove('identityKey'),
|
||||
textsecure.storage.remove('signaling_key'),
|
||||
textsecure.storage.remove('password'),
|
||||
textsecure.storage.remove('registrationId'),
|
||||
textsecure.storage.remove('number_id'),
|
||||
textsecure.storage.remove('device_name'),
|
||||
textsecure.storage.remove('userAgent'),
|
||||
textsecure.storage.remove('read-receipts-setting'),
|
||||
]);
|
||||
await Promise.all([
|
||||
textsecure.storage.remove('identityKey'),
|
||||
textsecure.storage.remove('signaling_key'),
|
||||
textsecure.storage.remove('password'),
|
||||
textsecure.storage.remove('registrationId'),
|
||||
textsecure.storage.remove('number_id'),
|
||||
textsecure.storage.remove('device_name'),
|
||||
textsecure.storage.remove('userAgent'),
|
||||
textsecure.storage.remove('read-receipts-setting'),
|
||||
]);
|
||||
|
||||
// update our own identity key, which may have changed
|
||||
// if we're relinking after a reinstall on the master device
|
||||
await textsecure.storage.protocol.saveIdentityWithAttributes(pubKeyString, {
|
||||
// update our own identity key, which may have changed
|
||||
// if we're relinking after a reinstall on the master device
|
||||
await textsecure.storage.protocol.saveIdentityWithAttributes(
|
||||
pubKeyString,
|
||||
{
|
||||
id: pubKeyString,
|
||||
publicKey: identityKeyPair.pubKey,
|
||||
firstUse: true,
|
||||
timestamp: Date.now(),
|
||||
verified: textsecure.storage.protocol.VerifiedStatus.VERIFIED,
|
||||
nonblockingApproval: true,
|
||||
});
|
||||
|
||||
await textsecure.storage.put('identityKey', identityKeyPair);
|
||||
await textsecure.storage.put('signaling_key', signalingKey);
|
||||
await textsecure.storage.put('password', password);
|
||||
await textsecure.storage.put('registrationId', registrationId);
|
||||
if (userAgent) {
|
||||
await textsecure.storage.put('userAgent', userAgent);
|
||||
}
|
||||
);
|
||||
|
||||
await textsecure.storage.put(
|
||||
'read-receipt-setting',
|
||||
Boolean(readReceipts)
|
||||
);
|
||||
await textsecure.storage.put('identityKey', identityKeyPair);
|
||||
await textsecure.storage.put('signaling_key', signalingKey);
|
||||
await textsecure.storage.put('password', password);
|
||||
await textsecure.storage.put('registrationId', registrationId);
|
||||
if (userAgent) {
|
||||
await textsecure.storage.put('userAgent', userAgent);
|
||||
}
|
||||
|
||||
await textsecure.storage.user.setNumberAndDeviceId(pubKeyString, 1);
|
||||
});
|
||||
await textsecure.storage.put(
|
||||
'read-receipt-setting',
|
||||
Boolean(readReceipts)
|
||||
);
|
||||
|
||||
await textsecure.storage.user.setNumberAndDeviceId(pubKeyString, 1);
|
||||
});
|
||||
},
|
||||
clearSessionsAndPreKeys() {
|
||||
const store = textsecure.storage.protocol;
|
||||
|
@ -462,7 +467,10 @@
|
|||
window.log.info('registration done');
|
||||
|
||||
// Ensure that we always have a conversation for ourself
|
||||
const conversation = await ConversationController.getOrCreateAndWait(number, 'private');
|
||||
const conversation = await ConversationController.getOrCreateAndWait(
|
||||
number,
|
||||
'private'
|
||||
);
|
||||
|
||||
await storage.setProfileName(profileName);
|
||||
|
||||
|
|
|
@ -128,17 +128,17 @@
|
|||
inherit(Error, UnregisteredUserError);
|
||||
|
||||
function PoWError(number, error) {
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
this.number = number.split('.')[0];
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
this.number = number.split('.')[0];
|
||||
|
||||
ReplayableError.call(this, {
|
||||
name: 'PoWError',
|
||||
message: 'Failed to calculate PoW',
|
||||
});
|
||||
ReplayableError.call(this, {
|
||||
name: 'PoWError',
|
||||
message: 'Failed to calculate PoW',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
appendStack(this, error);
|
||||
}
|
||||
if (error) {
|
||||
appendStack(this, error);
|
||||
}
|
||||
}
|
||||
inherit(ReplayableError, PoWError);
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
/* global window, dcodeIO, textsecure */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function () {
|
||||
(function() {
|
||||
let server;
|
||||
const development = (window.getEnvironment() !== 'production');
|
||||
const development = window.getEnvironment() !== 'production';
|
||||
const pollTime = development ? 100 : 5000;
|
||||
|
||||
function stringToArrayBufferBase64(string) {
|
||||
|
@ -42,9 +42,13 @@
|
|||
};
|
||||
};
|
||||
|
||||
const filterIncomingMessages = async function filterIncomingMessages(messages) {
|
||||
const filterIncomingMessages = async function filterIncomingMessages(
|
||||
messages
|
||||
) {
|
||||
const incomingHashes = messages.map(m => m.hash);
|
||||
const dupHashes = await window.Signal.Data.getSeenMessagesByHashList(incomingHashes);
|
||||
const dupHashes = await window.Signal.Data.getSeenMessagesByHashList(
|
||||
incomingHashes
|
||||
);
|
||||
const newMessages = messages.filter(m => !dupHashes.includes(m.hash));
|
||||
const newHashes = newMessages.map(m => ({
|
||||
expiresAt: m.expiration,
|
||||
|
@ -59,7 +63,7 @@
|
|||
let { handleRequest } = opts;
|
||||
if (typeof handleRequest !== 'function') {
|
||||
handleRequest = request => request.respond(404, 'Not found');
|
||||
};
|
||||
}
|
||||
let connected = false;
|
||||
|
||||
const processMessages = async messages => {
|
||||
|
@ -67,8 +71,12 @@
|
|||
newMessages.forEach(async message => {
|
||||
const { data } = message;
|
||||
const dataPlaintext = stringToArrayBufferBase64(data);
|
||||
const messageBuf = textsecure.protobuf.WebSocketMessage.decode(dataPlaintext);
|
||||
if (messageBuf.type === textsecure.protobuf.WebSocketMessage.Type.REQUEST) {
|
||||
const messageBuf = textsecure.protobuf.WebSocketMessage.decode(
|
||||
dataPlaintext
|
||||
);
|
||||
if (
|
||||
messageBuf.type === textsecure.protobuf.WebSocketMessage.Type.REQUEST
|
||||
) {
|
||||
handleRequest(
|
||||
new IncomingHttpResponse({
|
||||
verb: messageBuf.request.verb,
|
||||
|
@ -79,7 +87,7 @@
|
|||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
this.startPolling = async function pollServer(callback) {
|
||||
try {
|
||||
|
@ -89,11 +97,13 @@
|
|||
connected = false;
|
||||
}
|
||||
callback(connected);
|
||||
setTimeout(() => { pollServer(callback); }, pollTime);
|
||||
setTimeout(() => {
|
||||
pollServer(callback);
|
||||
}, pollTime);
|
||||
};
|
||||
|
||||
this.isConnected = function isConnected() {
|
||||
return connected;
|
||||
}
|
||||
};
|
||||
};
|
||||
})();
|
||||
|
|
|
@ -70,7 +70,7 @@ MessageReceiver.prototype.extend({
|
|||
this.httpPollingResource = new HttpResource(this.lokiMessageAPI, {
|
||||
handleRequest: this.handleRequest.bind(this),
|
||||
});
|
||||
this.httpPollingResource.startPolling((connected) => {
|
||||
this.httpPollingResource.startPolling(connected => {
|
||||
// Emulate receiving an 'empty' websocket messages from the server.
|
||||
// This is required to update the internal logic that checks
|
||||
// if we are connected to the server. Without this, for example,
|
||||
|
@ -328,7 +328,8 @@ MessageReceiver.prototype.extend({
|
|||
envelope.sourceDevice = envelope.sourceDevice || item.sourceDevice;
|
||||
envelope.serverTimestamp =
|
||||
envelope.serverTimestamp || item.serverTimestamp;
|
||||
envelope.preKeyBundleMessage = envelope.preKeyBundleMessage || item.preKeyBundleMessage;
|
||||
envelope.preKeyBundleMessage =
|
||||
envelope.preKeyBundleMessage || item.preKeyBundleMessage;
|
||||
|
||||
const { decrypted } = item;
|
||||
if (decrypted) {
|
||||
|
@ -381,7 +382,7 @@ MessageReceiver.prototype.extend({
|
|||
if (envelope.source) {
|
||||
return `${envelope.source}.${
|
||||
envelope.sourceDevice
|
||||
} ${envelope.timestamp.toNumber()} (${envelope.id})`;
|
||||
} ${envelope.timestamp.toNumber()} (${envelope.id})`;
|
||||
}
|
||||
|
||||
return envelope.id;
|
||||
|
@ -540,7 +541,9 @@ MessageReceiver.prototype.extend({
|
|||
},
|
||||
getStatus() {
|
||||
if (this.httpPollingResource) {
|
||||
return this.httpPollingResource.isConnected() ? WebSocket.OPEN : WebSocket.CLOSED;
|
||||
return this.httpPollingResource.isConnected()
|
||||
? WebSocket.OPEN
|
||||
: WebSocket.CLOSED;
|
||||
}
|
||||
if (this.socket) {
|
||||
return this.socket.readyState;
|
||||
|
@ -616,47 +619,57 @@ MessageReceiver.prototype.extend({
|
|||
|
||||
let conversation;
|
||||
try {
|
||||
conversation = await window.ConversationController.getOrCreateAndWait(envelope.source, 'private');
|
||||
conversation = await window.ConversationController.getOrCreateAndWait(
|
||||
envelope.source,
|
||||
'private'
|
||||
);
|
||||
} catch (e) {
|
||||
window.log.info('Error getting conversation: ', envelope.source);
|
||||
}
|
||||
const getCurrentSessionBaseKey = async () => {
|
||||
const record = await sessionCipher.getRecord(address.toString());
|
||||
if (!record)
|
||||
return null;
|
||||
if (!record) return null;
|
||||
const openSession = record.getOpenSession();
|
||||
if (!openSession)
|
||||
return null;
|
||||
if (!openSession) return null;
|
||||
const { baseKey } = openSession.indexInfo;
|
||||
return baseKey
|
||||
return baseKey;
|
||||
};
|
||||
const captureActiveSession = async () => {
|
||||
this.activeSessionBaseKey = await getCurrentSessionBaseKey(sessionCipher);
|
||||
};
|
||||
const restoreActiveSession = async () => {
|
||||
const record = await sessionCipher.getRecord(address.toString());
|
||||
if (!record)
|
||||
return
|
||||
if (!record) return;
|
||||
record.archiveCurrentState();
|
||||
const sessionToRestore = record.sessions[this.activeSessionBaseKey];
|
||||
record.promoteState(sessionToRestore);
|
||||
record.updateSessionState(sessionToRestore);
|
||||
await textsecure.storage.protocol.storeSession(address.toString(), record.serialize());
|
||||
await textsecure.storage.protocol.storeSession(
|
||||
address.toString(),
|
||||
record.serialize()
|
||||
);
|
||||
};
|
||||
const deleteAllSessionExcept = async (sessionBaseKey) => {
|
||||
const deleteAllSessionExcept = async sessionBaseKey => {
|
||||
const record = await sessionCipher.getRecord(address.toString());
|
||||
if (!record)
|
||||
return
|
||||
if (!record) return;
|
||||
const sessionToKeep = record.sessions[sessionBaseKey];
|
||||
record.sessions = {}
|
||||
record.sessions = {};
|
||||
record.updateSessionState(sessionToKeep);
|
||||
await textsecure.storage.protocol.storeSession(address.toString(), record.serialize());
|
||||
await textsecure.storage.protocol.storeSession(
|
||||
address.toString(),
|
||||
record.serialize()
|
||||
);
|
||||
};
|
||||
let handleSessionReset;
|
||||
if (conversation.isSessionResetOngoing()) {
|
||||
handleSessionReset = async (result) => {
|
||||
const currentSessionBaseKey = await getCurrentSessionBaseKey(sessionCipher);
|
||||
if (this.activeSessionBaseKey && currentSessionBaseKey !== this.activeSessionBaseKey) {
|
||||
handleSessionReset = async result => {
|
||||
const currentSessionBaseKey = await getCurrentSessionBaseKey(
|
||||
sessionCipher
|
||||
);
|
||||
if (
|
||||
this.activeSessionBaseKey &&
|
||||
currentSessionBaseKey !== this.activeSessionBaseKey
|
||||
) {
|
||||
if (conversation.isSessionResetReceived()) {
|
||||
await restoreActiveSession();
|
||||
} else {
|
||||
|
@ -670,7 +683,7 @@ MessageReceiver.prototype.extend({
|
|||
return result;
|
||||
};
|
||||
} else {
|
||||
handleSessionReset = async (result) => result;
|
||||
handleSessionReset = async result => result;
|
||||
}
|
||||
|
||||
switch (envelope.type) {
|
||||
|
@ -683,18 +696,17 @@ MessageReceiver.prototype.extend({
|
|||
break;
|
||||
case textsecure.protobuf.Envelope.Type.FRIEND_REQUEST: {
|
||||
window.log.info('friend-request message from ', envelope.source);
|
||||
promise = fallBackSessionCipher.decrypt(ciphertext.toArrayBuffer())
|
||||
promise = fallBackSessionCipher
|
||||
.decrypt(ciphertext.toArrayBuffer())
|
||||
.then(this.unpad);
|
||||
break;
|
||||
}
|
||||
case textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE:
|
||||
window.log.info('prekey message from', this.getEnvelopeId(envelope));
|
||||
promise = captureActiveSession(sessionCipher)
|
||||
.then(() => this.decryptPreKeyWhisperMessage(
|
||||
ciphertext,
|
||||
sessionCipher,
|
||||
address
|
||||
))
|
||||
.then(() =>
|
||||
this.decryptPreKeyWhisperMessage(ciphertext, sessionCipher, address)
|
||||
)
|
||||
.then(handleSessionReset);
|
||||
break;
|
||||
case textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER:
|
||||
|
@ -840,7 +852,7 @@ MessageReceiver.prototype.extend({
|
|||
const isMe = envelope.source === textsecure.storage.user.getNumber();
|
||||
const isLeavingGroup = Boolean(
|
||||
message.group &&
|
||||
message.group.type === textsecure.protobuf.GroupContext.Type.QUIT
|
||||
message.group.type === textsecure.protobuf.GroupContext.Type.QUIT
|
||||
);
|
||||
|
||||
if (groupId && isBlocked && !(isMe && isLeavingGroup)) {
|
||||
|
@ -883,7 +895,7 @@ MessageReceiver.prototype.extend({
|
|||
const conversation = window.ConversationController.get(envelope.source);
|
||||
const isLeavingGroup = Boolean(
|
||||
message.group &&
|
||||
message.group.type === textsecure.protobuf.GroupContext.Type.QUIT
|
||||
message.group.type === textsecure.protobuf.GroupContext.Type.QUIT
|
||||
);
|
||||
const friendRequest =
|
||||
envelope.type === textsecure.protobuf.Envelope.Type.FRIEND_REQUEST;
|
||||
|
@ -900,10 +912,8 @@ MessageReceiver.prototype.extend({
|
|||
}
|
||||
|
||||
if (friendRequest && isMe) {
|
||||
window.log.info(
|
||||
'refusing to add a friend request to ourselves'
|
||||
);
|
||||
throw new Error('Cannot add a friend request for ourselves!')
|
||||
window.log.info('refusing to add a friend request to ourselves');
|
||||
throw new Error('Cannot add a friend request for ourselves!');
|
||||
}
|
||||
|
||||
if (groupId && isBlocked && !(isMe && isLeavingGroup)) {
|
||||
|
@ -1227,10 +1237,7 @@ MessageReceiver.prototype.extend({
|
|||
preKeyBundleMessage.signature,
|
||||
].map(k => dcodeIO.ByteBuffer.wrap(k).toArrayBuffer());
|
||||
|
||||
const {
|
||||
preKeyId,
|
||||
signedKeyId,
|
||||
} = preKeyBundleMessage;
|
||||
const { preKeyId, signedKeyId } = preKeyBundleMessage;
|
||||
|
||||
if (pubKey !== StringView.arrayBufferToHex(identityKey)) {
|
||||
throw new Error(
|
||||
|
@ -1312,7 +1319,13 @@ MessageReceiver.prototype.extend({
|
|||
if (preKey === undefined || signedPreKey === undefined) {
|
||||
return;
|
||||
}
|
||||
const device = { identityKey, deviceId, preKey, signedPreKey, registrationId: 0 }
|
||||
const device = {
|
||||
identityKey,
|
||||
deviceId,
|
||||
preKey,
|
||||
signedPreKey,
|
||||
registrationId: 0,
|
||||
};
|
||||
const builder = new libsignal.SessionBuilder(
|
||||
textsecure.storage.protocol,
|
||||
address
|
||||
|
@ -1513,7 +1526,9 @@ textsecure.MessageReceiver = function MessageReceiverWrapper(
|
|||
);
|
||||
this.getStatus = messageReceiver.getStatus.bind(messageReceiver);
|
||||
this.close = messageReceiver.close.bind(messageReceiver);
|
||||
this.savePreKeyBundleMessage = messageReceiver.savePreKeyBundleMessage.bind(messageReceiver);
|
||||
this.savePreKeyBundleMessage = messageReceiver.savePreKeyBundleMessage.bind(
|
||||
messageReceiver
|
||||
);
|
||||
|
||||
messageReceiver.connect();
|
||||
};
|
||||
|
|
|
@ -119,22 +119,24 @@ OutgoingMessage.prototype = {
|
|||
if (device.registrationId === 0) {
|
||||
window.log.info('device registrationId 0!');
|
||||
}
|
||||
return builder.processPreKey(device).then(async () => {
|
||||
// TODO: only remove the keys that were used above!
|
||||
await window.libloki.storage.removeContactPreKeyBundle(number);
|
||||
return true;
|
||||
}
|
||||
).catch(error => {
|
||||
if (error.message === 'Identity key changed') {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
error.timestamp = this.timestamp;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
error.originalMessage = this.message.toArrayBuffer();
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
error.identityKey = device.identityKey;
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
return builder
|
||||
.processPreKey(device)
|
||||
.then(async () => {
|
||||
// TODO: only remove the keys that were used above!
|
||||
await window.libloki.storage.removeContactPreKeyBundle(number);
|
||||
return true;
|
||||
})
|
||||
.catch(error => {
|
||||
if (error.message === 'Identity key changed') {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
error.timestamp = this.timestamp;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
error.originalMessage = this.message.toArrayBuffer();
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
error.identityKey = device.identityKey;
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -184,7 +186,12 @@ OutgoingMessage.prototype = {
|
|||
async transmitMessage(number, data, timestamp, ttl = 24 * 60 * 60) {
|
||||
const pubKey = number;
|
||||
try {
|
||||
const result = await this.lokiMessageAPI.sendMessage(pubKey, data, timestamp, ttl);
|
||||
const result = await this.lokiMessageAPI.sendMessage(
|
||||
pubKey,
|
||||
data,
|
||||
timestamp,
|
||||
ttl
|
||||
);
|
||||
return result;
|
||||
} catch (e) {
|
||||
if (e.name === 'HTTPError' && (e.code !== 409 && e.code !== 410)) {
|
||||
|
@ -288,17 +295,24 @@ OutgoingMessage.prototype = {
|
|||
const address = new libsignal.SignalProtocolAddress(number, deviceId);
|
||||
const ourKey = textsecure.storage.user.getNumber();
|
||||
const options = {};
|
||||
const fallBackCipher = new libloki.crypto.FallBackSessionCipher(address);
|
||||
const fallBackCipher = new libloki.crypto.FallBackSessionCipher(
|
||||
address
|
||||
);
|
||||
|
||||
// Check if we need to attach the preKeys
|
||||
let sessionCipher;
|
||||
const isFriendRequest = this.messageType === 'friend-request';
|
||||
const flags = this.message.dataMessage ? this.message.dataMessage.get_flags() : null;
|
||||
const isEndSession = flags === textsecure.protobuf.DataMessage.Flags.END_SESSION;
|
||||
const flags = this.message.dataMessage
|
||||
? this.message.dataMessage.get_flags()
|
||||
: null;
|
||||
const isEndSession =
|
||||
flags === textsecure.protobuf.DataMessage.Flags.END_SESSION;
|
||||
if (isFriendRequest || isEndSession) {
|
||||
// Encrypt them with the fallback
|
||||
const pkb = await libloki.storage.getPreKeyBundleForContact(number);
|
||||
const preKeyBundleMessage = new textsecure.protobuf.PreKeyBundleMessage(pkb);
|
||||
const preKeyBundleMessage = new textsecure.protobuf.PreKeyBundleMessage(
|
||||
pkb
|
||||
);
|
||||
this.message.preKeyBundleMessage = preKeyBundleMessage;
|
||||
window.log.info('attaching prekeys to outgoing message');
|
||||
}
|
||||
|
@ -325,10 +339,7 @@ OutgoingMessage.prototype = {
|
|||
if (!this.fallBackEncryption) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
ciphertext.body = new Uint8Array(
|
||||
dcodeIO.ByteBuffer.wrap(
|
||||
ciphertext.body,
|
||||
'binary'
|
||||
).toArrayBuffer()
|
||||
dcodeIO.ByteBuffer.wrap(ciphertext.body, 'binary').toArrayBuffer()
|
||||
);
|
||||
}
|
||||
return {
|
||||
|
@ -345,7 +356,10 @@ OutgoingMessage.prototype = {
|
|||
const outgoingObject = outgoingObjects[0];
|
||||
const socketMessage = await this.wrapInWebsocketMessage(outgoingObject);
|
||||
let ttl;
|
||||
if (outgoingObject.type === textsecure.protobuf.Envelope.Type.FRIEND_REQUEST) {
|
||||
if (
|
||||
outgoingObject.type ===
|
||||
textsecure.protobuf.Envelope.Type.FRIEND_REQUEST
|
||||
) {
|
||||
ttl = 4 * 24 * 60 * 60; // 4 days for friend request message
|
||||
} else {
|
||||
const hours = window.getMessageTTL() || 24; // 1 day default for any other message
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/* global window, StringView */
|
||||
|
||||
/* eslint-disable no-bitwise, no-nested-ternary */
|
||||
/* eslint-disable no-bitwise, no-nested-ternary, */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
|
@ -102,16 +102,14 @@
|
|||
arrayBufferToHex(aArrayBuffer) {
|
||||
return Array.prototype.map
|
||||
.call(new Uint8Array(aArrayBuffer), x =>
|
||||
('00' + x.toString(16)).slice(-2)
|
||||
`00${x.toString(16)}`.slice(-2)
|
||||
)
|
||||
.join('');
|
||||
},
|
||||
|
||||
hexToArrayBuffer(aString) {
|
||||
return new Uint8Array(
|
||||
aString.match(/[\da-f]{2}/gi).map(function(h) {
|
||||
return parseInt(h, 16);
|
||||
})
|
||||
aString.match(/[\da-f]{2}/gi).map(h => parseInt(h, 16))
|
||||
).buffer;
|
||||
},
|
||||
};
|
||||
|
|
16
main.js
16
main.js
|
@ -453,11 +453,11 @@ function showPasswordWindow() {
|
|||
captureClicks(passwordWindow);
|
||||
|
||||
passwordWindow.on('close', e => {
|
||||
// If the application is terminating, just do the default
|
||||
if (
|
||||
// If the application is terminating, just do the default
|
||||
if (
|
||||
config.environment === 'test' ||
|
||||
config.environment === 'test-lib' ||
|
||||
(windowState.shouldQuit())
|
||||
windowState.shouldQuit()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
@ -965,7 +965,8 @@ ipc.on('update-tray-icon', (event, unreadCount) => {
|
|||
|
||||
// Password screen related IPC calls
|
||||
ipc.on('password-window-login', async (event, passPhrase) => {
|
||||
const sendResponse = (e) => event.sender.send('password-window-login-response', e);
|
||||
const sendResponse = e =>
|
||||
event.sender.send('password-window-login-response', e);
|
||||
|
||||
try {
|
||||
await showMainWindow(passPhrase);
|
||||
|
@ -981,7 +982,7 @@ ipc.on('password-window-login', async (event, passPhrase) => {
|
|||
});
|
||||
|
||||
ipc.on('set-password', async (event, passPhrase, oldPhrase) => {
|
||||
const sendResponse = (e) => event.sender.send('set-password-response', e);
|
||||
const sendResponse = e => event.sender.send('set-password-response', e);
|
||||
|
||||
try {
|
||||
// Check if the hash we have stored matches the hash of the old passphrase.
|
||||
|
@ -989,7 +990,10 @@ ipc.on('set-password', async (event, passPhrase, oldPhrase) => {
|
|||
const hashMatches = oldPhrase && passwordUtil.matchesHash(oldPhrase, hash);
|
||||
if (hash && !hashMatches) {
|
||||
const incorrectOldPassword = locale.messages.invalidOldPassword.message;
|
||||
sendResponse(incorrectOldPassword || 'Failed to set password: Old password provided is invalid');
|
||||
sendResponse(
|
||||
incorrectOldPassword ||
|
||||
'Failed to set password: Old password provided is invalid'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -20,42 +20,48 @@ const mimeType = {
|
|||
'.pdf': 'application/pdf',
|
||||
'.doc': 'application/msword',
|
||||
'.eot': 'appliaction/vnd.ms-fontobject',
|
||||
'.ttf': 'aplication/font-sfnt'
|
||||
'.ttf': 'aplication/font-sfnt',
|
||||
};
|
||||
http.createServer(function (req, res) {
|
||||
// console.log(`${req.method} ${req.url}`);
|
||||
// parse URL
|
||||
const parsedUrl = url.parse(req.url);
|
||||
// extract URL path
|
||||
// Avoid https://en.wikipedia.org/wiki/Directory_traversal_attack
|
||||
// e.g curl --path-as-is http://localhost:9000/../fileInDanger.txt
|
||||
// by limiting the path to current directory only
|
||||
const sanitizePath = path.normalize(parsedUrl.pathname).replace(/^(\.\.[\/\\])+/, '');
|
||||
let pathname = path.join(__dirname, sanitizePath);
|
||||
fs.exists(pathname, function (exist) {
|
||||
if(!exist) {
|
||||
// if the file is not found, return 404
|
||||
res.statusCode = 404;
|
||||
res.end(`File ${pathname} not found!`);
|
||||
return;
|
||||
}
|
||||
// if is a directory, then look for index.html
|
||||
if (fs.statSync(pathname).isDirectory()) {
|
||||
pathname += '/index.html';
|
||||
}
|
||||
// read file from file system
|
||||
fs.readFile(pathname, function(err, data){
|
||||
if(err){
|
||||
res.statusCode = 500;
|
||||
res.end(`Error getting the file: ${err}.`);
|
||||
} else {
|
||||
// based on the URL path, extract the file extention. e.g. .js, .doc, ...
|
||||
const ext = path.parse(pathname).ext;
|
||||
// if the file is found, set Content-type and send data
|
||||
res.setHeader('Content-type', mimeType[ext] || 'text/plain' );
|
||||
res.end(data);
|
||||
http
|
||||
.createServer((req, res) => {
|
||||
// console.log(`${req.method} ${req.url}`);
|
||||
// parse URL
|
||||
const parsedUrl = url.parse(req.url);
|
||||
// extract URL path
|
||||
// Avoid https://en.wikipedia.org/wiki/Directory_traversal_attack
|
||||
// e.g curl --path-as-is http://localhost:9000/../fileInDanger.txt
|
||||
// by limiting the path to current directory only
|
||||
const sanitizePath = path
|
||||
.normalize(parsedUrl.pathname)
|
||||
.replace(/^(\.\.[/\\])+/, '');
|
||||
let pathname = path.join(__dirname, sanitizePath);
|
||||
fs.exists(pathname, exist => {
|
||||
if (!exist) {
|
||||
// if the file is not found, return 404
|
||||
res.statusCode = 404;
|
||||
res.end(`File ${pathname} not found!`);
|
||||
return;
|
||||
}
|
||||
// if is a directory, then look for index.html
|
||||
if (fs.statSync(pathname).isDirectory()) {
|
||||
pathname += '/index.html';
|
||||
}
|
||||
// read file from file system
|
||||
fs.readFile(pathname, (err, data) => {
|
||||
if (err) {
|
||||
res.statusCode = 500;
|
||||
res.end(`Error getting the file: ${err}.`);
|
||||
} else {
|
||||
// based on the URL path, extract the file extention. e.g. .js, .doc, ...
|
||||
const { ext } = path.parse(pathname);
|
||||
// if the file is found, set Content-type and send data
|
||||
res.setHeader('Content-type', mimeType[ext] || 'text/plain');
|
||||
res.end(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}).listen(parseInt(port), hostname);
|
||||
console.log(`metrics running on http://${hostname}:${port}/metrics.html`);
|
||||
})
|
||||
.listen(parseInt(port, 10), hostname);
|
||||
|
||||
// eslint-disable-next-line
|
||||
console.log(`metrics running on http://${hostname}:${port}/metrics.html`);
|
||||
|
|
|
@ -26,14 +26,15 @@ window.getAppInstance = () => config.appInstance;
|
|||
|
||||
window.passwordUtil = require('./app/password_util');
|
||||
|
||||
window.onLogin = (passPhrase) => new Promise((resolve, reject) => {
|
||||
ipcRenderer.once('password-window-login-response', (event, error) => {
|
||||
if (error) {
|
||||
return reject(error);
|
||||
}
|
||||
return resolve();
|
||||
window.onLogin = passPhrase =>
|
||||
new Promise((resolve, reject) => {
|
||||
ipcRenderer.once('password-window-login-response', (event, error) => {
|
||||
if (error) {
|
||||
return reject(error);
|
||||
}
|
||||
return resolve();
|
||||
});
|
||||
ipcRenderer.send('password-window-login', passPhrase);
|
||||
});
|
||||
ipcRenderer.send('password-window-login', passPhrase);
|
||||
});
|
||||
|
||||
require('./js/logging');
|
||||
|
|
28
preload.js
28
preload.js
|
@ -50,16 +50,17 @@ const localeMessages = ipc.sendSync('locale-data');
|
|||
window.setBadgeCount = count => ipc.send('set-badge-count', count);
|
||||
|
||||
// Set the password for the database
|
||||
window.setPassword = (passPhrase, oldPhrase) => new Promise((resolve, reject) => {
|
||||
ipc.once('set-password-response', (event, error) => {
|
||||
if (error) {
|
||||
return reject(error);
|
||||
}
|
||||
Whisper.events.trigger('password-updated');
|
||||
return resolve();
|
||||
window.setPassword = (passPhrase, oldPhrase) =>
|
||||
new Promise((resolve, reject) => {
|
||||
ipc.once('set-password-response', (event, error) => {
|
||||
if (error) {
|
||||
return reject(error);
|
||||
}
|
||||
Whisper.events.trigger('password-updated');
|
||||
return resolve();
|
||||
});
|
||||
ipc.send('set-password', passPhrase, oldPhrase);
|
||||
});
|
||||
ipc.send('set-password', passPhrase, oldPhrase);
|
||||
});
|
||||
|
||||
// We never do these in our code, so we'll prevent it everywhere
|
||||
window.open = () => null;
|
||||
|
@ -107,7 +108,10 @@ ipc.on('on-unblock-number', (event, number) => {
|
|||
const conversation = window.ConversationController.get(number);
|
||||
conversation.unblock();
|
||||
} catch (e) {
|
||||
window.log.info('IPC on unblock: failed to fetch conversation for number: ', number);
|
||||
window.log.info(
|
||||
'IPC on unblock: failed to fetch conversation for number: ',
|
||||
number
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -155,7 +159,7 @@ installGetter('hide-menu-bar', 'getHideMenuBar');
|
|||
installSetter('hide-menu-bar', 'setHideMenuBar');
|
||||
|
||||
// Get the message TTL setting
|
||||
window.getMessageTTL = () => window.storage.get('message-ttl', 24)
|
||||
window.getMessageTTL = () => window.storage.get('message-ttl', 24);
|
||||
installGetter('message-ttl', 'getMessageTTL');
|
||||
installSetter('message-ttl', 'setMessageTTL');
|
||||
|
||||
|
@ -289,7 +293,7 @@ window.callWorker = (fnName, ...args) => utilWorker.callWorker(fnName, ...args);
|
|||
|
||||
// Linux seems to periodically let the event loop stop, so this is a global workaround
|
||||
setInterval(() => {
|
||||
window.nodeSetImmediate(() => { });
|
||||
window.nodeSetImmediate(() => {});
|
||||
}, 1000);
|
||||
|
||||
const { autoOrientImage } = require('./js/modules/auto_orient_image');
|
||||
|
|
|
@ -29,7 +29,8 @@ window.closeSettings = () => ipcRenderer.send('close-settings');
|
|||
// Events for updating block number states across different windows.
|
||||
// In this case we need these to update the blocked number
|
||||
// collection on the main window from the settings window.
|
||||
window.onUnblockNumber = number => ipcRenderer.send('on-unblock-number', number);
|
||||
window.onUnblockNumber = number =>
|
||||
ipcRenderer.send('on-unblock-number', number);
|
||||
|
||||
window.getDeviceName = makeGetter('device-name');
|
||||
|
||||
|
|
|
@ -382,7 +382,8 @@
|
|||
color: white;
|
||||
outline: none;
|
||||
|
||||
&:hover, &:disabled {
|
||||
&:hover,
|
||||
&:disabled {
|
||||
background-color: $color-loki-green-dark;
|
||||
border-color: $color-loki-green-dark;
|
||||
}
|
||||
|
|
|
@ -925,7 +925,8 @@ textarea {
|
|||
font-size: 14px;
|
||||
}
|
||||
|
||||
#generate-mnemonic, #copy-mnemonic {
|
||||
#generate-mnemonic,
|
||||
#copy-mnemonic {
|
||||
background: $grey;
|
||||
}
|
||||
|
||||
|
@ -936,7 +937,7 @@ textarea {
|
|||
line-height: 2.8;
|
||||
background: white;
|
||||
overflow: hidden;
|
||||
border-radius: .25em;
|
||||
border-radius: 0.25em;
|
||||
|
||||
select {
|
||||
// Reset select
|
||||
|
@ -954,7 +955,7 @@ textarea {
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0 0 0 .5em;
|
||||
padding: 0 0 0 0.5em;
|
||||
color: black;
|
||||
cursor: pointer;
|
||||
|
||||
|
@ -972,9 +973,9 @@ textarea {
|
|||
padding: 0 1em;
|
||||
background: $color-dark-85;
|
||||
pointer-events: none;
|
||||
-webkit-transition: .25s all ease;
|
||||
-o-transition: .25s all ease;
|
||||
transition: .25s all ease;
|
||||
-webkit-transition: 0.25s all ease;
|
||||
-o-transition: 0.25s all ease;
|
||||
transition: 0.25s all ease;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -92,7 +92,8 @@
|
|||
user-select: none;
|
||||
}
|
||||
|
||||
h4.section-toggle, .section-toggle h4 {
|
||||
h4.section-toggle,
|
||||
.section-toggle h4 {
|
||||
margin-top: 1px;
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
@ -106,10 +107,10 @@ h4.section-toggle, .section-toggle h4 {
|
|||
height: 3em;
|
||||
line-height: 3;
|
||||
text-align: center;
|
||||
-webkit-transition: all .35s;
|
||||
-o-transition: all .35s;
|
||||
transition: all .35s;
|
||||
content: "+";
|
||||
-webkit-transition: all 0.35s;
|
||||
-o-transition: all 0.35s;
|
||||
transition: all 0.35s;
|
||||
content: '+';
|
||||
}
|
||||
|
||||
.section-toggle-visible::after {
|
||||
|
@ -123,7 +124,7 @@ h4.section-toggle, .section-toggle h4 {
|
|||
align-items: center;
|
||||
|
||||
h4 {
|
||||
flex: 1,
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.section-conversations-unread-counter {
|
||||
|
|
|
@ -974,7 +974,7 @@
|
|||
}
|
||||
|
||||
.module-message-friend-request__container {
|
||||
flex: 1,
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.module-friend-request__buttonContainer {
|
||||
|
@ -994,7 +994,7 @@
|
|||
background-color: transparent;
|
||||
border: 0;
|
||||
overflow: hidden;
|
||||
outline:none;
|
||||
outline: none;
|
||||
color: $color-gray-90;
|
||||
}
|
||||
|
||||
|
@ -1028,7 +1028,6 @@
|
|||
color: $color-white-08;
|
||||
}
|
||||
|
||||
|
||||
// Module: Contact Detail
|
||||
|
||||
.module-contact-detail {
|
||||
|
@ -2156,9 +2155,9 @@
|
|||
text-align: center;
|
||||
|
||||
&:after {
|
||||
-webkit-transition: all .35s;
|
||||
-o-transition: all .35s;
|
||||
transition: all .35s;
|
||||
-webkit-transition: all 0.35s;
|
||||
-o-transition: all 0.35s;
|
||||
transition: all 0.35s;
|
||||
width: 3em;
|
||||
line-height: 3em;
|
||||
height: 3em;
|
||||
|
@ -2171,7 +2170,6 @@
|
|||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
|
||||
.module-main-header__app-name {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
|
|
|
@ -26,4 +26,4 @@
|
|||
font-size: 16px;
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,12 +67,12 @@
|
|||
}
|
||||
|
||||
.wordwrap {
|
||||
white-space: pre-wrap; /* CSS3 */
|
||||
white-space: pre-wrap; /* CSS3 */
|
||||
white-space: -moz-pre-wrap; /* Firefox */
|
||||
white-space: -pre-wrap; /* Opera <7 */
|
||||
white-space: -o-pre-wrap; /* Opera 7 */
|
||||
word-wrap: break-word; /* IE */
|
||||
}
|
||||
white-space: -pre-wrap; /* Opera <7 */
|
||||
white-space: -o-pre-wrap; /* Opera 7 */
|
||||
word-wrap: break-word; /* IE */
|
||||
}
|
||||
|
||||
.restart-needed {
|
||||
margin-top: 1em;
|
||||
|
@ -103,7 +103,11 @@
|
|||
color: red;
|
||||
}
|
||||
|
||||
input { flex: 1; }
|
||||
label { padding-left: 12px; }
|
||||
input {
|
||||
flex: 1;
|
||||
}
|
||||
label {
|
||||
padding-left: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,7 +31,11 @@ $color-loki-dark-gray: #323232;
|
|||
$color-loki-extra-dark-gray: #101010;
|
||||
$color-loki-green: #3bd110;
|
||||
$color-loki-green-dark: #32b10e;
|
||||
$color-loki-green-gradient: linear-gradient(to right, rgb(120, 190, 32) 0%, rgb(0, 133, 34) 100%);
|
||||
$color-loki-green-gradient: linear-gradient(
|
||||
to right,
|
||||
rgb(120, 190, 32) 0%,
|
||||
rgb(0, 133, 34) 100%
|
||||
);
|
||||
|
||||
// New colors
|
||||
|
||||
|
|
|
@ -49,28 +49,22 @@ describe('Password Util', () => {
|
|||
});
|
||||
|
||||
it('should return an error if password is not a string', () => {
|
||||
const invalid = [
|
||||
0,
|
||||
123456,
|
||||
[],
|
||||
{},
|
||||
null,
|
||||
undefined,
|
||||
];
|
||||
const invalid = [0, 123456, [], {}, null, undefined];
|
||||
invalid.forEach(pass => {
|
||||
assert.strictEqual(passwordUtil.validatePassword(pass), 'Password must be a string');
|
||||
assert.strictEqual(
|
||||
passwordUtil.validatePassword(pass),
|
||||
'Password must be a string'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an error if password is not between 6 and 50 characters',() => {
|
||||
const invalid = [
|
||||
'a',
|
||||
'abcde',
|
||||
'#'.repeat(51),
|
||||
'#'.repeat(100),
|
||||
];
|
||||
it('should return an error if password is not between 6 and 50 characters', () => {
|
||||
const invalid = ['a', 'abcde', '#'.repeat(51), '#'.repeat(100)];
|
||||
invalid.forEach(pass => {
|
||||
assert.strictEqual(passwordUtil.validatePassword(pass), 'Password must be between 6 and 50 characters long');
|
||||
assert.strictEqual(
|
||||
passwordUtil.validatePassword(pass),
|
||||
'Password must be between 6 and 50 characters long'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -87,7 +81,10 @@ describe('Password Util', () => {
|
|||
'<@ȦƘΉوۉaҋ<',
|
||||
];
|
||||
invalid.forEach(pass => {
|
||||
assert.strictEqual(passwordUtil.validatePassword(pass), 'Password must only contain letters, numbers and symbols');
|
||||
assert.strictEqual(
|
||||
passwordUtil.validatePassword(pass),
|
||||
'Password must only contain letters, numbers and symbols'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -366,12 +366,12 @@ describe('Backup', () => {
|
|||
(message.contact || []).map(async contact => {
|
||||
return contact && contact.avatar && contact.avatar.avatar
|
||||
? Object.assign({}, contact, {
|
||||
avatar: Object.assign({}, contact.avatar, {
|
||||
avatar: await wrappedLoadAttachment(
|
||||
contact.avatar.avatar
|
||||
),
|
||||
}),
|
||||
})
|
||||
avatar: Object.assign({}, contact.avatar, {
|
||||
avatar: await wrappedLoadAttachment(
|
||||
contact.avatar.avatar
|
||||
),
|
||||
}),
|
||||
})
|
||||
: contact;
|
||||
})
|
||||
),
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
'use strict';
|
||||
|
||||
describe('Blocked Number Controller', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
// Purge everything manually
|
||||
const numbers = storage.getBlockedNumbers();
|
||||
|
@ -43,18 +42,25 @@ describe('Blocked Number Controller', () => {
|
|||
|
||||
storage.addBlockedNumber('1');
|
||||
BlockedNumberController.refresh();
|
||||
assert.isNotEmpty(window.getBlockedNumbers().find(m => m.get('number') === '1'));
|
||||
assert.isNotEmpty(
|
||||
window.getBlockedNumbers().find(m => m.get('number') === '1')
|
||||
);
|
||||
|
||||
storage.removeBlockedNumber('1');
|
||||
storage.addBlockedNumber('2');
|
||||
BlockedNumberController.refresh();
|
||||
assert.isNotEmpty(window.getBlockedNumbers().find(m => m.get('number') === '2'));
|
||||
assert.isNotEmpty(
|
||||
window.getBlockedNumbers().find(m => m.get('number') === '2')
|
||||
);
|
||||
});
|
||||
|
||||
it('throws if storage is invalid', () => {
|
||||
const _storage = window.storage;
|
||||
window.storage = null;
|
||||
assert.throws(() => BlockedNumberController.refresh(), 'BlockedNumberController: Could not load blocked numbers');
|
||||
assert.throws(
|
||||
() => BlockedNumberController.refresh(),
|
||||
'BlockedNumberController: Could not load blocked numbers'
|
||||
);
|
||||
window.storage = _storage;
|
||||
});
|
||||
});
|
||||
|
@ -71,7 +77,7 @@ describe('Blocked Number Controller', () => {
|
|||
|
||||
const numbers = window.getBlockedNumbers().models;
|
||||
assert.lengthOf(numbers, 1);
|
||||
assert.strictEqual('1', numbers[0].get('number'))
|
||||
assert.strictEqual('1', numbers[0].get('number'));
|
||||
assert.deepEqual(['1'], storage.getBlockedNumbers());
|
||||
});
|
||||
|
||||
|
|
|
@ -29,7 +29,10 @@ describe('ConversationCollection', () => {
|
|||
});
|
||||
|
||||
describe('Conversation', () => {
|
||||
const attributes = { type: 'private', id: '051d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94ab' };
|
||||
const attributes = {
|
||||
type: 'private',
|
||||
id: '051d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94ab',
|
||||
};
|
||||
before(async () => {
|
||||
const convo = new Whisper.ConversationCollection().add(attributes);
|
||||
await window.Signal.Data.saveConversation(convo.attributes, {
|
||||
|
@ -50,7 +53,9 @@ describe('Conversation', () => {
|
|||
after(clearDatabase);
|
||||
|
||||
it('sorts its contacts in an intl-friendly way', () => {
|
||||
const convo = new Whisper.Conversation({ id: '051d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94ab' });
|
||||
const convo = new Whisper.Conversation({
|
||||
id: '051d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94ab',
|
||||
});
|
||||
convo.contactCollection.add(
|
||||
new Whisper.Conversation({
|
||||
name: 'C',
|
||||
|
@ -100,7 +105,10 @@ describe('Conversation', () => {
|
|||
it('has a title', () => {
|
||||
const convos = new Whisper.ConversationCollection();
|
||||
let convo = convos.add(attributes);
|
||||
assert.equal(convo.getTitle(), '051d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94ab');
|
||||
assert.equal(
|
||||
convo.getTitle(),
|
||||
'051d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94ab'
|
||||
);
|
||||
|
||||
convo = convos.add({ type: '' });
|
||||
assert.equal(convo.getTitle(), 'Unknown group');
|
||||
|
@ -112,7 +120,10 @@ describe('Conversation', () => {
|
|||
it('returns the number', () => {
|
||||
const convos = new Whisper.ConversationCollection();
|
||||
let convo = convos.add(attributes);
|
||||
assert.equal(convo.getNumber(), '051d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94ab');
|
||||
assert.equal(
|
||||
convo.getNumber(),
|
||||
'051d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94ab'
|
||||
);
|
||||
|
||||
convo = convos.add({ type: '' });
|
||||
assert.equal(convo.getNumber(), '');
|
||||
|
@ -127,17 +138,39 @@ describe('Conversation', () => {
|
|||
|
||||
describe('when set to private', () => {
|
||||
it('correctly validates hex numbers', () => {
|
||||
const regularId = new Whisper.Conversation({ type: 'private', id: '051d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94ab' });
|
||||
const invalidId = new Whisper.Conversation({ type: 'private', id: 'j71d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94ab' });
|
||||
const regularId = new Whisper.Conversation({
|
||||
type: 'private',
|
||||
id:
|
||||
'051d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94ab',
|
||||
});
|
||||
const invalidId = new Whisper.Conversation({
|
||||
type: 'private',
|
||||
id:
|
||||
'j71d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94ab',
|
||||
});
|
||||
assert.ok(regularId.isValid());
|
||||
assert.notOk(invalidId.isValid());
|
||||
});
|
||||
|
||||
it('correctly validates length', () => {
|
||||
const regularId33 = new Whisper.Conversation({ type: 'private', id: '051d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94ab' });
|
||||
const regularId32 = new Whisper.Conversation({ type: 'private', id: '1d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94ab' });
|
||||
const shortId = new Whisper.Conversation({ type: 'private', id: '771d11d' });
|
||||
const longId = new Whisper.Conversation({ type: 'private', id: '771d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94abaa' });
|
||||
const regularId33 = new Whisper.Conversation({
|
||||
type: 'private',
|
||||
id:
|
||||
'051d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94ab',
|
||||
});
|
||||
const regularId32 = new Whisper.Conversation({
|
||||
type: 'private',
|
||||
id: '1d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94ab',
|
||||
});
|
||||
const shortId = new Whisper.Conversation({
|
||||
type: 'private',
|
||||
id: '771d11d',
|
||||
});
|
||||
const longId = new Whisper.Conversation({
|
||||
type: 'private',
|
||||
id:
|
||||
'771d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94abaa',
|
||||
});
|
||||
assert.ok(regularId33.isValid());
|
||||
assert.ok(regularId32.isValid());
|
||||
assert.notOk(shortId.isValid());
|
||||
|
@ -152,7 +185,8 @@ describe('Conversation', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
convo = new Whisper.ConversationCollection().add({
|
||||
id: '771d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94ab',
|
||||
id:
|
||||
'771d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94ab',
|
||||
type: 'private',
|
||||
name: 'John Doe',
|
||||
});
|
||||
|
@ -169,7 +203,10 @@ describe('Conversation', () => {
|
|||
const collection = new Whisper.ConversationCollection();
|
||||
await collection.search(query);
|
||||
|
||||
assert.isDefined(collection.get(convo.id), `no result for "${query}"`);
|
||||
assert.isDefined(
|
||||
collection.get(convo.id),
|
||||
`no result for "${query}"`
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ describe('Profile', () => {
|
|||
describe('getLocalProfile', () => {
|
||||
it('returns the local profile', async () => {
|
||||
const values = [null, 'hello', { a: 'b' }];
|
||||
for(let i = 0; i < values.length; i += 1) {
|
||||
for (let i = 0; i < values.length; i += 1) {
|
||||
await storage.put(PROFILE_ID, values[i]);
|
||||
assert.strictEqual(values[i], storage.getLocalProfile());
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ describe('Profile', () => {
|
|||
describe('saveLocalProfile', () => {
|
||||
it('saves a profile', async () => {
|
||||
const values = [null, 'hello', { a: 'b' }];
|
||||
for(let i = 0; i < values.length; i += 1) {
|
||||
for (let i = 0; i < values.length; i += 1) {
|
||||
await storage.saveLocalProfile(values[i]);
|
||||
assert.strictEqual(values[i], storage.get(PROFILE_ID));
|
||||
}
|
||||
|
@ -44,20 +44,24 @@ describe('Profile', () => {
|
|||
|
||||
describe('setProfileName', () => {
|
||||
it('throws if a name is not a string', async () => {
|
||||
const values = [0, { a: 'b'}, [1, 2]];
|
||||
for(let i = 0; i < values.length; i += 1) {
|
||||
const values = [0, { a: 'b' }, [1, 2]];
|
||||
for (let i = 0; i < values.length; i += 1) {
|
||||
try {
|
||||
await storage.setProfileName(values[i]);
|
||||
assert.fail(`setProfileName did not throw an error for ${typeof values[i]}`);
|
||||
assert.fail(
|
||||
`setProfileName did not throw an error for ${typeof values[i]}`
|
||||
);
|
||||
} catch (e) {
|
||||
assert.throws(() => { throw e; }, 'Name must be a string!');
|
||||
assert.throws(() => {
|
||||
throw e;
|
||||
}, 'Name must be a string!');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('does not throw if we pass a string or null', async () => {
|
||||
const values = [null, '1'];
|
||||
for(let i = 0; i < values.length; i += 1) {
|
||||
for (let i = 0; i < values.length; i += 1) {
|
||||
try {
|
||||
await storage.setProfileName(values[i]);
|
||||
} catch (e) {
|
||||
|
|
|
@ -23,7 +23,7 @@ describe('Message', () => {
|
|||
body: 'Imagine there is no heaven…',
|
||||
schemaVersion: 2,
|
||||
};
|
||||
const writeExistingAttachmentData = () => { };
|
||||
const writeExistingAttachmentData = () => {};
|
||||
|
||||
const actual = await Message.createAttachmentDataWriter({
|
||||
writeExistingAttachmentData,
|
||||
|
@ -43,7 +43,7 @@ describe('Message', () => {
|
|||
schemaVersion: 4,
|
||||
attachments: [],
|
||||
};
|
||||
const writeExistingAttachmentData = () => { };
|
||||
const writeExistingAttachmentData = () => {};
|
||||
|
||||
const actual = await Message.createAttachmentDataWriter({
|
||||
writeExistingAttachmentData,
|
||||
|
@ -432,7 +432,7 @@ describe('Message', () => {
|
|||
|
||||
describe('_withSchemaVersion', () => {
|
||||
it('should require a version number', () => {
|
||||
const toVersionX = () => { };
|
||||
const toVersionX = () => {};
|
||||
assert.throws(
|
||||
() =>
|
||||
Message._withSchemaVersion({ schemaVersion: toVersionX, upgrade: 2 }),
|
||||
|
|
|
@ -19,7 +19,8 @@ describe('SignalProtocolStore', () => {
|
|||
privKey: libsignal.crypto.getRandomBytes(32),
|
||||
};
|
||||
|
||||
storage.put('registrationId', 1337)
|
||||
storage
|
||||
.put('registrationId', 1337)
|
||||
.then(() => storage.put('identityKey', identityKey))
|
||||
.then(() => storage.fetch())
|
||||
.then(done, done);
|
||||
|
|
|
@ -20,7 +20,7 @@ describe('InboxView', () => {
|
|||
inboxView = new Whisper.InboxView({
|
||||
model: {},
|
||||
window,
|
||||
initialLoadComplete() { },
|
||||
initialLoadComplete() {},
|
||||
}).render();
|
||||
|
||||
conversation = new Whisper.Conversation({
|
||||
|
|
|
@ -174,7 +174,7 @@ describe('NetworkStatusView', () => {
|
|||
/Attempting reconnect/
|
||||
);
|
||||
});
|
||||
it('should be reset by changing the socketStatus to CONNECTING', () => { });
|
||||
it('should be reset by changing the socketStatus to CONNECTING', () => {});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -165,7 +165,13 @@ export class ConversationListItem extends React.Component<Props> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
const { unreadCount, onClick, isSelected, showFriendRequestIndicator, isBlocked } = this.props;
|
||||
const {
|
||||
unreadCount,
|
||||
onClick,
|
||||
isSelected,
|
||||
showFriendRequestIndicator,
|
||||
isBlocked,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -175,8 +181,10 @@ export class ConversationListItem extends React.Component<Props> {
|
|||
'module-conversation-list-item',
|
||||
unreadCount > 0 ? 'module-conversation-list-item--has-unread' : null,
|
||||
isSelected ? 'module-conversation-list-item--is-selected' : null,
|
||||
showFriendRequestIndicator ? 'module-conversation-list-item--has-friend-request' : null,
|
||||
isBlocked ? 'module-conversation-list-item--is-blocked' : null,
|
||||
showFriendRequestIndicator
|
||||
? 'module-conversation-list-item--has-friend-request'
|
||||
: null,
|
||||
isBlocked ? 'module-conversation-list-item--is-blocked' : null
|
||||
)}
|
||||
>
|
||||
{this.renderAvatar()}
|
||||
|
|
|
@ -26,11 +26,11 @@ export class MainHeader extends React.Component<Props> {
|
|||
name,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
onClick
|
||||
onClick,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div role='button' className="module-main-header" onClick={onClick}>
|
||||
<div role="button" className="module-main-header" onClick={onClick}>
|
||||
<Avatar
|
||||
avatarPath={avatarPath}
|
||||
color={color}
|
||||
|
|
|
@ -28,7 +28,10 @@ export class ContactName extends React.Component<Props> {
|
|||
return (
|
||||
<span className={prefix}>
|
||||
{profileElement}
|
||||
<span title={phoneNumber} className={shouldShowProfile ? `${prefix}__profile-number` : ''}>
|
||||
<span
|
||||
title={phoneNumber}
|
||||
className={shouldShowProfile ? `${prefix}__profile-number` : ''}
|
||||
>
|
||||
<Emojify text={title} i18n={i18n} />
|
||||
</span>
|
||||
</span>
|
||||
|
|
|
@ -95,12 +95,7 @@ export class ConversationHeader extends React.Component<Props> {
|
|||
}
|
||||
|
||||
public renderTitle() {
|
||||
const {
|
||||
phoneNumber,
|
||||
i18n,
|
||||
profileName,
|
||||
isKeysPending,
|
||||
} = this.props;
|
||||
const { phoneNumber, i18n, profileName, isKeysPending } = this.props;
|
||||
|
||||
return (
|
||||
<div className="module-conversation-header__title">
|
||||
|
@ -236,7 +231,9 @@ export class ConversationHeader extends React.Component<Props> {
|
|||
<MenuItem onClick={blockHandler}>{blockTitle}</MenuItem>
|
||||
) : null}
|
||||
{!isMe ? (
|
||||
<MenuItem onClick={onChangeNickname}>{i18n('changeNickname')}</MenuItem>
|
||||
<MenuItem onClick={onChangeNickname}>
|
||||
{i18n('changeNickname')}
|
||||
</MenuItem>
|
||||
) : null}
|
||||
{!isMe && hasNickname ? (
|
||||
<MenuItem onClick={onClearNickname}>{i18n('clearNickname')}</MenuItem>
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
### Friend Requests
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Values |
|
||||
| -------------------- | -------------------------------------------- |
|
||||
| text | string |
|
||||
| text | string |
|
||||
| direction | 'outgoing' \| 'incoming |
|
||||
| status | 'sending' \| 'sent' \| 'read' \| 'delivered' |
|
||||
| friendStatus | 'pending' \| 'accepted' \| 'declined' |
|
||||
|
@ -13,8 +14,8 @@
|
|||
| onDeleteConversation | function |
|
||||
| onRetrySend | function |
|
||||
|
||||
|
||||
#### Example
|
||||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<li>
|
||||
|
|
|
@ -31,7 +31,7 @@ export class FriendRequest extends React.Component<Props> {
|
|||
case 'declined':
|
||||
return 'friendRequestDeclined';
|
||||
case 'expired':
|
||||
return 'friendRequestExpired'
|
||||
return 'friendRequestExpired';
|
||||
default:
|
||||
throw new Error(`Invalid friend request status: ${friendStatus}`);
|
||||
}
|
||||
|
@ -43,7 +43,9 @@ export class FriendRequest extends React.Component<Props> {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<div className={`module-friend-request__title module-friend-request__title--${direction}`}>
|
||||
<div
|
||||
className={`module-friend-request__title module-friend-request__title--${direction}`}
|
||||
>
|
||||
{i18n(id)}
|
||||
</div>
|
||||
<div dir="auto">
|
||||
|
@ -54,55 +56,69 @@ export class FriendRequest extends React.Component<Props> {
|
|||
}
|
||||
|
||||
public renderButtons() {
|
||||
const { i18n, friendStatus, direction, status, onAccept, onDecline, onDeleteConversation, onRetrySend, isBlocked, onBlockUser, onUnblockUser } = this.props;
|
||||
const {
|
||||
i18n,
|
||||
friendStatus,
|
||||
direction,
|
||||
status,
|
||||
onAccept,
|
||||
onDecline,
|
||||
onDeleteConversation,
|
||||
onRetrySend,
|
||||
isBlocked,
|
||||
onBlockUser,
|
||||
onUnblockUser,
|
||||
} = this.props;
|
||||
|
||||
if (direction === 'incoming') {
|
||||
if (friendStatus === 'pending') {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'module-message__metadata',
|
||||
'module-friend-request__buttonContainer',
|
||||
`module-friend-request__buttonContainer--${direction}`
|
||||
)}
|
||||
>
|
||||
<button onClick={onAccept}>Accept</button>
|
||||
<button onClick={onDecline}>Decline</button>
|
||||
</div>
|
||||
);
|
||||
} else if (friendStatus === 'declined') {
|
||||
const blockTitle = isBlocked ? i18n('unblockUser') : i18n('blockUser');
|
||||
const blockHandler = isBlocked ? onUnblockUser : onBlockUser;
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'module-message__metadata',
|
||||
'module-friend-request__buttonContainer',
|
||||
`module-friend-request__buttonContainer--${direction}`
|
||||
)}
|
||||
>
|
||||
<button onClick={onDeleteConversation}>Delete Conversation</button>
|
||||
<button onClick={blockHandler}>{blockTitle}</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Render the retry button if we errored
|
||||
if (status === 'error' && friendStatus === 'pending') {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'module-message__metadata',
|
||||
'module-friend-request__buttonContainer',
|
||||
`module-friend-request__buttonContainer--${direction}`
|
||||
)}
|
||||
>
|
||||
<button onClick={onRetrySend}>Retry Send</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (direction === 'incoming') {
|
||||
if (friendStatus === 'pending') {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'module-message__metadata',
|
||||
'module-friend-request__buttonContainer',
|
||||
`module-friend-request__buttonContainer--${direction}`
|
||||
)}
|
||||
>
|
||||
<button onClick={onAccept}>Accept</button>
|
||||
<button onClick={onDecline}>Decline</button>
|
||||
</div>
|
||||
);
|
||||
} else if (friendStatus === 'declined') {
|
||||
const blockTitle = isBlocked ? i18n('unblockUser') : i18n('blockUser');
|
||||
const blockHandler = isBlocked ? onUnblockUser : onBlockUser;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'module-message__metadata',
|
||||
'module-friend-request__buttonContainer',
|
||||
`module-friend-request__buttonContainer--${direction}`
|
||||
)}
|
||||
>
|
||||
<button onClick={onDeleteConversation}>Delete Conversation</button>
|
||||
<button onClick={blockHandler}>{blockTitle}</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
} else {
|
||||
// Render the retry button if we errored
|
||||
if (status === 'error' && friendStatus === 'pending') {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'module-message__metadata',
|
||||
'module-friend-request__buttonContainer',
|
||||
`module-friend-request__buttonContainer--${direction}`
|
||||
)}
|
||||
>
|
||||
<button onClick={onRetrySend}>Retry Send</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public renderError(isCorrectSide: boolean) {
|
||||
|
@ -127,7 +143,9 @@ export class FriendRequest extends React.Component<Props> {
|
|||
// Renders 'sending', 'read' icons
|
||||
public renderStatusIndicator() {
|
||||
const { direction, status } = this.props;
|
||||
if (direction !== 'outgoing' || status === 'error') return null;
|
||||
if (direction !== 'outgoing' || status === 'error') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="module-message__metadata">
|
||||
|
@ -135,7 +153,7 @@ export class FriendRequest extends React.Component<Props> {
|
|||
<div
|
||||
className={classNames(
|
||||
'module-message__metadata__status-icon',
|
||||
`module-message__metadata__status-icon--${status}`,
|
||||
`module-message__metadata__status-icon--${status}`
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
@ -149,27 +167,27 @@ export class FriendRequest extends React.Component<Props> {
|
|||
<div
|
||||
className={classNames(
|
||||
`module-message module-message--${direction}`,
|
||||
'module-message-friend-request',
|
||||
'module-message-friend-request'
|
||||
)}
|
||||
>
|
||||
{this.renderError(direction === 'incoming')}
|
||||
{this.renderError(direction === 'incoming')}
|
||||
<div
|
||||
className={classNames(
|
||||
'module-message__container',
|
||||
`module-message__container--${direction}`,
|
||||
'module-message-friend-request__container',
|
||||
'module-message-friend-request__container'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'module-message__text',
|
||||
`module-message__text--${direction}`,
|
||||
)}
|
||||
>
|
||||
{this.renderContents()}
|
||||
{this.renderStatusIndicator()}
|
||||
{this.renderButtons()}
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'module-message__text',
|
||||
`module-message__text--${direction}`
|
||||
)}
|
||||
>
|
||||
{this.renderContents()}
|
||||
{this.renderStatusIndicator()}
|
||||
{this.renderButtons()}
|
||||
</div>
|
||||
</div>
|
||||
{this.renderError(direction === 'outgoing')}
|
||||
</div>
|
||||
|
|
|
@ -13,7 +13,7 @@ export class ResetSessionNotification extends React.Component<Props> {
|
|||
|
||||
return (
|
||||
<div className="module-reset-session-notification">
|
||||
{ i18n(sessionResetMessageKey) }
|
||||
{i18n(sessionResetMessageKey)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -46,7 +46,9 @@ describe('Attachment', () => {
|
|||
contentType: MIME.VIDEO_QUICKTIME,
|
||||
};
|
||||
// Unix timestamp of start of year 2000 to fix odd sudo timezone bug
|
||||
const timestamp = new Date(946684800000 - moment().utcOffset() * 60 * 1000);
|
||||
const timestamp = new Date(
|
||||
946684800000 - moment().utcOffset() * 60 * 1000
|
||||
);
|
||||
const actual = Attachment.getSuggestedFilename({
|
||||
attachment,
|
||||
timestamp,
|
||||
|
|
Loading…
Reference in a new issue