Delete For Everyone Send

Co-authored-by: Chris Svenningsen <chris@carbonfive.com>
This commit is contained in:
Sidney Keese 2020-09-29 15:55:56 -07:00 committed by Josh Perez
parent 693deaebe8
commit 866217a724
14 changed files with 276 additions and 10 deletions

View file

@ -943,7 +943,12 @@
"message": "Delete" "message": "Delete"
}, },
"deleteWarning": { "deleteWarning": {
"message": "Are you sure? Clicking 'delete' will permanently remove this message from this device only." "message": "Clicking 'delete' will permanently remove this message from your devices only.",
"description": "Text shown in the confirmation dialog for deleting a message locally"
},
"deleteForEveryoneWarning": {
"message": "This message will be permanently deleted for everyone in the conversation. Members will be able to see that you deleted a message.",
"description": "Text shown in the confirmation dialog for deleting a message for everyone"
}, },
"deleteThisMessage": { "deleteThisMessage": {
"message": "Delete this message" "message": "Delete this message"
@ -1014,9 +1019,13 @@
"description": "Shown on the drop-down menu for an individual message, but only if it is an outgoing message that failed to send" "description": "Shown on the drop-down menu for an individual message, but only if it is an outgoing message that failed to send"
}, },
"deleteMessage": { "deleteMessage": {
"message": "Delete Message", "message": "Delete message for me",
"description": "Shown on the drop-down menu for an individual message, deletes single message" "description": "Shown on the drop-down menu for an individual message, deletes single message"
}, },
"deleteMessageForEveryone": {
"message": "Delete message for everyone",
"description": "Shown on the drop-down menu for an individual message, deletes single message for everyone"
},
"deleteMessages": { "deleteMessages": {
"message": "Delete messages", "message": "Delete messages",
"description": "Menu item for deleting messages, title case." "description": "Menu item for deleting messages, title case."

View file

@ -25,7 +25,7 @@
return []; return [];
}, },
async onDelete(del) { onDelete(del) {
try { try {
// The contact the delete message came from // The contact the delete message came from
const fromContact = ConversationController.get(del.get('fromId')); const fromContact = ConversationController.get(del.get('fromId'));

View file

@ -45,12 +45,14 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
authorTitle: text('authorTitle', overrideProps.authorTitle || ''), authorTitle: text('authorTitle', overrideProps.authorTitle || ''),
bodyRanges: overrideProps.bodyRanges, bodyRanges: overrideProps.bodyRanges,
canReply: true, canReply: true,
canDeleteForEveryone: overrideProps.canDeleteForEveryone || false,
clearSelectedMessage: action('clearSelectedMessage'), clearSelectedMessage: action('clearSelectedMessage'),
collapseMetadata: overrideProps.collapseMetadata, collapseMetadata: overrideProps.collapseMetadata,
conversationId: text('conversationId', overrideProps.conversationId || ''), conversationId: text('conversationId', overrideProps.conversationId || ''),
conversationType: overrideProps.conversationType || 'direct', conversationType: overrideProps.conversationType || 'direct',
deletedForEveryone: overrideProps.deletedForEveryone, deletedForEveryone: overrideProps.deletedForEveryone,
deleteMessage: action('deleteMessage'), deleteMessage: action('deleteMessage'),
deleteMessageForEveryone: action('deleteMessageForEveryone'),
disableMenu: overrideProps.disableMenu, disableMenu: overrideProps.disableMenu,
disableScroll: overrideProps.disableScroll, disableScroll: overrideProps.disableScroll,
direction: overrideProps.direction || 'incoming', direction: overrideProps.direction || 'incoming',
@ -324,6 +326,16 @@ story.add('Deleted', () => {
return renderBothDirections(props); return renderBothDirections(props);
}); });
story.add('Can delete for everyone', () => {
const props = createProps({
status: 'read',
text: 'I hope you get this.',
canDeleteForEveryone: true,
});
return <Message {...props} direction="outgoing" />;
});
story.add('Error', () => { story.add('Error', () => {
const props = createProps({ const props = createProps({
status: 'error', status: 'error',

View file

@ -53,6 +53,7 @@ interface Trigger {
const MINIMUM_LINK_PREVIEW_IMAGE_WIDTH = 200; const MINIMUM_LINK_PREVIEW_IMAGE_WIDTH = 200;
const STICKER_SIZE = 200; const STICKER_SIZE = 200;
const SELECTED_TIMEOUT = 1000; const SELECTED_TIMEOUT = 1000;
const THREE_HOURS = 3 * 60 * 60 * 1000;
interface LinkPreviewType { interface LinkPreviewType {
title: string; title: string;
@ -134,6 +135,7 @@ export type PropsData = {
deletedForEveryone?: boolean; deletedForEveryone?: boolean;
canReply: boolean; canReply: boolean;
canDeleteForEveryone: boolean;
bodyRanges?: BodyRangesType; bodyRanges?: BodyRangesType;
}; };
@ -154,6 +156,7 @@ export type PropsActions = {
replyToMessage: (id: string) => void; replyToMessage: (id: string) => void;
retrySend: (id: string) => void; retrySend: (id: string) => void;
deleteMessage: (id: string) => void; deleteMessage: (id: string) => void;
deleteMessageForEveryone: (id: string) => void;
showMessageDetail: (id: string) => void; showMessageDetail: (id: string) => void;
openConversation: (conversationId: string, messageId?: string) => void; openConversation: (conversationId: string, messageId?: string) => void;
@ -200,6 +203,7 @@ interface State {
isWide: boolean; isWide: boolean;
containerWidth: number; containerWidth: number;
canDeleteForEveryone: boolean;
} }
const EXPIRATION_CHECK_MINIMUM = 2000; const EXPIRATION_CHECK_MINIMUM = 2000;
@ -226,9 +230,13 @@ export class Message extends React.PureComponent<Props, State> {
public selectedTimeout: NodeJS.Timeout | undefined; public selectedTimeout: NodeJS.Timeout | undefined;
public deleteForEveryoneTimeout: NodeJS.Timeout | undefined;
public constructor(props: Props) { public constructor(props: Props) {
super(props); super(props);
const { canDeleteForEveryone } = props;
this.wideMl = window.matchMedia('(min-width: 926px)'); this.wideMl = window.matchMedia('(min-width: 926px)');
this.wideMl.addEventListener('change', this.handleWideMlChange); this.wideMl.addEventListener('change', this.handleWideMlChange);
@ -246,6 +254,7 @@ export class Message extends React.PureComponent<Props, State> {
isWide: this.wideMl.matches, isWide: this.wideMl.matches,
containerWidth: 0, containerWidth: 0,
canDeleteForEveryone,
}; };
} }
@ -322,6 +331,7 @@ export class Message extends React.PureComponent<Props, State> {
public componentDidMount(): void { public componentDidMount(): void {
this.startSelectedTimer(); this.startSelectedTimer();
this.startDeleteForEveryoneTimer();
const { isSelected } = this.props; const { isSelected } = this.props;
if (isSelected) { if (isSelected) {
@ -353,6 +363,9 @@ export class Message extends React.PureComponent<Props, State> {
if (this.expiredTimeout) { if (this.expiredTimeout) {
clearTimeout(this.expiredTimeout); clearTimeout(this.expiredTimeout);
} }
if (this.deleteForEveryoneTimeout) {
clearTimeout(this.deleteForEveryoneTimeout);
}
this.toggleReactionViewer(true); this.toggleReactionViewer(true);
this.toggleReactionPicker(true); this.toggleReactionPicker(true);
@ -360,7 +373,7 @@ export class Message extends React.PureComponent<Props, State> {
} }
public componentDidUpdate(prevProps: Props): void { public componentDidUpdate(prevProps: Props): void {
const { isSelected } = this.props; const { canDeleteForEveryone, isSelected } = this.props;
this.startSelectedTimer(); this.startSelectedTimer();
@ -369,6 +382,10 @@ export class Message extends React.PureComponent<Props, State> {
} }
this.checkExpired(); this.checkExpired();
if (canDeleteForEveryone !== prevProps.canDeleteForEveryone) {
this.startDeleteForEveryoneTimer();
}
} }
public startSelectedTimer(): void { public startSelectedTimer(): void {
@ -388,6 +405,29 @@ export class Message extends React.PureComponent<Props, State> {
} }
} }
public startDeleteForEveryoneTimer(): void {
if (this.deleteForEveryoneTimeout) {
clearTimeout(this.deleteForEveryoneTimeout);
}
const { canDeleteForEveryone } = this.props;
if (!canDeleteForEveryone) {
return;
}
const { timestamp } = this.props;
const timeToDeletion = timestamp - Date.now() + THREE_HOURS;
if (timeToDeletion <= 0) {
this.setState({ canDeleteForEveryone: false });
} else {
this.deleteForEveryoneTimeout = setTimeout(() => {
this.setState({ canDeleteForEveryone: false });
}, timeToDeletion);
}
}
public checkExpired(): void { public checkExpired(): void {
const now = Date.now(); const now = Date.now();
const { isExpired, expirationTimestamp, expirationLength } = this.props; const { isExpired, expirationTimestamp, expirationLength } = this.props;
@ -1285,6 +1325,7 @@ export class Message extends React.PureComponent<Props, State> {
attachments, attachments,
canReply, canReply,
deleteMessage, deleteMessage,
deleteMessageForEveryone,
direction, direction,
i18n, i18n,
id, id,
@ -1296,6 +1337,8 @@ export class Message extends React.PureComponent<Props, State> {
status, status,
} = this.props; } = this.props;
const { canDeleteForEveryone } = this.state;
const showRetry = status === 'error' && direction === 'outgoing'; const showRetry = status === 'error' && direction === 'outgoing';
const multipleAttachments = attachments && attachments.length > 1; const multipleAttachments = attachments && attachments.length > 1;
@ -1386,6 +1429,21 @@ export class Message extends React.PureComponent<Props, State> {
> >
{i18n('deleteMessage')} {i18n('deleteMessage')}
</MenuItem> </MenuItem>
{canDeleteForEveryone ? (
<MenuItem
attributes={{
className: 'module-message__context__delete-message-for-everyone',
}}
onClick={(event: React.MouseEvent) => {
event.stopPropagation();
event.preventDefault();
deleteMessageForEveryone(id);
}}
>
{i18n('deleteMessageForEveryone')}
</MenuItem>
) : null}
</ContextMenu> </ContextMenu>
); );

View file

@ -16,10 +16,12 @@ const story = storiesOf('Components/Conversation/MessageDetail', module);
const defaultMessage: MessageProps = { const defaultMessage: MessageProps = {
authorTitle: 'Max', authorTitle: 'Max',
canReply: true, canReply: true,
canDeleteForEveryone: true,
clearSelectedMessage: () => null, clearSelectedMessage: () => null,
conversationId: 'my-convo', conversationId: 'my-convo',
conversationType: 'direct', conversationType: 'direct',
deleteMessage: action('deleteMessage'), deleteMessage: action('deleteMessage'),
deleteMessageForEveryone: action('deleteMessageForEveryone'),
direction: 'incoming', direction: 'incoming',
displayTapToViewMessage: () => null, displayTapToViewMessage: () => null,
downloadAttachment: () => null, downloadAttachment: () => null,

View file

@ -19,10 +19,12 @@ const story = storiesOf('Components/Conversation/Quote', module);
const defaultMessageProps: MessagesProps = { const defaultMessageProps: MessagesProps = {
authorTitle: 'Person X', authorTitle: 'Person X',
canReply: true, canReply: true,
canDeleteForEveryone: true,
clearSelectedMessage: () => null, clearSelectedMessage: () => null,
conversationId: 'conversationId', conversationId: 'conversationId',
conversationType: 'direct', // override conversationType: 'direct', // override
deleteMessage: () => null, deleteMessage: () => null,
deleteMessageForEveryone: () => null,
direction: 'incoming', direction: 'incoming',
displayTapToViewMessage: () => null, displayTapToViewMessage: () => null,
downloadAttachment: () => null, downloadAttachment: () => null,

View file

@ -223,6 +223,7 @@ const actions = () => ({
replyToMessage: action('replyToMessage'), replyToMessage: action('replyToMessage'),
retrySend: action('retrySend'), retrySend: action('retrySend'),
deleteMessage: action('deleteMessage'), deleteMessage: action('deleteMessage'),
deleteMessageForEveryone: action('deleteMessageForEveryone'),
showMessageDetail: action('showMessageDetail'), showMessageDetail: action('showMessageDetail'),
openConversation: action('openConversation'), openConversation: action('openConversation'),
showContactDetail: action('showContactDetail'), showContactDetail: action('showContactDetail'),

View file

@ -40,6 +40,7 @@ const getDefaultProps = () => ({
replyToMessage: action('replyToMessage'), replyToMessage: action('replyToMessage'),
retrySend: action('retrySend'), retrySend: action('retrySend'),
deleteMessage: action('deleteMessage'), deleteMessage: action('deleteMessage'),
deleteMessageForEveryone: action('deleteMessageForEveryone'),
showMessageDetail: action('showMessageDetail'), showMessageDetail: action('showMessageDetail'),
openConversation: action('openConversation'), openConversation: action('openConversation'),
showContactDetail: action('showContactDetail'), showContactDetail: action('showContactDetail'),

1
ts/model-types.d.ts vendored
View file

@ -50,6 +50,7 @@ export type MessageAttributesType = {
dataMessage: ArrayBuffer | null; dataMessage: ArrayBuffer | null;
decrypted_at: number; decrypted_at: number;
deletedForEveryone: boolean; deletedForEveryone: boolean;
deletedForEveryoneTimestamp?: number;
delivered: number; delivered: number;
delivered_to: Array<string | null>; delivered_to: Array<string | null>;
errors: Array<CustomError> | null; errors: Array<CustomError> | null;

View file

@ -63,6 +63,8 @@ const COLORS = [
'ultramarine', 'ultramarine',
]; ];
const THREE_HOURS = 3 * 60 * 60 * 1000;
interface CustomError extends Error { interface CustomError extends Error {
identifier?: string; identifier?: string;
number?: string; number?: string;
@ -1811,6 +1813,106 @@ export class ConversationModel extends window.Backbone.Model<
window.reduxActions.stickers.useSticker(packId, stickerId); window.reduxActions.stickers.useSticker(packId, stickerId);
} }
async sendDeleteForEveryoneMessage(targetTimestamp: number): Promise<void> {
const timestamp = Date.now();
if (timestamp - targetTimestamp > THREE_HOURS) {
throw new Error('Cannot send DOE for a message older than three hours');
}
const deleteModel = window.Whisper.Deletes.add({
targetSentTimestamp: targetTimestamp,
fromId: window.ConversationController.getOurConversationId(),
});
window.Whisper.Deletes.onDelete(deleteModel);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const destination = this.getSendTarget()!;
const recipients = this.getRecipients();
let profileKey: ArrayBuffer | undefined;
if (this.get('profileSharing')) {
profileKey = window.storage.get('profileKey');
}
return this.queueJob(async () => {
window.log.info(
'Sending deleteForEveryone to conversation',
this.idForLogging(),
'with timestamp',
timestamp
);
const attributes = ({
id: window.getGuid(),
type: 'outgoing',
conversationId: this.get('id'),
sent_at: timestamp,
received_at: timestamp,
recipients,
deletedForEveryoneTimestamp: targetTimestamp,
// TODO: DESKTOP-722
} as unknown) as typeof window.Whisper.MessageAttributesType;
if (this.isPrivate()) {
attributes.destination = destination;
}
// We are only creating this model so we can use its sync message
// sending functionality. It will not be saved to the datbase.
const message = new window.Whisper.Message(attributes);
// We're offline!
if (!window.textsecure.messaging) {
throw new Error('Cannot send DOE while offline!');
}
const options = this.getSendOptions();
const promise = (() => {
if (this.isPrivate()) {
return window.textsecure.messaging.sendMessageToIdentifier(
destination,
undefined, // body
[], // attachments
undefined, // quote
[], // preview
undefined, // sticker
undefined, // reaction
targetTimestamp,
timestamp,
undefined, // expireTimer
profileKey,
options
);
}
return window.textsecure.messaging.sendMessageToGroup(
{
groupV1: this.getGroupV1Info(),
groupV2: this.getGroupV2Info(),
deletedForEveryoneTimestamp: targetTimestamp,
timestamp,
profileKey,
},
options
);
})();
return message.send(this.wrapSend(promise));
}).catch(error => {
window.log.error(
'Error sending deleteForEveryone',
deleteModel,
targetTimestamp,
error
);
throw error;
});
}
async sendReactionMessage( async sendReactionMessage(
reaction: { emoji: string; remove: boolean }, reaction: { emoji: string; remove: boolean },
target: { target: {
@ -1882,6 +1984,7 @@ export class ConversationModel extends window.Backbone.Model<
[], // preview [], // preview
undefined, // sticker undefined, // sticker
outgoingReaction, outgoingReaction,
undefined, // deletedForEveryoneTimestamp
timestamp, timestamp,
expireTimer, expireTimer,
profileKey profileKey
@ -1901,6 +2004,7 @@ export class ConversationModel extends window.Backbone.Model<
[], // preview [], // preview
undefined, // sticker undefined, // sticker
outgoingReaction, outgoingReaction,
undefined, // deletedForEveryoneTimestamp
timestamp, timestamp,
expireTimer, expireTimer,
profileKey, profileKey,
@ -2072,6 +2176,7 @@ export class ConversationModel extends window.Backbone.Model<
preview, preview,
sticker, sticker,
null, // reaction null, // reaction
undefined, // deletedForEveryoneTimestamp
now, now,
expireTimer, expireTimer,
profileKey profileKey
@ -2108,6 +2213,7 @@ export class ConversationModel extends window.Backbone.Model<
preview, preview,
sticker, sticker,
null, // reaction null, // reaction
undefined, // deletedForEveryoneTimestamp
now, now,
expireTimer, expireTimer,
profileKey, profileKey,
@ -2553,6 +2659,7 @@ export class ConversationModel extends window.Backbone.Model<
[], // preview [], // preview
undefined, // sticker undefined, // sticker
undefined, // reaction undefined, // reaction
undefined, // deletedForEveryoneTimestamp
message.get('sent_at'), message.get('sent_at'),
expireTimer, expireTimer,
profileKey, profileKey,

View file

@ -63,6 +63,8 @@ const PLACEHOLDER_CONTACT = {
title: window.i18n('unknownContact'), title: window.i18n('unknownContact'),
}; };
const THREE_HOURS = 3 * 60 * 60 * 1000;
window.AccountCache = Object.create(null); window.AccountCache = Object.create(null);
window.AccountJobs = Object.create(null); window.AccountJobs = Object.create(null);
@ -373,6 +375,9 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
deleteMessage: (messageId: string) => { deleteMessage: (messageId: string) => {
this.trigger('delete', messageId); this.trigger('delete', messageId);
}, },
deleteMessageForEveryone: (messageId: string) => {
this.trigger('delete-for-everyone', messageId);
},
showVisualAttachment: (options: unknown) => { showVisualAttachment: (options: unknown) => {
this.trigger('show-visual-attachment', options); this.trigger('show-visual-attachment', options);
}, },
@ -730,6 +735,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
status: this.getMessagePropStatus(), status: this.getMessagePropStatus(),
contact: this.getPropsForEmbeddedContact(), contact: this.getPropsForEmbeddedContact(),
canReply: this.canReply(), canReply: this.canReply(),
canDeleteForEveryone: this.canDeleteForEveryone(),
authorTitle: contact.title, authorTitle: contact.title,
authorColor, authorColor,
authorName: contact.name, authorName: contact.name,
@ -1897,6 +1903,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
previewWithData, previewWithData,
stickerWithData, stickerWithData,
null, null,
this.get('deletedForEveryoneTimestamp'),
this.get('sent_at'), this.get('sent_at'),
this.get('expireTimer'), this.get('expireTimer'),
profileKey profileKey
@ -1966,6 +1973,25 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
); );
} }
canDeleteForEveryone(): boolean {
// is someone else's message
if (this.isIncoming()) {
return false;
}
// has already been deleted for everyone
if (this.get('deletedForEveryone')) {
return false;
}
// is too old to delete
if (Date.now() - this.get('sent_at') > THREE_HOURS) {
return false;
}
return true;
}
canReply(): boolean { canReply(): boolean {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const isAccepted = this.getConversation()!.getAccepted(); const isAccepted = this.getConversation()!.getAccepted();
@ -2051,6 +2077,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
previewWithData, previewWithData,
stickerWithData, stickerWithData,
null, null,
this.get('deletedForEveryoneTimestamp'),
this.get('sent_at'), this.get('sent_at'),
this.get('expireTimer'), this.get('expireTimer'),
profileKey, profileKey,

View file

@ -118,6 +118,7 @@ type MessageOptionsType = {
recipients: Array<string>; recipients: Array<string>;
sticker?: any; sticker?: any;
reaction?: any; reaction?: any;
deletedForEveryoneTimestamp?: number;
timestamp: number; timestamp: number;
}; };
@ -157,6 +158,8 @@ class Message {
attachmentPointers?: Array<any>; attachmentPointers?: Array<any>;
deletedForEveryoneTimestamp?: number;
constructor(options: MessageOptionsType) { constructor(options: MessageOptionsType) {
this.attachments = options.attachments || []; this.attachments = options.attachments || [];
this.body = options.body; this.body = options.body;
@ -172,6 +175,7 @@ class Message {
this.sticker = options.sticker; this.sticker = options.sticker;
this.reaction = options.reaction; this.reaction = options.reaction;
this.timestamp = options.timestamp; this.timestamp = options.timestamp;
this.deletedForEveryoneTimestamp = options.deletedForEveryoneTimestamp;
if (!(this.recipients instanceof Array)) { if (!(this.recipients instanceof Array)) {
throw new Error('Invalid recipient list'); throw new Error('Invalid recipient list');
@ -330,6 +334,11 @@ class Message {
if (this.profileKey) { if (this.profileKey) {
proto.profileKey = this.profileKey; proto.profileKey = this.profileKey;
} }
if (this.deletedForEveryoneTimestamp) {
proto.delete = {
targetSentTimestamp: this.deletedForEveryoneTimestamp,
};
}
this.dataMessage = proto; this.dataMessage = proto;
return proto; return proto;
@ -1412,6 +1421,7 @@ export default class MessageSender {
preview: Array<PreviewType>, preview: Array<PreviewType>,
sticker: unknown, sticker: unknown,
reaction: unknown, reaction: unknown,
deletedForEveryoneTimestamp: number | undefined,
timestamp: number, timestamp: number,
expireTimer: number | undefined, expireTimer: number | undefined,
profileKey?: ArrayBuffer, profileKey?: ArrayBuffer,
@ -1427,6 +1437,7 @@ export default class MessageSender {
preview, preview,
sticker, sticker,
reaction, reaction,
deletedForEveryoneTimestamp,
expireTimer, expireTimer,
profileKey, profileKey,
flags, flags,
@ -1457,6 +1468,7 @@ export default class MessageSender {
preview: Array<PreviewType> | undefined, preview: Array<PreviewType> | undefined,
sticker: unknown, sticker: unknown,
reaction: unknown, reaction: unknown,
deletedForEveryoneTimestamp: number | undefined,
timestamp: number, timestamp: number,
expireTimer: number | undefined, expireTimer: number | undefined,
profileKey?: ArrayBuffer, profileKey?: ArrayBuffer,
@ -1472,6 +1484,7 @@ export default class MessageSender {
preview, preview,
sticker, sticker,
reaction, reaction,
deletedForEveryoneTimestamp,
expireTimer, expireTimer,
profileKey, profileKey,
}, },
@ -1575,6 +1588,7 @@ export default class MessageSender {
quote, quote,
reaction, reaction,
sticker, sticker,
deletedForEveryoneTimestamp,
timestamp, timestamp,
}: { }: {
attachments?: Array<AttachmentType>; attachments?: Array<AttachmentType>;
@ -1587,6 +1601,7 @@ export default class MessageSender {
quote?: any; quote?: any;
reaction?: any; reaction?: any;
sticker?: any; sticker?: any;
deletedForEveryoneTimestamp?: number;
timestamp: number; timestamp: number;
}, },
options?: SendOptionsType options?: SendOptionsType
@ -1625,6 +1640,7 @@ export default class MessageSender {
reaction, reaction,
expireTimer, expireTimer,
profileKey, profileKey,
deletedForEveryoneTimestamp,
groupV2, groupV2,
group: groupV1 group: groupV1
? { ? {

View file

@ -13057,7 +13057,7 @@
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/conversation/Message.js", "path": "ts/components/conversation/Message.js",
"line": " this.audioRef = react_1.default.createRef();", "line": " this.audioRef = react_1.default.createRef();",
"lineNumber": 58, "lineNumber": 59,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-08-28T16:12:19.904Z" "updated": "2020-08-28T16:12:19.904Z"
}, },
@ -13065,7 +13065,7 @@
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/conversation/Message.js", "path": "ts/components/conversation/Message.js",
"line": " this.focusRef = react_1.default.createRef();", "line": " this.focusRef = react_1.default.createRef();",
"lineNumber": 59, "lineNumber": 60,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-09-11T17:24:56.124Z", "updated": "2020-09-11T17:24:56.124Z",
"reasonDetail": "Used for managing focus only" "reasonDetail": "Used for managing focus only"
@ -13074,7 +13074,7 @@
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/conversation/Message.js", "path": "ts/components/conversation/Message.js",
"line": " this.reactionsContainerRef = react_1.default.createRef();", "line": " this.reactionsContainerRef = react_1.default.createRef();",
"lineNumber": 60, "lineNumber": 61,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-08-28T16:12:19.904Z", "updated": "2020-08-28T16:12:19.904Z",
"reasonDetail": "Used for detecting clicks outside reaction viewer" "reasonDetail": "Used for detecting clicks outside reaction viewer"
@ -13083,7 +13083,7 @@
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/conversation/Message.tsx", "path": "ts/components/conversation/Message.tsx",
"line": " public audioRef: React.RefObject<HTMLAudioElement> = React.createRef();", "line": " public audioRef: React.RefObject<HTMLAudioElement> = React.createRef();",
"lineNumber": 211, "lineNumber": 215,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-09-08T20:19:01.913Z" "updated": "2020-09-08T20:19:01.913Z"
}, },
@ -13091,7 +13091,7 @@
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/conversation/Message.tsx", "path": "ts/components/conversation/Message.tsx",
"line": " public focusRef: React.RefObject<HTMLDivElement> = React.createRef();", "line": " public focusRef: React.RefObject<HTMLDivElement> = React.createRef();",
"lineNumber": 213, "lineNumber": 217,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-09-08T20:19:01.913Z" "updated": "2020-09-08T20:19:01.913Z"
}, },
@ -13099,7 +13099,7 @@
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/conversation/Message.tsx", "path": "ts/components/conversation/Message.tsx",
"line": " > = React.createRef();", "line": " > = React.createRef();",
"lineNumber": 217, "lineNumber": 221,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-08-28T19:36:40.817Z" "updated": "2020-08-28T19:36:40.817Z"
}, },

View file

@ -308,6 +308,11 @@ Whisper.ConversationView = Whisper.View.extend({
); );
this.listenTo(this.model.messageCollection, 'force-send', this.forceSend); this.listenTo(this.model.messageCollection, 'force-send', this.forceSend);
this.listenTo(this.model.messageCollection, 'delete', this.deleteMessage); this.listenTo(this.model.messageCollection, 'delete', this.deleteMessage);
this.listenTo(
this.model.messageCollection,
'delete-for-everyone',
this.deleteMessageForEveryone
);
this.listenTo( this.listenTo(
this.model.messageCollection, this.model.messageCollection,
'show-visual-attachment', 'show-visual-attachment',
@ -618,6 +623,9 @@ Whisper.ConversationView = Whisper.View.extend({
const deleteMessage = (messageId: any) => { const deleteMessage = (messageId: any) => {
this.deleteMessage(messageId); this.deleteMessage(messageId);
}; };
const deleteMessageForEveryone = (messageId: string) => {
this.deleteMessageForEveryone(messageId);
};
const showMessageDetail = (messageId: any) => { const showMessageDetail = (messageId: any) => {
this.showMessageDetail(messageId); this.showMessageDetail(messageId);
}; };
@ -796,6 +804,7 @@ Whisper.ConversationView = Whisper.View.extend({
id, id,
deleteMessage, deleteMessage,
deleteMessageForEveryone,
displayTapToViewMessage, displayTapToViewMessage,
downloadAttachment, downloadAttachment,
downloadNewVersion, downloadNewVersion,
@ -2330,6 +2339,27 @@ Whisper.ConversationView = Whisper.View.extend({
dialog.focusCancel(); dialog.focusCancel();
}, },
deleteMessageForEveryone(messageId: string) {
const message = this.model.messageCollection.get(messageId);
if (!message) {
throw new Error(
`deleteMessageForEveryone: Did not find message for id ${messageId}`
);
}
const dialog = new Whisper.ConfirmationDialogView({
message: window.i18n('deleteForEveryoneWarning'),
okText: window.i18n('delete'),
resolve: async () => {
await this.model.sendDeleteForEveryoneMessage(message.get('sent_at'));
this.resetPanel();
},
});
this.$el.prepend(dialog.el);
dialog.focusCancel();
},
showStickerPackPreview(packId: any, packKey: any) { showStickerPackPreview(packId: any, packKey: any) {
window.Signal.Stickers.downloadEphemeralPack(packId, packKey); window.Signal.Stickers.downloadEphemeralPack(packId, packKey);