Delete For Everyone Send
Co-authored-by: Chris Svenningsen <chris@carbonfive.com>
This commit is contained in:
parent
693deaebe8
commit
866217a724
14 changed files with 276 additions and 10 deletions
|
@ -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."
|
||||||
|
|
|
@ -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'));
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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'),
|
||||||
|
|
|
@ -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
1
ts/model-types.d.ts
vendored
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
? {
|
? {
|
||||||
|
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue