ConversationView: Move setQuotedMessage/scrollToMessage to redux

This commit is contained in:
Scott Nonnenberg 2022-12-09 11:11:14 -08:00 committed by GitHub
parent 7c68f9ef1a
commit 07f7fa93d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 183 additions and 159 deletions

View file

@ -1631,7 +1631,16 @@ export async function startApp(): Promise<void> {
) { ) {
const { selectedMessage } = state.conversations; const { selectedMessage } = state.conversations;
conversation.trigger('toggle-reply', selectedMessage); const composerState = window.reduxStore
? window.reduxStore.getState().composer
: undefined;
const quote = composerState?.quotedMessage?.quote;
window.reduxActions.composer.setQuoteByMessageId(
conversation.id,
quote ? undefined : selectedMessage
);
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
return; return;

View file

@ -45,12 +45,13 @@ type PropsType = {
executeMenuRole: ExecuteMenuRoleType; executeMenuRole: ExecuteMenuRoleType;
executeMenuAction: (action: MenuActionType) => void; executeMenuAction: (action: MenuActionType) => void;
hideToast: () => unknown;
titleBarDoubleClick: () => void; titleBarDoubleClick: () => void;
toast?: { toast?: {
toastType: ToastType; toastType: ToastType;
parameters?: ReplacementValuesType; parameters?: ReplacementValuesType;
}; };
hideToast: () => unknown; scrollToMessage: (conversationId: string, messageId: string) => unknown;
toggleStoriesView: () => unknown; toggleStoriesView: () => unknown;
viewStory: ViewStoryActionCreatorType; viewStory: ViewStoryActionCreatorType;
} & ComponentProps<typeof Inbox>; } & ComponentProps<typeof Inbox>;
@ -79,6 +80,7 @@ export function App({
renderStories, renderStories,
renderStoryViewer, renderStoryViewer,
requestVerification, requestVerification,
scrollToMessage,
selectedConversationId, selectedConversationId,
selectedMessage, selectedMessage,
selectedMessageSource, selectedMessageSource,
@ -116,6 +118,7 @@ export function App({
renderCustomizingPreferredReactionsModal renderCustomizingPreferredReactionsModal
} }
renderLeftPane={renderLeftPane} renderLeftPane={renderLeftPane}
scrollToMessage={scrollToMessage}
selectedConversationId={selectedConversationId} selectedConversationId={selectedConversationId}
selectedMessage={selectedMessage} selectedMessage={selectedMessage}
selectedMessageSource={selectedMessageSource} selectedMessageSource={selectedMessageSource}

View file

@ -43,6 +43,7 @@ const useProps = (overrideProps: Partial<Props> = {}): Props => ({
removeAttachment: action('removeAttachment'), removeAttachment: action('removeAttachment'),
theme: React.useContext(StorybookThemeContext), theme: React.useContext(StorybookThemeContext),
setComposerFocus: action('setComposerFocus'), setComposerFocus: action('setComposerFocus'),
setQuoteByMessageId: action('setQuoteByMessageId'),
// AttachmentList // AttachmentList
draftAttachments: overrideProps.draftAttachments || [], draftAttachments: overrideProps.draftAttachments || [],
@ -63,8 +64,7 @@ const useProps = (overrideProps: Partial<Props> = {}): Props => ({
onCloseLinkPreview: action('onCloseLinkPreview'), onCloseLinkPreview: action('onCloseLinkPreview'),
// Quote // Quote
quotedMessageProps: overrideProps.quotedMessageProps, quotedMessageProps: overrideProps.quotedMessageProps,
onClickQuotedMessage: action('onClickQuotedMessage'), scrollToMessage: action('scrollToMessage'),
setQuotedMessage: action('setQuotedMessage'),
// MediaEditor // MediaEditor
imageToBlurHash: async () => 'LDA,FDBnm+I=p{tkIUI;~UkpELV]', imageToBlurHash: async () => 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
// MediaQualitySelector // MediaQualitySelector

View file

@ -97,7 +97,6 @@ export type OwnProps = Readonly<{
linkPreviewResult?: LinkPreviewType; linkPreviewResult?: LinkPreviewType;
messageRequestsEnabled?: boolean; messageRequestsEnabled?: boolean;
onClearAttachments(): unknown; onClearAttachments(): unknown;
onClickQuotedMessage(): unknown;
onCloseLinkPreview(): unknown; onCloseLinkPreview(): unknown;
processAttachments: (options: { processAttachments: (options: {
conversationId: string; conversationId: string;
@ -119,13 +118,18 @@ export type OwnProps = Readonly<{
} }
): unknown; ): unknown;
openConversation(conversationId: string): unknown; openConversation(conversationId: string): unknown;
quotedMessageId?: string;
quotedMessageProps?: Omit< quotedMessageProps?: Omit<
QuoteProps, QuoteProps,
'i18n' | 'onClick' | 'onClose' | 'withContentAbove' 'i18n' | 'onClick' | 'onClose' | 'withContentAbove'
>; >;
removeAttachment: (conversationId: string, filePath: string) => unknown; removeAttachment: (conversationId: string, filePath: string) => unknown;
scrollToMessage: (conversationId: string, messageId: string) => unknown;
setComposerFocus: (conversationId: string) => unknown; setComposerFocus: (conversationId: string) => unknown;
setQuotedMessage(message: undefined): unknown; setQuoteByMessageId(
conversationId: string,
messageId: string | undefined
): unknown;
shouldSendHighQualityAttachments: boolean; shouldSendHighQualityAttachments: boolean;
startRecording: () => unknown; startRecording: () => unknown;
theme: ThemeType; theme: ThemeType;
@ -179,6 +183,7 @@ export function CompositionArea({
removeAttachment, removeAttachment,
sendMultiMediaMessage, sendMultiMediaMessage,
setComposerFocus, setComposerFocus,
setQuoteByMessageId,
theme, theme,
// AttachmentList // AttachmentList
@ -196,9 +201,9 @@ export function CompositionArea({
linkPreviewResult, linkPreviewResult,
onCloseLinkPreview, onCloseLinkPreview,
// Quote // Quote
quotedMessageId,
quotedMessageProps, quotedMessageProps,
onClickQuotedMessage, scrollToMessage,
setQuotedMessage,
// MediaQualitySelector // MediaQualitySelector
onSelectMediaQuality, onSelectMediaQuality,
shouldSendHighQualityAttachments, shouldSendHighQualityAttachments,
@ -639,18 +644,15 @@ export function CompositionArea({
'CompositionArea__row--column' 'CompositionArea__row--column'
)} )}
> >
{quotedMessageProps && ( {quotedMessageId && quotedMessageProps && (
<div className="quote-wrapper"> <div className="quote-wrapper">
<Quote <Quote
isCompose isCompose
{...quotedMessageProps} {...quotedMessageProps}
i18n={i18n} i18n={i18n}
onClick={onClickQuotedMessage} onClick={() => scrollToMessage(conversationId, quotedMessageId)}
onClose={() => { onClose={() => {
// This one is for redux... setQuoteByMessageId(conversationId, undefined);
setQuotedMessage(undefined);
// and this is for conversation_view.
clearQuotedMessage?.();
}} }}
/> />
</div> </div>

View file

@ -23,6 +23,7 @@ export type PropsType = {
isCustomizingPreferredReactions: boolean; isCustomizingPreferredReactions: boolean;
renderCustomizingPreferredReactionsModal: () => JSX.Element; renderCustomizingPreferredReactionsModal: () => JSX.Element;
renderLeftPane: () => JSX.Element; renderLeftPane: () => JSX.Element;
scrollToMessage: (conversationId: string, messageId: string) => unknown;
selectedConversationId?: string; selectedConversationId?: string;
selectedMessage?: string; selectedMessage?: string;
selectedMessageSource?: SelectedMessageSource; selectedMessageSource?: SelectedMessageSource;
@ -36,6 +37,7 @@ export function Inbox({
isCustomizingPreferredReactions, isCustomizingPreferredReactions,
renderCustomizingPreferredReactionsModal, renderCustomizingPreferredReactionsModal,
renderLeftPane, renderLeftPane,
scrollToMessage,
selectedConversationId, selectedConversationId,
selectedMessage, selectedMessage,
selectedMessageSource, selectedMessageSource,
@ -93,10 +95,11 @@ export function Inbox({
selectedMessage && selectedMessage &&
selectedMessageSource !== SelectedMessageSource.Focus selectedMessageSource !== SelectedMessageSource.Focus
) { ) {
conversation.trigger('scroll-to-message', selectedMessage); scrollToMessage(conversation.id, selectedMessage);
} }
}, [ }, [
prevConversation, prevConversation,
scrollToMessage,
selectedConversationId, selectedConversationId,
selectedMessage, selectedMessage,
selectedMessageSource, selectedMessageSource,

View file

@ -122,7 +122,7 @@ const defaultMessageProps: TimelineMessagesProps = {
renderEmojiPicker: () => <div />, renderEmojiPicker: () => <div />,
renderReactionPicker: () => <div />, renderReactionPicker: () => <div />,
renderAudioAttachment: () => <div>*AudioAttachment*</div>, renderAudioAttachment: () => <div>*AudioAttachment*</div>,
replyToMessage: action('default--replyToMessage'), setQuoteByMessageId: action('default--setQuoteByMessageId'),
retrySend: action('default--retrySend'), retrySend: action('default--retrySend'),
retryDeleteForEveryone: action('default--retryDeleteForEveryone'), retryDeleteForEveryone: action('default--retryDeleteForEveryone'),
scrollToQuotedMessage: action('default--scrollToQuotedMessage'), scrollToQuotedMessage: action('default--scrollToQuotedMessage'),

View file

@ -276,7 +276,7 @@ const actions = () => ({
updateSharedGroups: action('updateSharedGroups'), updateSharedGroups: action('updateSharedGroups'),
reactToMessage: action('reactToMessage'), reactToMessage: action('reactToMessage'),
replyToMessage: action('replyToMessage'), setQuoteByMessageId: action('setQuoteByMessageId'),
retryDeleteForEveryone: action('retryDeleteForEveryone'), retryDeleteForEveryone: action('retryDeleteForEveryone'),
retrySend: action('retrySend'), retrySend: action('retrySend'),
deleteMessage: action('deleteMessage'), deleteMessage: action('deleteMessage'),

View file

@ -239,7 +239,6 @@ const getActions = createSelector(
'doubleCheckMissingQuoteReference', 'doubleCheckMissingQuoteReference',
'checkForAccount', 'checkForAccount',
'reactToMessage', 'reactToMessage',
'replyToMessage',
'retryDeleteForEveryone', 'retryDeleteForEveryone',
'retrySend', 'retrySend',
'toggleForwardMessageModal', 'toggleForwardMessageModal',
@ -248,6 +247,7 @@ const getActions = createSelector(
'showMessageDetail', 'showMessageDetail',
'openConversation', 'openConversation',
'openGiftBadge', 'openGiftBadge',
'setQuoteByMessageId',
'showContactDetail', 'showContactDetail',
'showContactModal', 'showContactModal',
'kickOffAttachmentDownload', 'kickOffAttachmentDownload',

View file

@ -66,7 +66,7 @@ const getDefaultProps = () => ({
checkForAccount: action('checkForAccount'), checkForAccount: action('checkForAccount'),
clearSelectedMessage: action('clearSelectedMessage'), clearSelectedMessage: action('clearSelectedMessage'),
contactSupport: action('contactSupport'), contactSupport: action('contactSupport'),
replyToMessage: action('replyToMessage'), setQuoteByMessageId: action('setQuoteByMessageId'),
retryDeleteForEveryone: action('retryDeleteForEveryone'), retryDeleteForEveryone: action('retryDeleteForEveryone'),
retrySend: action('retrySend'), retrySend: action('retrySend'),
blockGroupLinkRequests: action('blockGroupLinkRequests'), blockGroupLinkRequests: action('blockGroupLinkRequests'),

View file

@ -294,7 +294,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
renderEmojiPicker, renderEmojiPicker,
renderReactionPicker, renderReactionPicker,
renderAudioAttachment, renderAudioAttachment,
replyToMessage: action('replyToMessage'), setQuoteByMessageId: action('setQuoteByMessageId'),
retrySend: action('retrySend'), retrySend: action('retrySend'),
retryDeleteForEveryone: action('retryDeleteForEveryone'), retryDeleteForEveryone: action('retryDeleteForEveryone'),
scrollToQuotedMessage: action('scrollToQuotedMessage'), scrollToQuotedMessage: action('scrollToQuotedMessage'),

View file

@ -49,8 +49,7 @@ export type PropsActions = {
) => void; ) => void;
retrySend: (id: string) => void; retrySend: (id: string) => void;
retryDeleteForEveryone: (id: string) => void; retryDeleteForEveryone: (id: string) => void;
setQuoteByMessageId: (conversationId: string, messageId: string) => void;
replyToMessage: (id: string) => void;
} & MessagePropsActions; } & MessagePropsActions;
export type Props = PropsData & export type Props = PropsData &
@ -83,6 +82,7 @@ export function TimelineMessage(props: Props): JSX.Element {
canRetryDeleteForEveryone, canRetryDeleteForEveryone,
contact, contact,
payment, payment,
conversationId,
containerElementRef, containerElementRef,
containerWidthBreakpoint, containerWidthBreakpoint,
deletedForEveryone, deletedForEveryone,
@ -94,7 +94,7 @@ export function TimelineMessage(props: Props): JSX.Element {
isSticker, isSticker,
isTapToView, isTapToView,
reactToMessage, reactToMessage,
replyToMessage, setQuoteByMessageId,
renderReactionPicker, renderReactionPicker,
renderEmojiPicker, renderEmojiPicker,
retrySend, retrySend,
@ -234,7 +234,9 @@ export function TimelineMessage(props: Props): JSX.Element {
? openGenericAttachment ? openGenericAttachment
: undefined; : undefined;
const handleReplyToMessage = canReply ? () => replyToMessage(id) : undefined; const handleReplyToMessage = canReply
? () => setQuoteByMessageId(conversationId, id)
: undefined;
const handleReact = canReact ? () => toggleReactionPicker() : undefined; const handleReact = canReact ? () => toggleReactionPicker() : undefined;

View file

@ -61,6 +61,9 @@ import { resolveAttachmentDraftData } from '../../util/resolveAttachmentDraftDat
import { resolveDraftAttachmentOnDisk } from '../../util/resolveDraftAttachmentOnDisk'; import { resolveDraftAttachmentOnDisk } from '../../util/resolveDraftAttachmentOnDisk';
import { shouldShowInvalidMessageToast } from '../../util/shouldShowInvalidMessageToast'; import { shouldShowInvalidMessageToast } from '../../util/shouldShowInvalidMessageToast';
import { writeDraftAttachment } from '../../util/writeDraftAttachment'; import { writeDraftAttachment } from '../../util/writeDraftAttachment';
import { getMessageById } from '../../messages/getMessageById';
import { canReply } from '../selectors/message';
import { getConversationSelector } from '../selectors/conversations';
// State // State
@ -143,6 +146,7 @@ export const actions = {
sendStickerMessage, sendStickerMessage,
setComposerDisabledState, setComposerDisabledState,
setComposerFocus, setComposerFocus,
setQuoteByMessageId,
setMediaQualitySetting, setMediaQualitySetting,
setQuotedMessage, setQuotedMessage,
}; };
@ -267,7 +271,11 @@ function sendMultiMediaMessage(
conversation.setMarkedUnread(false); conversation.setMarkedUnread(false);
resetLinkPreview(); resetLinkPreview();
clearConversationDraftAttachments(conversationId, draftAttachments); clearConversationDraftAttachments(conversationId, draftAttachments);
dispatch(setQuotedMessage(undefined)); setQuoteByMessageId(conversationId, undefined)(
dispatch,
getState,
undefined
);
dispatch(resetComposer()); dispatch(resetComposer());
}, },
} }
@ -349,6 +357,77 @@ function getAttachmentsFromConversationModel(
return conversation?.get('draftAttachments') || []; return conversation?.get('draftAttachments') || [];
} }
function setQuoteByMessageId(
conversationId: string,
messageId: string | undefined
): ThunkAction<
void,
RootStateType,
unknown,
SetComposerDisabledStateActionType | SetQuotedMessageActionType
> {
return async (dispatch, getState) => {
const conversation = window.ConversationController.get(conversationId);
if (!conversation) {
throw new Error('sendStickerMessage: No conversation found');
}
const message = messageId ? await getMessageById(messageId) : undefined;
const state = getState();
if (
message &&
!canReply(
message.attributes,
window.ConversationController.getOurConversationIdOrThrow(),
getConversationSelector(state)
)
) {
return;
}
if (message && !message.isNormalBubble()) {
return;
}
const existing = conversation.get('quotedMessageId');
if (existing !== messageId) {
const now = Date.now();
let activeAt = conversation.get('active_at');
let timestamp = conversation.get('timestamp');
if (!activeAt && messageId) {
activeAt = now;
timestamp = now;
}
conversation.set({
active_at: activeAt,
draftChanged: true,
quotedMessageId: messageId,
timestamp,
});
window.Signal.Data.updateConversation(conversation.attributes);
}
if (message) {
const quote = await conversation.makeQuote(message);
dispatch(
setQuotedMessage({
conversationId,
quote,
})
);
dispatch(setComposerFocus(conversation.id));
dispatch(setComposerDisabledState(false));
} else {
dispatch(setQuotedMessage(undefined));
}
};
}
function addAttachment( function addAttachment(
conversationId: string, conversationId: string,
attachment: InMemoryAttachmentDraftType attachment: InMemoryAttachmentDraftType

View file

@ -70,6 +70,7 @@ import {
getConversationUuidsStoppingSend, getConversationUuidsStoppingSend,
getConversationIdsStoppedForVerification, getConversationIdsStoppedForVerification,
getMe, getMe,
getMessagesByConversation,
} from '../selectors/conversations'; } from '../selectors/conversations';
import type { AvatarDataType, AvatarUpdateType } from '../../types/Avatar'; import type { AvatarDataType, AvatarUpdateType } from '../../types/Avatar';
import { getDefaultAvatars } from '../../types/Avatar'; import { getDefaultAvatars } from '../../types/Avatar';
@ -113,6 +114,7 @@ import {
buildPromotePendingAdminApprovalMemberChange, buildPromotePendingAdminApprovalMemberChange,
initiateMigrationToGroupV2 as doInitiateMigrationToGroupV2, initiateMigrationToGroupV2 as doInitiateMigrationToGroupV2,
} from '../../groups'; } from '../../groups';
import { getMessageById } from '../../messages/getMessageById';
// State // State
@ -2480,16 +2482,55 @@ function closeMaximumGroupSizeModal(): CloseMaximumGroupSizeModalActionType {
function closeRecommendedGroupSizeModal(): CloseRecommendedGroupSizeModalActionType { function closeRecommendedGroupSizeModal(): CloseRecommendedGroupSizeModalActionType {
return { type: 'CLOSE_RECOMMENDED_GROUP_SIZE_MODAL' }; return { type: 'CLOSE_RECOMMENDED_GROUP_SIZE_MODAL' };
} }
function scrollToMessage( function scrollToMessage(
conversationId: string, conversationId: string,
messageId: string messageId: string
): ScrollToMessageActionType { ): ThunkAction<void, RootStateType, unknown, ScrollToMessageActionType> {
return { return async (dispatch, getState) => {
type: 'SCROLL_TO_MESSAGE', const conversation = window.ConversationController.get(conversationId);
payload: { if (!conversation) {
conversationId, throw new Error('scrollToMessage: No conversation found');
messageId, }
},
const message = await getMessageById(messageId);
if (!message) {
throw new Error(`scrollToMessage: failed to load message ${messageId}`);
}
if (message.get('conversationId') !== conversationId) {
throw new Error(
`scrollToMessage: ${messageId} didn't have conversationId ${conversationId}`
);
}
const state = getState();
let isInMemory = true;
if (!window.MessageController.getById(messageId)) {
isInMemory = false;
}
// Message might be in memory, but not in the redux anymore because
// we call `messageReset()` in `loadAndScroll()`.
const messagesByConversation =
getMessagesByConversation(state)[conversationId];
if (!messagesByConversation?.messageIds.includes(messageId)) {
isInMemory = false;
}
if (isInMemory) {
dispatch({
type: 'SCROLL_TO_MESSAGE',
payload: {
conversationId,
messageId,
},
});
return;
}
conversation.loadAndScroll(messageId);
}; };
} }

View file

@ -33,13 +33,12 @@ import { isSignalConversation } from '../../util/isSignalConversation';
type ExternalProps = { type ExternalProps = {
id: string; id: string;
handleClickQuotedMessage: (id: string) => unknown;
}; };
export type CompositionAreaPropsType = ExternalProps & ComponentPropsType; export type CompositionAreaPropsType = ExternalProps & ComponentPropsType;
const mapStateToProps = (state: StateType, props: ExternalProps) => { const mapStateToProps = (state: StateType, props: ExternalProps) => {
const { id, handleClickQuotedMessage } = props; const { id } = props;
const conversationSelector = getConversationSelector(state); const conversationSelector = getConversationSelector(state);
const conversation = conversationSelector(id); const conversation = conversationSelector(id);
@ -108,18 +107,13 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
linkPreviewLoading, linkPreviewLoading,
linkPreviewResult, linkPreviewResult,
// Quote // Quote
quotedMessageId: quotedMessage?.quote?.messageId,
quotedMessageProps: quotedMessage quotedMessageProps: quotedMessage
? getPropsForQuote(quotedMessage, { ? getPropsForQuote(quotedMessage, {
conversationSelector, conversationSelector,
ourConversationId: getUserConversationId(state), ourConversationId: getUserConversationId(state),
}) })
: undefined, : undefined,
onClickQuotedMessage: () => {
const messageId = quotedMessage?.quote?.messageId;
if (messageId) {
handleClickQuotedMessage(messageId);
}
},
// Emojis // Emojis
recentEmojis, recentEmojis,
skinTone: getEmojiSkinTone(state), skinTone: getEmojiSkinTone(state),

View file

@ -17,9 +17,6 @@ export type PropsType = {
conversationId: string; conversationId: string;
compositionAreaProps: Pick< compositionAreaProps: Pick<
CompositionAreaPropsType, CompositionAreaPropsType,
| 'clearQuotedMessage'
| 'getQuotedMessage'
| 'handleClickQuotedMessage'
| 'id' | 'id'
| 'onCancelJoinRequest' | 'onCancelJoinRequest'
| 'onClearAttachments' | 'onClearAttachments'

View file

@ -81,7 +81,6 @@ export type TimelinePropsType = ExternalProps &
| 'openLink' | 'openLink'
| 'reactToMessage' | 'reactToMessage'
| 'removeMember' | 'removeMember'
| 'replyToMessage'
| 'retryDeleteForEveryone' | 'retryDeleteForEveryone'
| 'retrySend' | 'retrySend'
| 'scrollToQuotedMessage' | 'scrollToQuotedMessage'

View file

@ -15,6 +15,7 @@ import { fakeDraftAttachment } from '../../helpers/fakeAttachment';
describe('both/state/ducks/composer', () => { describe('both/state/ducks/composer', () => {
const QUOTED_MESSAGE = { const QUOTED_MESSAGE = {
conversationId: '123', conversationId: '123',
id: 'quoted-message-id',
quote: { quote: {
attachments: [], attachments: [],
id: 456, id: 456,

View file

@ -21,18 +21,13 @@ import { strictAssert } from '../util/assert';
import { enqueueReactionForSend } from '../reactions/enqueueReactionForSend'; import { enqueueReactionForSend } from '../reactions/enqueueReactionForSend';
import type { GroupNameCollisionsWithIdsByTitle } from '../util/groupMemberNameCollisions'; import type { GroupNameCollisionsWithIdsByTitle } from '../util/groupMemberNameCollisions';
import { isGroup } from '../util/whatTypeOfConversation'; import { isGroup } from '../util/whatTypeOfConversation';
import { findAndFormatContact } from '../util/findAndFormatContact';
import { getPreferredBadgeSelector } from '../state/selectors/badges'; import { getPreferredBadgeSelector } from '../state/selectors/badges';
import { import {
canReply,
isIncoming, isIncoming,
isOutgoing, isOutgoing,
isTapToView, isTapToView,
} from '../state/selectors/message'; } from '../state/selectors/message';
import { import { getConversationSelector } from '../state/selectors/conversations';
getConversationSelector,
getMessagesByConversation,
} from '../state/selectors/conversations';
import { getActiveCallState } from '../state/selectors/calling'; import { getActiveCallState } from '../state/selectors/calling';
import { getTheme } from '../state/selectors/user'; import { getTheme } from '../state/selectors/user';
import { ReactWrapperView } from './ReactWrapperView'; import { ReactWrapperView } from './ReactWrapperView';
@ -113,7 +108,6 @@ type MessageActionsType = {
messageId: string, messageId: string,
reaction: { emoji: string; remove: boolean } reaction: { emoji: string; remove: boolean }
) => unknown; ) => unknown;
replyToMessage: (messageId: string) => unknown;
retrySend: (messageId: string) => unknown; retrySend: (messageId: string) => unknown;
retryDeleteForEveryone: (messageId: string) => unknown; retryDeleteForEveryone: (messageId: string) => unknown;
showContactDetail: (options: { showContactDetail: (options: {
@ -171,7 +165,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
// These are triggered by InboxView // These are triggered by InboxView
this.listenTo(this.model, 'opened', this.onOpened); this.listenTo(this.model, 'opened', this.onOpened);
this.listenTo(this.model, 'scroll-to-message', this.scrollToMessage);
this.listenTo(this.model, 'unload', (reason: string) => this.listenTo(this.model, 'unload', (reason: string) =>
this.unload(`model trigger - ${reason}`) this.unload(`model trigger - ${reason}`)
); );
@ -180,18 +173,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
this.listenTo(this.model, 'open-all-media', this.showAllMedia); this.listenTo(this.model, 'open-all-media', this.showAllMedia);
this.listenTo(this.model, 'escape-pressed', this.resetPanel); this.listenTo(this.model, 'escape-pressed', this.resetPanel);
this.listenTo(this.model, 'show-message-details', this.showMessageDetail); this.listenTo(this.model, 'show-message-details', this.showMessageDetail);
this.listenTo(
this.model,
'toggle-reply',
(messageId: string | undefined) => {
const composerState = window.reduxStore
? window.reduxStore.getState().composer
: undefined;
const quote = composerState?.quotedMessage?.quote;
this.setQuoteMessage(quote ? undefined : messageId);
}
);
this.listenTo( this.listenTo(
this.model, this.model,
'save-attachment', 'save-attachment',
@ -332,7 +313,10 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
return; return;
} }
this.scrollToMessage(message.id); window.reduxActions.conversations.scrollToMessage(
conversationId,
message.id
);
}; };
const markMessageRead = async (messageId: string) => { const markMessageRead = async (messageId: string) => {
@ -396,8 +380,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
id: this.model.id, id: this.model.id,
onClickAddPack: () => this.showStickerManager(), onClickAddPack: () => this.showStickerManager(),
onTextTooLong: () => showToast(ToastMessageBodyTooLong), onTextTooLong: () => showToast(ToastMessageBodyTooLong),
getQuotedMessage: () => this.model.get('quotedMessageId'),
clearQuotedMessage: () => this.setQuoteMessage(undefined),
onCancelJoinRequest: async () => { onCancelJoinRequest: async () => {
await window.showConfirmationDialog({ await window.showConfirmationDialog({
dialogName: 'GroupV2CancelRequestToJoin', dialogName: 'GroupV2CancelRequestToJoin',
@ -421,8 +403,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
window.reduxActions.composer.setMediaQualitySetting(isHQ); window.reduxActions.composer.setMediaQualitySetting(isHQ);
}, },
handleClickQuotedMessage: (id: string) => this.scrollToMessage(id),
onCloseLinkPreview: () => { onCloseLinkPreview: () => {
suspendLinkPreviews(); suspendLinkPreviews();
removeLinkPreview(); removeLinkPreview();
@ -461,9 +441,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
showToast(ToastReactionFailed); showToast(ToastReactionFailed);
} }
}; };
const replyToMessage = (messageId: string) => {
this.setQuoteMessage(messageId);
};
const retrySend = retryMessageSend; const retrySend = retryMessageSend;
const deleteMessage = (messageId: string) => { const deleteMessage = (messageId: string) => {
this.deleteMessage(messageId); this.deleteMessage(messageId);
@ -555,7 +532,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
openGiftBadge, openGiftBadge,
openLink, openLink,
reactToMessage, reactToMessage,
replyToMessage,
retrySend, retrySend,
retryDeleteForEveryone, retryDeleteForEveryone,
showContactDetail, showContactDetail,
@ -567,37 +543,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
}; };
} }
async scrollToMessage(messageId: string): Promise<void> {
const message = await getMessageById(messageId);
if (!message) {
throw new Error(`scrollToMessage: failed to load message ${messageId}`);
}
const state = window.reduxStore.getState();
let isInMemory = true;
if (!window.MessageController.getById(messageId)) {
isInMemory = false;
}
// Message might be in memory, but not in the redux anymore because
// we call `messageReset()` in `loadAndScroll()`.
const messagesByConversation =
getMessagesByConversation(state)[this.model.id];
if (!messagesByConversation?.messageIds.includes(messageId)) {
isInMemory = false;
}
if (isInMemory) {
const { scrollToMessage } = window.reduxActions.conversations;
scrollToMessage(this.model.id, messageId);
return;
}
this.model.loadAndScroll(messageId);
}
unload(reason: string): void { unload(reason: string): void {
log.info( log.info(
'unloading conversation', 'unloading conversation',
@ -697,7 +642,10 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
const quotedMessageId = this.model.get('quotedMessageId'); const quotedMessageId = this.model.get('quotedMessageId');
if (quotedMessageId) { if (quotedMessageId) {
this.setQuoteMessage(quotedMessageId); window.reduxActions.composer.setQuoteByMessageId(
this.model.id,
quotedMessageId
);
} }
this.model.fetchLatestGroupV2Data(); this.model.fetchLatestGroupV2Data();
@ -1532,60 +1480,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
); );
} }
async setQuoteMessage(messageId: string | undefined): Promise<void> {
const { model } = this;
const message = messageId ? await getMessageById(messageId) : undefined;
if (
message &&
!canReply(
message.attributes,
window.ConversationController.getOurConversationIdOrThrow(),
findAndFormatContact
)
) {
return;
}
if (message && !message.isNormalBubble()) {
return;
}
const existing = model.get('quotedMessageId');
if (existing !== messageId) {
const now = Date.now();
let active_at = this.model.get('active_at');
let timestamp = this.model.get('timestamp');
if (!active_at && messageId) {
active_at = now;
timestamp = now;
}
this.model.set({
active_at,
draftChanged: true,
quotedMessageId: messageId,
timestamp,
});
await this.saveModel();
}
if (message) {
const quote = await model.makeQuote(message);
window.reduxActions.composer.setQuotedMessage({
conversationId: model.id,
quote,
});
window.reduxActions.composer.setComposerFocus(this.model.id);
window.reduxActions.composer.setComposerDisabledState(false);
} else {
window.reduxActions.composer.setQuotedMessage(undefined);
}
}
async clearAttachments(): Promise<void> { async clearAttachments(): Promise<void> {
const draftAttachments = this.model.get('draftAttachments') || []; const draftAttachments = this.model.get('draftAttachments') || [];
this.model.set({ this.model.set({