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, getOutgoingWithoutExpiresAt,
getNextExpiringMessage, getNextExpiringMessage,
getMessagesByConversation, getMessagesByConversation,
getNextTapToViewMessageToExpire,
getNextTapToViewMessageToAgeOut, getNextTapToViewMessageToAgeOut,
getTapToViewMessagesNeedingErase, 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 = [ const SCHEMA_VERSIONS = [
updateToSchemaVersion1, updateToSchemaVersion1,
updateToSchemaVersion2, updateToSchemaVersion2,
@ -969,6 +1030,7 @@ const SCHEMA_VERSIONS = [
updateToSchemaVersion14, updateToSchemaVersion14,
updateToSchemaVersion15, updateToSchemaVersion15,
updateToSchemaVersion16, updateToSchemaVersion16,
updateToSchemaVersion17,
]; ];
async function updateSchema(instance) { async function updateSchema(instance) {
@ -1566,9 +1628,7 @@ async function saveMessage(data, { forceSave } = {}) {
hasVisualMediaAttachments, hasVisualMediaAttachments,
id, id,
isErased, isErased,
messageTimer, isViewOnce,
messageTimerStart,
messageTimerExpiresAt,
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
received_at, received_at,
schemaVersion, schemaVersion,
@ -1595,9 +1655,7 @@ async function saveMessage(data, { forceSave } = {}) {
$hasFileAttachments: hasFileAttachments, $hasFileAttachments: hasFileAttachments,
$hasVisualMediaAttachments: hasVisualMediaAttachments, $hasVisualMediaAttachments: hasVisualMediaAttachments,
$isErased: isErased, $isErased: isErased,
$messageTimer: messageTimer, $isViewOnce: isViewOnce,
$messageTimerStart: messageTimerStart,
$messageTimerExpiresAt: messageTimerExpiresAt,
$received_at: received_at, $received_at: received_at,
$schemaVersion: schemaVersion, $schemaVersion: schemaVersion,
$sent_at: sent_at, $sent_at: sent_at,
@ -1622,9 +1680,7 @@ async function saveMessage(data, { forceSave } = {}) {
hasFileAttachments = $hasFileAttachments, hasFileAttachments = $hasFileAttachments,
hasVisualMediaAttachments = $hasVisualMediaAttachments, hasVisualMediaAttachments = $hasVisualMediaAttachments,
isErased = $isErased, isErased = $isErased,
messageTimer = $messageTimer, isViewOnce = $isViewOnce,
messageTimerStart = $messageTimerStart,
messageTimerExpiresAt = $messageTimerExpiresAt,
received_at = $received_at, received_at = $received_at,
schemaVersion = $schemaVersion, schemaVersion = $schemaVersion,
sent_at = $sent_at, sent_at = $sent_at,
@ -1658,9 +1714,7 @@ async function saveMessage(data, { forceSave } = {}) {
hasFileAttachments, hasFileAttachments,
hasVisualMediaAttachments, hasVisualMediaAttachments,
isErased, isErased,
messageTimer, isViewOnce,
messageTimerStart,
messageTimerExpiresAt,
received_at, received_at,
schemaVersion, schemaVersion,
sent_at, sent_at,
@ -1681,9 +1735,7 @@ async function saveMessage(data, { forceSave } = {}) {
$hasFileAttachments, $hasFileAttachments,
$hasVisualMediaAttachments, $hasVisualMediaAttachments,
$isErased, $isErased,
$messageTimer, $isViewOnce,
$messageTimerStart,
$messageTimerExpiresAt,
$received_at, $received_at,
$schemaVersion, $schemaVersion,
$sent_at, $sent_at,
@ -1862,31 +1914,11 @@ async function getNextExpiringMessage() {
return map(rows, row => jsonToObject(row.json)); 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() { async function getNextTapToViewMessageToAgeOut() {
// Note: we avoid 'IS NOT NULL' here because it does seem to bypass our index
const rows = await db.all(` const rows = await db.all(`
SELECT json FROM messages SELECT json FROM messages
WHERE WHERE
messageTimer > 0 isViewOnce = 1
AND (isErased IS NULL OR isErased != 1) AND (isErased IS NULL OR isErased != 1)
ORDER BY received_at ASC ORDER BY received_at ASC
LIMIT 1; LIMIT 1;
@ -1903,18 +1935,12 @@ async function getTapToViewMessagesNeedingErase() {
const THIRTY_DAYS_AGO = Date.now() - 30 * 24 * 60 * 60 * 1000; const THIRTY_DAYS_AGO = Date.now() - 30 * 24 * 60 * 60 * 1000;
const NOW = Date.now(); const NOW = Date.now();
// Note: we avoid 'IS NOT NULL' here because it does seem to bypass our index
const rows = await db.all( const rows = await db.all(
`SELECT json FROM messages `SELECT json FROM messages
WHERE WHERE
messageTimer > 0 isViewOnce = 1
AND (isErased IS NULL OR isErased != 1) AND (isErased IS NULL OR isErased != 1)
AND ( AND received_at <= $THIRTY_DAYS_AGO
(messageTimerExpiresAt > 0
AND messageTimerExpiresAt <= $NOW)
OR
received_at <= $THIRTY_DAYS_AGO
)
ORDER BY received_at ASC;`, ORDER BY received_at ASC;`,
{ {
$NOW: NOW, $NOW: NOW,

View file

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

View file

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

View file

@ -495,8 +495,7 @@
expirationTimestamp, expirationTimestamp,
isTapToView, isTapToView,
isTapToViewExpired: isTapToViewExpired: isTapToView && this.get('isErased'),
isTapToView && (this.get('isErased') || this.isTapToViewExpired()),
isTapToViewError: isTapToViewError:
isTapToView && this.isIncoming() && this.get('isTapToViewInvalid'), isTapToView && this.isIncoming() && this.get('isTapToViewInvalid'),
@ -870,7 +869,7 @@
} }
}, },
isTapToView() { isTapToView() {
return Boolean(this.get('messageTimer')); return Boolean(this.get('isViewOnce') || this.get('messageTimer'));
}, },
isValidTapToView() { isValidTapToView() {
const body = this.get('body'); const body = this.get('body');
@ -908,66 +907,27 @@
return true; return true;
}, },
isTapToViewExpired() { async markViewed(options) {
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) {
const { fromSync } = 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')) { if (this.get('unread')) {
await this.markRead(); await this.markRead();
} }
const messageTimer = this.get('messageTimer'); await this.eraseContents();
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,
});
if (!fromSync) { if (!fromSync) {
const sender = this.getSource(); const sender = this.getSource();
@ -979,14 +939,13 @@
); );
await wrap( await wrap(
textsecure.messaging.syncMessageTimerRead( textsecure.messaging.syncViewOnceOpen(sender, timestamp, sendOptions)
sender,
timestamp,
sendOptions
)
); );
} }
}, },
isErased() {
return Boolean(this.get('isErased'));
},
async eraseContents() { async eraseContents() {
if (this.get('isErased')) { if (this.get('isErased')) {
return; return;
@ -1940,7 +1899,7 @@
hasAttachments: dataMessage.hasAttachments, hasAttachments: dataMessage.hasAttachments,
hasFileAttachments: dataMessage.hasFileAttachments, hasFileAttachments: dataMessage.hasFileAttachments,
hasVisualMediaAttachments: dataMessage.hasVisualMediaAttachments, hasVisualMediaAttachments: dataMessage.hasVisualMediaAttachments,
messageTimer: dataMessage.messageTimer, isViewOnce: Boolean(dataMessage.isViewOnce),
preview, preview,
requiredProtocolVersion: requiredProtocolVersion:
dataMessage.requiredProtocolVersion || dataMessage.requiredProtocolVersion ||

View file

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

View file

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

View file

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

View file

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

View file

@ -19,6 +19,9 @@
const { const {
upgradeMessageSchema, upgradeMessageSchema,
getAbsoluteAttachmentPath, getAbsoluteAttachmentPath,
copyIntoTempDirectory,
getAbsoluteTempPath,
deleteTempFile,
} = window.Signal.Migrations; } = window.Signal.Migrations;
Whisper.ExpiredToast = Whisper.ToastView.extend({ Whisper.ExpiredToast = Whisper.ToastView.extend({
@ -1324,17 +1327,33 @@
if (!message.isTapToView()) { if (!message.isTapToView()) {
throw new Error( 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()) { if (message.isErased()) {
return; 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) { if (!this.lightboxView) {
return; return;
} }
@ -1345,6 +1364,8 @@
this.stopListening(message); this.stopListening(message);
Signal.Backbone.Views.Lightbox.hide(); Signal.Backbone.Views.Lightbox.hide();
lightboxView.remove(); lightboxView.remove();
await deleteTempFile(tempPath);
}; };
this.listenTo(message, 'expired', closeLightbox); this.listenTo(message, 'expired', closeLightbox);
this.listenTo(message, 'change', () => { this.listenTo(message, 'change', () => {
@ -1354,14 +1375,11 @@
}); });
const getProps = () => { const getProps = () => {
const firstAttachment = message.get('attachments')[0]; const { path, contentType } = tempAttachment;
const { path, contentType } = firstAttachment;
return { return {
objectURL: getAbsoluteAttachmentPath(path), objectURL: getAbsoluteTempPath(path),
contentType, contentType,
timerExpiresAt: message.get('messageTimerExpiresAt'),
timerDuration: message.get('messageTimer') * 1000,
}; };
}; };
this.lightboxView = new Whisper.ReactWrapperView({ this.lightboxView = new Whisper.ReactWrapperView({

View file

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

View file

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

View file

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

View file

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

View file

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