Automatic session reset
This commit is contained in:
parent
fe187226bb
commit
98e7e65d25
26 changed files with 803 additions and 225 deletions
|
@ -1077,6 +1077,22 @@
|
||||||
"message": "Secure session reset",
|
"message": "Secure session reset",
|
||||||
"description": "This is a past tense, informational message. In other words, your secure session has been reset."
|
"description": "This is a past tense, informational message. In other words, your secure session has been reset."
|
||||||
},
|
},
|
||||||
|
"ChatRefresh--notification": {
|
||||||
|
"message": "Chat session refreshed",
|
||||||
|
"description": "Shown in timeline when a error happened, and the session was automatically reset."
|
||||||
|
},
|
||||||
|
"ChatRefresh--learnMore": {
|
||||||
|
"message": "Learn More",
|
||||||
|
"description": "Shown in timeline when session is automatically reset, to provide access to a popup info dialog"
|
||||||
|
},
|
||||||
|
"ChatRefresh--summary": {
|
||||||
|
"message": "Signal uses end-to-end encryption and it may need to refresh your chat session sometimes. This doesn’t affect your chat’s security but you may have missed a message from this contact and you can ask them to resend it.",
|
||||||
|
"description": "Shown on explainer dialog available from chat session refreshed timeline events"
|
||||||
|
},
|
||||||
|
"ChatRefresh--contactSupport": {
|
||||||
|
"message": "Contact Support",
|
||||||
|
"description": "Shown on explainer dialog available from chat session refreshed timeline events"
|
||||||
|
},
|
||||||
"quoteThumbnailAlt": {
|
"quoteThumbnailAlt": {
|
||||||
"message": "Thumbnail of image from quoted message",
|
"message": "Thumbnail of image from quoted message",
|
||||||
"description": "Used in alt tag of thumbnail images inside of an embedded message quote"
|
"description": "Used in alt tag of thumbnail images inside of an embedded message quote"
|
||||||
|
|
12
images/chat-session-refresh.svg
Normal file
12
images/chat-session-refresh.svg
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<svg width="200" height="110" viewBox="0 0 200 110" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M39.3 15.1L12.9 15C9.1 15 6 18.1 6 21.9L5.9 88C5.9 91.8 9 94.9 12.8 94.9L39.1 95C42.9 95 46 91.9 46 88.1L46.2 22C46.2 18.2 43.1 15.1 39.3 15.1ZM42.8 88.1C42.8 90.1 41.1 91.8 39.1 91.8L12.7 91.7C10.7 91.7 9 90 9 88L9.1 21.9C9.1 19.9 10.8 18.2 12.8 18.2L39.2 18.3C41.2 18.3 42.9 20 42.9 22L42.8 88.1Z" fill="#C6C6C6"/>
|
||||||
|
<path d="M187.3 15.1L160.9 15C157.1 15 154 18.1 154 21.9L153.9 88C153.9 91.8 157 94.9 160.8 94.9L187.2 95C191 95 194.1 91.9 194.1 88.1L194.2 22C194.2 18.2 191.1 15.1 187.3 15.1ZM190.8 88.1C190.8 90.1 189.1 91.8 187.1 91.8L160.7 91.7C158.7 91.7 157 90 157 88L157.1 21.9C157.1 19.9 158.8 18.2 160.8 18.2L187.2 18.3C189.2 18.3 190.9 20 190.9 22L190.8 88.1Z" fill="#C6C6C6"/>
|
||||||
|
<circle cx="126" cy="56" r="2" fill="#848484"/>
|
||||||
|
<circle cx="136" cy="56" r="2" fill="#848484"/>
|
||||||
|
<circle cx="146" cy="56" r="2" fill="#848484"/>
|
||||||
|
<circle cx="54" cy="56" r="2" fill="#848484"/>
|
||||||
|
<circle cx="64" cy="56" r="2" fill="#848484"/>
|
||||||
|
<circle cx="74" cy="56" r="2" fill="#848484"/>
|
||||||
|
<circle cx="100" cy="56" r="18" fill="#2C6BED"/>
|
||||||
|
<path d="M109.38 56C109.393 58.5029 108.413 60.9088 106.655 62.6898C104.896 64.4708 102.503 65.4815 100 65.5C98.4434 65.4794 96.9157 65.0765 95.5513 64.3269C94.1869 63.5773 93.0276 62.5038 92.1755 61.201C91.3233 59.8982 90.8044 58.4059 90.6643 56.8555C90.5243 55.305 90.7674 53.7439 91.3723 52.3095C91.9772 50.875 92.9254 49.6112 94.1333 48.6292C95.3413 47.6473 96.7721 46.9772 98.2998 46.678C99.8276 46.3787 101.405 46.4596 102.895 46.9133C104.384 47.367 105.739 48.1797 106.84 49.28L107.84 50.28L107.49 48.98V46.56H109V52.5H103V51H105.39L106.81 51.38L105.81 50.38C104.695 49.252 103.27 48.4808 101.716 48.1644C100.162 47.8479 98.549 48.0006 97.0819 48.6028C95.6147 49.2051 94.3597 50.2299 93.4762 51.547C92.5928 52.8641 92.1207 54.414 92.12 56C92.12 57.0348 92.3238 58.0595 92.7198 59.0155C93.1158 59.9716 93.6963 60.8403 94.428 61.572C95.1597 62.3037 96.0284 62.8842 96.9845 63.2802C97.9405 63.6762 98.9652 63.88 100 63.88C101.035 63.88 102.059 63.6762 103.016 63.2802C103.972 62.8842 104.84 62.3037 105.572 61.572C106.304 60.8403 106.884 59.9716 107.28 59.0155C107.676 58.0595 107.88 57.0348 107.88 56H109.38Z" fill="white"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.2 KiB |
3
images/icons/v2/refresh-16.svg
Normal file
3
images/icons/v2/refresh-16.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg id="Export" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||||
|
<path d="M13.46,8A5.46,5.46,0,1,1,2.55,8,5.54,5.54,0,0,1,12,4.06l.44.44-1.13-.33H10.17v1.5h4.49V1.18h-1.5V2.3l.31,1.09L13.08,3a7,7,0,0,0-12,5,7,7,0,0,0,7,7,7,7,0,0,0,7-7Zm.45-3.06Z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 289 B |
|
@ -14,7 +14,7 @@ const fakeAPI = {
|
||||||
getAvatar: fakeCall,
|
getAvatar: fakeCall,
|
||||||
getDevices: fakeCall,
|
getDevices: fakeCall,
|
||||||
// getKeysForIdentifier : fakeCall,
|
// getKeysForIdentifier : fakeCall,
|
||||||
getMessageSocket: fakeCall,
|
getMessageSocket: () => new window.MockSocket('ws://localhost:8081/'),
|
||||||
getMyKeys: fakeCall,
|
getMyKeys: fakeCall,
|
||||||
getProfile: fakeCall,
|
getProfile: fakeCall,
|
||||||
getProvisioningSocket: fakeCall,
|
getProvisioningSocket: fakeCall,
|
||||||
|
|
|
@ -166,4 +166,15 @@ SignalProtocolStore.prototype = {
|
||||||
resolve(deviceIds);
|
resolve(deviceIds);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getUnprocessedCount: () => Promise.resolve(0),
|
||||||
|
getAllUnprocessed: () => Promise.resolve([]),
|
||||||
|
getUnprocessedById: () => Promise.resolve(null),
|
||||||
|
addUnprocessed: () => Promise.resolve(),
|
||||||
|
addMultipleUnprocessed: () => Promise.resolve(),
|
||||||
|
updateUnprocessedAttempts: () => Promise.resolve(),
|
||||||
|
updateUnprocessedWithData: () => Promise.resolve(),
|
||||||
|
updateUnprocessedsWithData: () => Promise.resolve(),
|
||||||
|
removeUnprocessed: () => Promise.resolve(),
|
||||||
|
removeAllUnprocessed: () => Promise.resolve(),
|
||||||
};
|
};
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
<script type="text/javascript" src="../libsignal-protocol.js"></script>
|
<script type="text/javascript" src="../libsignal-protocol.js"></script>
|
||||||
<script type="text/javascript" src="../protobufs.js" data-cover></script>
|
<script type="text/javascript" src="../protobufs.js" data-cover></script>
|
||||||
<script type="text/javascript" src="../storage/user.js" data-cover></script>
|
<script type="text/javascript" src="../storage/user.js" data-cover></script>
|
||||||
|
<script type="text/javascript" src="../storage/unprocessed.js" data-cover></script>
|
||||||
<script type="text/javascript" src="../protocol_wrapper.js" data-cover></script>
|
<script type="text/javascript" src="../protocol_wrapper.js" data-cover></script>
|
||||||
|
|
||||||
<script type="text/javascript" src="../../js/libphonenumber-util.js"></script>
|
<script type="text/javascript" src="../../js/libphonenumber-util.js"></script>
|
||||||
|
@ -31,10 +32,7 @@
|
||||||
<script type="text/javascript" src="../../js/signal_protocol_store.js" data-cover></script>
|
<script type="text/javascript" src="../../js/signal_protocol_store.js" data-cover></script>
|
||||||
<script type="text/javascript" src="../../js/storage.js" data-cover></script>
|
<script type="text/javascript" src="../../js/storage.js" data-cover></script>
|
||||||
<script type="text/javascript" src="../../js/models/blockedNumbers.js" data-cover></script>
|
<script type="text/javascript" src="../../js/models/blockedNumbers.js" data-cover></script>
|
||||||
<script type="text/javascript" src="../../js/models/messages.js" data-cover></script>
|
|
||||||
<script type="text/javascript" src="../../js/models/conversations.js" data-cover></script>
|
|
||||||
|
|
||||||
<script type="text/javascript" src="errors_test.js"></script>
|
|
||||||
<script type="text/javascript" src="helpers_test.js"></script>
|
<script type="text/javascript" src="helpers_test.js"></script>
|
||||||
<script type="text/javascript" src="storage_test.js"></script>
|
<script type="text/javascript" src="storage_test.js"></script>
|
||||||
<script type="text/javascript" src="crypto_test.js"></script>
|
<script type="text/javascript" src="crypto_test.js"></script>
|
||||||
|
@ -44,6 +42,7 @@
|
||||||
<script type="text/javascript" src="websocket-resources_test.js"></script>
|
<script type="text/javascript" src="websocket-resources_test.js"></script>
|
||||||
<script type="text/javascript" src="task_with_timeout_test.js"></script>
|
<script type="text/javascript" src="task_with_timeout_test.js"></script>
|
||||||
<script type="text/javascript" src="account_manager_test.js"></script>
|
<script type="text/javascript" src="account_manager_test.js"></script>
|
||||||
|
<script type="text/javascript" src="message_receiver_test.js"></script>
|
||||||
<script type="text/javascript" src="sendmessage_test.js"></script>
|
<script type="text/javascript" src="sendmessage_test.js"></script>
|
||||||
|
|
||||||
<!-- Comment out to turn off code coverage. Useful for getting real callstacks. -->
|
<!-- Comment out to turn off code coverage. Useful for getting real callstacks. -->
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
// Copyright 2015-2020 Signal Messenger, LLC
|
// Copyright 2015-2020 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
/* global libsignal, textsecure, SignalProtocolStore */
|
/* global libsignal, textsecure */
|
||||||
|
|
||||||
describe('MessageReceiver', () => {
|
describe('MessageReceiver', () => {
|
||||||
textsecure.storage.impl = new SignalProtocolStore();
|
|
||||||
const { WebSocket } = window;
|
const { WebSocket } = window;
|
||||||
const number = '+19999999999';
|
const number = '+19999999999';
|
||||||
const uuid = 'AAAAAAAA-BBBB-4CCC-9DDD-EEEEEEEEEEEE';
|
const uuid = 'AAAAAAAA-BBBB-4CCC-9DDD-EEEEEEEEEEEE';
|
||||||
|
@ -12,6 +11,7 @@ describe('MessageReceiver', () => {
|
||||||
const signalingKey = libsignal.crypto.getRandomBytes(32 + 20);
|
const signalingKey = libsignal.crypto.getRandomBytes(32 + 20);
|
||||||
|
|
||||||
before(() => {
|
before(() => {
|
||||||
|
localStorage.clear();
|
||||||
window.WebSocket = MockSocket;
|
window.WebSocket = MockSocket;
|
||||||
textsecure.storage.user.setNumberAndDeviceId(number, deviceId, 'name');
|
textsecure.storage.user.setNumberAndDeviceId(number, deviceId, 'name');
|
||||||
textsecure.storage.user.setUuidAndDeviceId(uuid, deviceId);
|
textsecure.storage.user.setUuidAndDeviceId(uuid, deviceId);
|
||||||
|
@ -19,94 +19,126 @@ describe('MessageReceiver', () => {
|
||||||
textsecure.storage.put('signaling_key', signalingKey);
|
textsecure.storage.put('signaling_key', signalingKey);
|
||||||
});
|
});
|
||||||
after(() => {
|
after(() => {
|
||||||
|
localStorage.clear();
|
||||||
window.WebSocket = WebSocket;
|
window.WebSocket = WebSocket;
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('connecting', () => {
|
describe('connecting', () => {
|
||||||
const attrs = {
|
let attrs;
|
||||||
type: textsecure.protobuf.Envelope.Type.CIPHERTEXT,
|
let websocketmessage;
|
||||||
source: number,
|
|
||||||
sourceUuid: uuid,
|
before(() => {
|
||||||
sourceDevice: deviceId,
|
attrs = {
|
||||||
timestamp: Date.now(),
|
type: textsecure.protobuf.Envelope.Type.CIPHERTEXT,
|
||||||
};
|
source: number,
|
||||||
const websocketmessage = new textsecure.protobuf.WebSocketMessage({
|
sourceUuid: uuid,
|
||||||
type: textsecure.protobuf.WebSocketMessage.Type.REQUEST,
|
sourceDevice: deviceId,
|
||||||
request: { verb: 'PUT', path: '/messages' },
|
timestamp: Date.now(),
|
||||||
|
content: libsignal.crypto.getRandomBytes(200),
|
||||||
|
};
|
||||||
|
const body = new textsecure.protobuf.Envelope(attrs).toArrayBuffer();
|
||||||
|
|
||||||
|
websocketmessage = new textsecure.protobuf.WebSocketMessage({
|
||||||
|
type: textsecure.protobuf.WebSocketMessage.Type.REQUEST,
|
||||||
|
request: { verb: 'PUT', path: '/api/v1/message', body },
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
before(done => {
|
it('generates light-session-reset event when it cannot decrypt', done => {
|
||||||
const signal = new textsecure.protobuf.Envelope(attrs).toArrayBuffer();
|
const mockServer = new MockServer('ws://localhost:8081/');
|
||||||
|
|
||||||
const aesKey = signalingKey.slice(0, 32);
|
|
||||||
const macKey = signalingKey.slice(32, 32 + 20);
|
|
||||||
|
|
||||||
window.crypto.subtle
|
|
||||||
.importKey('raw', aesKey, { name: 'AES-CBC' }, false, ['encrypt'])
|
|
||||||
.then(key => {
|
|
||||||
const iv = libsignal.crypto.getRandomBytes(16);
|
|
||||||
window.crypto.subtle
|
|
||||||
.encrypt({ name: 'AES-CBC', iv: new Uint8Array(iv) }, key, signal)
|
|
||||||
.then(ciphertext => {
|
|
||||||
window.crypto.subtle
|
|
||||||
.importKey(
|
|
||||||
'raw',
|
|
||||||
macKey,
|
|
||||||
{ name: 'HMAC', hash: { name: 'SHA-256' } },
|
|
||||||
false,
|
|
||||||
['sign']
|
|
||||||
)
|
|
||||||
.then(innerKey => {
|
|
||||||
window.crypto.subtle
|
|
||||||
.sign({ name: 'HMAC', hash: 'SHA-256' }, innerKey, signal)
|
|
||||||
.then(mac => {
|
|
||||||
const version = new Uint8Array([1]);
|
|
||||||
const message = dcodeIO.ByteBuffer.concat([
|
|
||||||
version,
|
|
||||||
iv,
|
|
||||||
ciphertext,
|
|
||||||
mac,
|
|
||||||
]);
|
|
||||||
websocketmessage.request.body = message.toArrayBuffer();
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('connects', done => {
|
|
||||||
const mockServer = new MockServer(
|
|
||||||
`ws://localhost:8080/v1/websocket/?login=${encodeURIComponent(
|
|
||||||
uuid
|
|
||||||
)}.1&password=password`
|
|
||||||
);
|
|
||||||
|
|
||||||
mockServer.on('connection', server => {
|
mockServer.on('connection', server => {
|
||||||
server.send(new Blob([websocketmessage.toArrayBuffer()]));
|
setTimeout(() => {
|
||||||
|
server.send(new Blob([websocketmessage.toArrayBuffer()]));
|
||||||
|
}, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('textsecure:message', ev => {
|
const messageReceiver = new textsecure.MessageReceiver(
|
||||||
const signal = ev.proto;
|
'oldUsername',
|
||||||
const keys = Object.keys(attrs);
|
|
||||||
|
|
||||||
for (let i = 0, max = keys.length; i < max; i += 1) {
|
|
||||||
const key = keys[i];
|
|
||||||
assert.strictEqual(attrs[key], signal[key]);
|
|
||||||
}
|
|
||||||
assert.strictEqual(signal.message.body, 'hello');
|
|
||||||
mockServer.close();
|
|
||||||
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
window.messageReceiver = new textsecure.MessageReceiver(
|
|
||||||
'username',
|
'username',
|
||||||
'password',
|
'password',
|
||||||
'signalingKey'
|
'signalingKey',
|
||||||
// 'ws://localhost:8080',
|
{
|
||||||
// window,
|
serverTrustRoot: 'AAAAAAAA',
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
messageReceiver.addEventListener('light-session-reset', done());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('methods', () => {
|
||||||
|
let messageReceiver;
|
||||||
|
let mockServer;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Necessary to populate the server property inside of MockSocket. Without it, we
|
||||||
|
// crash when doing any number of things to a MockSocket instance.
|
||||||
|
mockServer = new MockServer('ws://localhost:8081');
|
||||||
|
|
||||||
|
messageReceiver = new textsecure.MessageReceiver(
|
||||||
|
'oldUsername',
|
||||||
|
'username',
|
||||||
|
'password',
|
||||||
|
'signalingKey',
|
||||||
|
{
|
||||||
|
serverTrustRoot: 'AAAAAAAA',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
mockServer.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#isOverHourIntoPast', () => {
|
||||||
|
it('returns false for now', () => {
|
||||||
|
assert.isFalse(messageReceiver.isOverHourIntoPast(Date.now()));
|
||||||
|
});
|
||||||
|
it('returns false for 5 minutes ago', () => {
|
||||||
|
const fiveMinutesAgo = Date.now() - 5 * 60 * 1000;
|
||||||
|
assert.isFalse(messageReceiver.isOverHourIntoPast(fiveMinutesAgo));
|
||||||
|
});
|
||||||
|
it('returns true for 65 minutes ago', () => {
|
||||||
|
const sixtyFiveMinutesAgo = Date.now() - 65 * 60 * 1000;
|
||||||
|
assert.isTrue(messageReceiver.isOverHourIntoPast(sixtyFiveMinutesAgo));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#cleanupSessionResets', () => {
|
||||||
|
it('leaves empty object alone', () => {
|
||||||
|
window.storage.put('sessionResets', {});
|
||||||
|
messageReceiver.cleanupSessionResets();
|
||||||
|
const actual = window.storage.get('sessionResets');
|
||||||
|
|
||||||
|
const expected = {};
|
||||||
|
assert.deepEqual(actual, expected);
|
||||||
|
});
|
||||||
|
it('filters out any timestamp older than one hour', () => {
|
||||||
|
const startValue = {
|
||||||
|
one: Date.now() - 1,
|
||||||
|
two: Date.now(),
|
||||||
|
three: Date.now() - 65 * 60 * 1000,
|
||||||
|
};
|
||||||
|
window.storage.put('sessionResets', startValue);
|
||||||
|
messageReceiver.cleanupSessionResets();
|
||||||
|
const actual = window.storage.get('sessionResets');
|
||||||
|
|
||||||
|
const expected = window._.pick(startValue, ['one', 'two']);
|
||||||
|
assert.deepEqual(actual, expected);
|
||||||
|
});
|
||||||
|
it('filters out falsey items', () => {
|
||||||
|
const startValue = {
|
||||||
|
one: 0,
|
||||||
|
two: false,
|
||||||
|
three: Date.now(),
|
||||||
|
};
|
||||||
|
window.storage.put('sessionResets', startValue);
|
||||||
|
messageReceiver.cleanupSessionResets();
|
||||||
|
const actual = window.storage.get('sessionResets');
|
||||||
|
|
||||||
|
const expected = window._.pick(startValue, ['three']);
|
||||||
|
assert.deepEqual(actual, expected);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -45,6 +45,7 @@ try {
|
||||||
|
|
||||||
window.platform = process.platform;
|
window.platform = process.platform;
|
||||||
window.getTitle = () => title;
|
window.getTitle = () => title;
|
||||||
|
window.getLocale = () => config.locale;
|
||||||
window.getEnvironment = getEnvironment;
|
window.getEnvironment = getEnvironment;
|
||||||
window.getAppInstance = () => config.appInstance;
|
window.getAppInstance = () => config.appInstance;
|
||||||
window.getVersion = () => config.version;
|
window.getVersion = () => config.version;
|
||||||
|
|
|
@ -11453,6 +11453,88 @@ $contact-modal-padding: 18px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Module: Chat Session Refreshed Notification
|
||||||
|
|
||||||
|
.module-chat-session-refreshed-notification {
|
||||||
|
@include font-body-2;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-chat-session-refreshed-notification__first-line {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
.module-chat-session-refreshed-notification__icon {
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 8px;
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
@include color-svg('../images/icons/v2/refresh-16.svg', $color-gray-60);
|
||||||
|
}
|
||||||
|
@include dark-theme {
|
||||||
|
@include color-svg('../images/icons/v2/refresh-16.svg', $color-gray-25);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.module-chat-session-refreshed-notification__button {
|
||||||
|
@include button-reset;
|
||||||
|
@include button-light-blue-text;
|
||||||
|
@include button-small;
|
||||||
|
|
||||||
|
@include font-body-2;
|
||||||
|
padding: 5px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Module: Chat Session Refreshed Dialog
|
||||||
|
|
||||||
|
.module-chat-session-refreshed-dialog {
|
||||||
|
width: 360px;
|
||||||
|
padding: 16px;
|
||||||
|
padding-top: 28px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
background-color: $color-white;
|
||||||
|
}
|
||||||
|
@include dark-theme {
|
||||||
|
background-color: $color-gray-95;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.module-chat-session-refreshed-dialog__image {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.module-chat-session-refreshed-dialog__title {
|
||||||
|
@include font-body-1-bold;
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
.module-chat-session-refreshed-dialog__buttons {
|
||||||
|
text-align: right;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.module-chat-session-refreshed-dialog__button {
|
||||||
|
@include font-body-1-bold;
|
||||||
|
@include button-reset;
|
||||||
|
@include button-primary;
|
||||||
|
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 7px 14px;
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
.module-chat-session-refreshed-dialog__button--secondary {
|
||||||
|
@include button-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
/* Third-party module: react-contextmenu*/
|
/* Third-party module: react-contextmenu*/
|
||||||
|
|
||||||
.react-contextmenu {
|
.react-contextmenu {
|
||||||
|
|
|
@ -337,15 +337,12 @@
|
||||||
<script type="text/javascript" src="../js/reliable_trigger.js" data-cover></script>
|
<script type="text/javascript" src="../js/reliable_trigger.js" data-cover></script>
|
||||||
<script type="text/javascript" src="test.js"></script>
|
<script type="text/javascript" src="test.js"></script>
|
||||||
|
|
||||||
<script type="text/javascript" src="../js/chromium.js" data-cover></script>
|
|
||||||
<script type="text/javascript" src="../js/database.js" data-cover></script>
|
<script type="text/javascript" src="../js/database.js" data-cover></script>
|
||||||
<script type="text/javascript" src="../js/storage.js" data-cover></script>
|
<script type="text/javascript" src="../js/storage.js" data-cover></script>
|
||||||
<script type="text/javascript" src="../js/signal_protocol_store.js" data-cover></script>
|
<script type="text/javascript" src="../js/signal_protocol_store.js" data-cover></script>
|
||||||
<script type="text/javascript" src="../js/libtextsecure.js" data-cover></script>
|
<script type="text/javascript" src="../js/libtextsecure.js" data-cover></script>
|
||||||
|
|
||||||
<script type="text/javascript" src="../js/libphonenumber-util.js"></script>
|
<script type="text/javascript" src="../js/libphonenumber-util.js"></script>
|
||||||
<script type="text/javascript" src="../js/models/messages.js" data-cover></script>
|
|
||||||
<script type="text/javascript" src="../js/models/conversations.js" data-cover></script>
|
|
||||||
<script type="text/javascript" src="../js/models/blockedNumbers.js" data-cover></script>
|
<script type="text/javascript" src="../js/models/blockedNumbers.js" data-cover></script>
|
||||||
<script type="text/javascript" src="../js/message_controller.js" data-cover></script>
|
<script type="text/javascript" src="../js/message_controller.js" data-cover></script>
|
||||||
<script type="text/javascript" src="../js/keychange_listener.js" data-cover></script>
|
<script type="text/javascript" src="../js/keychange_listener.js" data-cover></script>
|
||||||
|
@ -353,20 +350,15 @@
|
||||||
<script type='text/javascript' src='../js/expiring_tap_to_view_messages.js' data-cover></script>
|
<script type='text/javascript' src='../js/expiring_tap_to_view_messages.js' data-cover></script>
|
||||||
<script type='text/javascript' src='../js/notifications.js' data-cover></script>
|
<script type='text/javascript' src='../js/notifications.js' data-cover></script>
|
||||||
|
|
||||||
<script type="text/javascript" src="../js/chromium.js" data-cover></script>
|
|
||||||
|
|
||||||
<script type='text/javascript' src='../js/views/react_wrapper_view.js'></script>
|
<script type='text/javascript' src='../js/views/react_wrapper_view.js'></script>
|
||||||
<script type='text/javascript' src='../js/views/whisper_view.js' data-cover></script>
|
<script type='text/javascript' src='../js/views/whisper_view.js' data-cover></script>
|
||||||
<script type='text/javascript' src='../js/views/debug_log_view.js' data-cover></script>
|
|
||||||
<script type='text/javascript' src='../js/views/toast_view.js' data-cover></script>
|
<script type='text/javascript' src='../js/views/toast_view.js' data-cover></script>
|
||||||
<script type='text/javascript' src='../js/views/list_view.js' data-cover></script>
|
<script type='text/javascript' src='../js/views/list_view.js' data-cover></script>
|
||||||
<script type='text/javascript' src='../js/views/contact_list_view.js' data-cover></script>
|
<script type='text/javascript' src='../js/views/contact_list_view.js' data-cover></script>
|
||||||
<script type='text/javascript' src='../js/views/key_verification_view.js' data-cover></script>
|
<script type='text/javascript' src='../js/views/key_verification_view.js' data-cover></script>
|
||||||
<script type='text/javascript' src='../js/views/group_member_list_view.js' data-cover></script>
|
<script type='text/javascript' src='../js/views/group_member_list_view.js' data-cover></script>
|
||||||
<script type='text/javascript' src='../js/views/recorder_view.js' data-cover></script>
|
<script type='text/javascript' src='../js/views/recorder_view.js' data-cover></script>
|
||||||
<script type='text/javascript' src='../js/views/conversation_view.js' data-cover></script>
|
|
||||||
<script type='text/javascript' src='../js/views/inbox_view.js' data-cover></script>
|
<script type='text/javascript' src='../js/views/inbox_view.js' data-cover></script>
|
||||||
<script type='text/javascript' src='../js/views/confirmation_dialog_view.js' data-cover></script>
|
|
||||||
<script type='text/javascript' src='../js/views/identicon_svg_view.js' data-cover></script>
|
<script type='text/javascript' src='../js/views/identicon_svg_view.js' data-cover></script>
|
||||||
<script type='text/javascript' src='../js/views/banner_view.js' data-cover></script>
|
<script type='text/javascript' src='../js/views/banner_view.js' data-cover></script>
|
||||||
<script type='text/javascript' src='../js/views/clear_data_view.js'></script>
|
<script type='text/javascript' src='../js/views/clear_data_view.js'></script>
|
||||||
|
|
117
ts/background.ts
117
ts/background.ts
|
@ -1937,6 +1937,7 @@ type WhatIsThis = import('./window.d').WhatIsThis;
|
||||||
addQueuedEventListener('read', onReadReceipt);
|
addQueuedEventListener('read', onReadReceipt);
|
||||||
addQueuedEventListener('verified', onVerified);
|
addQueuedEventListener('verified', onVerified);
|
||||||
addQueuedEventListener('error', onError);
|
addQueuedEventListener('error', onError);
|
||||||
|
addQueuedEventListener('light-session-reset', onLightSessionReset);
|
||||||
addQueuedEventListener('empty', onEmpty);
|
addQueuedEventListener('empty', onEmpty);
|
||||||
addQueuedEventListener('reconnect', onReconnect);
|
addQueuedEventListener('reconnect', onReconnect);
|
||||||
addQueuedEventListener('configuration', onConfiguration);
|
addQueuedEventListener('configuration', onConfiguration);
|
||||||
|
@ -3121,7 +3122,8 @@ type WhatIsThis = import('./window.d').WhatIsThis;
|
||||||
error.name === 'HTTPError' &&
|
error.name === 'HTTPError' &&
|
||||||
(error.code === 401 || error.code === 403)
|
(error.code === 401 || error.code === 403)
|
||||||
) {
|
) {
|
||||||
return unlinkAndDisconnect();
|
unlinkAndDisconnect();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -3136,101 +3138,40 @@ type WhatIsThis = import('./window.d').WhatIsThis;
|
||||||
|
|
||||||
window.Whisper.events.trigger('reconnectTimer');
|
window.Whisper.events.trigger('reconnectTimer');
|
||||||
}
|
}
|
||||||
return Promise.resolve();
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ev.proto) {
|
window.log.warn('background onError: Doing nothing with incoming error');
|
||||||
if (error && error.name === 'MessageCounterError') {
|
}
|
||||||
if (ev.confirm) {
|
|
||||||
ev.confirm();
|
|
||||||
}
|
|
||||||
// Ignore this message. It is likely a duplicate delivery
|
|
||||||
// because the server lost our ack the first time.
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
const envelope = ev.proto;
|
|
||||||
const id = window.ConversationController.ensureContactIds({
|
|
||||||
e164: envelope.source,
|
|
||||||
uuid: envelope.sourceUuid,
|
|
||||||
});
|
|
||||||
if (!id) {
|
|
||||||
throw new Error('onError: ensureContactIds returned falsey id!');
|
|
||||||
}
|
|
||||||
const message = initIncomingMessage(envelope, {
|
|
||||||
type: Message.PRIVATE,
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const conversationId = message.get('conversationId');
|
type LightSessionResetEventType = {
|
||||||
const conversation = window.ConversationController.get(conversationId);
|
senderUuid: string;
|
||||||
|
};
|
||||||
|
|
||||||
if (!conversation) {
|
function onLightSessionReset(event: LightSessionResetEventType) {
|
||||||
window.log.warn(
|
const conversationId = window.ConversationController.ensureContactIds({
|
||||||
'onError: No conversation id, cannot save error bubble'
|
uuid: event.senderUuid,
|
||||||
);
|
});
|
||||||
ev.confirm();
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
// This matches the queueing behavior used in Message.handleDataMessage
|
if (!conversationId) {
|
||||||
conversation.queueJob(async () => {
|
window.log.warn(
|
||||||
const existingMessage = await window.Signal.Data.getMessageBySender(
|
'onLightSessionReset: No conversation id, cannot add message to timeline'
|
||||||
message.attributes,
|
);
|
||||||
{
|
return;
|
||||||
Message: window.Whisper.Message,
|
}
|
||||||
}
|
const conversation = window.ConversationController.get(conversationId);
|
||||||
);
|
|
||||||
if (existingMessage) {
|
|
||||||
ev.confirm();
|
|
||||||
window.log.warn(
|
|
||||||
`Got duplicate error for message ${message.idForLogging()}`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const model = new window.Whisper.Message({
|
if (!conversation) {
|
||||||
...message.attributes,
|
window.log.warn(
|
||||||
id: window.getGuid(),
|
'onLightSessionReset: No conversation, cannot add message to timeline'
|
||||||
});
|
);
|
||||||
await model.saveErrors(error || new Error('Error was null'), {
|
return;
|
||||||
skipSave: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
window.MessageController.register(model.id, model);
|
|
||||||
await window.Signal.Data.saveMessage(model.attributes, {
|
|
||||||
Message: window.Whisper.Message,
|
|
||||||
forceSave: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
conversation.set({
|
|
||||||
active_at: Date.now(),
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
unreadCount: conversation.get('unreadCount')! + 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
const conversationTimestamp = conversation.get('timestamp');
|
|
||||||
const messageTimestamp = model.get('timestamp');
|
|
||||||
if (
|
|
||||||
!conversationTimestamp ||
|
|
||||||
messageTimestamp > conversationTimestamp
|
|
||||||
) {
|
|
||||||
conversation.set({ timestamp: model.get('sent_at') });
|
|
||||||
}
|
|
||||||
|
|
||||||
conversation.trigger('newmessage', model);
|
|
||||||
conversation.notify(model);
|
|
||||||
|
|
||||||
window.Whisper.events.trigger('incrementProgress');
|
|
||||||
|
|
||||||
if (ev.confirm) {
|
|
||||||
ev.confirm();
|
|
||||||
}
|
|
||||||
|
|
||||||
window.Signal.Data.updateConversation(conversation.attributes);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw error;
|
const receivedAt = Date.now();
|
||||||
|
conversation.queueJob(async () => {
|
||||||
|
conversation.addChatSessionRefreshed(receivedAt);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onViewSync(ev: WhatIsThis) {
|
async function onViewSync(ev: WhatIsThis) {
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
|
||||||
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
import { ChatSessionRefreshedDialog } from './ChatSessionRefreshedDialog';
|
||||||
|
|
||||||
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
storiesOf('Components/Conversation/ChatSessionRefreshedDialog', module).add(
|
||||||
|
'Default',
|
||||||
|
() => {
|
||||||
|
return (
|
||||||
|
<ChatSessionRefreshedDialog
|
||||||
|
contactSupport={action('contactSupport')}
|
||||||
|
onClose={action('onClose')}
|
||||||
|
i18n={i18n}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
57
ts/components/conversation/ChatSessionRefreshedDialog.tsx
Normal file
57
ts/components/conversation/ChatSessionRefreshedDialog.tsx
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import { LocalizerType } from '../../types/Util';
|
||||||
|
|
||||||
|
export type PropsType = {
|
||||||
|
i18n: LocalizerType;
|
||||||
|
contactSupport: () => unknown;
|
||||||
|
onClose: () => unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ChatSessionRefreshedDialog(
|
||||||
|
props: PropsType
|
||||||
|
): React.ReactElement {
|
||||||
|
const { i18n, contactSupport, onClose } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="module-chat-session-refreshed-dialog">
|
||||||
|
<div className="module-chat-session-refreshed-dialog__image">
|
||||||
|
<img
|
||||||
|
src="images/chat-session-refresh.svg"
|
||||||
|
height="110"
|
||||||
|
width="200"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="module-chat-session-refreshed-dialog__title">
|
||||||
|
{i18n('ChatRefresh--notification')}
|
||||||
|
</div>
|
||||||
|
<div className="module-chat-session-refreshed-dialog__description">
|
||||||
|
{i18n('ChatRefresh--summary')}
|
||||||
|
</div>
|
||||||
|
<div className="module-chat-session-refreshed-dialog__buttons">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={contactSupport}
|
||||||
|
className={classNames(
|
||||||
|
'module-chat-session-refreshed-dialog__button',
|
||||||
|
'module-chat-session-refreshed-dialog__button--secondary'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{i18n('ChatRefresh--contactSupport')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="module-chat-session-refreshed-dialog__button"
|
||||||
|
>
|
||||||
|
{i18n('Confirmation--confirm')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
|
||||||
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
import { ChatSessionRefreshedNotification } from './ChatSessionRefreshedNotification';
|
||||||
|
|
||||||
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
storiesOf(
|
||||||
|
'Components/Conversation/ChatSessionRefreshedNotification',
|
||||||
|
module
|
||||||
|
).add('Default', () => {
|
||||||
|
return (
|
||||||
|
<ChatSessionRefreshedNotification
|
||||||
|
contactSupport={action('contactSupport')}
|
||||||
|
i18n={i18n}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
|
@ -0,0 +1,63 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import React, { useCallback, useState, ReactElement } from 'react';
|
||||||
|
|
||||||
|
import { LocalizerType } from '../../types/Util';
|
||||||
|
|
||||||
|
import { ModalHost } from '../ModalHost';
|
||||||
|
import { ChatSessionRefreshedDialog } from './ChatSessionRefreshedDialog';
|
||||||
|
|
||||||
|
type PropsHousekeepingType = {
|
||||||
|
i18n: LocalizerType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PropsActionsType = {
|
||||||
|
contactSupport: () => unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PropsType = PropsHousekeepingType & PropsActionsType;
|
||||||
|
|
||||||
|
export function ChatSessionRefreshedNotification(
|
||||||
|
props: PropsType
|
||||||
|
): ReactElement {
|
||||||
|
const { contactSupport, i18n } = props;
|
||||||
|
const [isDialogOpen, setIsDialogOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const openDialog = useCallback(() => {
|
||||||
|
setIsDialogOpen(true);
|
||||||
|
}, [setIsDialogOpen]);
|
||||||
|
const closeDialog = useCallback(() => {
|
||||||
|
setIsDialogOpen(false);
|
||||||
|
}, [setIsDialogOpen]);
|
||||||
|
|
||||||
|
const wrappedContactSupport = useCallback(() => {
|
||||||
|
setIsDialogOpen(false);
|
||||||
|
contactSupport();
|
||||||
|
}, [contactSupport, setIsDialogOpen]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="module-chat-session-refreshed-notification">
|
||||||
|
<div className="module-chat-session-refreshed-notification__first-line">
|
||||||
|
<span className="module-chat-session-refreshed-notification__icon" />
|
||||||
|
{i18n('ChatRefresh--notification')}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={openDialog}
|
||||||
|
className="module-chat-session-refreshed-notification__button"
|
||||||
|
>
|
||||||
|
{i18n('ChatRefresh--learnMore')}
|
||||||
|
</button>
|
||||||
|
{isDialogOpen ? (
|
||||||
|
<ModalHost onClose={closeDialog}>
|
||||||
|
<ChatSessionRefreshedDialog
|
||||||
|
onClose={closeDialog}
|
||||||
|
contactSupport={wrappedContactSupport}
|
||||||
|
i18n={i18n}
|
||||||
|
/>
|
||||||
|
</ModalHost>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -252,6 +252,8 @@ const actions = () => ({
|
||||||
messageSizeChanged: action('messageSizeChanged'),
|
messageSizeChanged: action('messageSizeChanged'),
|
||||||
startCallingLobby: action('startCallingLobby'),
|
startCallingLobby: action('startCallingLobby'),
|
||||||
returnToActiveCall: action('returnToActiveCall'),
|
returnToActiveCall: action('returnToActiveCall'),
|
||||||
|
|
||||||
|
contactSupport: action('contactSupport'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderItem = (id: string) => (
|
const renderItem = (id: string) => (
|
||||||
|
|
|
@ -41,6 +41,7 @@ const getDefaultProps = () => ({
|
||||||
selectMessage: action('selectMessage'),
|
selectMessage: action('selectMessage'),
|
||||||
reactToMessage: action('reactToMessage'),
|
reactToMessage: action('reactToMessage'),
|
||||||
clearSelectedMessage: action('clearSelectedMessage'),
|
clearSelectedMessage: action('clearSelectedMessage'),
|
||||||
|
contactSupport: action('contactSupport'),
|
||||||
replyToMessage: action('replyToMessage'),
|
replyToMessage: action('replyToMessage'),
|
||||||
retrySend: action('retrySend'),
|
retrySend: action('retrySend'),
|
||||||
deleteMessage: action('deleteMessage'),
|
deleteMessage: action('deleteMessage'),
|
||||||
|
|
|
@ -10,11 +10,14 @@ import {
|
||||||
PropsActions as MessageActionsType,
|
PropsActions as MessageActionsType,
|
||||||
PropsData as MessageProps,
|
PropsData as MessageProps,
|
||||||
} from './Message';
|
} from './Message';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CallingNotification,
|
CallingNotification,
|
||||||
PropsActionsType as CallingNotificationActionsType,
|
PropsActionsType as CallingNotificationActionsType,
|
||||||
} from './CallingNotification';
|
} from './CallingNotification';
|
||||||
|
import {
|
||||||
|
ChatSessionRefreshedNotification,
|
||||||
|
PropsActionsType as PropsChatSessionRefreshedActionsType,
|
||||||
|
} from './ChatSessionRefreshedNotification';
|
||||||
import { CallingNotificationType } from '../../util/callingNotification';
|
import { CallingNotificationType } from '../../util/callingNotification';
|
||||||
import { InlineNotificationWrapper } from './InlineNotificationWrapper';
|
import { InlineNotificationWrapper } from './InlineNotificationWrapper';
|
||||||
import {
|
import {
|
||||||
|
@ -58,6 +61,10 @@ type CallHistoryType = {
|
||||||
type: 'callHistory';
|
type: 'callHistory';
|
||||||
data: CallingNotificationType;
|
data: CallingNotificationType;
|
||||||
};
|
};
|
||||||
|
type ChatSessionRefreshedType = {
|
||||||
|
type: 'chatSessionRefreshed';
|
||||||
|
data: null;
|
||||||
|
};
|
||||||
type LinkNotificationType = {
|
type LinkNotificationType = {
|
||||||
type: 'linkNotification';
|
type: 'linkNotification';
|
||||||
data: null;
|
data: null;
|
||||||
|
@ -105,6 +112,7 @@ type ProfileChangeNotificationType = {
|
||||||
|
|
||||||
export type TimelineItemType =
|
export type TimelineItemType =
|
||||||
| CallHistoryType
|
| CallHistoryType
|
||||||
|
| ChatSessionRefreshedType
|
||||||
| GroupNotificationType
|
| GroupNotificationType
|
||||||
| GroupV1MigrationType
|
| GroupV1MigrationType
|
||||||
| GroupV2ChangeType
|
| GroupV2ChangeType
|
||||||
|
@ -131,6 +139,7 @@ type PropsLocalType = {
|
||||||
|
|
||||||
type PropsActionsType = MessageActionsType &
|
type PropsActionsType = MessageActionsType &
|
||||||
CallingNotificationActionsType &
|
CallingNotificationActionsType &
|
||||||
|
PropsChatSessionRefreshedActionsType &
|
||||||
UnsupportedMessageActionsType &
|
UnsupportedMessageActionsType &
|
||||||
SafetyNumberActionsType;
|
SafetyNumberActionsType;
|
||||||
|
|
||||||
|
@ -184,6 +193,14 @@ export class TimelineItem extends React.PureComponent<PropsType> {
|
||||||
{...item.data}
|
{...item.data}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
} else if (item.type === 'chatSessionRefreshed') {
|
||||||
|
notification = (
|
||||||
|
<ChatSessionRefreshedNotification
|
||||||
|
{...this.props}
|
||||||
|
{...item.data}
|
||||||
|
i18n={i18n}
|
||||||
|
/>
|
||||||
|
);
|
||||||
} else if (item.type === 'linkNotification') {
|
} else if (item.type === 'linkNotification') {
|
||||||
notification = (
|
notification = (
|
||||||
<div className="module-message-unsynced">
|
<div className="module-message-unsynced">
|
||||||
|
|
|
@ -2277,6 +2277,35 @@ export class ConversationModel extends window.Backbone.Model<
|
||||||
return this.setVerified();
|
return this.setVerified();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async addChatSessionRefreshed(receivedAt: number): Promise<void> {
|
||||||
|
window.log.info(
|
||||||
|
`addChatSessionRefreshed: adding for ${this.idForLogging()}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const message = ({
|
||||||
|
conversationId: this.id,
|
||||||
|
type: 'chat-session-refreshed',
|
||||||
|
sent_at: receivedAt,
|
||||||
|
received_at: receivedAt,
|
||||||
|
unread: 1,
|
||||||
|
// TODO: DESKTOP-722
|
||||||
|
// this type does not fully implement the interface it is expected to
|
||||||
|
} as unknown) as typeof window.Whisper.MessageAttributesType;
|
||||||
|
|
||||||
|
const id = await window.Signal.Data.saveMessage(message, {
|
||||||
|
Message: window.Whisper.Message,
|
||||||
|
});
|
||||||
|
const model = window.MessageController.register(
|
||||||
|
id,
|
||||||
|
new window.Whisper.Message({
|
||||||
|
...message,
|
||||||
|
id,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this.trigger('newmessage', model);
|
||||||
|
}
|
||||||
|
|
||||||
async addKeyChange(keyChangedId: string): Promise<void> {
|
async addKeyChange(keyChangedId: string): Promise<void> {
|
||||||
window.log.info(
|
window.log.info(
|
||||||
'adding key change advisory for',
|
'adding key change advisory for',
|
||||||
|
|
|
@ -201,6 +201,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
isNormalBubble(): boolean {
|
isNormalBubble(): boolean {
|
||||||
return (
|
return (
|
||||||
!this.isCallHistory() &&
|
!this.isCallHistory() &&
|
||||||
|
!this.isChatSessionRefreshed() &&
|
||||||
!this.isEndSession() &&
|
!this.isEndSession() &&
|
||||||
!this.isExpirationTimerUpdate() &&
|
!this.isExpirationTimerUpdate() &&
|
||||||
!this.isGroupUpdate() &&
|
!this.isGroupUpdate() &&
|
||||||
|
@ -282,6 +283,12 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
data: this.getPropsForProfileChange(),
|
data: this.getPropsForProfileChange(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (this.isChatSessionRefreshed()) {
|
||||||
|
return {
|
||||||
|
type: 'chatSessionRefreshed',
|
||||||
|
data: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'message',
|
type: 'message',
|
||||||
|
@ -461,6 +468,10 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
return this.get('type') === 'call-history';
|
return this.get('type') === 'call-history';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isChatSessionRefreshed(): boolean {
|
||||||
|
return this.get('type') === 'chat-session-refreshed';
|
||||||
|
}
|
||||||
|
|
||||||
isProfileChange(): boolean {
|
isProfileChange(): boolean {
|
||||||
return this.get('type') === 'profile-change';
|
return this.get('type') === 'profile-change';
|
||||||
}
|
}
|
||||||
|
@ -1178,6 +1189,13 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
}
|
}
|
||||||
|
|
||||||
getNotificationData(): { emoji?: string; text: string } {
|
getNotificationData(): { emoji?: string; text: string } {
|
||||||
|
if (this.isChatSessionRefreshed()) {
|
||||||
|
return {
|
||||||
|
emoji: '🔁',
|
||||||
|
text: window.i18n('ChatRefresh--notification'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isUnsupportedMessage()) {
|
if (this.isUnsupportedMessage()) {
|
||||||
return {
|
return {
|
||||||
text: window.i18n('message--getDescription--unsupported-message'),
|
text: window.i18n('message--getDescription--unsupported-message'),
|
||||||
|
@ -1695,6 +1713,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
|
|
||||||
// Rendered sync messages
|
// Rendered sync messages
|
||||||
const isCallHistory = this.isCallHistory();
|
const isCallHistory = this.isCallHistory();
|
||||||
|
const isChatSessionRefreshed = this.isChatSessionRefreshed();
|
||||||
const isGroupUpdate = this.isGroupUpdate();
|
const isGroupUpdate = this.isGroupUpdate();
|
||||||
const isGroupV2Change = this.isGroupV2Change();
|
const isGroupV2Change = this.isGroupV2Change();
|
||||||
const isEndSession = this.isEndSession();
|
const isEndSession = this.isEndSession();
|
||||||
|
@ -1723,6 +1742,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
isSticker ||
|
isSticker ||
|
||||||
// Rendered sync messages
|
// Rendered sync messages
|
||||||
isCallHistory ||
|
isCallHistory ||
|
||||||
|
isChatSessionRefreshed ||
|
||||||
isGroupUpdate ||
|
isGroupUpdate ||
|
||||||
isGroupV2Change ||
|
isGroupV2Change ||
|
||||||
isEndSession ||
|
isEndSession ||
|
||||||
|
|
|
@ -15,6 +15,7 @@ import { v4 as getGuid } from 'uuid';
|
||||||
|
|
||||||
import { SessionCipherClass, SignalProtocolAddressClass } from '../libsignal.d';
|
import { SessionCipherClass, SignalProtocolAddressClass } from '../libsignal.d';
|
||||||
import { BatcherType, createBatcher } from '../util/batcher';
|
import { BatcherType, createBatcher } from '../util/batcher';
|
||||||
|
import { assert } from '../util/assert';
|
||||||
|
|
||||||
import EventTarget from './EventTarget';
|
import EventTarget from './EventTarget';
|
||||||
import { WebAPIType } from './WebAPI';
|
import { WebAPIType } from './WebAPI';
|
||||||
|
@ -48,6 +49,8 @@ const GROUPV1_ID_LENGTH = 16;
|
||||||
const GROUPV2_ID_LENGTH = 32;
|
const GROUPV2_ID_LENGTH = 32;
|
||||||
const RETRY_TIMEOUT = 2 * 60 * 1000;
|
const RETRY_TIMEOUT = 2 * 60 * 1000;
|
||||||
|
|
||||||
|
type SessionResetsType = Record<string, number>;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
// We want to extend `Event`, so we need an interface.
|
// We want to extend `Event`, so we need an interface.
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
@ -208,6 +211,8 @@ class MessageReceiverInner extends EventTarget {
|
||||||
maxSize: 30,
|
maxSize: 30,
|
||||||
processBatch: this.cacheRemoveBatch.bind(this),
|
processBatch: this.cacheRemoveBatch.bind(this),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.cleanupSessionResets();
|
||||||
}
|
}
|
||||||
|
|
||||||
static stringToArrayBuffer = (string: string): ArrayBuffer =>
|
static stringToArrayBuffer = (string: string): ArrayBuffer =>
|
||||||
|
@ -237,6 +242,7 @@ class MessageReceiverInner extends EventTarget {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isEmptied = false;
|
this.isEmptied = false;
|
||||||
|
|
||||||
this.hasConnected = true;
|
this.hasConnected = true;
|
||||||
|
|
||||||
if (this.socket && this.socket.readyState !== WebSocket.CLOSED) {
|
if (this.socket && this.socket.readyState !== WebSocket.CLOSED) {
|
||||||
|
@ -1089,34 +1095,120 @@ class MessageReceiverInner extends EventTarget {
|
||||||
return plaintext;
|
return plaintext;
|
||||||
})
|
})
|
||||||
.catch(async error => {
|
.catch(async error => {
|
||||||
let errorToThrow = error;
|
this.removeFromCache(envelope);
|
||||||
|
|
||||||
if (error && error.message === 'Unknown identity key') {
|
const uuid = envelope.sourceUuid;
|
||||||
// create an error that the UI will pick up and ask the
|
const deviceId = envelope.sourceDevice;
|
||||||
// user if they want to re-negotiate
|
|
||||||
const buffer = window.dcodeIO.ByteBuffer.wrap(ciphertext);
|
// We don't do a light session reset if it's just a duplicated message
|
||||||
errorToThrow = new IncomingIdentityKeyError(
|
if (error && error.name === 'MessageCounterError') {
|
||||||
address.toString(),
|
throw error;
|
||||||
buffer.toArrayBuffer(),
|
}
|
||||||
error.identityKey
|
|
||||||
|
if (uuid && deviceId) {
|
||||||
|
await this.lightSessionReset(uuid, deviceId);
|
||||||
|
} else {
|
||||||
|
const envelopeId = this.getEnvelopeId(envelope);
|
||||||
|
window.log.error(
|
||||||
|
`MessageReceiver.decrypt: Envelope ${envelopeId} missing uuid or deviceId`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (envelope.timestamp && envelope.timestamp.toNumber) {
|
throw error;
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
envelope.timestamp = envelope.timestamp.toNumber();
|
|
||||||
}
|
|
||||||
|
|
||||||
const ev = new Event('error');
|
|
||||||
ev.error = errorToThrow;
|
|
||||||
ev.proto = envelope;
|
|
||||||
ev.confirm = this.removeFromCache.bind(this, envelope);
|
|
||||||
|
|
||||||
const returnError = async () => Promise.reject(errorToThrow);
|
|
||||||
return this.dispatchAndWait(ev).then(returnError, returnError);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isOverHourIntoPast(timestamp: number): boolean {
|
||||||
|
const HOUR = 1000 * 60 * 60;
|
||||||
|
const now = Date.now();
|
||||||
|
const oneHourIntoPast = now - HOUR;
|
||||||
|
|
||||||
|
return isNumber(timestamp) && timestamp <= oneHourIntoPast;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't lose anything if we delete keys over an hour into the past, because we only
|
||||||
|
// change our behavior if the timestamps stored are less than an hour ago.
|
||||||
|
cleanupSessionResets(): void {
|
||||||
|
const sessionResets = window.storage.get(
|
||||||
|
'sessionResets',
|
||||||
|
{}
|
||||||
|
) as SessionResetsType;
|
||||||
|
|
||||||
|
const keys = Object.keys(sessionResets);
|
||||||
|
keys.forEach(key => {
|
||||||
|
const timestamp = sessionResets[key];
|
||||||
|
if (!timestamp || this.isOverHourIntoPast(timestamp)) {
|
||||||
|
delete sessionResets[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.storage.put('sessionResets', sessionResets);
|
||||||
|
}
|
||||||
|
|
||||||
|
async lightSessionReset(uuid: string, deviceId: number) {
|
||||||
|
const id = `${uuid}.${deviceId}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sessionResets = window.storage.get(
|
||||||
|
'sessionResets',
|
||||||
|
{}
|
||||||
|
) as SessionResetsType;
|
||||||
|
const lastReset = sessionResets[id];
|
||||||
|
|
||||||
|
if (lastReset && !this.isOverHourIntoPast(lastReset)) {
|
||||||
|
window.log.warn(
|
||||||
|
`lightSessionReset: Skipping session reset for ${id}, last reset at ${lastReset}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sessionResets[id] = Date.now();
|
||||||
|
window.storage.put('sessionResets', sessionResets);
|
||||||
|
|
||||||
|
// First, fetch this conversation
|
||||||
|
const conversationId = window.ConversationController.ensureContactIds({
|
||||||
|
uuid,
|
||||||
|
});
|
||||||
|
assert(conversationId, 'lightSessionReset: missing conversationId');
|
||||||
|
|
||||||
|
const conversation = window.ConversationController.get(conversationId);
|
||||||
|
assert(conversation, 'lightSessionReset: missing conversation');
|
||||||
|
|
||||||
|
window.log.warn(`lightSessionReset: Resetting session for ${id}`);
|
||||||
|
|
||||||
|
// Archive open session with this device
|
||||||
|
const address = new window.libsignal.SignalProtocolAddress(
|
||||||
|
uuid,
|
||||||
|
deviceId
|
||||||
|
);
|
||||||
|
const sessionCipher = new window.libsignal.SessionCipher(
|
||||||
|
window.textsecure.storage.protocol,
|
||||||
|
address
|
||||||
|
);
|
||||||
|
|
||||||
|
await sessionCipher.closeOpenSessionForDevice();
|
||||||
|
|
||||||
|
// Send a null message with newly-created session
|
||||||
|
const sendOptions = conversation.getSendOptions();
|
||||||
|
await window.textsecure.messaging.sendNullMessage({ uuid }, sendOptions);
|
||||||
|
|
||||||
|
// Emit event for app to put item into conversation timeline
|
||||||
|
const event = new Event('light-session-reset');
|
||||||
|
event.senderUuid = uuid;
|
||||||
|
await this.dispatchAndWait(event);
|
||||||
|
} catch (error) {
|
||||||
|
// If we failed to do the session reset, then we'll allow another attempt
|
||||||
|
const sessionResets = window.storage.get(
|
||||||
|
'sessionResets',
|
||||||
|
{}
|
||||||
|
) as SessionResetsType;
|
||||||
|
delete sessionResets[id];
|
||||||
|
window.storage.put('sessionResets', sessionResets);
|
||||||
|
|
||||||
|
const errorString = error && error.stack ? error.stack : error;
|
||||||
|
window.log.error('lightSessionReset: Enountered error', errorString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async decryptPreKeyWhisperMessage(
|
async decryptPreKeyWhisperMessage(
|
||||||
ciphertext: ArrayBuffer,
|
ciphertext: ArrayBuffer,
|
||||||
sessionCipher: SessionCipherClass,
|
sessionCipher: SessionCipherClass,
|
||||||
|
@ -2266,6 +2358,10 @@ export default class MessageReceiver {
|
||||||
this.stopProcessing = inner.stopProcessing.bind(inner);
|
this.stopProcessing = inner.stopProcessing.bind(inner);
|
||||||
this.unregisterBatchers = inner.unregisterBatchers.bind(inner);
|
this.unregisterBatchers = inner.unregisterBatchers.bind(inner);
|
||||||
|
|
||||||
|
// For tests
|
||||||
|
this.isOverHourIntoPast = inner.isOverHourIntoPast.bind(inner);
|
||||||
|
this.cleanupSessionResets = inner.cleanupSessionResets.bind(inner);
|
||||||
|
|
||||||
inner.connect();
|
inner.connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2287,6 +2383,10 @@ export default class MessageReceiver {
|
||||||
|
|
||||||
unregisterBatchers: () => void;
|
unregisterBatchers: () => void;
|
||||||
|
|
||||||
|
isOverHourIntoPast: (timestamp: number) => boolean;
|
||||||
|
|
||||||
|
cleanupSessionResets: () => void;
|
||||||
|
|
||||||
static stringToArrayBuffer = MessageReceiverInner.stringToArrayBuffer;
|
static stringToArrayBuffer = MessageReceiverInner.stringToArrayBuffer;
|
||||||
|
|
||||||
static arrayBufferToString = MessageReceiverInner.arrayBufferToString;
|
static arrayBufferToString = MessageReceiverInner.arrayBufferToString;
|
||||||
|
|
|
@ -742,12 +742,7 @@ export default class MessageSender {
|
||||||
createSyncMessage(): SyncMessageClass {
|
createSyncMessage(): SyncMessageClass {
|
||||||
const syncMessage = new window.textsecure.protobuf.SyncMessage();
|
const syncMessage = new window.textsecure.protobuf.SyncMessage();
|
||||||
|
|
||||||
// Generate a random int from 1 and 512
|
syncMessage.padding = this.getRandomPadding();
|
||||||
const buffer = window.libsignal.crypto.getRandomBytes(1);
|
|
||||||
const paddingLength = (new Uint8Array(buffer)[0] & 0x1ff) + 1;
|
|
||||||
|
|
||||||
// Generate a random padding buffer of the chosen size
|
|
||||||
syncMessage.padding = window.libsignal.crypto.getRandomBytes(paddingLength);
|
|
||||||
|
|
||||||
return syncMessage;
|
return syncMessage;
|
||||||
}
|
}
|
||||||
|
@ -1374,6 +1369,47 @@ export default class MessageSender {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getRandomPadding(): ArrayBuffer {
|
||||||
|
// Generate a random int from 1 and 512
|
||||||
|
const buffer = window.libsignal.crypto.getRandomBytes(2);
|
||||||
|
const paddingLength = (new Uint16Array(buffer)[0] & 0x1ff) + 1;
|
||||||
|
|
||||||
|
// Generate a random padding buffer of the chosen size
|
||||||
|
return window.libsignal.crypto.getRandomBytes(paddingLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendNullMessage(
|
||||||
|
{
|
||||||
|
uuid,
|
||||||
|
e164,
|
||||||
|
padding,
|
||||||
|
}: { uuid?: string; e164?: string; padding?: ArrayBuffer },
|
||||||
|
options?: SendOptionsType
|
||||||
|
): Promise<CallbackResultType> {
|
||||||
|
const nullMessage = new window.textsecure.protobuf.NullMessage();
|
||||||
|
|
||||||
|
const identifier = uuid || e164;
|
||||||
|
if (!identifier) {
|
||||||
|
throw new Error('sendNullMessage: Got neither uuid nor e164!');
|
||||||
|
}
|
||||||
|
|
||||||
|
nullMessage.padding = padding || this.getRandomPadding();
|
||||||
|
|
||||||
|
const contentMessage = new window.textsecure.protobuf.Content();
|
||||||
|
contentMessage.nullMessage = nullMessage;
|
||||||
|
|
||||||
|
// We want the NullMessage to look like a normal outgoing message; not silent
|
||||||
|
const silent = false;
|
||||||
|
const timestamp = Date.now();
|
||||||
|
return this.sendIndividualProto(
|
||||||
|
identifier,
|
||||||
|
contentMessage,
|
||||||
|
timestamp,
|
||||||
|
silent,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async syncVerification(
|
async syncVerification(
|
||||||
destinationE164: string,
|
destinationE164: string,
|
||||||
destinationUuid: string,
|
destinationUuid: string,
|
||||||
|
@ -1390,26 +1426,12 @@ export default class MessageSender {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get padding which we can share between null message and verified sync
|
||||||
|
const padding = this.getRandomPadding();
|
||||||
|
|
||||||
// First send a null message to mask the sync message.
|
// First send a null message to mask the sync message.
|
||||||
const nullMessage = new window.textsecure.protobuf.NullMessage();
|
const promise = this.sendNullMessage(
|
||||||
|
{ uuid: destinationUuid, e164: destinationE164, padding },
|
||||||
// Generate a random int from 1 and 512
|
|
||||||
const buffer = window.libsignal.crypto.getRandomBytes(1);
|
|
||||||
const paddingLength = (new Uint8Array(buffer)[0] & 0x1ff) + 1;
|
|
||||||
|
|
||||||
// Generate a random padding buffer of the chosen size
|
|
||||||
nullMessage.padding = window.libsignal.crypto.getRandomBytes(paddingLength);
|
|
||||||
|
|
||||||
const contentMessage = new window.textsecure.protobuf.Content();
|
|
||||||
contentMessage.nullMessage = nullMessage;
|
|
||||||
|
|
||||||
// We want the NullMessage to look like a normal outgoing message; not silent
|
|
||||||
const silent = false;
|
|
||||||
const promise = this.sendIndividualProto(
|
|
||||||
destinationUuid || destinationE164,
|
|
||||||
contentMessage,
|
|
||||||
now,
|
|
||||||
silent,
|
|
||||||
options
|
options
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1423,7 +1445,7 @@ export default class MessageSender {
|
||||||
verified.destinationUuid = destinationUuid;
|
verified.destinationUuid = destinationUuid;
|
||||||
}
|
}
|
||||||
verified.identityKey = identityKey;
|
verified.identityKey = identityKey;
|
||||||
verified.nullMessage = nullMessage.padding;
|
verified.nullMessage = padding;
|
||||||
|
|
||||||
const syncMessage = this.createSyncMessage();
|
const syncMessage = this.createSyncMessage();
|
||||||
syncMessage.verified = verified;
|
syncMessage.verified = verified;
|
||||||
|
|
|
@ -24,6 +24,7 @@ import { parseRemoteClientExpiration } from './parseRemoteClientExpiration';
|
||||||
import { sleep } from './sleep';
|
import { sleep } from './sleep';
|
||||||
import { longRunningTaskWrapper } from './longRunningTaskWrapper';
|
import { longRunningTaskWrapper } from './longRunningTaskWrapper';
|
||||||
import { toWebSafeBase64, fromWebSafeBase64 } from './webSafeBase64';
|
import { toWebSafeBase64, fromWebSafeBase64 } from './webSafeBase64';
|
||||||
|
import { mapToSupportLocale } from './mapToSupportLocale';
|
||||||
import * as zkgroup from './zkgroup';
|
import * as zkgroup from './zkgroup';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@ -44,6 +45,7 @@ export {
|
||||||
isFileDangerous,
|
isFileDangerous,
|
||||||
longRunningTaskWrapper,
|
longRunningTaskWrapper,
|
||||||
makeLookup,
|
makeLookup,
|
||||||
|
mapToSupportLocale,
|
||||||
missingCaseError,
|
missingCaseError,
|
||||||
parseRemoteClientExpiration,
|
parseRemoteClientExpiration,
|
||||||
Registration,
|
Registration,
|
||||||
|
|
115
ts/util/mapToSupportLocale.ts
Normal file
115
ts/util/mapToSupportLocale.ts
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
export type SupportLocaleType =
|
||||||
|
| 'ar'
|
||||||
|
| 'de'
|
||||||
|
| 'en-us'
|
||||||
|
| 'es'
|
||||||
|
| 'fr'
|
||||||
|
| 'it'
|
||||||
|
| 'ja'
|
||||||
|
| 'pl'
|
||||||
|
| 'pt-br'
|
||||||
|
| 'ru'
|
||||||
|
| 'sq'
|
||||||
|
| 'zh-tw';
|
||||||
|
|
||||||
|
export type ElectronLocaleType =
|
||||||
|
| 'af'
|
||||||
|
| 'ar'
|
||||||
|
| 'bg'
|
||||||
|
| 'bn'
|
||||||
|
| 'ca'
|
||||||
|
| 'cs'
|
||||||
|
| 'cy'
|
||||||
|
| 'da'
|
||||||
|
| 'de'
|
||||||
|
| 'el'
|
||||||
|
| 'en'
|
||||||
|
| 'eo'
|
||||||
|
| 'es'
|
||||||
|
| 'es_419'
|
||||||
|
| 'et'
|
||||||
|
| 'eu'
|
||||||
|
| 'fa'
|
||||||
|
| 'fi'
|
||||||
|
| 'fr'
|
||||||
|
| 'he'
|
||||||
|
| 'hi'
|
||||||
|
| 'hr'
|
||||||
|
| 'hu'
|
||||||
|
| 'id'
|
||||||
|
| 'it'
|
||||||
|
| 'ja'
|
||||||
|
| 'km'
|
||||||
|
| 'kn'
|
||||||
|
| 'ko'
|
||||||
|
| 'lt'
|
||||||
|
| 'mk'
|
||||||
|
| 'mr'
|
||||||
|
| 'ms'
|
||||||
|
| 'nb'
|
||||||
|
| 'nl'
|
||||||
|
| 'nn'
|
||||||
|
| 'no'
|
||||||
|
| 'pl'
|
||||||
|
| 'pt_BR'
|
||||||
|
| 'pt_PT'
|
||||||
|
| 'ro'
|
||||||
|
| 'ru'
|
||||||
|
| 'sk'
|
||||||
|
| 'sl'
|
||||||
|
| 'sq'
|
||||||
|
| 'sr'
|
||||||
|
| 'sv'
|
||||||
|
| 'sw'
|
||||||
|
| 'ta'
|
||||||
|
| 'te'
|
||||||
|
| 'th'
|
||||||
|
| 'tr'
|
||||||
|
| 'uk'
|
||||||
|
| 'ur'
|
||||||
|
| 'vi'
|
||||||
|
| 'zh_CN'
|
||||||
|
| 'zh_TW';
|
||||||
|
|
||||||
|
export function mapToSupportLocale(
|
||||||
|
ourLocale: ElectronLocaleType
|
||||||
|
): SupportLocaleType {
|
||||||
|
if (ourLocale === 'ar') {
|
||||||
|
return ourLocale;
|
||||||
|
}
|
||||||
|
if (ourLocale === 'de') {
|
||||||
|
return ourLocale;
|
||||||
|
}
|
||||||
|
if (ourLocale === 'es') {
|
||||||
|
return ourLocale;
|
||||||
|
}
|
||||||
|
if (ourLocale === 'fr') {
|
||||||
|
return ourLocale;
|
||||||
|
}
|
||||||
|
if (ourLocale === 'it') {
|
||||||
|
return ourLocale;
|
||||||
|
}
|
||||||
|
if (ourLocale === 'ja') {
|
||||||
|
return ourLocale;
|
||||||
|
}
|
||||||
|
if (ourLocale === 'pl') {
|
||||||
|
return ourLocale;
|
||||||
|
}
|
||||||
|
if (ourLocale === 'pt_BR') {
|
||||||
|
return 'pt-br';
|
||||||
|
}
|
||||||
|
if (ourLocale === 'ru') {
|
||||||
|
return ourLocale;
|
||||||
|
}
|
||||||
|
if (ourLocale === 'sq') {
|
||||||
|
return ourLocale;
|
||||||
|
}
|
||||||
|
if (ourLocale === 'zh_TW') {
|
||||||
|
return 'zh-tw';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'en-us';
|
||||||
|
}
|
|
@ -774,6 +774,15 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
const showExpiredOutgoingTapToViewToast = () => {
|
const showExpiredOutgoingTapToViewToast = () => {
|
||||||
this.showToast(Whisper.TapToViewExpiredOutgoingToast);
|
this.showToast(Whisper.TapToViewExpiredOutgoingToast);
|
||||||
};
|
};
|
||||||
|
const contactSupport = () => {
|
||||||
|
const baseUrl =
|
||||||
|
'https://support.signal.org/hc/LOCALE/requests/new?desktop&chat_refreshed';
|
||||||
|
const locale = window.getLocale();
|
||||||
|
const supportLocale = window.Signal.Util.mapToSupportLocale(locale);
|
||||||
|
const url = baseUrl.replace('LOCALE', supportLocale);
|
||||||
|
|
||||||
|
this.navigateTo(url);
|
||||||
|
};
|
||||||
|
|
||||||
const scrollToQuotedMessage = async (options: any) => {
|
const scrollToQuotedMessage = async (options: any) => {
|
||||||
const { authorId, sentAt } = options;
|
const { authorId, sentAt } = options;
|
||||||
|
@ -928,6 +937,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
JSX: window.Signal.State.Roots.createTimeline(window.reduxStore, {
|
JSX: window.Signal.State.Roots.createTimeline(window.reduxStore, {
|
||||||
id,
|
id,
|
||||||
|
|
||||||
|
contactSupport,
|
||||||
deleteMessage,
|
deleteMessage,
|
||||||
deleteMessageForEveryone,
|
deleteMessageForEveryone,
|
||||||
displayTapToViewMessage,
|
displayTapToViewMessage,
|
||||||
|
|
2
ts/window.d.ts
vendored
2
ts/window.d.ts
vendored
|
@ -92,6 +92,7 @@ import { ProgressModal } from './components/ProgressModal';
|
||||||
import { Quote } from './components/conversation/Quote';
|
import { Quote } from './components/conversation/Quote';
|
||||||
import { StagedLinkPreview } from './components/conversation/StagedLinkPreview';
|
import { StagedLinkPreview } from './components/conversation/StagedLinkPreview';
|
||||||
import { MIMEType } from './types/MIME';
|
import { MIMEType } from './types/MIME';
|
||||||
|
import { ElectronLocaleType } from './util/mapToSupportLocale';
|
||||||
|
|
||||||
export { Long } from 'long';
|
export { Long } from 'long';
|
||||||
|
|
||||||
|
@ -148,6 +149,7 @@ declare global {
|
||||||
getInboxCollection: () => ConversationModelCollectionType;
|
getInboxCollection: () => ConversationModelCollectionType;
|
||||||
getIncomingCallNotification: () => Promise<boolean>;
|
getIncomingCallNotification: () => Promise<boolean>;
|
||||||
getInteractionMode: () => 'mouse' | 'keyboard';
|
getInteractionMode: () => 'mouse' | 'keyboard';
|
||||||
|
getLocale: () => ElectronLocaleType;
|
||||||
getMediaCameraPermissions: () => Promise<boolean>;
|
getMediaCameraPermissions: () => Promise<boolean>;
|
||||||
getMediaPermissions: () => Promise<boolean>;
|
getMediaPermissions: () => Promise<boolean>;
|
||||||
getNodeVersion: () => string;
|
getNodeVersion: () => string;
|
||||||
|
|
Loading…
Reference in a new issue