Retry outbound "normal" messages for up to a day
This commit is contained in:
parent
62cf51c060
commit
a85dd1be36
30 changed files with 1414 additions and 603 deletions
|
@ -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];
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue