Request Configuration Sync After Relink (#2218)

Users lose their read receipt setting after relinking their app.
See: https://github.com/signalapp/Signal-Android/issues/7535

- [x] Request configuration sync after relink, not just on first run.
- [x] Minor: Add `is` dependency as discussed.
- [x] Unit tests!! Started a pattern where functions return their status that
      can be logged instead of logging themselves. Makes for great tests and
      might also help us debug user logs as the status will always be logged
      (with `reason` field) whether it was done or not. Otherwise, we have to
      look for the presence or absence of a log line. Of course, we have to be
      careful about log noise, but I thought this was a nice fit here.

**Manual Tests**

- Started app and set read receipts on iOS.
- Sent message from another device with read receipts enabled.
- Confirmed messages were marked read on both sides.
- Unlinked primary device.
- Relinked primary device.
- Sent messages between primary and other device.
- Confirmed read receipts are still shown.
- Confirmed logs show configuration sync.
pull/1/head
Daniel Gasienica 7 years ago committed by GitHub
commit 18dddfe436
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -294,7 +294,7 @@
}
var connectCount = 0;
function connect(firstRun) {
async function connect(firstRun) {
console.log('connect');
// Bootstrap our online/offline detection, only the first time we connect
@ -359,22 +359,18 @@
}
}
// If we've just upgraded to read receipt support on desktop, kick off a
// one-time configuration sync request to get the read-receipt setting
// from the master device.
var readReceiptConfigurationSync = 'read-receipt-configuration-sync';
if (!storage.get(readReceiptConfigurationSync)) {
if (!firstRun && textsecure.storage.user.getDeviceId() != '1') {
textsecure.messaging.sendRequestConfigurationSyncMessage().then(function() {
storage.put(readReceiptConfigurationSync, true);
}).catch(function(e) {
console.log(e);
});
}
}
/* eslint-enable */
const deviceId = textsecure.storage.user.getDeviceId();
const { sendRequestConfigurationSyncMessage } = textsecure.messaging;
const status = await Signal.Startup.syncReadReceiptConfiguration({
deviceId,
sendRequestConfigurationSyncMessage,
storage,
});
console.log('Sync read receipt configuration status:', status);
/* eslint-disable */
if (firstRun === true && textsecure.storage.user.getDeviceId() != '1') {
if (firstRun === true && deviceId != '1') {
if (!storage.get('theme-setting') && textsecure.storage.get('userAgent') === 'OWI') {
storage.put('theme-setting', 'ios');
}

@ -6,6 +6,8 @@ const LAST_PROCESSED_INDEX_KEY = 'attachmentMigration_lastProcessedIndex';
const IS_MIGRATION_COMPLETE_KEY = 'attachmentMigration_isComplete';
// Public API
exports.READ_RECEIPT_CONFIGURATION_SYNC = 'read-receipt-configuration-sync';
exports.getAttachmentMigrationLastProcessedIndex = connection =>
exports._getItem(connection, LAST_PROCESSED_INDEX_KEY);

@ -0,0 +1,55 @@
const is = require('@sindresorhus/is');
const Errors = require('./types/errors');
const Settings = require('./settings');
exports.syncReadReceiptConfiguration = async ({
deviceId,
sendRequestConfigurationSyncMessage,
storage,
}) => {
if (!is.string(deviceId)) {
throw new TypeError('"deviceId" is required');
}
if (!is.function(sendRequestConfigurationSyncMessage)) {
throw new TypeError('"sendRequestConfigurationSyncMessage" is required');
}
if (!is.object(storage)) {
throw new TypeError('"storage" is required');
}
const isPrimaryDevice = deviceId === '1';
if (isPrimaryDevice) {
return {
status: 'skipped',
reason: 'isPrimaryDevice',
};
}
const settingName = Settings.READ_RECEIPT_CONFIGURATION_SYNC;
const hasPreviouslySynced = Boolean(storage.get(settingName));
if (hasPreviouslySynced) {
return {
status: 'skipped',
reason: 'hasPreviouslySynced',
};
}
try {
await sendRequestConfigurationSyncMessage();
storage.put(settingName, true);
} catch (error) {
return {
status: 'error',
reason: 'failedToSendSyncMessage',
error: Errors.toLogFormat(error),
};
}
return {
status: 'complete',
};
};

@ -44,6 +44,7 @@
"open-coverage": "open coverage/lcov-report/index.html"
},
"dependencies": {
"@sindresorhus/is": "^0.8.0",
"archiver": "^2.1.1",
"blob-util": "^1.3.0",
"blueimp-canvas-to-blob": "^3.14.0",

@ -146,6 +146,7 @@ window.Signal.Migrations.Migrations1DatabaseWithoutAttachmentData =
window.Signal.Migrations.upgradeMessageSchema = upgradeMessageSchema;
window.Signal.OS = require('./js/modules/os');
window.Signal.Settings = require('./js/modules/settings');
window.Signal.Startup = require('./js/modules/startup');
window.Signal.Types = {};
window.Signal.Types.Attachment = Attachment;

@ -0,0 +1,123 @@
const sinon = require('sinon');
const { assert } = require('chai');
const Startup = require('../../js/modules/startup');
describe('Startup', () => {
const sandbox = sinon.createSandbox();
describe('syncReadReceiptConfiguration', () => {
afterEach(() => {
sandbox.restore();
});
it('should complete if user hasnt previously synced', async () => {
const deviceId = '2';
const sendRequestConfigurationSyncMessage = sandbox.spy();
const storagePutSpy = sandbox.spy();
const storage = {
get(name) {
if (name !== 'read-receipt-configuration-sync') {
return true;
}
return false;
},
put: storagePutSpy,
};
const expected = {
status: 'complete',
};
const actual = await Startup.syncReadReceiptConfiguration({
deviceId,
sendRequestConfigurationSyncMessage,
storage,
});
assert.deepEqual(actual, expected);
assert.equal(sendRequestConfigurationSyncMessage.callCount, 1);
assert.equal(storagePutSpy.callCount, 1);
assert(storagePutSpy.calledWith('read-receipt-configuration-sync', true));
});
it('should be skipped if this is the primary device', async () => {
const deviceId = '1';
const sendRequestConfigurationSyncMessage = () => {};
const storage = {};
const expected = {
status: 'skipped',
reason: 'isPrimaryDevice',
};
const actual = await Startup.syncReadReceiptConfiguration({
deviceId,
sendRequestConfigurationSyncMessage,
storage,
});
assert.deepEqual(actual, expected);
});
it('should be skipped if user has previously synced', async () => {
const deviceId = '2';
const sendRequestConfigurationSyncMessage = () => {};
const storage = {
get(name) {
if (name !== 'read-receipt-configuration-sync') {
return false;
}
return true;
},
};
const expected = {
status: 'skipped',
reason: 'hasPreviouslySynced',
};
const actual = await Startup.syncReadReceiptConfiguration({
deviceId,
sendRequestConfigurationSyncMessage,
storage,
});
assert.deepEqual(actual, expected);
});
it('should return error if sending of sync request fails', async () => {
const deviceId = '2';
const sendRequestConfigurationSyncMessage = sandbox.stub();
sendRequestConfigurationSyncMessage.rejects(new Error('boom'));
const storagePutSpy = sandbox.spy();
const storage = {
get(name) {
if (name !== 'read-receipt-configuration-sync') {
return true;
}
return false;
},
put: storagePutSpy,
};
const actual = await Startup.syncReadReceiptConfiguration({
deviceId,
sendRequestConfigurationSyncMessage,
storage,
});
assert.equal(actual.status, 'error');
assert.include(actual.error, 'boom');
assert.equal(sendRequestConfigurationSyncMessage.callCount, 1);
assert.equal(storagePutSpy.callCount, 0);
});
});
});

@ -26,6 +26,10 @@
version "0.7.0"
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd"
"@sindresorhus/is@^0.8.0":
version "0.8.0"
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.8.0.tgz#073aee40b0aab2d4ace33c0a2a2672a37da6fa12"
"@sinonjs/formatio@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-2.0.0.tgz#84db7e9eb5531df18a8c5e0bfb6e449e55e654b2"

Loading…
Cancel
Save