Receive support for editing messages

This commit is contained in:
Josh Perez 2023-03-27 19:48:57 -04:00 committed by GitHub
parent 2781e621ad
commit 36e21c0134
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 2053 additions and 405 deletions

View file

@ -88,6 +88,7 @@ import type {
import { longRunningTaskWrapper } from '../../util/longRunningTaskWrapper';
import { drop } from '../../util/drop';
import { strictAssert } from '../../util/assert';
import { makeQuote } from '../../util/makeQuote';
// State
// eslint-disable-next-line local-rules/type-alias-readonlydeep
@ -630,7 +631,7 @@ export function setQuoteByMessageId(
}
if (message) {
const quote = await conversation.makeQuote(message);
const quote = await makeQuote(message.attributes);
// In case the conversation changed while we were about to set the quote
if (getState().conversations.selectedConversationId !== conversationId) {

View file

@ -1991,7 +1991,15 @@ function kickOffAttachmentDownload(
`kickOffAttachmentDownload: Message ${options.messageId} missing!`
);
}
await message.queueAttachmentDownloads();
const didUpdateValues = await message.queueAttachmentDownloads();
if (didUpdateValues) {
drop(
window.Signal.Data.saveMessage(message.attributes, {
ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(),
})
);
}
dispatch({
type: 'NOOP',

View file

@ -4,15 +4,24 @@
import type { ThunkAction } from 'redux-thunk';
import type { ReadonlyDeep } from 'type-fest';
import type { ExplodePromiseResultType } from '../../util/explodePromise';
import type { GroupV2PendingMemberType } from '../../model-types.d';
import type { PropsForMessage } from '../selectors/message';
import type {
GroupV2PendingMemberType,
MessageAttributesType,
} from '../../model-types.d';
import type {
MessageChangedActionType,
MessageDeletedActionType,
MessageExpiredActionType,
} from './conversations';
import type { MessagePropsType } from '../selectors/message';
import type { RecipientsByConversation } from './stories';
import type { SafetyNumberChangeSource } from '../../components/SafetyNumberChangeDialog';
import type { StateType as RootStateType } from '../reducer';
import type { UUIDStringType } from '../../types/UUID';
import * as Errors from '../../types/errors';
import * as SingleServePromise from '../../services/singleServePromise';
import * as Stickers from '../../types/Stickers';
import * as Errors from '../../types/errors';
import * as log from '../../logging/log';
import { getMessageById } from '../../messages/getMessageById';
import { getMessagePropsSelector } from '../selectors/message';
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
@ -22,19 +31,24 @@ import { isGroupV1 } from '../../util/whatTypeOfConversation';
import { authorizeArtCreator } from '../../textsecure/authorizeArtCreator';
import type { AuthorizeArtCreatorOptionsType } from '../../textsecure/authorizeArtCreator';
import { getGroupMigrationMembers } from '../../groups';
import * as log from '../../logging/log';
import { ToastType } from '../../types/Toast';
import {
MESSAGE_CHANGED,
MESSAGE_DELETED,
MESSAGE_EXPIRED,
} from './conversations';
import { SHOW_TOAST } from './toast';
import type { ShowToastActionType } from './toast';
// State
export type EditHistoryMessagesType = ReadonlyDeep<
Array<MessageAttributesType>
>;
export type ConfirmDeleteForMeModalProps = ReadonlyDeep<{
count: number;
}>;
export type ForwardMessagePropsType = ReadonlyDeep<
Omit<PropsForMessage, 'renderingContext' | 'menu' | 'contextMenu'>
>;
export type ForwardMessagePropsType = ReadonlyDeep<MessagePropsType>;
export type ForwardMessagesPropsType = ReadonlyDeep<{
messages: Array<ForwardMessagePropsType>;
onForward?: () => void;
@ -57,6 +71,7 @@ type MigrateToGV2PropsType = ReadonlyDeep<{
export type GlobalModalsStateType = ReadonlyDeep<{
addUserToAnotherGroupModalContactId?: string;
contactModalState?: ContactModalStateType;
editHistoryMessages?: EditHistoryMessagesType;
errorModalProps?: {
description?: string;
title?: string;
@ -115,6 +130,8 @@ const CONFIRM_AUTH_ART_CREATOR_PENDING =
'globalModals/CONFIRM_AUTH_ART_CREATOR_PENDING';
const CONFIRM_AUTH_ART_CREATOR_FULFILLED =
'globalModals/CONFIRM_AUTH_ART_CREATOR_FULFILLED';
const SHOW_EDIT_HISTORY_MODAL = 'globalModals/SHOW_EDIT_HISTORY_MODAL';
const CLOSE_EDIT_HISTORY_MODAL = 'globalModals/CLOSE_EDIT_HISTORY_MODAL';
export type ContactModalStateType = ReadonlyDeep<{
contactId: string;
@ -264,34 +281,50 @@ type ConfirmAuthArtCreatorFulfilledActionType = ReadonlyDeep<{
type: typeof CONFIRM_AUTH_ART_CREATOR_FULFILLED;
}>;
type ShowEditHistoryModalActionType = ReadonlyDeep<{
type: typeof SHOW_EDIT_HISTORY_MODAL;
payload: {
messages: EditHistoryMessagesType;
};
}>;
type CloseEditHistoryModalActionType = ReadonlyDeep<{
type: typeof CLOSE_EDIT_HISTORY_MODAL;
}>;
export type GlobalModalsActionType = ReadonlyDeep<
| StartMigrationToGV2ActionType
| CloseGV2MigrationDialogActionType
| HideContactModalActionType
| ShowContactModalActionType
| HideWhatsNewModalActionType
| ShowWhatsNewModalActionType
| HideUserNotFoundModalActionType
| ShowUserNotFoundModalActionType
| HideStoriesSettingsActionType
| ShowStoriesSettingsActionType
| HideSendAnywayDialogActiontype
| ShowSendAnywayDialogActionType
| CloseStickerPackPreviewActionType
| ShowStickerPackPreviewActionType
| CloseErrorModalActionType
| ShowErrorModalActionType
| CloseShortcutGuideModalActionType
| ShowShortcutGuideModalActionType
| CancelAuthArtCreatorActionType
| ConfirmAuthArtCreatorPendingActionType
| CloseEditHistoryModalActionType
| CloseErrorModalActionType
| CloseGV2MigrationDialogActionType
| CloseShortcutGuideModalActionType
| CloseStickerPackPreviewActionType
| ConfirmAuthArtCreatorFulfilledActionType
| ConfirmAuthArtCreatorPendingActionType
| HideContactModalActionType
| HideSendAnywayDialogActiontype
| HideStoriesSettingsActionType
| HideUserNotFoundModalActionType
| HideWhatsNewModalActionType
| MessageChangedActionType
| MessageDeletedActionType
| MessageExpiredActionType
| ShowAuthArtCreatorActionType
| ShowContactModalActionType
| ShowEditHistoryModalActionType
| ShowErrorModalActionType
| ShowSendAnywayDialogActionType
| ShowShortcutGuideModalActionType
| ShowStickerPackPreviewActionType
| ShowStoriesSettingsActionType
| ShowUserNotFoundModalActionType
| ShowWhatsNewModalActionType
| StartMigrationToGV2ActionType
| ToggleAddUserToAnotherGroupModalActionType
| ToggleForwardMessagesModalActionType
| ToggleProfileEditorActionType
| ToggleProfileEditorErrorActionType
| ToggleSafetyNumberModalActionType
| ToggleAddUserToAnotherGroupModalActionType
| ToggleSignalConnectionsModalActionType
| ToggleConfirmationModalActionType
>;
@ -299,34 +332,36 @@ export type GlobalModalsActionType = ReadonlyDeep<
// Action Creators
export const actions = {
hideContactModal,
showContactModal,
hideWhatsNewModal,
showWhatsNewModal,
hideUserNotFoundModal,
showUserNotFoundModal,
hideStoriesSettings,
showStoriesSettings,
cancelAuthorizeArtCreator,
closeEditHistoryModal,
closeErrorModal,
closeGV2MigrationDialog,
closeShortcutGuideModal,
closeStickerPackPreview,
confirmAuthorizeArtCreator,
hideBlockingSafetyNumberChangeDialog,
hideContactModal,
hideStoriesSettings,
hideUserNotFoundModal,
hideWhatsNewModal,
showAuthorizeArtCreator,
showBlockingSafetyNumberChangeDialog,
showContactModal,
showEditHistoryModal,
showErrorModal,
showGV2MigrationDialog,
showShortcutGuideModal,
showStickerPackPreview,
showStoriesSettings,
showUserNotFoundModal,
showWhatsNewModal,
toggleAddUserToAnotherGroupModal,
toggleConfirmationModal,
toggleForwardMessagesModal,
toggleProfileEditor,
toggleProfileEditorHasError,
toggleSafetyNumberModal,
toggleAddUserToAnotherGroupModal,
toggleSignalConnectionsModal,
toggleConfirmationModal,
showGV2MigrationDialog,
closeGV2MigrationDialog,
showStickerPackPreview,
closeStickerPackPreview,
closeErrorModal,
showErrorModal,
closeShortcutGuideModal,
showShortcutGuideModal,
showAuthorizeArtCreator,
cancelAuthorizeArtCreator,
confirmAuthorizeArtCreator,
};
export const useGlobalModalActions = (): BoundActionCreatorsMapObject<
@ -632,6 +667,56 @@ function cancelAuthorizeArtCreator(): ThunkAction<
};
}
function copyOverMessageAttributesIntoEditHistory(
messageAttributes: ReadonlyDeep<MessageAttributesType>
): EditHistoryMessagesType | undefined {
if (!messageAttributes.editHistory) {
return;
}
return messageAttributes.editHistory.map(editedMessageAttributes => ({
...messageAttributes,
...editedMessageAttributes,
// For timestamp uniqueness of messages
sent_at: editedMessageAttributes.timestamp,
}));
}
function showEditHistoryModal(
messageId: string
): ThunkAction<void, RootStateType, unknown, ShowEditHistoryModalActionType> {
return async dispatch => {
const message = await getMessageById(messageId);
if (!message) {
log.warn('showEditHistoryModal: no message found');
return;
}
const messageAttributes = message.attributes;
const nextEditHistoryMessages =
copyOverMessageAttributesIntoEditHistory(messageAttributes);
if (!nextEditHistoryMessages) {
log.warn('showEditHistoryModal: no edit history for message');
return;
}
dispatch({
type: SHOW_EDIT_HISTORY_MODAL,
payload: {
messages: nextEditHistoryMessages,
},
});
};
}
function closeEditHistoryModal(): CloseEditHistoryModalActionType {
return {
type: CLOSE_EDIT_HISTORY_MODAL,
};
}
export function showAuthorizeArtCreator(
data: AuthorizeArtCreatorDataType
): ShowAuthArtCreatorActionType {
@ -896,5 +981,71 @@ export function reducer(
};
}
if (action.type === SHOW_EDIT_HISTORY_MODAL) {
return {
...state,
editHistoryMessages: action.payload.messages,
};
}
if (action.type === CLOSE_EDIT_HISTORY_MODAL) {
return {
...state,
editHistoryMessages: undefined,
};
}
if (
action.type === MESSAGE_CHANGED ||
action.type === MESSAGE_DELETED ||
action.type === MESSAGE_EXPIRED
) {
if (!state.editHistoryMessages) {
return state;
}
if (action.type === MESSAGE_DELETED || action.type === MESSAGE_EXPIRED) {
const hasMessageId = state.editHistoryMessages.some(
edit => edit.id === action.payload.id
);
if (!hasMessageId) {
return state;
}
return {
...state,
editHistoryMessages: undefined,
};
}
if (action.type === MESSAGE_CHANGED) {
if (!action.payload.data.editHistory) {
return state;
}
const hasMessageId = state.editHistoryMessages.some(
edit => edit.id === action.payload.id
);
if (!hasMessageId) {
return state;
}
const nextEditHistoryMessages = copyOverMessageAttributesIntoEditHistory(
action.payload.data
);
if (!nextEditHistoryMessages) {
return state;
}
return {
...state,
editHistoryMessages: nextEditHistoryMessages,
};
}
}
return state;
}

View file

@ -136,6 +136,10 @@ type FormattedContact = Partial<ConversationType> &
| 'unblurredAvatarPath'
>;
export type PropsForMessage = Omit<TimelineMessagePropsData, 'interactionMode'>;
export type MessagePropsType = Omit<
PropsForMessage,
'renderingContext' | 'menu' | 'contextMenu'
>;
type PropsForUnsupportedMessage = {
canProcessNow: boolean;
contact: FormattedContact;
@ -718,6 +722,7 @@ export const getPropsForMessage = (
giftBadge: message.giftBadge,
id: message.id,
isBlocked: conversation.isBlocked || false,
isEditedMessage: Boolean(message.editHistory),
isMessageRequestAccepted: conversation?.acceptedMessageRequest ?? true,
isTargeted,
isTargetedCounter: isTargeted ? targetedMessageCounter : undefined,

View file

@ -0,0 +1,57 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useMemo } from 'react';
import { useSelector } from 'react-redux';
import type { GlobalModalsStateType } from '../ducks/globalModals';
import type { MessageAttributesType } from '../../model-types.d';
import type { StateType } from '../reducer';
import { EditHistoryMessagesModal } from '../../components/EditHistoryMessagesModal';
import { getIntl } from '../selectors/user';
import { getMessagePropsSelector } from '../selectors/message';
import { getPreferredBadgeSelector } from '../selectors/badges';
import { useConversationsActions } from '../ducks/conversations';
import { useGlobalModalActions } from '../ducks/globalModals';
import { useLightboxActions } from '../ducks/lightbox';
import { strictAssert } from '../../util/assert';
export function SmartEditHistoryMessagesModal(): JSX.Element {
const i18n = useSelector(getIntl);
const { closeEditHistoryModal } = useGlobalModalActions();
const { kickOffAttachmentDownload } = useConversationsActions();
const { showLightbox } = useLightboxActions();
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const { editHistoryMessages: messagesAttributes } = useSelector<
StateType,
GlobalModalsStateType
>(state => state.globalModals);
const messagePropsSelector = useSelector(getMessagePropsSelector);
strictAssert(messagesAttributes, 'messages not provided');
const editHistoryMessages = useMemo(() => {
return messagesAttributes.map(messageAttributes => ({
...messagePropsSelector(messageAttributes as MessageAttributesType),
// Make sure the messages don't get an "edited" badge
editHistory: undefined,
// Do not show the same reactions in the message history UI
reactions: undefined,
}));
}, [messagesAttributes, messagePropsSelector]);
return (
<EditHistoryMessagesModal
closeEditHistoryModal={closeEditHistoryModal}
editHistoryMessages={editHistoryMessages}
getPreferredBadge={getPreferredBadge}
i18n={i18n}
kickOffAttachmentDownload={kickOffAttachmentDownload}
showLightbox={showLightbox}
/>
);
}

View file

@ -10,6 +10,7 @@ import { ErrorModal } from '../../components/ErrorModal';
import { GlobalModalContainer } from '../../components/GlobalModalContainer';
import { SmartAddUserToAnotherGroupModal } from './AddUserToAnotherGroupModal';
import { SmartContactModal } from './ContactModal';
import { SmartEditHistoryMessagesModal } from './EditHistoryMessagesModal';
import { SmartForwardMessagesModal } from './ForwardMessagesModal';
import { SmartProfileEditorModal } from './ProfileEditorModal';
import { SmartSafetyNumberModal } from './SafetyNumberModal';
@ -21,6 +22,10 @@ import { getConversationsStoppingSend } from '../selectors/conversations';
import { getIntl, getTheme } from '../selectors/user';
import { useGlobalModalActions } from '../ducks/globalModals';
function renderEditHistoryMessagesModal(): JSX.Element {
return <SmartEditHistoryMessagesModal />;
}
function renderProfileEditor(): JSX.Element {
return <SmartProfileEditorModal />;
}
@ -55,6 +60,7 @@ export function SmartGlobalModalContainer(): JSX.Element {
const {
addUserToAnotherGroupModalContactId,
contactModalState,
editHistoryMessages,
errorModalProps,
forwardMessagesProps,
isProfileEditorVisible,
@ -120,6 +126,7 @@ export function SmartGlobalModalContainer(): JSX.Element {
<GlobalModalContainer
addUserToAnotherGroupModalContactId={addUserToAnotherGroupModalContactId}
contactModalState={contactModalState}
editHistoryMessages={editHistoryMessages}
errorModalProps={errorModalProps}
forwardMessagesProps={forwardMessagesProps}
hasSafetyNumberChangeModal={hasSafetyNumberChangeModal}
@ -133,6 +140,7 @@ export function SmartGlobalModalContainer(): JSX.Element {
isWhatsNewVisible={isWhatsNewVisible}
renderAddUserToAnotherGroup={renderAddUserToAnotherGroup}
renderContactModal={renderContactModal}
renderEditHistoryMessagesModal={renderEditHistoryMessagesModal}
renderErrorModal={renderErrorModal}
renderForwardMessagesModal={renderForwardMessagesModal}
renderProfileEditor={renderProfileEditor}

View file

@ -130,6 +130,7 @@ export function SmartTimelineItem(props: ExternalProps): JSX.Element {
const {
showContactModal,
showEditHistoryModal,
toggleForwardMessagesModal,
toggleSafetyNumberModal,
} = useGlobalModalActions();
@ -161,6 +162,7 @@ export function SmartTimelineItem(props: ExternalProps): JSX.Element {
shouldCollapseBelow={shouldCollapseBelow}
shouldHideMetadata={shouldHideMetadata}
shouldRenderDateHeader={shouldRenderDateHeader}
showEditHistoryModal={showEditHistoryModal}
i18n={i18n}
interactionMode={interactionMode}
theme={theme}