Delete for everyone: Track sends and show failure states
This commit is contained in:
parent
688cca1806
commit
0a52318be6
24 changed files with 426 additions and 60 deletions
|
@ -1172,6 +1172,10 @@
|
||||||
"message": "Retry Send",
|
"message": "Retry Send",
|
||||||
"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"
|
||||||
},
|
},
|
||||||
|
"retryDeleteForEveryone": {
|
||||||
|
"message": "Retry Delete for Everyone",
|
||||||
|
"description": "Shown on the drop-down menu for an individual message, but only if a previous delete for everyone failed to send"
|
||||||
|
},
|
||||||
"forwardMessage": {
|
"forwardMessage": {
|
||||||
"message": "Forward message",
|
"message": "Forward message",
|
||||||
"description": "Shown on the drop-down menu for an individual message, forwards a message"
|
"description": "Shown on the drop-down menu for an individual message, forwards a message"
|
||||||
|
@ -1983,6 +1987,10 @@
|
||||||
"message": "Send failed",
|
"message": "Send failed",
|
||||||
"description": "Shown on outgoing message if it fails to send"
|
"description": "Shown on outgoing message if it fails to send"
|
||||||
},
|
},
|
||||||
|
"deleteFailed": {
|
||||||
|
"message": "Delete failed",
|
||||||
|
"description": "Shown on a message which was deleted for everyone if the delete wasn't successfully sent to anyone"
|
||||||
|
},
|
||||||
"sendPaused": {
|
"sendPaused": {
|
||||||
"message": "Send paused",
|
"message": "Send paused",
|
||||||
"description": "Shown on outgoing message if it cannot be sent immediately"
|
"description": "Shown on outgoing message if it cannot be sent immediately"
|
||||||
|
@ -1991,6 +1999,10 @@
|
||||||
"message": "Partially sent, click for details",
|
"message": "Partially sent, click for details",
|
||||||
"description": "Shown on outgoing message if it is partially sent"
|
"description": "Shown on outgoing message if it is partially sent"
|
||||||
},
|
},
|
||||||
|
"partiallyDeleted": {
|
||||||
|
"message": "Partially deleted, click to retry",
|
||||||
|
"description": "Shown on a message which was deleted for everyone if the delete wasn't successfully sent to everyone"
|
||||||
|
},
|
||||||
"showMore": {
|
"showMore": {
|
||||||
"message": "Details",
|
"message": "Details",
|
||||||
"description": "Displays the details of a key change"
|
"description": "Displays the details of a key change"
|
||||||
|
|
|
@ -104,6 +104,8 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||||
canReply: true,
|
canReply: true,
|
||||||
canDownload: true,
|
canDownload: true,
|
||||||
canDeleteForEveryone: overrideProps.canDeleteForEveryone || false,
|
canDeleteForEveryone: overrideProps.canDeleteForEveryone || false,
|
||||||
|
canRetry: overrideProps.canRetry || false,
|
||||||
|
canRetryDeleteForEveryone: overrideProps.canRetryDeleteForEveryone || false,
|
||||||
checkForAccount: action('checkForAccount'),
|
checkForAccount: action('checkForAccount'),
|
||||||
clearSelectedMessage: action('clearSelectedMessage'),
|
clearSelectedMessage: action('clearSelectedMessage'),
|
||||||
collapseMetadata: overrideProps.collapseMetadata,
|
collapseMetadata: overrideProps.collapseMetadata,
|
||||||
|
@ -165,6 +167,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||||
renderAudioAttachment,
|
renderAudioAttachment,
|
||||||
replyToMessage: action('replyToMessage'),
|
replyToMessage: action('replyToMessage'),
|
||||||
retrySend: action('retrySend'),
|
retrySend: action('retrySend'),
|
||||||
|
retryDeleteForEveryone: action('retryDeleteForEveryone'),
|
||||||
scrollToQuotedMessage: action('scrollToQuotedMessage'),
|
scrollToQuotedMessage: action('scrollToQuotedMessage'),
|
||||||
selectMessage: action('selectMessage'),
|
selectMessage: action('selectMessage'),
|
||||||
showContactDetail: action('showContactDetail'),
|
showContactDetail: action('showContactDetail'),
|
||||||
|
@ -574,13 +577,23 @@ story.add('Sticker', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
story.add('Deleted', () => {
|
story.add('Deleted', () => {
|
||||||
const props = createProps({
|
const propsSent = createProps({
|
||||||
conversationType: 'group',
|
conversationType: 'group',
|
||||||
deletedForEveryone: true,
|
deletedForEveryone: true,
|
||||||
status: 'sent',
|
status: 'sent',
|
||||||
});
|
});
|
||||||
|
const propsSending = createProps({
|
||||||
|
conversationType: 'group',
|
||||||
|
deletedForEveryone: true,
|
||||||
|
status: 'sending',
|
||||||
|
});
|
||||||
|
|
||||||
return renderBothDirections(props);
|
return (
|
||||||
|
<>
|
||||||
|
{renderBothDirections(propsSent)}
|
||||||
|
{renderBothDirections(propsSending)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
story.add('Deleted with expireTimer', () => {
|
story.add('Deleted with expireTimer', () => {
|
||||||
|
@ -596,6 +609,30 @@ story.add('Deleted with expireTimer', () => {
|
||||||
return renderBothDirections(props);
|
return renderBothDirections(props);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
story.add('Deleted with error', () => {
|
||||||
|
const propsPartialError = createProps({
|
||||||
|
timestamp: Date.now() - 60 * 1000,
|
||||||
|
canDeleteForEveryone: true,
|
||||||
|
conversationType: 'group',
|
||||||
|
deletedForEveryone: true,
|
||||||
|
status: 'partial-sent',
|
||||||
|
});
|
||||||
|
const propsError = createProps({
|
||||||
|
timestamp: Date.now() - 60 * 1000,
|
||||||
|
canDeleteForEveryone: true,
|
||||||
|
conversationType: 'group',
|
||||||
|
deletedForEveryone: true,
|
||||||
|
status: 'error',
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{renderBothDirections(propsPartialError)}
|
||||||
|
{renderBothDirections(propsError)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
story.add('Can delete for everyone', () => {
|
story.add('Can delete for everyone', () => {
|
||||||
const props = createProps({
|
const props = createProps({
|
||||||
status: 'read',
|
status: 'read',
|
||||||
|
@ -609,6 +646,7 @@ story.add('Can delete for everyone', () => {
|
||||||
story.add('Error', () => {
|
story.add('Error', () => {
|
||||||
const props = createProps({
|
const props = createProps({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
|
canRetry: true,
|
||||||
text: 'I hope you get this.',
|
text: 'I hope you get this.',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1298,6 +1336,8 @@ story.add('All the context menus', () => {
|
||||||
],
|
],
|
||||||
status: 'partial-sent',
|
status: 'partial-sent',
|
||||||
canDeleteForEveryone: true,
|
canDeleteForEveryone: true,
|
||||||
|
canRetry: true,
|
||||||
|
canRetryDeleteForEveryone: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
return <Message {...props} direction="outgoing" />;
|
return <Message {...props} direction="outgoing" />;
|
||||||
|
|
|
@ -197,6 +197,8 @@ export type PropsData = {
|
||||||
|
|
||||||
deletedForEveryone?: boolean;
|
deletedForEveryone?: boolean;
|
||||||
|
|
||||||
|
canRetry: boolean;
|
||||||
|
canRetryDeleteForEveryone: boolean;
|
||||||
canReact: boolean;
|
canReact: boolean;
|
||||||
canReply: boolean;
|
canReply: boolean;
|
||||||
canDownload: boolean;
|
canDownload: boolean;
|
||||||
|
@ -234,6 +236,7 @@ export type PropsActions = {
|
||||||
{ emoji, remove }: { emoji: string; remove: boolean }
|
{ emoji, remove }: { emoji: string; remove: boolean }
|
||||||
) => void;
|
) => void;
|
||||||
replyToMessage: (id: string) => void;
|
replyToMessage: (id: string) => void;
|
||||||
|
retryDeleteForEveryone: (id: string) => void;
|
||||||
retrySend: (id: string) => void;
|
retrySend: (id: string) => void;
|
||||||
showForwardMessageModal: (id: string) => void;
|
showForwardMessageModal: (id: string) => void;
|
||||||
deleteMessage: (id: string) => void;
|
deleteMessage: (id: string) => void;
|
||||||
|
@ -424,7 +427,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
|
|
||||||
public override componentDidMount(): void {
|
public override componentDidMount(): void {
|
||||||
const { conversationId } = this.props;
|
const { conversationId } = this.props;
|
||||||
window.ConversationController.onConvoMessageMount(conversationId);
|
window.ConversationController?.onConvoMessageMount(conversationId);
|
||||||
|
|
||||||
this.startSelectedTimer();
|
this.startSelectedTimer();
|
||||||
this.startDeleteForEveryoneTimerIfApplicable();
|
this.startDeleteForEveryoneTimerIfApplicable();
|
||||||
|
@ -1486,29 +1489,24 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
canDownload,
|
canDownload,
|
||||||
canReact,
|
canReact,
|
||||||
canReply,
|
canReply,
|
||||||
|
canRetry,
|
||||||
|
canRetryDeleteForEveryone,
|
||||||
deleteMessage,
|
deleteMessage,
|
||||||
deleteMessageForEveryone,
|
deleteMessageForEveryone,
|
||||||
deletedForEveryone,
|
deletedForEveryone,
|
||||||
direction,
|
|
||||||
i18n,
|
i18n,
|
||||||
id,
|
id,
|
||||||
isSticker,
|
isSticker,
|
||||||
isTapToView,
|
isTapToView,
|
||||||
replyToMessage,
|
replyToMessage,
|
||||||
retrySend,
|
retrySend,
|
||||||
|
retryDeleteForEveryone,
|
||||||
showForwardMessageModal,
|
showForwardMessageModal,
|
||||||
showMessageDetail,
|
showMessageDetail,
|
||||||
status,
|
|
||||||
text,
|
text,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const canForward = !isTapToView && !deletedForEveryone;
|
const canForward = !isTapToView && !deletedForEveryone;
|
||||||
|
|
||||||
const showRetry =
|
|
||||||
(status === 'paused' ||
|
|
||||||
status === 'error' ||
|
|
||||||
status === 'partial-sent') &&
|
|
||||||
direction === 'outgoing';
|
|
||||||
const multipleAttachments = attachments && attachments.length > 1;
|
const multipleAttachments = attachments && attachments.length > 1;
|
||||||
|
|
||||||
const shouldShowAdditional =
|
const shouldShowAdditional =
|
||||||
|
@ -1583,7 +1581,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
>
|
>
|
||||||
{i18n('moreInfo')}
|
{i18n('moreInfo')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{showRetry ? (
|
{canRetry ? (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
attributes={{
|
attributes={{
|
||||||
className:
|
className:
|
||||||
|
@ -1599,6 +1597,22 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
{i18n('retrySend')}
|
{i18n('retrySend')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
) : null}
|
) : null}
|
||||||
|
{canRetryDeleteForEveryone ? (
|
||||||
|
<MenuItem
|
||||||
|
attributes={{
|
||||||
|
className:
|
||||||
|
'module-message__context--icon module-message__context__delete-message-for-everyone',
|
||||||
|
}}
|
||||||
|
onClick={(event: React.MouseEvent) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
retryDeleteForEveryone(id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{i18n('retryDeleteForEveryone')}
|
||||||
|
</MenuItem>
|
||||||
|
) : null}
|
||||||
{canForward ? (
|
{canForward ? (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
attributes={{
|
attributes={{
|
||||||
|
|
|
@ -29,6 +29,8 @@ const defaultMessage: MessageDataPropsType = {
|
||||||
}),
|
}),
|
||||||
canReact: true,
|
canReact: true,
|
||||||
canReply: true,
|
canReply: true,
|
||||||
|
canRetry: true,
|
||||||
|
canRetryDeleteForEveryone: true,
|
||||||
canDeleteForEveryone: true,
|
canDeleteForEveryone: true,
|
||||||
canDownload: true,
|
canDownload: true,
|
||||||
conversationColor: 'crimson',
|
conversationColor: 'crimson',
|
||||||
|
@ -84,6 +86,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||||
renderReactionPicker: () => <div />,
|
renderReactionPicker: () => <div />,
|
||||||
replyToMessage: action('replyToMessage'),
|
replyToMessage: action('replyToMessage'),
|
||||||
retrySend: action('retrySend'),
|
retrySend: action('retrySend'),
|
||||||
|
retryDeleteForEveryone: action('retryDeleteForEveryone'),
|
||||||
showContactDetail: action('showContactDetail'),
|
showContactDetail: action('showContactDetail'),
|
||||||
showContactModal: action('showContactModal'),
|
showContactModal: action('showContactModal'),
|
||||||
showExpiredIncomingTapToViewToast: action(
|
showExpiredIncomingTapToViewToast: action(
|
||||||
|
|
|
@ -81,6 +81,7 @@ export type PropsBackboneActions = Pick<
|
||||||
| 'renderEmojiPicker'
|
| 'renderEmojiPicker'
|
||||||
| 'renderReactionPicker'
|
| 'renderReactionPicker'
|
||||||
| 'replyToMessage'
|
| 'replyToMessage'
|
||||||
|
| 'retryDeleteForEveryone'
|
||||||
| 'retrySend'
|
| 'retrySend'
|
||||||
| 'showContactDetail'
|
| 'showContactDetail'
|
||||||
| 'showContactModal'
|
| 'showContactModal'
|
||||||
|
@ -303,6 +304,7 @@ export class MessageDetail extends React.Component<Props, State> {
|
||||||
renderEmojiPicker,
|
renderEmojiPicker,
|
||||||
renderReactionPicker,
|
renderReactionPicker,
|
||||||
replyToMessage,
|
replyToMessage,
|
||||||
|
retryDeleteForEveryone,
|
||||||
retrySend,
|
retrySend,
|
||||||
showContactDetail,
|
showContactDetail,
|
||||||
showContactModal,
|
showContactModal,
|
||||||
|
@ -358,6 +360,7 @@ export class MessageDetail extends React.Component<Props, State> {
|
||||||
renderEmojiPicker={renderEmojiPicker}
|
renderEmojiPicker={renderEmojiPicker}
|
||||||
renderReactionPicker={renderReactionPicker}
|
renderReactionPicker={renderReactionPicker}
|
||||||
replyToMessage={replyToMessage}
|
replyToMessage={replyToMessage}
|
||||||
|
retryDeleteForEveryone={retryDeleteForEveryone}
|
||||||
retrySend={retrySend}
|
retrySend={retrySend}
|
||||||
showForwardMessageModal={showForwardMessageModal}
|
showForwardMessageModal={showForwardMessageModal}
|
||||||
scrollToQuotedMessage={() => {
|
scrollToQuotedMessage={() => {
|
||||||
|
|
|
@ -61,7 +61,9 @@ export const MessageMetadata: FunctionComponent<PropsType> = props => {
|
||||||
if (isError || isPartiallySent || isPaused) {
|
if (isError || isPartiallySent || isPaused) {
|
||||||
let statusInfo: React.ReactChild;
|
let statusInfo: React.ReactChild;
|
||||||
if (isError) {
|
if (isError) {
|
||||||
statusInfo = i18n('sendFailed');
|
statusInfo = deletedForEveryone
|
||||||
|
? i18n('deleteFailed')
|
||||||
|
: i18n('sendFailed');
|
||||||
} else if (isPaused) {
|
} else if (isPaused) {
|
||||||
statusInfo = i18n('sendPaused');
|
statusInfo = i18n('sendPaused');
|
||||||
} else {
|
} else {
|
||||||
|
@ -76,7 +78,9 @@ export const MessageMetadata: FunctionComponent<PropsType> = props => {
|
||||||
showMessageDetail(id);
|
showMessageDetail(id);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{i18n('partiallySent')}
|
{deletedForEveryone
|
||||||
|
? i18n('partiallyDeleted')
|
||||||
|
: i18n('partiallySent')}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -136,7 +140,7 @@ export const MessageMetadata: FunctionComponent<PropsType> = props => {
|
||||||
<Spinner svgSize="small" size="14px" direction={direction} />
|
<Spinner svgSize="small" size="14px" direction={direction} />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{!deletedForEveryone &&
|
{(!deletedForEveryone || status === 'sending') &&
|
||||||
!textPending &&
|
!textPending &&
|
||||||
direction === 'outgoing' &&
|
direction === 'outgoing' &&
|
||||||
status !== 'error' &&
|
status !== 'error' &&
|
||||||
|
|
|
@ -39,6 +39,8 @@ const defaultMessageProps: MessagesProps = {
|
||||||
}),
|
}),
|
||||||
canReact: true,
|
canReact: true,
|
||||||
canReply: true,
|
canReply: true,
|
||||||
|
canRetry: true,
|
||||||
|
canRetryDeleteForEveryone: true,
|
||||||
canDeleteForEveryone: true,
|
canDeleteForEveryone: true,
|
||||||
canDownload: true,
|
canDownload: true,
|
||||||
checkForAccount: action('checkForAccount'),
|
checkForAccount: action('checkForAccount'),
|
||||||
|
@ -78,6 +80,7 @@ const defaultMessageProps: MessagesProps = {
|
||||||
renderAudioAttachment: () => <div>*AudioAttachment*</div>,
|
renderAudioAttachment: () => <div>*AudioAttachment*</div>,
|
||||||
replyToMessage: action('default--replyToMessage'),
|
replyToMessage: action('default--replyToMessage'),
|
||||||
retrySend: action('default--retrySend'),
|
retrySend: action('default--retrySend'),
|
||||||
|
retryDeleteForEveryone: action('default--retryDeleteForEveryone'),
|
||||||
scrollToQuotedMessage: action('default--scrollToQuotedMessage'),
|
scrollToQuotedMessage: action('default--scrollToQuotedMessage'),
|
||||||
selectMessage: action('default--selectMessage'),
|
selectMessage: action('default--selectMessage'),
|
||||||
showContactDetail: action('default--showContactDetail'),
|
showContactDetail: action('default--showContactDetail'),
|
||||||
|
|
|
@ -50,6 +50,8 @@ const items: Record<string, TimelineItemType> = {
|
||||||
canDownload: true,
|
canDownload: true,
|
||||||
canReact: true,
|
canReact: true,
|
||||||
canReply: true,
|
canReply: true,
|
||||||
|
canRetry: true,
|
||||||
|
canRetryDeleteForEveryone: true,
|
||||||
conversationColor: 'forest',
|
conversationColor: 'forest',
|
||||||
conversationId: 'conversation-id',
|
conversationId: 'conversation-id',
|
||||||
conversationType: 'group',
|
conversationType: 'group',
|
||||||
|
@ -72,6 +74,8 @@ const items: Record<string, TimelineItemType> = {
|
||||||
canDownload: true,
|
canDownload: true,
|
||||||
canReact: true,
|
canReact: true,
|
||||||
canReply: true,
|
canReply: true,
|
||||||
|
canRetry: true,
|
||||||
|
canRetryDeleteForEveryone: true,
|
||||||
conversationColor: 'forest',
|
conversationColor: 'forest',
|
||||||
conversationId: 'conversation-id',
|
conversationId: 'conversation-id',
|
||||||
conversationType: 'group',
|
conversationType: 'group',
|
||||||
|
@ -108,6 +112,8 @@ const items: Record<string, TimelineItemType> = {
|
||||||
canDownload: true,
|
canDownload: true,
|
||||||
canReact: true,
|
canReact: true,
|
||||||
canReply: true,
|
canReply: true,
|
||||||
|
canRetry: true,
|
||||||
|
canRetryDeleteForEveryone: true,
|
||||||
conversationColor: 'crimson',
|
conversationColor: 'crimson',
|
||||||
conversationId: 'conversation-id',
|
conversationId: 'conversation-id',
|
||||||
conversationType: 'group',
|
conversationType: 'group',
|
||||||
|
@ -205,6 +211,8 @@ const items: Record<string, TimelineItemType> = {
|
||||||
canDownload: true,
|
canDownload: true,
|
||||||
canReact: true,
|
canReact: true,
|
||||||
canReply: true,
|
canReply: true,
|
||||||
|
canRetry: true,
|
||||||
|
canRetryDeleteForEveryone: true,
|
||||||
conversationColor: 'plum',
|
conversationColor: 'plum',
|
||||||
conversationId: 'conversation-id',
|
conversationId: 'conversation-id',
|
||||||
conversationType: 'group',
|
conversationType: 'group',
|
||||||
|
@ -228,6 +236,8 @@ const items: Record<string, TimelineItemType> = {
|
||||||
canDownload: true,
|
canDownload: true,
|
||||||
canReact: true,
|
canReact: true,
|
||||||
canReply: true,
|
canReply: true,
|
||||||
|
canRetry: true,
|
||||||
|
canRetryDeleteForEveryone: true,
|
||||||
conversationColor: 'crimson',
|
conversationColor: 'crimson',
|
||||||
conversationId: 'conversation-id',
|
conversationId: 'conversation-id',
|
||||||
conversationType: 'group',
|
conversationType: 'group',
|
||||||
|
@ -251,6 +261,8 @@ const items: Record<string, TimelineItemType> = {
|
||||||
canDownload: true,
|
canDownload: true,
|
||||||
canReact: true,
|
canReact: true,
|
||||||
canReply: true,
|
canReply: true,
|
||||||
|
canRetry: true,
|
||||||
|
canRetryDeleteForEveryone: true,
|
||||||
conversationColor: 'crimson',
|
conversationColor: 'crimson',
|
||||||
conversationId: 'conversation-id',
|
conversationId: 'conversation-id',
|
||||||
conversationType: 'group',
|
conversationType: 'group',
|
||||||
|
@ -274,6 +286,8 @@ const items: Record<string, TimelineItemType> = {
|
||||||
canDownload: true,
|
canDownload: true,
|
||||||
canReact: true,
|
canReact: true,
|
||||||
canReply: true,
|
canReply: true,
|
||||||
|
canRetry: true,
|
||||||
|
canRetryDeleteForEveryone: true,
|
||||||
conversationColor: 'crimson',
|
conversationColor: 'crimson',
|
||||||
conversationId: 'conversation-id',
|
conversationId: 'conversation-id',
|
||||||
conversationType: 'group',
|
conversationType: 'group',
|
||||||
|
@ -297,6 +311,8 @@ const items: Record<string, TimelineItemType> = {
|
||||||
canDownload: true,
|
canDownload: true,
|
||||||
canReact: true,
|
canReact: true,
|
||||||
canReply: true,
|
canReply: true,
|
||||||
|
canRetry: true,
|
||||||
|
canRetryDeleteForEveryone: true,
|
||||||
conversationColor: 'crimson',
|
conversationColor: 'crimson',
|
||||||
conversationId: 'conversation-id',
|
conversationId: 'conversation-id',
|
||||||
conversationType: 'group',
|
conversationType: 'group',
|
||||||
|
@ -340,6 +356,7 @@ const actions = () => ({
|
||||||
|
|
||||||
reactToMessage: action('reactToMessage'),
|
reactToMessage: action('reactToMessage'),
|
||||||
replyToMessage: action('replyToMessage'),
|
replyToMessage: action('replyToMessage'),
|
||||||
|
retryDeleteForEveryone: action('retryDeleteForEveryone'),
|
||||||
retrySend: action('retrySend'),
|
retrySend: action('retrySend'),
|
||||||
deleteMessage: action('deleteMessage'),
|
deleteMessage: action('deleteMessage'),
|
||||||
deleteMessageForEveryone: action('deleteMessageForEveryone'),
|
deleteMessageForEveryone: action('deleteMessageForEveryone'),
|
||||||
|
|
|
@ -217,6 +217,7 @@ const getActions = createSelector(
|
||||||
'checkForAccount',
|
'checkForAccount',
|
||||||
'reactToMessage',
|
'reactToMessage',
|
||||||
'replyToMessage',
|
'replyToMessage',
|
||||||
|
'retryDeleteForEveryone',
|
||||||
'retrySend',
|
'retrySend',
|
||||||
'showForwardMessageModal',
|
'showForwardMessageModal',
|
||||||
'deleteMessage',
|
'deleteMessage',
|
||||||
|
|
|
@ -63,6 +63,7 @@ const getDefaultProps = () => ({
|
||||||
clearSelectedMessage: action('clearSelectedMessage'),
|
clearSelectedMessage: action('clearSelectedMessage'),
|
||||||
contactSupport: action('contactSupport'),
|
contactSupport: action('contactSupport'),
|
||||||
replyToMessage: action('replyToMessage'),
|
replyToMessage: action('replyToMessage'),
|
||||||
|
retryDeleteForEveryone: action('retryDeleteForEveryone'),
|
||||||
retrySend: action('retrySend'),
|
retrySend: action('retrySend'),
|
||||||
deleteMessage: action('deleteMessage'),
|
deleteMessage: action('deleteMessage'),
|
||||||
deleteMessageForEveryone: action('deleteMessageForEveryone'),
|
deleteMessageForEveryone: action('deleteMessageForEveryone'),
|
||||||
|
|
|
@ -657,14 +657,6 @@ export async function buildAddMembersChange(
|
||||||
const profileKey = contact.get('profileKey');
|
const profileKey = contact.get('profileKey');
|
||||||
const profileKeyCredential = contact.get('profileKeyCredential');
|
const profileKeyCredential = contact.get('profileKeyCredential');
|
||||||
|
|
||||||
if (!profileKey) {
|
|
||||||
assert(
|
|
||||||
false,
|
|
||||||
`buildAddMembersChange/${logId}: member is missing profile key; skipping`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const member = new Proto.Member();
|
const member = new Proto.Member();
|
||||||
member.userId = encryptUuid(clientZkGroupCipher, uuid);
|
member.userId = encryptUuid(clientZkGroupCipher, uuid);
|
||||||
member.role = MEMBER_ROLE_ENUM.DEFAULT;
|
member.role = MEMBER_ROLE_ENUM.DEFAULT;
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { isGroup } from '../../util/whatTypeOfConversation';
|
||||||
export function areAllErrorsUnregistered(
|
export function areAllErrorsUnregistered(
|
||||||
conversation: ConversationAttributesType,
|
conversation: ConversationAttributesType,
|
||||||
error: unknown
|
error: unknown
|
||||||
): boolean {
|
): error is SendMessageProtoError {
|
||||||
return Boolean(
|
return Boolean(
|
||||||
isGroup(conversation) &&
|
isGroup(conversation) &&
|
||||||
error instanceof SendMessageProtoError &&
|
error instanceof SendMessageProtoError &&
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
import { isNumber } from 'lodash';
|
import { isNumber } from 'lodash';
|
||||||
|
|
||||||
|
import * as Errors from '../../types/errors';
|
||||||
import { getSendOptions } from '../../util/getSendOptions';
|
import { getSendOptions } from '../../util/getSendOptions';
|
||||||
import {
|
import {
|
||||||
isDirectConversation,
|
isDirectConversation,
|
||||||
|
@ -26,10 +27,14 @@ import { getUntrustedConversationIds } from './getUntrustedConversationIds';
|
||||||
import { handleMessageSend } from '../../util/handleMessageSend';
|
import { handleMessageSend } from '../../util/handleMessageSend';
|
||||||
import { isConversationAccepted } from '../../util/isConversationAccepted';
|
import { isConversationAccepted } from '../../util/isConversationAccepted';
|
||||||
import { isConversationUnregistered } from '../../util/isConversationUnregistered';
|
import { isConversationUnregistered } from '../../util/isConversationUnregistered';
|
||||||
|
import { getMessageById } from '../../messages/getMessageById';
|
||||||
|
import { isNotNil } from '../../util/isNotNil';
|
||||||
|
import type { CallbackResultType } from '../../textsecure/Types.d';
|
||||||
|
import type { MessageModel } from '../../models/messages';
|
||||||
|
import { SendMessageProtoError } from '../../textsecure/Errors';
|
||||||
|
import { strictAssert } from '../../util/assert';
|
||||||
|
import type { LoggerType } from '../../types/Logging';
|
||||||
|
|
||||||
// Note: because we don't have a recipient map, if some sends fail, we will resend this
|
|
||||||
// message to folks that got it on the first go-round. This is okay, because a delete
|
|
||||||
// for everyone has no effect when applied the second time on a message.
|
|
||||||
export async function sendDeleteForEveryone(
|
export async function sendDeleteForEveryone(
|
||||||
conversation: ConversationModel,
|
conversation: ConversationModel,
|
||||||
{
|
{
|
||||||
|
@ -41,12 +46,25 @@ export async function sendDeleteForEveryone(
|
||||||
}: ConversationQueueJobBundle,
|
}: ConversationQueueJobBundle,
|
||||||
data: DeleteForEveryoneJobData
|
data: DeleteForEveryoneJobData
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!shouldContinue) {
|
const {
|
||||||
log.info('Ran out of time. Giving up on sending delete for everyone');
|
messageId,
|
||||||
|
recipients: recipientsFromJob,
|
||||||
|
revision,
|
||||||
|
targetTimestamp,
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
const message = await getMessageById(messageId);
|
||||||
|
if (!message) {
|
||||||
|
log.error(`Failed to fetch message ${messageId}. Failing job.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shouldContinue) {
|
||||||
|
log.info('Ran out of time. Giving up on sending delete for everyone');
|
||||||
|
updateMessageWithFailure(message, [new Error('Ran out of time!')], log);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { messageId, recipients, revision, targetTimestamp } = data;
|
|
||||||
const sendType = 'deleteForEveryone';
|
const sendType = 'deleteForEveryone';
|
||||||
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
|
||||||
const contentHint = ContentHint.RESENDABLE;
|
const contentHint = ContentHint.RESENDABLE;
|
||||||
|
@ -54,6 +72,13 @@ export async function sendDeleteForEveryone(
|
||||||
|
|
||||||
const logId = `deleteForEveryone/${conversation.idForLogging()}`;
|
const logId = `deleteForEveryone/${conversation.idForLogging()}`;
|
||||||
|
|
||||||
|
const deletedForEveryoneSendStatus = message.get(
|
||||||
|
'deletedForEveryoneSendStatus'
|
||||||
|
);
|
||||||
|
const recipients = deletedForEveryoneSendStatus
|
||||||
|
? getRecipients(deletedForEveryoneSendStatus)
|
||||||
|
: recipientsFromJob;
|
||||||
|
|
||||||
const untrustedConversationIds = getUntrustedConversationIds(recipients);
|
const untrustedConversationIds = getUntrustedConversationIds(recipients);
|
||||||
if (untrustedConversationIds.length) {
|
if (untrustedConversationIds.length) {
|
||||||
window.reduxActions.conversations.conversationStoppedByMissingVerification({
|
window.reduxActions.conversations.conversationStoppedByMissingVerification({
|
||||||
|
@ -89,13 +114,10 @@ export async function sendDeleteForEveryone(
|
||||||
recipients: conversation.getRecipients(),
|
recipients: conversation.getRecipients(),
|
||||||
timestamp,
|
timestamp,
|
||||||
});
|
});
|
||||||
|
strictAssert(
|
||||||
if (!proto.dataMessage) {
|
proto.dataMessage,
|
||||||
log.error(
|
'ContentMessage must have dataMessage'
|
||||||
"ContentMessage proto didn't have a data message; cancelling job."
|
|
||||||
);
|
);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await handleMessageSend(
|
await handleMessageSend(
|
||||||
window.textsecure.messaging.sendSyncMessage({
|
window.textsecure.messaging.sendSyncMessage({
|
||||||
|
@ -110,23 +132,39 @@ export async function sendDeleteForEveryone(
|
||||||
}),
|
}),
|
||||||
{ messageIds, sendType }
|
{ messageIds, sendType }
|
||||||
);
|
);
|
||||||
|
await updateMessageWithSuccessfulSends(message);
|
||||||
} else if (isDirectConversation(conversation.attributes)) {
|
} else if (isDirectConversation(conversation.attributes)) {
|
||||||
if (!isConversationAccepted(conversation.attributes)) {
|
if (!isConversationAccepted(conversation.attributes)) {
|
||||||
log.info(
|
log.info(
|
||||||
`conversation ${conversation.idForLogging()} is not accepted; refusing to send`
|
`conversation ${conversation.idForLogging()} is not accepted; refusing to send`
|
||||||
);
|
);
|
||||||
|
updateMessageWithFailure(
|
||||||
|
message,
|
||||||
|
[new Error('Message request was not accepted')],
|
||||||
|
log
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isConversationUnregistered(conversation.attributes)) {
|
if (isConversationUnregistered(conversation.attributes)) {
|
||||||
log.info(
|
log.info(
|
||||||
`conversation ${conversation.idForLogging()} is unregistered; refusing to send`
|
`conversation ${conversation.idForLogging()} is unregistered; refusing to send`
|
||||||
);
|
);
|
||||||
|
updateMessageWithFailure(
|
||||||
|
message,
|
||||||
|
[new Error('Contact no longer has a Signal account')],
|
||||||
|
log
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (conversation.isBlocked()) {
|
if (conversation.isBlocked()) {
|
||||||
log.info(
|
log.info(
|
||||||
`conversation ${conversation.idForLogging()} is blocked; refusing to send`
|
`conversation ${conversation.idForLogging()} is blocked; refusing to send`
|
||||||
);
|
);
|
||||||
|
updateMessageWithFailure(
|
||||||
|
message,
|
||||||
|
[new Error('Contact is blocked')],
|
||||||
|
log
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,6 +189,8 @@ export async function sendDeleteForEveryone(
|
||||||
sendType,
|
sendType,
|
||||||
timestamp,
|
timestamp,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await updateMessageWithSuccessfulSends(message);
|
||||||
} else {
|
} else {
|
||||||
if (isGroupV2(conversation.attributes) && !isNumber(revision)) {
|
if (isGroupV2(conversation.attributes) && !isNumber(revision)) {
|
||||||
log.error('No revision provided, but conversation is GroupV2');
|
log.error('No revision provided, but conversation is GroupV2');
|
||||||
|
@ -185,12 +225,20 @@ export async function sendDeleteForEveryone(
|
||||||
sendType,
|
sendType,
|
||||||
timestamp,
|
timestamp,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await updateMessageWithSuccessfulSends(message);
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof SendMessageProtoError) {
|
||||||
|
await updateMessageWithSuccessfulSends(message, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const errors = maybeExpandErrors(error);
|
||||||
await handleMultipleSendErrors({
|
await handleMultipleSendErrors({
|
||||||
errors: maybeExpandErrors(error),
|
errors,
|
||||||
isFinalAttempt,
|
isFinalAttempt,
|
||||||
log,
|
log,
|
||||||
|
markFailed: () => updateMessageWithFailure(message, errors, log),
|
||||||
timeRemaining,
|
timeRemaining,
|
||||||
toThrow: error,
|
toThrow: error,
|
||||||
});
|
});
|
||||||
|
@ -198,3 +246,71 @@ export async function sendDeleteForEveryone(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getRecipients(
|
||||||
|
sendStatusByConversationId: Record<string, boolean>
|
||||||
|
): Array<string> {
|
||||||
|
return Object.entries(sendStatusByConversationId)
|
||||||
|
.filter(([_, isSent]) => !isSent)
|
||||||
|
.map(([conversationId]) => {
|
||||||
|
const recipient = window.ConversationController.get(conversationId);
|
||||||
|
if (!recipient) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return recipient.get('uuid');
|
||||||
|
})
|
||||||
|
.filter(isNotNil);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateMessageWithSuccessfulSends(
|
||||||
|
message: MessageModel,
|
||||||
|
result?: CallbackResultType | SendMessageProtoError
|
||||||
|
): Promise<void> {
|
||||||
|
if (!result) {
|
||||||
|
message.set({
|
||||||
|
deletedForEveryoneSendStatus: {},
|
||||||
|
deletedForEveryoneFailed: undefined,
|
||||||
|
});
|
||||||
|
await window.Signal.Data.saveMessage(message.attributes, {
|
||||||
|
ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deletedForEveryoneSendStatus = {
|
||||||
|
...message.get('deletedForEveryoneSendStatus'),
|
||||||
|
};
|
||||||
|
|
||||||
|
result.successfulIdentifiers?.forEach(identifier => {
|
||||||
|
const conversation = window.ConversationController.get(identifier);
|
||||||
|
if (!conversation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
deletedForEveryoneSendStatus[conversation.id] = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
message.set({
|
||||||
|
deletedForEveryoneSendStatus,
|
||||||
|
deletedForEveryoneFailed: undefined,
|
||||||
|
});
|
||||||
|
await window.Signal.Data.saveMessage(message.attributes, {
|
||||||
|
ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateMessageWithFailure(
|
||||||
|
message: MessageModel,
|
||||||
|
errors: ReadonlyArray<unknown>,
|
||||||
|
log: LoggerType
|
||||||
|
): Promise<void> {
|
||||||
|
log.error(
|
||||||
|
'updateMessageWithFailure: Setting this set of errors',
|
||||||
|
errors.map(Errors.toLogFormat)
|
||||||
|
);
|
||||||
|
|
||||||
|
message.set({ deletedForEveryoneFailed: true });
|
||||||
|
await window.Signal.Data.saveMessage(message.attributes, {
|
||||||
|
ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -333,8 +333,7 @@ function getMessageRecipients({
|
||||||
const recipientIdentifiersWithoutMe: Array<string> = [];
|
const recipientIdentifiersWithoutMe: Array<string> = [];
|
||||||
const untrustedConversationIds: Array<string> = [];
|
const untrustedConversationIds: Array<string> = [];
|
||||||
|
|
||||||
const currentConversationRecipients =
|
const currentConversationRecipients = conversation.getMemberConversationIds();
|
||||||
conversation.getRecipientConversationIds();
|
|
||||||
|
|
||||||
Object.entries(message.get('sendStateByConversationId') || {}).forEach(
|
Object.entries(message.get('sendStateByConversationId') || {}).forEach(
|
||||||
([recipientConversationId, sendState]) => {
|
([recipientConversationId, sendState]) => {
|
||||||
|
@ -360,6 +359,10 @@ function getMessageRecipients({
|
||||||
|
|
||||||
if (recipient.isUntrusted()) {
|
if (recipient.isUntrusted()) {
|
||||||
untrustedConversationIds.push(recipientConversationId);
|
untrustedConversationIds.push(recipientConversationId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (recipient.isUnregistered()) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const recipientIdentifier = recipient.getSendTarget();
|
const recipientIdentifier = recipient.getSendTarget();
|
||||||
|
|
|
@ -352,8 +352,7 @@ function getRecipients(
|
||||||
const recipientIdentifiersWithoutMe: Array<string> = [];
|
const recipientIdentifiersWithoutMe: Array<string> = [];
|
||||||
const untrustedConversationIds: Array<string> = [];
|
const untrustedConversationIds: Array<string> = [];
|
||||||
|
|
||||||
const currentConversationRecipients =
|
const currentConversationRecipients = conversation.getMemberConversationIds();
|
||||||
conversation.getRecipientConversationIds();
|
|
||||||
|
|
||||||
for (const id of reactionUtil.getUnsentConversationIds(reaction)) {
|
for (const id of reactionUtil.getUnsentConversationIds(reaction)) {
|
||||||
const recipient = window.ConversationController.get(id);
|
const recipient = window.ConversationController.get(id);
|
||||||
|
@ -375,6 +374,10 @@ function getRecipients(
|
||||||
untrustedConversationIds.push(recipientIdentifier);
|
untrustedConversationIds.push(recipientIdentifier);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (recipient.isUnregistered()) {
|
||||||
|
untrustedConversationIds.push(recipientIdentifier);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
allRecipientIdentifiers.push(recipientIdentifier);
|
allRecipientIdentifiers.push(recipientIdentifier);
|
||||||
if (!isRecipientMe) {
|
if (!isRecipientMe) {
|
||||||
|
|
4
ts/model-types.d.ts
vendored
4
ts/model-types.d.ts
vendored
|
@ -213,6 +213,10 @@ export type MessageAttributesType = {
|
||||||
|
|
||||||
// Should only be present for outgoing messages
|
// Should only be present for outgoing messages
|
||||||
sendStateByConversationId?: SendStateByConversationId;
|
sendStateByConversationId?: SendStateByConversationId;
|
||||||
|
|
||||||
|
// Should only be present for messages deleted for everyone
|
||||||
|
deletedForEveryoneSendStatus?: Record<string, boolean>;
|
||||||
|
deletedForEveryoneFailed?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ConversationAttributesTypeType = 'private' | 'group';
|
export type ConversationAttributesTypeType = 'private' | 'group';
|
||||||
|
|
|
@ -98,8 +98,9 @@ import {
|
||||||
conversationJobQueue,
|
conversationJobQueue,
|
||||||
conversationQueueJobEnum,
|
conversationQueueJobEnum,
|
||||||
} from '../jobs/conversationJobQueue';
|
} from '../jobs/conversationJobQueue';
|
||||||
|
import type { ConversationQueueJobData } from '../jobs/conversationJobQueue';
|
||||||
import { readReceiptsJobQueue } from '../jobs/readReceiptsJobQueue';
|
import { readReceiptsJobQueue } from '../jobs/readReceiptsJobQueue';
|
||||||
import { Deletes } from '../messageModifiers/Deletes';
|
import { DeleteModel } from '../messageModifiers/Deletes';
|
||||||
import type { ReactionModel } from '../messageModifiers/Reactions';
|
import type { ReactionModel } from '../messageModifiers/Reactions';
|
||||||
import { isAnnouncementGroupReady } from '../util/isAnnouncementGroupReady';
|
import { isAnnouncementGroupReady } from '../util/isAnnouncementGroupReady';
|
||||||
import { getProfile } from '../util/getProfile';
|
import { getProfile } from '../util/getProfile';
|
||||||
|
@ -1760,7 +1761,7 @@ export class ConversationModel extends window.Backbone
|
||||||
sortConversationTitles(left, right, this.intlCollator)
|
sortConversationTitles(left, right, this.intlCollator)
|
||||||
)
|
)
|
||||||
.map(member => member.format())
|
.map(member => member.format())
|
||||||
.filter((member): member is ConversationType => member !== null)
|
.filter(isNotNil)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const { customColor, customColorId } = this.getCustomColorData();
|
const { customColor, customColorId } = this.getCustomColorData();
|
||||||
|
@ -3517,10 +3518,29 @@ export class ConversationModel extends window.Backbone
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getRecipientConversationIds(): Set<string> {
|
// Members is all people in the group
|
||||||
|
getMemberConversationIds(): Set<string> {
|
||||||
return new Set(map(this.getMembers(), conversation => conversation.id));
|
return new Set(map(this.getMembers(), conversation => conversation.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recipients includes only the people we'll actually send to for this conversation
|
||||||
|
getRecipientConversationIds(): Set<string> {
|
||||||
|
const recipients = this.getRecipients();
|
||||||
|
const conversationIds = recipients.map(identifier => {
|
||||||
|
const conversation = window.ConversationController.getOrCreate(
|
||||||
|
identifier,
|
||||||
|
'private'
|
||||||
|
);
|
||||||
|
strictAssert(
|
||||||
|
conversation,
|
||||||
|
'getRecipientConversationIds should have created conversation!'
|
||||||
|
);
|
||||||
|
return conversation.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Set(conversationIds);
|
||||||
|
}
|
||||||
|
|
||||||
async getQuoteAttachment(
|
async getQuoteAttachment(
|
||||||
attachments?: Array<WhatIsThis>,
|
attachments?: Array<WhatIsThis>,
|
||||||
preview?: Array<WhatIsThis>,
|
preview?: Array<WhatIsThis>,
|
||||||
|
@ -3682,20 +3702,43 @@ export class ConversationModel extends window.Backbone
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const { timestamp: targetTimestamp, id: messageId } = options;
|
const { timestamp: targetTimestamp, id: messageId } = options;
|
||||||
const timestamp = Date.now();
|
const message = await getMessageById(messageId);
|
||||||
|
if (!message) {
|
||||||
|
throw new Error('sendDeleteForEveryoneMessage: Cannot find message!');
|
||||||
|
}
|
||||||
|
const messageModel = window.MessageController.register(messageId, message);
|
||||||
|
|
||||||
|
const timestamp = Date.now();
|
||||||
if (timestamp - targetTimestamp > THREE_HOURS) {
|
if (timestamp - targetTimestamp > THREE_HOURS) {
|
||||||
throw new Error('Cannot send DOE for a message older than three hours');
|
throw new Error('Cannot send DOE for a message older than three hours');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
messageModel.set({
|
||||||
|
deletedForEveryoneSendStatus: zipObject(
|
||||||
|
this.getRecipientConversationIds(),
|
||||||
|
repeat(false)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await conversationJobQueue.add({
|
const jobData: ConversationQueueJobData = {
|
||||||
type: conversationQueueJobEnum.enum.DeleteForEveryone,
|
type: conversationQueueJobEnum.enum.DeleteForEveryone,
|
||||||
conversationId: this.id,
|
conversationId: this.id,
|
||||||
messageId,
|
messageId,
|
||||||
recipients: this.getRecipients(),
|
recipients: this.getRecipients(),
|
||||||
revision: this.get('revision'),
|
revision: this.get('revision'),
|
||||||
targetTimestamp,
|
targetTimestamp,
|
||||||
|
};
|
||||||
|
await conversationJobQueue.add(jobData, async jobToInsert => {
|
||||||
|
log.info(
|
||||||
|
`sendDeleteForEveryoneMessage: saving message ${this.idForLogging()} and job ${
|
||||||
|
jobToInsert.id
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
await window.Signal.Data.saveMessage(messageModel.attributes, {
|
||||||
|
jobToInsert,
|
||||||
|
ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(
|
log.error(
|
||||||
|
@ -3705,11 +3748,12 @@ export class ConversationModel extends window.Backbone
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteModel = Deletes.getSingleton().add({
|
const deleteModel = new DeleteModel({
|
||||||
targetSentTimestamp: targetTimestamp,
|
targetSentTimestamp: targetTimestamp,
|
||||||
fromId: window.ConversationController.getOurConversationId(),
|
serverTimestamp: Date.now(),
|
||||||
|
fromId: window.ConversationController.getOurConversationIdOrThrow(),
|
||||||
});
|
});
|
||||||
Deletes.getSingleton().onDelete(deleteModel);
|
await window.Signal.Util.deleteForEveryone(messageModel, deleteModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendProfileKeyUpdate(): Promise<void> {
|
async sendProfileKeyUpdate(): Promise<void> {
|
||||||
|
|
|
@ -1135,7 +1135,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
const conversation = this.getConversation()!;
|
const conversation = this.getConversation()!;
|
||||||
|
|
||||||
const currentConversationRecipients =
|
const currentConversationRecipients =
|
||||||
conversation.getRecipientConversationIds();
|
conversation.getMemberConversationIds();
|
||||||
|
|
||||||
// Determine retry recipients and get their most up-to-date addressing information
|
// Determine retry recipients and get their most up-to-date addressing information
|
||||||
const oldSendStateByConversationId =
|
const oldSendStateByConversationId =
|
||||||
|
@ -3100,7 +3100,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
targetTimestamp: reaction.get('targetTimestamp'),
|
targetTimestamp: reaction.get('targetTimestamp'),
|
||||||
timestamp: reaction.get('timestamp'),
|
timestamp: reaction.get('timestamp'),
|
||||||
isSentByConversationId: zipObject(
|
isSentByConversationId: zipObject(
|
||||||
conversation.getRecipientConversationIds(),
|
conversation.getMemberConversationIds(),
|
||||||
repeat(false)
|
repeat(false)
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
@ -3244,8 +3244,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update the conversation's last message in case this was the last message
|
// Update the conversation's last message in case this was the last message
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
this.getConversation()?.updateLastMessage();
|
||||||
this.getConversation()!.updateLastMessage();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clearNotifications(reaction: Partial<ReactionType> = {}): void {
|
clearNotifications(reaction: Partial<ReactionType> = {}): void {
|
||||||
|
|
|
@ -82,8 +82,9 @@ import {
|
||||||
} from '../../messages/MessageSendState';
|
} from '../../messages/MessageSendState';
|
||||||
import * as log from '../../logging/log';
|
import * as log from '../../logging/log';
|
||||||
import { getConversationColorAttributes } from '../../util/getConversationColorAttributes';
|
import { getConversationColorAttributes } from '../../util/getConversationColorAttributes';
|
||||||
|
import { DAY, HOUR } from '../../util/durations';
|
||||||
|
|
||||||
const THREE_HOURS = 3 * 60 * 60 * 1000;
|
const THREE_HOURS = 3 * HOUR;
|
||||||
|
|
||||||
type FormattedContact = Partial<ConversationType> &
|
type FormattedContact = Partial<ConversationType> &
|
||||||
Pick<
|
Pick<
|
||||||
|
@ -510,6 +511,8 @@ type ShallowPropsType = Pick<
|
||||||
| 'canDownload'
|
| 'canDownload'
|
||||||
| 'canReact'
|
| 'canReact'
|
||||||
| 'canReply'
|
| 'canReply'
|
||||||
|
| 'canRetry'
|
||||||
|
| 'canRetryDeleteForEveryone'
|
||||||
| 'contact'
|
| 'contact'
|
||||||
| 'contactNameColor'
|
| 'contactNameColor'
|
||||||
| 'conversationColor'
|
| 'conversationColor'
|
||||||
|
@ -592,6 +595,8 @@ const getShallowPropsForMessage = createSelectorCreator(memoizeByRoot, isEqual)(
|
||||||
canDownload: canDownload(message, conversationSelector),
|
canDownload: canDownload(message, conversationSelector),
|
||||||
canReact: canReact(message, ourConversationId, conversationSelector),
|
canReact: canReact(message, ourConversationId, conversationSelector),
|
||||||
canReply: canReply(message, ourConversationId, conversationSelector),
|
canReply: canReply(message, ourConversationId, conversationSelector),
|
||||||
|
canRetry: hasErrors(message),
|
||||||
|
canRetryDeleteForEveryone: canRetryDeleteForEveryone(message),
|
||||||
contact: getPropsForEmbeddedContact(message, regionCode, accountSelector),
|
contact: getPropsForEmbeddedContact(message, regionCode, accountSelector),
|
||||||
contactNameColor,
|
contactNameColor,
|
||||||
conversationColor,
|
conversationColor,
|
||||||
|
@ -1284,7 +1289,12 @@ function createNonBreakingLastSeparator(text?: string): string {
|
||||||
export function getMessagePropStatus(
|
export function getMessagePropStatus(
|
||||||
message: Pick<
|
message: Pick<
|
||||||
MessageWithUIFieldsType,
|
MessageWithUIFieldsType,
|
||||||
'type' | 'errors' | 'sendStateByConversationId'
|
| 'deletedForEveryone'
|
||||||
|
| 'deletedForEveryoneFailed'
|
||||||
|
| 'deletedForEveryoneSendStatus'
|
||||||
|
| 'errors'
|
||||||
|
| 'sendStateByConversationId'
|
||||||
|
| 'type'
|
||||||
>,
|
>,
|
||||||
ourConversationId: string | undefined
|
ourConversationId: string | undefined
|
||||||
): LastMessageStatus | undefined {
|
): LastMessageStatus | undefined {
|
||||||
|
@ -1296,7 +1306,30 @@ export function getMessagePropStatus(
|
||||||
return 'paused';
|
return 'paused';
|
||||||
}
|
}
|
||||||
|
|
||||||
const { sendStateByConversationId = {} } = message;
|
const {
|
||||||
|
deletedForEveryone,
|
||||||
|
deletedForEveryoneFailed,
|
||||||
|
deletedForEveryoneSendStatus,
|
||||||
|
sendStateByConversationId = {},
|
||||||
|
} = message;
|
||||||
|
|
||||||
|
// Note: we only do anything here if deletedForEveryoneSendStatus exists, because old
|
||||||
|
// messages deleted for everyone won't have send status.
|
||||||
|
if (deletedForEveryone && deletedForEveryoneSendStatus) {
|
||||||
|
if (deletedForEveryoneFailed) {
|
||||||
|
const anySuccessfulSends = Object.values(
|
||||||
|
deletedForEveryoneSendStatus
|
||||||
|
).some(item => item);
|
||||||
|
|
||||||
|
return anySuccessfulSends ? 'partial-sent' : 'error';
|
||||||
|
}
|
||||||
|
const missingSends = Object.values(deletedForEveryoneSendStatus).some(
|
||||||
|
item => !item
|
||||||
|
);
|
||||||
|
if (missingSends) {
|
||||||
|
return 'sending';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
ourConversationId &&
|
ourConversationId &&
|
||||||
|
@ -1531,6 +1564,20 @@ export function canDeleteForEveryone(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function canRetryDeleteForEveryone(
|
||||||
|
message: Pick<
|
||||||
|
MessageWithUIFieldsType,
|
||||||
|
'deletedForEveryone' | 'deletedForEveryoneFailed' | 'sent_at'
|
||||||
|
>
|
||||||
|
): boolean {
|
||||||
|
return Boolean(
|
||||||
|
message.deletedForEveryone &&
|
||||||
|
message.deletedForEveryoneFailed &&
|
||||||
|
// Is it too old to delete?
|
||||||
|
isMoreRecentThan(message.sent_at, DAY)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function canDownload(
|
export function canDownload(
|
||||||
message: MessageWithUIFieldsType,
|
message: MessageWithUIFieldsType,
|
||||||
conversationSelector: GetConversationByIdType
|
conversationSelector: GetConversationByIdType
|
||||||
|
|
|
@ -48,6 +48,7 @@ const mapStateToProps = (
|
||||||
openLink,
|
openLink,
|
||||||
reactToMessage,
|
reactToMessage,
|
||||||
replyToMessage,
|
replyToMessage,
|
||||||
|
retryDeleteForEveryone,
|
||||||
retrySend,
|
retrySend,
|
||||||
showContactDetail,
|
showContactDetail,
|
||||||
showContactModal,
|
showContactModal,
|
||||||
|
@ -93,6 +94,7 @@ const mapStateToProps = (
|
||||||
renderEmojiPicker,
|
renderEmojiPicker,
|
||||||
renderReactionPicker,
|
renderReactionPicker,
|
||||||
replyToMessage,
|
replyToMessage,
|
||||||
|
retryDeleteForEveryone,
|
||||||
retrySend,
|
retrySend,
|
||||||
showContactDetail,
|
showContactDetail,
|
||||||
showContactModal,
|
showContactModal,
|
||||||
|
|
|
@ -84,6 +84,7 @@ export type TimelinePropsType = ExternalProps &
|
||||||
| 'reactToMessage'
|
| 'reactToMessage'
|
||||||
| 'removeMember'
|
| 'removeMember'
|
||||||
| 'replyToMessage'
|
| 'replyToMessage'
|
||||||
|
| 'retryDeleteForEveryone'
|
||||||
| 'retrySend'
|
| 'retrySend'
|
||||||
| 'scrollToQuotedMessage'
|
| 'scrollToQuotedMessage'
|
||||||
| 'showContactDetail'
|
| 'showContactDetail'
|
||||||
|
|
|
@ -4,8 +4,7 @@
|
||||||
import type { DeleteModel } from '../messageModifiers/Deletes';
|
import type { DeleteModel } from '../messageModifiers/Deletes';
|
||||||
import type { MessageModel } from '../models/messages';
|
import type { MessageModel } from '../models/messages';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
|
import { DAY } from './durations';
|
||||||
const ONE_DAY = 24 * 60 * 60 * 1000;
|
|
||||||
|
|
||||||
export async function deleteForEveryone(
|
export async function deleteForEveryone(
|
||||||
message: MessageModel,
|
message: MessageModel,
|
||||||
|
@ -19,7 +18,7 @@ export async function deleteForEveryone(
|
||||||
// are less than one day apart
|
// are less than one day apart
|
||||||
const delta = Math.abs(doe.get('serverTimestamp') - messageTimestamp);
|
const delta = Math.abs(doe.get('serverTimestamp') - messageTimestamp);
|
||||||
|
|
||||||
if (delta > ONE_DAY) {
|
if (delta > DAY) {
|
||||||
log.info('Received late DOE. Dropping.', {
|
log.info('Received late DOE. Dropping.', {
|
||||||
fromId: doe.get('fromId'),
|
fromId: doe.get('fromId'),
|
||||||
targetSentTimestamp: doe.get('targetSentTimestamp'),
|
targetSentTimestamp: doe.get('targetSentTimestamp'),
|
||||||
|
|
55
ts/util/retryDeleteForEveryone.ts
Normal file
55
ts/util/retryDeleteForEveryone.ts
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import * as log from '../logging/log';
|
||||||
|
import * as Errors from '../types/errors';
|
||||||
|
|
||||||
|
import type { ConversationQueueJobData } from '../jobs/conversationJobQueue';
|
||||||
|
import {
|
||||||
|
conversationJobQueue,
|
||||||
|
conversationQueueJobEnum,
|
||||||
|
} from '../jobs/conversationJobQueue';
|
||||||
|
import { isOlderThan } from './timestamp';
|
||||||
|
import { DAY } from './durations';
|
||||||
|
|
||||||
|
export async function retryDeleteForEveryone(messageId: string): Promise<void> {
|
||||||
|
const message = window.MessageController.getById(messageId);
|
||||||
|
if (!message) {
|
||||||
|
throw new Error(`retryDeleteForEveryone: Message ${messageId} missing!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOlderThan(message.get('sent_at'), DAY)) {
|
||||||
|
throw new Error(
|
||||||
|
'retryDeleteForEveryone: Message too old to retry delete for everyone!'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const conversation = message.getConversation();
|
||||||
|
if (!conversation) {
|
||||||
|
throw new Error(
|
||||||
|
`retryDeleteForEveryone: Conversation for ${messageId} missing!`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const jobData: ConversationQueueJobData = {
|
||||||
|
type: conversationQueueJobEnum.enum.DeleteForEveryone,
|
||||||
|
conversationId: conversation.id,
|
||||||
|
messageId,
|
||||||
|
recipients: conversation.getRecipients(),
|
||||||
|
revision: conversation.get('revision'),
|
||||||
|
targetTimestamp: message.get('sent_at'),
|
||||||
|
};
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
`retryDeleteForEveryone: Adding job for message ${message.idForLogging()}!`
|
||||||
|
);
|
||||||
|
await conversationJobQueue.add(jobData);
|
||||||
|
} catch (error) {
|
||||||
|
log.error(
|
||||||
|
'retryDeleteForEveryone: Failed to queue delete for everyone',
|
||||||
|
Errors.toLogFormat(error)
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
|
@ -114,6 +114,7 @@ import { viewSyncJobQueue } from '../jobs/viewSyncJobQueue';
|
||||||
import { viewedReceiptsJobQueue } from '../jobs/viewedReceiptsJobQueue';
|
import { viewedReceiptsJobQueue } from '../jobs/viewedReceiptsJobQueue';
|
||||||
import { RecordingState } from '../state/ducks/audioRecorder';
|
import { RecordingState } from '../state/ducks/audioRecorder';
|
||||||
import { UUIDKind } from '../types/UUID';
|
import { UUIDKind } from '../types/UUID';
|
||||||
|
import { retryDeleteForEveryone } from '../util/retryDeleteForEveryone';
|
||||||
|
|
||||||
type AttachmentOptions = {
|
type AttachmentOptions = {
|
||||||
messageId: string;
|
messageId: string;
|
||||||
|
@ -167,6 +168,7 @@ type MessageActionsType = {
|
||||||
) => unknown;
|
) => unknown;
|
||||||
replyToMessage: (messageId: string) => unknown;
|
replyToMessage: (messageId: string) => unknown;
|
||||||
retrySend: (messageId: string) => unknown;
|
retrySend: (messageId: string) => unknown;
|
||||||
|
retryDeleteForEveryone: (messageId: string) => unknown;
|
||||||
showContactDetail: (options: {
|
showContactDetail: (options: {
|
||||||
contact: EmbeddedContactType;
|
contact: EmbeddedContactType;
|
||||||
signalAccount?: string;
|
signalAccount?: string;
|
||||||
|
@ -874,6 +876,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
reactToMessage,
|
reactToMessage,
|
||||||
replyToMessage,
|
replyToMessage,
|
||||||
retrySend,
|
retrySend,
|
||||||
|
retryDeleteForEveryone,
|
||||||
showContactDetail,
|
showContactDetail,
|
||||||
showContactModal,
|
showContactModal,
|
||||||
showSafetyNumber,
|
showSafetyNumber,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue