Changes to View Once

This commit is contained in:
Scott Nonnenberg 2019-08-05 13:53:15 -07:00
parent 9d88abdb90
commit d42eb2126e
14 changed files with 152 additions and 167 deletions

View File

@ -94,7 +94,6 @@ module.exports = {
getOutgoingWithoutExpiresAt,
getNextExpiringMessage,
getMessagesByConversation,
getNextTapToViewMessageToExpire,
getNextTapToViewMessageToAgeOut,
getTapToViewMessagesNeedingErase,
@ -952,6 +951,68 @@ async function updateToSchemaVersion16(currentVersion, instance) {
}
}
async function updateToSchemaVersion17(currentVersion, instance) {
if (currentVersion >= 17) {
return;
}
console.log('updateToSchemaVersion17: starting...');
await instance.run('BEGIN TRANSACTION;');
try {
await instance.run(
`ALTER TABLE messages
ADD COLUMN isViewOnce INTEGER;`
);
await instance.run('DROP INDEX messages_message_timer;');
await instance.run(`CREATE INDEX messages_view_once ON messages (
isErased
) WHERE isViewOnce = 1;`);
// Updating full-text triggers to avoid anything with isViewOnce = 1
await instance.run('DROP TRIGGER messages_on_insert;');
await instance.run('DROP TRIGGER messages_on_update;');
await instance.run(`
CREATE TRIGGER messages_on_insert AFTER INSERT ON messages
WHEN new.isViewOnce != 1
BEGIN
INSERT INTO messages_fts (
id,
body
) VALUES (
new.id,
new.body
);
END;
`);
await instance.run(`
CREATE TRIGGER messages_on_update AFTER UPDATE ON messages
WHEN new.isViewOnce != 1
BEGIN
DELETE FROM messages_fts WHERE id = old.id;
INSERT INTO messages_fts(
id,
body
) VALUES (
new.id,
new.body
);
END;
`);
await instance.run('PRAGMA schema_version = 17;');
await instance.run('COMMIT TRANSACTION;');
console.log('updateToSchemaVersion17: success!');
} catch (error) {
await instance.run('ROLLBACK;');
throw error;
}
}
const SCHEMA_VERSIONS = [
updateToSchemaVersion1,
updateToSchemaVersion2,
@ -969,6 +1030,7 @@ const SCHEMA_VERSIONS = [
updateToSchemaVersion14,
updateToSchemaVersion15,
updateToSchemaVersion16,
updateToSchemaVersion17,
];
async function updateSchema(instance) {
@ -1566,9 +1628,7 @@ async function saveMessage(data, { forceSave } = {}) {
hasVisualMediaAttachments,
id,
isErased,
messageTimer,
messageTimerStart,
messageTimerExpiresAt,
isViewOnce,
// eslint-disable-next-line camelcase
received_at,
schemaVersion,
@ -1595,9 +1655,7 @@ async function saveMessage(data, { forceSave } = {}) {
$hasFileAttachments: hasFileAttachments,
$hasVisualMediaAttachments: hasVisualMediaAttachments,
$isErased: isErased,
$messageTimer: messageTimer,
$messageTimerStart: messageTimerStart,
$messageTimerExpiresAt: messageTimerExpiresAt,
$isViewOnce: isViewOnce,
$received_at: received_at,
$schemaVersion: schemaVersion,
$sent_at: sent_at,
@ -1622,9 +1680,7 @@ async function saveMessage(data, { forceSave } = {}) {
hasFileAttachments = $hasFileAttachments,
hasVisualMediaAttachments = $hasVisualMediaAttachments,
isErased = $isErased,
messageTimer = $messageTimer,
messageTimerStart = $messageTimerStart,
messageTimerExpiresAt = $messageTimerExpiresAt,
isViewOnce = $isViewOnce,
received_at = $received_at,
schemaVersion = $schemaVersion,
sent_at = $sent_at,
@ -1658,9 +1714,7 @@ async function saveMessage(data, { forceSave } = {}) {
hasFileAttachments,
hasVisualMediaAttachments,
isErased,
messageTimer,
messageTimerStart,
messageTimerExpiresAt,
isViewOnce,
received_at,
schemaVersion,
sent_at,
@ -1681,9 +1735,7 @@ async function saveMessage(data, { forceSave } = {}) {
$hasFileAttachments,
$hasVisualMediaAttachments,
$isErased,
$messageTimer,
$messageTimerStart,
$messageTimerExpiresAt,
$isViewOnce,
$received_at,
$schemaVersion,
$sent_at,
@ -1862,31 +1914,11 @@ async function getNextExpiringMessage() {
return map(rows, row => jsonToObject(row.json));
}
async function getNextTapToViewMessageToExpire() {
// Note: we avoid 'IS NOT NULL' here because it does seem to bypass our index
const rows = await db.all(`
SELECT json FROM messages
WHERE
messageTimer > 0
AND messageTimerExpiresAt > 0
AND (isErased IS NULL OR isErased != 1)
ORDER BY messageTimerExpiresAt ASC
LIMIT 1;
`);
if (!rows || rows.length < 1) {
return null;
}
return jsonToObject(rows[0].json);
}
async function getNextTapToViewMessageToAgeOut() {
// Note: we avoid 'IS NOT NULL' here because it does seem to bypass our index
const rows = await db.all(`
SELECT json FROM messages
WHERE
messageTimer > 0
isViewOnce = 1
AND (isErased IS NULL OR isErased != 1)
ORDER BY received_at ASC
LIMIT 1;
@ -1903,18 +1935,12 @@ async function getTapToViewMessagesNeedingErase() {
const THIRTY_DAYS_AGO = Date.now() - 30 * 24 * 60 * 60 * 1000;
const NOW = Date.now();
// Note: we avoid 'IS NOT NULL' here because it does seem to bypass our index
const rows = await db.all(
`SELECT json FROM messages
WHERE
messageTimer > 0
isViewOnce = 1
AND (isErased IS NULL OR isErased != 1)
AND (
(messageTimerExpiresAt > 0
AND messageTimerExpiresAt <= $NOW)
OR
received_at <= $THIRTY_DAYS_AGO
)
AND received_at <= $THIRTY_DAYS_AGO
ORDER BY received_at ASC;`,
{
$NOW: NOW,

View File

@ -1698,13 +1698,12 @@
}
async function onViewSync(ev) {
const { viewedAt, source, timestamp } = ev;
window.log.info(`view sync ${source} ${timestamp}, viewed at ${viewedAt}`);
const { source, timestamp } = ev;
window.log.info(`view sync ${source} ${timestamp}`);
const sync = Whisper.ViewSyncs.add({
source,
timestamp,
viewedAt,
});
sync.on('remove', ev.confirm);

View File

@ -52,22 +52,12 @@
const toAgeOut = await window.Signal.Data.getNextTapToViewMessageToAgeOut({
Message: Whisper.Message,
});
const toExpire = await window.Signal.Data.getNextTapToViewMessageToExpire({
Message: Whisper.Message,
});
if (!toAgeOut && !toExpire) {
if (!toAgeOut) {
return;
}
const ageOutAt = toAgeOut
? toAgeOut.get('received_at') + THIRTY_DAYS
: Number.MAX_VALUE;
const expireAt = toExpire
? toExpire.get('messageTimerExpiresAt')
: Number.MAX_VALUE;
const nextCheck = Math.min(ageOutAt, expireAt);
const nextCheck = toAgeOut.get('received_at') + THIRTY_DAYS;
Whisper.TapToViewMessagesListener.nextCheck = nextCheck;
window.log.info(

View File

@ -495,8 +495,7 @@
expirationTimestamp,
isTapToView,
isTapToViewExpired:
isTapToView && (this.get('isErased') || this.isTapToViewExpired()),
isTapToViewExpired: isTapToView && this.get('isErased'),
isTapToViewError:
isTapToView && this.isIncoming() && this.get('isTapToViewInvalid'),
@ -870,7 +869,7 @@
}
},
isTapToView() {
return Boolean(this.get('messageTimer'));
return Boolean(this.get('isViewOnce') || this.get('messageTimer'));
},
isValidTapToView() {
const body = this.get('body');
@ -908,66 +907,27 @@
return true;
},
isTapToViewExpired() {
const THIRTY_DAYS = 30 * 24 * 60 * 60 * 1000;
const now = Date.now();
const receivedAt = this.get('received_at');
if (now >= receivedAt + THIRTY_DAYS) {
return true;
}
const messageTimer = this.get('messageTimer');
const messageTimerStart = this.get('messageTimerStart');
if (!messageTimerStart) {
return false;
}
const expiresAt = messageTimerStart + messageTimer * 1000;
if (now >= expiresAt) {
return true;
}
return false;
},
async startTapToViewTimer(viewedAt, options) {
async markViewed(options) {
const { fromSync } = options || {};
if (!this.isValidTapToView()) {
window.log.warn(
`markViewed: Message ${this.idForLogging()} is not a valid tap to view message!`
);
return;
}
if (this.isErased()) {
window.log.warn(
`markViewed: Message ${this.idForLogging()} is already erased!`
);
return;
}
if (this.get('unread')) {
await this.markRead();
}
const messageTimer = this.get('messageTimer');
if (!messageTimer) {
window.log.warn(
`startTapToViewTimer: Message ${this.idForLogging()} has no messageTimer!`
);
return;
}
const existingTimerStart = this.get('messageTimerStart');
const messageTimerStart = Math.min(
Date.now(),
viewedAt || Date.now(),
existingTimerStart || Date.now()
);
const messageTimerExpiresAt = messageTimerStart + messageTimer * 1000;
// Because we're not using Backbone-integrated saves, we need to manually
// clear the changed fields here so our hasChanged() check below is useful.
this.changed = {};
this.set({
messageTimerStart,
messageTimerExpiresAt,
});
if (!this.hasChanged()) {
return;
}
await window.Signal.Data.saveMessage(this.attributes, {
Message: Whisper.Message,
});
await this.eraseContents();
if (!fromSync) {
const sender = this.getSource();
@ -979,14 +939,13 @@
);
await wrap(
textsecure.messaging.syncMessageTimerRead(
sender,
timestamp,
sendOptions
)
textsecure.messaging.syncViewOnceOpen(sender, timestamp, sendOptions)
);
}
},
isErased() {
return Boolean(this.get('isErased'));
},
async eraseContents() {
if (this.get('isErased')) {
return;
@ -1940,7 +1899,7 @@
hasAttachments: dataMessage.hasAttachments,
hasFileAttachments: dataMessage.hasFileAttachments,
hasVisualMediaAttachments: dataMessage.hasVisualMediaAttachments,
messageTimer: dataMessage.messageTimer,
isViewOnce: Boolean(dataMessage.isViewOnce),
preview,
requiredProtocolVersion:
dataMessage.requiredProtocolVersion ||

View File

@ -715,7 +715,7 @@ async function exportConversation(conversation, options = {}) {
count += 1;
// skip message if it is disappearing, no matter the amount of time left
if (message.expireTimer || message.messageTimer) {
if (message.expireTimer || message.messageTimer || message.isViewOnce) {
// eslint-disable-next-line no-continue
continue;
}

View File

@ -122,7 +122,6 @@ module.exports = {
getOutgoingWithoutExpiresAt,
getNextExpiringMessage,
getMessagesByConversation,
getNextTapToViewMessageToExpire,
getNextTapToViewMessageToAgeOut,
getTapToViewMessagesNeedingErase,
@ -842,14 +841,6 @@ async function getNextExpiringMessage({ MessageCollection }) {
return new MessageCollection(messages);
}
async function getNextTapToViewMessageToExpire({ Message }) {
const message = await channels.getNextTapToViewMessageToExpire();
if (!message) {
return null;
}
return new Message(message);
}
async function getNextTapToViewMessageToAgeOut({ Message }) {
const message = await channels.getNextTapToViewMessageToAgeOut();
if (!message) {

View File

@ -169,10 +169,14 @@ function initializeMigrations({
const writeNewTempData = createWriterForNew(tempPath);
const deleteTempFile = Attachments.createDeleter(tempPath);
const readTempData = createReader(tempPath);
const copyIntoTempDirectory = Attachments.copyIntoAttachmentsDirectory(
tempPath
);
return {
attachmentsPath,
copyIntoAttachmentsDirectory,
copyIntoTempDirectory,
deleteAttachmentData: deleteOnDisk,
deleteExternalMessageFiles: MessageType.deleteAllExternalFiles({
deleteAttachmentData: Type.deleteData(deleteOnDisk),
@ -182,6 +186,7 @@ function initializeMigrations({
deleteTempFile,
getAbsoluteAttachmentPath,
getAbsoluteStickerPath,
getAbsoluteTempPath,
getPlaceholderMigrations,
getCurrentVersion,
loadAttachmentData,

View File

@ -51,9 +51,7 @@
}
const message = MessageController.register(found.id, found);
const viewedAt = sync.get('viewedAt');
await message.startTapToViewTimer(viewedAt, { fromSync: true });
await message.markViewed({ fromSync: true });
this.remove(sync);
} catch (error) {

View File

@ -19,6 +19,9 @@
const {
upgradeMessageSchema,
getAbsoluteAttachmentPath,
copyIntoTempDirectory,
getAbsoluteTempPath,
deleteTempFile,
} = window.Signal.Migrations;
Whisper.ExpiredToast = Whisper.ToastView.extend({
@ -1324,17 +1327,33 @@
if (!message.isTapToView()) {
throw new Error(
`displayTapToViewMessage: Message ${message.idForLogging()} is not tap to view`
`displayTapToViewMessage: Message ${message.idForLogging()} is not a tap to view message`
);
}
if (message.isTapToViewExpired()) {
return;
if (message.isErased()) {
throw new Error(
`displayTapToViewMessage: Message ${message.idForLogging()} is already erased`
);
}
await message.startTapToViewTimer();
const firstAttachment = message.get('attachments')[0];
if (!firstAttachment || !firstAttachment.path) {
throw new Error(
`displayTapToViewMessage: Message ${message.idForLogging()} had no first attachment with path`
);
}
const closeLightbox = () => {
const absolutePath = getAbsoluteAttachmentPath(firstAttachment.path);
const tempPath = await copyIntoTempDirectory(absolutePath);
const tempAttachment = {
...firstAttachment,
path: tempPath,
};
await message.markViewed();
const closeLightbox = async () => {
if (!this.lightboxView) {
return;
}
@ -1345,6 +1364,8 @@
this.stopListening(message);
Signal.Backbone.Views.Lightbox.hide();
lightboxView.remove();
await deleteTempFile(tempPath);
};
this.listenTo(message, 'expired', closeLightbox);
this.listenTo(message, 'change', () => {
@ -1354,14 +1375,11 @@
});
const getProps = () => {
const firstAttachment = message.get('attachments')[0];
const { path, contentType } = firstAttachment;
const { path, contentType } = tempAttachment;
return {
objectURL: getAbsoluteAttachmentPath(path),
objectURL: getAbsoluteTempPath(path),
contentType,
timerExpiresAt: message.get('messageTimerExpiresAt'),
timerDuration: message.get('messageTimer') * 1000,
};
};
this.lightboxView = new Whisper.ReactWrapperView({

View File

@ -1090,11 +1090,8 @@ MessageReceiver.prototype.extend({
envelope,
syncMessage.stickerPackOperation
);
} else if (syncMessage.messageTimerRead) {
return this.handleMessageTimerRead(
envelope,
syncMessage.messageTimerRead
);
} else if (syncMessage.viewOnceOpen) {
return this.handleViewOnceOpen(envelope, syncMessage.viewOnceOpen);
}
throw new Error('Got empty SyncMessage');
},
@ -1105,14 +1102,13 @@ MessageReceiver.prototype.extend({
ev.configuration = configuration;
return this.dispatchAndWait(ev);
},
handleMessageTimerRead(envelope, sync) {
window.log.info('got message timer read sync message');
handleViewOnceOpen(envelope, sync) {
window.log.info('got view once open sync message');
const ev = new Event('viewSync');
ev.confirm = this.removeFromCache.bind(this, envelope);
ev.source = sync.sender;
ev.timestamp = sync.timestamp ? sync.timestamp.toNumber() : null;
ev.viewedAt = envelope.timestamp;
return this.dispatchAndWait(ev);
},

View File

@ -745,7 +745,7 @@ MessageSender.prototype = {
return Promise.resolve();
},
async syncMessageTimerRead(sender, timestamp, options) {
async syncViewOnceOpen(sender, timestamp, options) {
const myNumber = textsecure.storage.user.getNumber();
const myDevice = textsecure.storage.user.getDeviceId();
if (myDevice === 1 || myDevice === '1') {
@ -754,10 +754,10 @@ MessageSender.prototype = {
const syncMessage = this.createSyncMessage();
const messageTimerRead = new textsecure.protobuf.SyncMessage.MessageTimerRead();
messageTimerRead.sender = sender;
messageTimerRead.timestamp = timestamp;
syncMessage.messageTimerRead = messageTimerRead;
const viewOnceOpen = new textsecure.protobuf.SyncMessage.ViewOnceOpen();
viewOnceOpen.sender = sender;
viewOnceOpen.timestamp = timestamp;
syncMessage.viewOnceOpen = viewOnceOpen;
const contentMessage = new textsecure.protobuf.Content();
contentMessage.syncMessage = syncMessage;
@ -1260,7 +1260,7 @@ textsecure.MessageSender = function MessageSenderWrapper(username, password) {
this.getSticker = sender.getSticker.bind(sender);
this.getStickerPackManifest = sender.getStickerPackManifest.bind(sender);
this.sendStickerPackSync = sender.sendStickerPackSync.bind(sender);
this.syncMessageTimerRead = sender.syncMessageTimerRead.bind(sender);
this.syncViewOnceOpen = sender.syncViewOnceOpen.bind(sender);
};
textsecure.MessageSender.prototype = {

View File

@ -174,7 +174,8 @@ message DataMessage {
INITIAL = 0;
MESSAGE_TIMERS = 1;
CURRENT = 1;
VIEW_ONCE = 2;
CURRENT = 2;
}
optional string body = 1;
@ -189,7 +190,7 @@ message DataMessage {
repeated Preview preview = 10;
optional Sticker sticker = 11;
optional uint32 requiredProtocolVersion = 12;
optional uint32 messageTimer = 13;
optional bool isViewOnce = 14;
}
message NullMessage {
@ -293,7 +294,7 @@ message SyncMessage {
optional Type type = 3;
}
message MessageTimerRead {
message ViewOnceOpen {
optional string sender = 1;
optional uint64 timestamp = 2;
}
@ -308,7 +309,7 @@ message SyncMessage {
optional Configuration configuration = 9;
optional bytes padding = 8;
repeated StickerPackOperation stickerPackOperation = 10;
optional MessageTimerRead messageTimerRead = 11;
optional ViewOnceOpen viewOnceOpen = 11;
}
message AttachmentPointer {

View File

@ -18,7 +18,8 @@ export type IncomingMessage = Readonly<
decrypted_at?: number;
errors?: Array<any>;
expireTimer?: number;
messageTimer?: number;
messageTimer?: number; // deprecated
isViewOnce?: number;
flags?: number;
source?: string;
sourceDevice?: number;
@ -47,7 +48,8 @@ export type OutgoingMessage = Readonly<
body?: string;
expires_at?: number;
expireTimer?: number;
messageTimer?: number;
messageTimer?: number; // deprecated
isViewOnce?: number;
recipients?: Array<string>; // Array<PhoneNumber>
synced: boolean;
} & SharedMessageProperties &

View File

@ -16,7 +16,7 @@ export const initializeAttachmentMetadata = async (
if (message.type === 'verified-change') {
return message;
}
if (message.messageTimer) {
if (message.messageTimer || message.isViewOnce) {
return message;
}