Retry outbound "normal" messages for up to a day

This commit is contained in:
Evan Hahn 2021-08-31 15:58:39 -05:00 committed by GitHub
parent 62cf51c060
commit a85dd1be36
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 1414 additions and 603 deletions

View file

@ -45,12 +45,16 @@ import {
getGroupSizeRecommendedLimit,
getGroupSizeHardLimit,
} from '../../groups/limits';
import { getMessagesById } from '../../messages/getMessagesById';
import { isMessageUnread } from '../../util/isMessageUnread';
import { toggleSelectedContactForGroupAddition } from '../../groups/toggleSelectedContactForGroupAddition';
import { GroupNameCollisionsWithIdsByTitle } from '../../util/groupMemberNameCollisions';
import { ContactSpoofingType } from '../../util/contactSpoofing';
import { writeProfile } from '../../services/writeProfile';
import { getMe } from '../selectors/conversations';
import {
getMe,
getMessageIdsPendingBecauseOfVerification,
} from '../selectors/conversations';
import { AvatarDataType, getDefaultAvatars } from '../../types/Avatar';
import { getAvatarData } from '../../util/getAvatarData';
import { isSameAvatarData } from '../../util/isSameAvatarData';
@ -302,6 +306,15 @@ export type ConversationsStateType = {
composer?: ComposerStateType;
contactSpoofingReview?: ContactSpoofingReviewStateType;
/**
* Each key is a conversation ID. Each value is an array of message IDs stopped by that
* conversation being unverified.
*/
outboundMessagesPendingConversationVerification: Record<
string,
Array<string>
>;
// Note: it's very important that both of these locations are always kept up to date
messagesLookup: MessageLookupType;
messagesByConversation: MessagesByConversationType;
@ -336,14 +349,21 @@ export const getConversationCallMode = (
export const COLORS_CHANGED = 'conversations/COLORS_CHANGED';
export const COLOR_SELECTED = 'conversations/COLOR_SELECTED';
const CANCEL_MESSAGES_PENDING_CONVERSATION_VERIFICATION =
'conversations/CANCEL_MESSAGES_PENDING_CONVERSATION_VERIFICATION';
const COMPOSE_TOGGLE_EDITING_AVATAR =
'conversations/compose/COMPOSE_TOGGLE_EDITING_AVATAR';
const COMPOSE_ADD_AVATAR = 'conversations/compose/ADD_AVATAR';
const COMPOSE_REMOVE_AVATAR = 'conversations/compose/REMOVE_AVATAR';
const COMPOSE_REPLACE_AVATAR = 'conversations/compose/REPLACE_AVATAR';
const CUSTOM_COLOR_REMOVED = 'conversations/CUSTOM_COLOR_REMOVED';
const MESSAGE_STOPPED_BY_MISSING_VERIFICATION =
'conversations/MESSAGE_STOPPED_BY_MISSING_VERIFICATION';
const REPLACE_AVATARS = 'conversations/REPLACE_AVATARS';
type CancelMessagesPendingConversationVerificationActionType = {
type: typeof CANCEL_MESSAGES_PENDING_CONVERSATION_VERIFICATION;
};
type CantAddContactToGroupActionType = {
type: 'CANT_ADD_CONTACT_TO_GROUP';
payload: {
@ -465,6 +485,13 @@ export type MessageSelectedActionType = {
conversationId: string;
};
};
type MessageStoppedByMissingVerificationActionType = {
type: typeof MESSAGE_STOPPED_BY_MISSING_VERIFICATION;
payload: {
messageId: string;
untrustedConversationIds: ReadonlyArray<string>;
};
};
export type MessageChangedActionType = {
type: 'MESSAGE_CHANGED';
payload: {
@ -656,6 +683,7 @@ type ReplaceAvatarsActionType = {
};
};
export type ConversationActionType =
| CancelMessagesPendingConversationVerificationActionType
| CantAddContactToGroupActionType
| ClearChangedMessagesActionType
| ClearGroupCreationErrorActionType
@ -679,6 +707,7 @@ export type ConversationActionType =
| CreateGroupPendingActionType
| CreateGroupRejectedActionType
| CustomColorRemovedActionType
| MessageStoppedByMissingVerificationActionType
| MessageChangedActionType
| MessageDeletedActionType
| MessageSelectedActionType
@ -716,6 +745,7 @@ export type ConversationActionType =
// Action Creators
export const actions = {
cancelMessagesPendingConversationVerification,
cantAddContactToGroup,
clearChangedMessages,
clearGroupCreationError,
@ -737,6 +767,7 @@ export const actions = {
createGroup,
deleteAvatarFromDisk,
doubleCheckMissingQuoteReference,
messageStoppedByMissingVerification,
messageChanged,
messageDeleted,
messageSizeChanged,
@ -775,6 +806,7 @@ export const actions = {
startSettingGroupMetadata,
toggleConversationInChooseMembers,
toggleComposeEditingAvatar,
verifyConversationsStoppingMessageSend,
};
function filterAvatarData(
@ -1074,6 +1106,26 @@ function toggleComposeEditingAvatar(): ToggleComposeEditingAvatarActionType {
};
}
function verifyConversationsStoppingMessageSend(): ThunkAction<
void,
RootStateType,
unknown,
never
> {
return async (_dispatch, getState) => {
const conversationIds = Object.keys(
getState().conversations.outboundMessagesPendingConversationVerification
);
await Promise.all(
conversationIds.map(async conversationId => {
const conversation = window.ConversationController.get(conversationId);
await conversation?.setVerifiedDefault();
})
);
};
}
function composeSaveAvatarToDisk(
avatarData: AvatarDataType
): ThunkAction<void, RootStateType, unknown, ComposeSaveAvatarActionType> {
@ -1128,6 +1180,31 @@ function composeReplaceAvatar(
};
}
function cancelMessagesPendingConversationVerification(): ThunkAction<
void,
RootStateType,
unknown,
CancelMessagesPendingConversationVerificationActionType
> {
return async (dispatch, getState) => {
const messageIdsPending = getMessageIdsPendingBecauseOfVerification(
getState()
);
const messagesStopped = await getMessagesById([...messageIdsPending]);
messagesStopped.forEach(message => {
message.markFailed();
});
dispatch({
type: CANCEL_MESSAGES_PENDING_CONVERSATION_VERIFICATION,
});
await window.Signal.Data.saveMessages(
messagesStopped.map(message => message.attributes)
);
};
}
function cantAddContactToGroup(
conversationId: string
): CantAddContactToGroupActionType {
@ -1162,9 +1239,22 @@ function conversationChanged(
id: string,
data: ConversationType
): ThunkAction<void, RootStateType, unknown, ConversationChangedActionType> {
return dispatch => {
return async (dispatch, getState) => {
calling.groupMembersChanged(id);
if (!data.isUntrusted) {
const messageIdsPending =
getOwn(
getState().conversations
.outboundMessagesPendingConversationVerification,
id
) ?? [];
const messagesPending = await getMessagesById(messageIdsPending);
messagesPending.forEach(message => {
message.retrySend();
});
}
dispatch({
type: 'CONVERSATION_CHANGED',
payload: {
@ -1264,6 +1354,19 @@ function selectMessage(
};
}
function messageStoppedByMissingVerification(
messageId: string,
untrustedConversationIds: ReadonlyArray<string>
): MessageStoppedByMissingVerificationActionType {
return {
type: MESSAGE_STOPPED_BY_MISSING_VERIFICATION,
payload: {
messageId,
untrustedConversationIds,
},
};
}
function messageChanged(
id: string,
conversationId: string,
@ -1651,6 +1754,7 @@ export function getEmptyState(): ConversationsStateType {
conversationsByE164: {},
conversationsByUuid: {},
conversationsByGroupId: {},
outboundMessagesPendingConversationVerification: {},
messagesByConversation: {},
messagesLookup: {},
selectedMessageCounter: 0,
@ -1799,6 +1903,13 @@ export function reducer(
state: Readonly<ConversationsStateType> = getEmptyState(),
action: Readonly<ConversationActionType>
): ConversationsStateType {
if (action.type === CANCEL_MESSAGES_PENDING_CONVERSATION_VERIFICATION) {
return {
...state,
outboundMessagesPendingConversationVerification: {},
};
}
if (action.type === 'CANT_ADD_CONTACT_TO_GROUP') {
const { composer } = state;
if (composer?.step !== ComposerStep.ChooseGroupMembers) {
@ -1887,6 +1998,9 @@ export function reducer(
[id]: data,
},
...updateConversationLookups(data, undefined, state),
outboundMessagesPendingConversationVerification: data.isUntrusted
? state.outboundMessagesPendingConversationVerification
: omit(state.outboundMessagesPendingConversationVerification, id),
};
}
if (action.type === 'CONVERSATION_CHANGED') {
@ -1933,6 +2047,9 @@ export function reducer(
[id]: data,
},
...updateConversationLookups(data, existing, state),
outboundMessagesPendingConversationVerification: data.isUntrusted
? state.outboundMessagesPendingConversationVerification
: omit(state.outboundMessagesPendingConversationVerification, id),
};
}
if (action.type === 'CONVERSATION_REMOVED') {
@ -2037,6 +2154,31 @@ export function reducer(
selectedMessageCounter: state.selectedMessageCounter + 1,
};
}
if (action.type === MESSAGE_STOPPED_BY_MISSING_VERIFICATION) {
const { messageId, untrustedConversationIds } = action.payload;
const newOutboundMessagesPendingConversationVerification = {
...state.outboundMessagesPendingConversationVerification,
};
untrustedConversationIds.forEach(conversationId => {
const existingPendingMessageIds =
getOwn(
newOutboundMessagesPendingConversationVerification,
conversationId
) ?? [];
if (!existingPendingMessageIds.includes(messageId)) {
newOutboundMessagesPendingConversationVerification[conversationId] = [
...existingPendingMessageIds,
messageId,
];
}
});
return {
...state,
outboundMessagesPendingConversationVerification: newOutboundMessagesPendingConversationVerification,
};
}
if (action.type === 'MESSAGE_CHANGED') {
const { id, conversationId, data } = action.payload;
const existingConversation = state.messagesByConversation[conversationId];

View file

@ -18,6 +18,7 @@ import {
PreJoinConversationType,
} from '../ducks/conversations';
import { getOwn } from '../../util/getOwn';
import { isNotNil } from '../../util/isNotNil';
import { deconstructLookup } from '../../util/deconstructLookup';
import { PropsDataType as TimelinePropsType } from '../../components/conversation/Timeline';
import { TimelineItemType } from '../../components/conversation/TimelineItem';
@ -27,6 +28,7 @@ import { filterAndSortConversationsByTitle } from '../../util/filterAndSortConve
import { ContactNameColors, ContactNameColorType } from '../../types/Colors';
import { AvatarDataType } from '../../types/Avatar';
import { isInSystemContacts } from '../../util/isInSystemContacts';
import { sortByTitle } from '../../util/sortByTitle';
import { isGroupV2 } from '../../util/whatTypeOfConversation';
import {
@ -956,3 +958,51 @@ export const getGroupAdminsSelector = createSelector(
};
}
);
const getOutboundMessagesPendingConversationVerification = createSelector(
getConversations,
(
conversations: Readonly<ConversationsStateType>
): Record<string, Array<string>> =>
conversations.outboundMessagesPendingConversationVerification
);
const getConversationIdsStoppingMessageSendBecauseOfVerification = createSelector(
getOutboundMessagesPendingConversationVerification,
(outboundMessagesPendingConversationVerification): Array<string> =>
Object.keys(outboundMessagesPendingConversationVerification)
);
export const getConversationsStoppingMessageSendBecauseOfVerification = createSelector(
getConversationByIdSelector,
getConversationIdsStoppingMessageSendBecauseOfVerification,
(
conversationSelector: (id: string) => undefined | ConversationType,
conversationIds: ReadonlyArray<string>
): Array<ConversationType> => {
const conversations = conversationIds
.map(conversationId => conversationSelector(conversationId))
.filter(isNotNil);
return sortByTitle(conversations);
}
);
export const getMessageIdsPendingBecauseOfVerification = createSelector(
getOutboundMessagesPendingConversationVerification,
(outboundMessagesPendingConversationVerification): Set<string> => {
const result = new Set<string>();
Object.values(outboundMessagesPendingConversationVerification).forEach(
messageGroup => {
messageGroup.forEach(messageId => {
result.add(messageId);
});
}
);
return result;
}
);
export const getNumberOfMessagesPendingBecauseOfVerification = createSelector(
getMessageIdsPendingBecauseOfVerification,
(messageIds: Readonly<Set<string>>): number => messageIds.size
);

View file

@ -67,6 +67,7 @@ import {
import {
SendStatus,
isDelivered,
isFailed,
isMessageJustForMe,
isRead,
isSent,
@ -1234,7 +1235,10 @@ export function getMessagePropStatus(
sendStateByConversationId[ourConversationId]?.status ??
SendStatus.Pending;
const sent = isSent(status);
if (hasErrors(message)) {
if (
hasErrors(message) ||
someSendStatus(sendStateByConversationId, isFailed)
) {
return sent ? 'partial-sent' : 'error';
}
return sent ? 'viewed' : 'sending';
@ -1248,7 +1252,10 @@ export function getMessagePropStatus(
SendStatus.Pending
);
if (hasErrors(message)) {
if (
hasErrors(message) ||
someSendStatus(sendStateByConversationId, isFailed)
) {
return isSent(highestSuccessfulStatus) ? 'partial-sent' : 'error';
}
if (isViewed(highestSuccessfulStatus)) {

View file

@ -4,18 +4,34 @@
import React from 'react';
import { connect } from 'react-redux';
import { App, PropsType } from '../../components/App';
import { App } from '../../components/App';
import { SmartCallManager } from './CallManager';
import { SmartGlobalModalContainer } from './GlobalModalContainer';
import { SmartSafetyNumberViewer } from './SafetyNumberViewer';
import { StateType } from '../reducer';
import { getTheme } from '../selectors/user';
import { getIntl, getTheme } from '../selectors/user';
import {
getConversationsStoppingMessageSendBecauseOfVerification,
getNumberOfMessagesPendingBecauseOfVerification,
} from '../selectors/conversations';
import { mapDispatchToProps } from '../actions';
import type { SafetyNumberProps } from '../../components/SafetyNumberChangeDialog';
const mapStateToProps = (state: StateType): PropsType => {
const mapStateToProps = (state: StateType) => {
return {
...state.app,
conversationsStoppingMessageSendBecauseOfVerification: getConversationsStoppingMessageSendBecauseOfVerification(
state
),
i18n: getIntl(state),
numberOfMessagesPendingBecauseOfVerification: getNumberOfMessagesPendingBecauseOfVerification(
state
),
renderCallManager: () => <SmartCallManager />,
renderGlobalModalContainer: () => <SmartGlobalModalContainer />,
renderSafetyNumber: (props: SafetyNumberProps) => (
<SmartSafetyNumberViewer {...props} />
),
theme: getTheme(state),
};
};

View file

@ -32,7 +32,6 @@ const mapStateToProps = (
receivedAt,
sentAt,
sendAnyway,
showSafetyNumber,
displayTapToViewMessage,
@ -71,7 +70,6 @@ const mapStateToProps = (
i18n: getIntl(state),
interactionMode: getInteractionMode(state),
sendAnyway,
showSafetyNumber,
displayTapToViewMessage,