Make composer duck aware of the conversation it is in

This commit is contained in:
Josh Perez 2023-01-04 19:22:36 -05:00 committed by GitHub
parent 7a076be0e7
commit 198d6f7e26
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 449 additions and 170 deletions

View file

@ -160,6 +160,7 @@ import { downloadOnboardingStory } from './util/downloadOnboardingStory';
import { clearConversationDraftAttachments } from './util/clearConversationDraftAttachments'; import { clearConversationDraftAttachments } from './util/clearConversationDraftAttachments';
import { removeLinkPreview } from './services/LinkPreview'; import { removeLinkPreview } from './services/LinkPreview';
import { PanelType } from './types/Panels'; import { PanelType } from './types/Panels';
import { getQuotedMessageSelector } from './state/selectors/composer';
const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000; const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000;
@ -1628,10 +1629,8 @@ export async function startApp(): Promise<void> {
const { selectedMessage } = state.conversations; const { selectedMessage } = state.conversations;
const composerState = window.reduxStore const quotedMessageSelector = getQuotedMessageSelector(state);
? window.reduxStore.getState().composer const quote = quotedMessageSelector(conversation.id);
: undefined;
const quote = composerState?.quotedMessage?.quote;
window.reduxActions.composer.setQuoteByMessageId( window.reduxActions.composer.setQuoteByMessageId(
conversation.id, conversation.id,
@ -1708,7 +1707,7 @@ export async function startApp(): Promise<void> {
!shiftKey && !shiftKey &&
(key === 'p' || key === 'P') (key === 'p' || key === 'P')
) { ) {
removeLinkPreview(); removeLinkPreview(conversation.id);
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();

View file

@ -102,12 +102,12 @@ export type OwnProps = Readonly<{
linkPreviewResult?: LinkPreviewType; linkPreviewResult?: LinkPreviewType;
messageRequestsEnabled?: boolean; messageRequestsEnabled?: boolean;
onClearAttachments(conversationId: string): unknown; onClearAttachments(conversationId: string): unknown;
onCloseLinkPreview(): unknown; onCloseLinkPreview(conversationId: string): unknown;
processAttachments: (options: { processAttachments: (options: {
conversationId: string; conversationId: string;
files: ReadonlyArray<File>; files: ReadonlyArray<File>;
}) => unknown; }) => unknown;
setMediaQualitySetting(isHQ: boolean): unknown; setMediaQualitySetting(conversationId: string, isHQ: boolean): unknown;
sendStickerMessage( sendStickerMessage(
id: string, id: string,
opts: { packId: string; stickerId: number } opts: { packId: string; stickerId: number }
@ -136,7 +136,7 @@ export type OwnProps = Readonly<{
): unknown; ): unknown;
shouldSendHighQualityAttachments: boolean; shouldSendHighQualityAttachments: boolean;
showConversation: ShowConversationType; showConversation: ShowConversationType;
startRecording: () => unknown; startRecording: (id: string) => unknown;
theme: ThemeType; theme: ThemeType;
}>; }>;
@ -366,6 +366,20 @@ export function CompositionArea({
[inputApiRef, onPickEmoji] [inputApiRef, onPickEmoji]
); );
const previousConversationId = usePrevious(conversationId, conversationId);
useEffect(() => {
if (!draftText) {
inputApiRef.current?.setText('');
return;
}
if (conversationId === previousConversationId) {
return;
}
inputApiRef.current?.setText(draftText, true);
}, [conversationId, draftText, previousConversationId]);
const handleToggleLarge = useCallback(() => { const handleToggleLarge = useCallback(() => {
setLarge(l => !l); setLarge(l => !l);
}, [setLarge]); }, [setLarge]);
@ -391,6 +405,7 @@ export function CompositionArea({
{showMediaQualitySelector ? ( {showMediaQualitySelector ? (
<div className="CompositionArea__button-cell"> <div className="CompositionArea__button-cell">
<MediaQualitySelector <MediaQualitySelector
conversationId={conversationId}
i18n={i18n} i18n={i18n}
isHighQuality={shouldSendHighQualityAttachments} isHighQuality={shouldSendHighQualityAttachments}
onSelectQuality={setMediaQualitySetting} onSelectQuality={setMediaQualitySetting}
@ -672,7 +687,7 @@ export function CompositionArea({
<StagedLinkPreview <StagedLinkPreview
{...linkPreviewResult} {...linkPreviewResult}
i18n={i18n} i18n={i18n}
onClose={onCloseLinkPreview} onClose={() => onCloseLinkPreview(conversationId)}
/> />
</div> </div>
)} )}

View file

@ -18,6 +18,7 @@ export default {
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
conversationId: 'abc123',
i18n, i18n,
isHighQuality: boolean('isHighQuality', Boolean(overrideProps.isHighQuality)), isHighQuality: boolean('isHighQuality', Boolean(overrideProps.isHighQuality)),
onSelectQuality: action('onSelectQuality'), onSelectQuality: action('onSelectQuality'),

View file

@ -12,12 +12,14 @@ import { useRefMerger } from '../hooks/useRefMerger';
import { handleOutsideClick } from '../util/handleOutsideClick'; import { handleOutsideClick } from '../util/handleOutsideClick';
export type PropsType = { export type PropsType = {
conversationId: string;
i18n: LocalizerType; i18n: LocalizerType;
isHighQuality: boolean; isHighQuality: boolean;
onSelectQuality: (isHQ: boolean) => unknown; onSelectQuality: (conversationId: string, isHQ: boolean) => unknown;
}; };
export function MediaQualitySelector({ export function MediaQualitySelector({
conversationId,
i18n, i18n,
isHighQuality, isHighQuality,
onSelectQuality, onSelectQuality,
@ -50,7 +52,7 @@ export function MediaQualitySelector({
} }
if (ev.key === 'Enter') { if (ev.key === 'Enter') {
onSelectQuality(Boolean(focusedOption)); onSelectQuality(conversationId, Boolean(focusedOption));
setMenuShowing(false); setMenuShowing(false);
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
@ -136,7 +138,7 @@ export function MediaQualitySelector({
})} })}
type="button" type="button"
onClick={() => { onClick={() => {
onSelectQuality(false); onSelectQuality(conversationId, false);
setMenuShowing(false); setMenuShowing(false);
}} }}
> >
@ -169,7 +171,7 @@ export function MediaQualitySelector({
})} })}
type="button" type="button"
onClick={() => { onClick={() => {
onSelectQuality(true); onSelectQuality(conversationId, true);
setMenuShowing(false); setMenuShowing(false);
}} }}
> >

View file

@ -38,7 +38,7 @@ export type PropsType = {
i18n: LocalizerType; i18n: LocalizerType;
recordingState: RecordingState; recordingState: RecordingState;
onSendAudioRecording: OnSendAudioRecordingType; onSendAudioRecording: OnSendAudioRecordingType;
startRecording: () => unknown; startRecording: (id: string) => unknown;
}; };
enum ToastType { enum ToastType {
@ -96,7 +96,11 @@ export function AudioCapture({
useEscapeHandling(escapeRecording); useEscapeHandling(escapeRecording);
const startRecordingShortcut = useStartRecordingShortcut(startRecording); const recordConversation = useCallback(
() => startRecording(conversationId),
[conversationId, startRecording]
);
const startRecordingShortcut = useStartRecordingShortcut(recordConversation);
useKeyboardShortcuts(startRecordingShortcut); useKeyboardShortcuts(startRecordingShortcut);
const closeToast = useCallback(() => { const closeToast = useCallback(() => {
@ -240,7 +244,7 @@ export function AudioCapture({
if (draftAttachments.length) { if (draftAttachments.length) {
setToastType(ToastType.VoiceNoteMustBeOnlyAttachment); setToastType(ToastType.VoiceNoteMustBeOnlyAttachment);
} else { } else {
startRecording(); startRecording(conversationId);
} }
}} }}
title={i18n('voiceRecording--start')} title={i18n('voiceRecording--start')}

View file

@ -26,6 +26,7 @@ import { dropNull } from '../util/dropNull';
import { fileToBytes } from '../util/fileToBytes'; import { fileToBytes } from '../util/fileToBytes';
import { maybeParseUrl } from '../util/url'; import { maybeParseUrl } from '../util/url';
import { sniffImageMimeType } from '../util/sniffImageMimeType'; import { sniffImageMimeType } from '../util/sniffImageMimeType';
import { drop } from '../util/drop';
const LINK_PREVIEW_TIMEOUT = 60 * SECOND; const LINK_PREVIEW_TIMEOUT = 60 * SECOND;
@ -48,7 +49,11 @@ export const maybeGrabLinkPreview = debounce(_maybeGrabLinkPreview, 200);
function _maybeGrabLinkPreview( function _maybeGrabLinkPreview(
message: string, message: string,
source: LinkPreviewSourceType, source: LinkPreviewSourceType,
{ caretLocation, mode = 'conversation' }: MaybeGrabLinkPreviewOptionsType = {} {
caretLocation,
conversationId,
mode = 'conversation',
}: MaybeGrabLinkPreviewOptionsType = {}
): void { ): void {
// Don't generate link previews if user has turned them off. When posting a // Don't generate link previews if user has turned them off. When posting a
// story we should return minimal (url-only) link previews. // story we should return minimal (url-only) link previews.
@ -67,7 +72,7 @@ function _maybeGrabLinkPreview(
} }
if (!message) { if (!message) {
resetLinkPreview(); resetLinkPreview(conversationId);
return; return;
} }
@ -88,22 +93,25 @@ function _maybeGrabLinkPreview(
LinkPreview.shouldPreviewHref(item) && !excludedPreviewUrls.includes(item) LinkPreview.shouldPreviewHref(item) && !excludedPreviewUrls.includes(item)
); );
if (!link) { if (!link) {
removeLinkPreview(); removeLinkPreview(conversationId);
return; return;
} }
void addLinkPreview(link, source, { drop(
disableFetch: !window.Events.getLinkPreviewSetting(), addLinkPreview(link, source, {
}); conversationId,
disableFetch: !window.Events.getLinkPreviewSetting(),
})
);
} }
export function resetLinkPreview(): void { export function resetLinkPreview(conversationId?: string): void {
disableLinkPreviews = false; disableLinkPreviews = false;
excludedPreviewUrls = []; excludedPreviewUrls = [];
removeLinkPreview(); removeLinkPreview(conversationId);
} }
export function removeLinkPreview(): void { export function removeLinkPreview(conversationId?: string): void {
(linkPreviewResult || []).forEach((item: LinkPreviewResult) => { (linkPreviewResult || []).forEach((item: LinkPreviewResult) => {
if (item.url) { if (item.url) {
URL.revokeObjectURL(item.url); URL.revokeObjectURL(item.url);
@ -114,13 +122,13 @@ export function removeLinkPreview(): void {
linkPreviewAbortController?.abort(); linkPreviewAbortController?.abort();
linkPreviewAbortController = undefined; linkPreviewAbortController = undefined;
window.reduxActions.linkPreviews.removeLinkPreview(); window.reduxActions.linkPreviews.removeLinkPreview(conversationId);
} }
export async function addLinkPreview( export async function addLinkPreview(
url: string, url: string,
source: LinkPreviewSourceType, source: LinkPreviewSourceType,
{ disableFetch }: AddLinkPreviewOptionsType = {} { conversationId, disableFetch }: AddLinkPreviewOptionsType = {}
): Promise<void> { ): Promise<void> {
if (currentlyMatchedLink === url) { if (currentlyMatchedLink === url) {
log.warn('addLinkPreview should not be called with the same URL like this'); log.warn('addLinkPreview should not be called with the same URL like this');
@ -132,7 +140,7 @@ export async function addLinkPreview(
URL.revokeObjectURL(item.url); URL.revokeObjectURL(item.url);
} }
}); });
window.reduxActions.linkPreviews.removeLinkPreview(); window.reduxActions.linkPreviews.removeLinkPreview(conversationId);
linkPreviewResult = undefined; linkPreviewResult = undefined;
// Cancel other in-flight link preview requests. // Cancel other in-flight link preview requests.
@ -156,7 +164,8 @@ export async function addLinkPreview(
{ {
url, url,
}, },
source source,
conversationId
); );
try { try {
@ -186,7 +195,7 @@ export async function addLinkPreview(
const failedToFetch = currentlyMatchedLink === url; const failedToFetch = currentlyMatchedLink === url;
if (failedToFetch) { if (failedToFetch) {
excludedPreviewUrls.push(url); excludedPreviewUrls.push(url);
removeLinkPreview(); removeLinkPreview(conversationId);
} }
return; return;
} }
@ -198,7 +207,7 @@ export async function addLinkPreview(
result.image.url = URL.createObjectURL(blob); result.image.url = URL.createObjectURL(blob);
} else if (!result.title && !disableFetch) { } else if (!result.title && !disableFetch) {
// A link preview isn't worth showing unless we have either a title or an image // A link preview isn't worth showing unless we have either a title or an image
removeLinkPreview(); removeLinkPreview(conversationId);
return; return;
} }
@ -211,7 +220,8 @@ export async function addLinkPreview(
domain: LinkPreview.getDomain(result.url), domain: LinkPreview.getDomain(result.url),
isStickerPack: LinkPreview.isStickerPack(result.url), isStickerPack: LinkPreview.isStickerPack(result.url),
}, },
source source,
conversationId
); );
linkPreviewResult = [result]; linkPreviewResult = [result];
} catch (error) { } catch (error) {
@ -220,7 +230,7 @@ export async function addLinkPreview(
Errors.toLogFormat(error) Errors.toLogFormat(error)
); );
disableLinkPreviews = true; disableLinkPreviews = true;
removeLinkPreview(); removeLinkPreview(conversationId);
} finally { } finally {
clearTimeout(timeout); clearTimeout(timeout);
} }

View file

@ -12,6 +12,7 @@ import { recorder } from '../../services/audioRecorder';
import { stringToMIMEType } from '../../types/MIME'; import { stringToMIMEType } from '../../types/MIME';
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions'; import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
import { useBoundActions } from '../../hooks/useBoundActions'; import { useBoundActions } from '../../hooks/useBoundActions';
import { getComposerStateForConversation } from './composer';
export enum ErrorDialogAudioRecorderType { export enum ErrorDialogAudioRecorderType {
Blur, Blur,
@ -80,17 +81,24 @@ export const actions = {
export const useActions = (): BoundActionCreatorsMapObject<typeof actions> => export const useActions = (): BoundActionCreatorsMapObject<typeof actions> =>
useBoundActions(actions); useBoundActions(actions);
function startRecording(): ThunkAction< function startRecording(
conversationId: string
): ThunkAction<
void, void,
RootStateType, RootStateType,
unknown, unknown,
StartRecordingAction | NowRecordingAction | ErrorRecordingAction StartRecordingAction | NowRecordingAction | ErrorRecordingAction
> { > {
return async (dispatch, getState) => { return async (dispatch, getState) => {
if (getState().composer.attachments.length) { const state = getState();
if (
getComposerStateForConversation(state.composer, conversationId)
.attachments.length
) {
return; return;
} }
if (getState().audioRecorder.recordingState !== RecordingState.Idle) { if (state.audioRecorder.recordingState !== RecordingState.Idle) {
return; return;
} }

View file

@ -71,14 +71,23 @@ import { getContactId } from '../../messages/helpers';
import { getConversationSelector } from '../selectors/conversations'; import { getConversationSelector } from '../selectors/conversations';
import { enqueueReactionForSend } from '../../reactions/enqueueReactionForSend'; import { enqueueReactionForSend } from '../../reactions/enqueueReactionForSend';
import { useBoundActions } from '../../hooks/useBoundActions'; import { useBoundActions } from '../../hooks/useBoundActions';
import { scrollToMessage } from './conversations'; import {
import type { ScrollToMessageActionType } from './conversations'; CONVERSATION_UNLOADED,
SELECTED_CONVERSATION_CHANGED,
scrollToMessage,
} from './conversations';
import type {
ConversationUnloadedActionType,
SelectedConversationChangedActionType,
ScrollToMessageActionType,
} from './conversations';
import { longRunningTaskWrapper } from '../../util/longRunningTaskWrapper'; import { longRunningTaskWrapper } from '../../util/longRunningTaskWrapper';
import { drop } from '../../util/drop'; import { drop } from '../../util/drop';
import { strictAssert } from '../../util/assert';
// State // State
export type ComposerStateType = { type ComposerStateByConversationType = {
attachments: ReadonlyArray<AttachmentDraftType>; attachments: ReadonlyArray<AttachmentDraftType>;
focusCounter: number; focusCounter: number;
isDisabled: boolean; isDisabled: boolean;
@ -89,6 +98,32 @@ export type ComposerStateType = {
shouldSendHighQualityAttachments?: boolean; shouldSendHighQualityAttachments?: boolean;
}; };
export type QuotedMessageType = Pick<
MessageAttributesType,
'conversationId' | 'quote'
>;
export type ComposerStateType = {
conversations: Record<string, ComposerStateByConversationType>;
};
function getEmptyComposerState(): ComposerStateByConversationType {
return {
attachments: [],
focusCounter: 0,
isDisabled: false,
linkPreviewLoading: false,
messageCompositionId: UUID.generate().toString(),
};
}
export function getComposerStateForConversation(
composer: ComposerStateType,
conversationId: string
): ComposerStateByConversationType {
return composer.conversations[conversationId] ?? getEmptyComposerState();
}
// Actions // Actions
const ADD_PENDING_ATTACHMENT = 'composer/ADD_PENDING_ATTACHMENT'; const ADD_PENDING_ATTACHMENT = 'composer/ADD_PENDING_ATTACHMENT';
@ -101,43 +136,66 @@ const SET_COMPOSER_DISABLED = 'composer/SET_COMPOSER_DISABLED';
type AddPendingAttachmentActionType = { type AddPendingAttachmentActionType = {
type: typeof ADD_PENDING_ATTACHMENT; type: typeof ADD_PENDING_ATTACHMENT;
payload: AttachmentDraftType; payload: {
conversationId: string;
attachment: AttachmentDraftType;
};
}; };
export type ReplaceAttachmentsActionType = { export type ReplaceAttachmentsActionType = {
type: typeof REPLACE_ATTACHMENTS; type: typeof REPLACE_ATTACHMENTS;
payload: ReadonlyArray<AttachmentDraftType>; payload: {
conversationId: string;
attachments: ReadonlyArray<AttachmentDraftType>;
};
}; };
export type ResetComposerActionType = { export type ResetComposerActionType = {
type: typeof RESET_COMPOSER; type: typeof RESET_COMPOSER;
payload: {
conversationId: string;
};
}; };
type SetComposerDisabledStateActionType = { type SetComposerDisabledStateActionType = {
type: typeof SET_COMPOSER_DISABLED; type: typeof SET_COMPOSER_DISABLED;
payload: boolean; payload: {
conversationId: string;
value: boolean;
};
}; };
export type SetFocusActionType = { export type SetFocusActionType = {
type: typeof SET_FOCUS; type: typeof SET_FOCUS;
payload: {
conversationId: string;
};
}; };
type SetHighQualitySettingActionType = { type SetHighQualitySettingActionType = {
type: typeof SET_HIGH_QUALITY_SETTING; type: typeof SET_HIGH_QUALITY_SETTING;
payload: boolean; payload: {
conversationId: string;
value: boolean;
};
}; };
export type SetQuotedMessageActionType = { export type SetQuotedMessageActionType = {
type: typeof SET_QUOTED_MESSAGE; type: typeof SET_QUOTED_MESSAGE;
payload?: Pick<MessageAttributesType, 'conversationId' | 'quote'>; payload: {
conversationId: string;
quotedMessage?: QuotedMessageType;
};
}; };
type ComposerActionType = type ComposerActionType =
| AddLinkPreviewActionType | AddLinkPreviewActionType
| AddPendingAttachmentActionType | AddPendingAttachmentActionType
| ConversationUnloadedActionType
| RemoveLinkPreviewActionType | RemoveLinkPreviewActionType
| ReplaceAttachmentsActionType | ReplaceAttachmentsActionType
| ResetComposerActionType | ResetComposerActionType
| SelectedConversationChangedActionType
| SetComposerDisabledStateActionType | SetComposerDisabledStateActionType
| SetFocusActionType | SetFocusActionType
| SetHighQualitySettingActionType | SetHighQualitySettingActionType
@ -207,9 +265,9 @@ function cancelJoinRequest(conversationId: string): NoopActionType {
}; };
} }
function onCloseLinkPreview(): NoopActionType { function onCloseLinkPreview(conversationId: string): NoopActionType {
suspendLinkPreviews(); suspendLinkPreviews();
removeLinkPreview(); removeLinkPreview(conversationId);
return { return {
type: 'NOOP', type: 'NOOP',
@ -308,18 +366,18 @@ function sendMultiMediaMessage(
]); ]);
try { try {
dispatch(setComposerDisabledState(true)); dispatch(setComposerDisabledState(conversationId, true));
const sendAnyway = await blockSendUntilConversationsAreVerified( const sendAnyway = await blockSendUntilConversationsAreVerified(
recipientsByConversation, recipientsByConversation,
SafetyNumberChangeSource.MessageSend SafetyNumberChangeSource.MessageSend
); );
if (!sendAnyway) { if (!sendAnyway) {
dispatch(setComposerDisabledState(false)); dispatch(setComposerDisabledState(conversationId, false));
return; return;
} }
} catch (error) { } catch (error) {
dispatch(setComposerDisabledState(false)); dispatch(setComposerDisabledState(conversationId, false));
log.error('sendMessage error:', Errors.toLogFormat(error)); log.error('sendMessage error:', Errors.toLogFormat(error));
return; return;
} }
@ -334,7 +392,7 @@ function sendMultiMediaMessage(
toastType, toastType,
}, },
}); });
dispatch(setComposerDisabledState(false)); dispatch(setComposerDisabledState(conversationId, false));
return; return;
} }
@ -345,7 +403,7 @@ function sendMultiMediaMessage(
}) && }) &&
!voiceNoteAttachment !voiceNoteAttachment
) { ) {
dispatch(setComposerDisabledState(false)); dispatch(setComposerDisabledState(conversationId, false));
return; return;
} }
@ -359,10 +417,15 @@ function sendMultiMediaMessage(
).filter(isNotNil); ).filter(isNotNil);
} }
const quote = state.composer.quotedMessage?.quote; const conversationComposerState = getComposerStateForConversation(
state.composer,
conversationId
);
const quote = conversationComposerState.quotedMessage?.quote;
const shouldSendHighQualityAttachments = window.reduxStore const shouldSendHighQualityAttachments = window.reduxStore
? state.composer.shouldSendHighQualityAttachments ? conversationComposerState.shouldSendHighQualityAttachments
: undefined; : undefined;
const sendHQImages = const sendHQImages =
@ -388,7 +451,7 @@ function sendMultiMediaMessage(
// We rely on enqueueMessageForSend to call these within redux's batch // We rely on enqueueMessageForSend to call these within redux's batch
extraReduxActions: () => { extraReduxActions: () => {
conversation.setMarkedUnread(false); conversation.setMarkedUnread(false);
resetLinkPreview(); resetLinkPreview(conversationId);
drop( drop(
clearConversationDraftAttachments( clearConversationDraftAttachments(
conversationId, conversationId,
@ -400,8 +463,8 @@ function sendMultiMediaMessage(
getState, getState,
undefined undefined
); );
dispatch(resetComposer()); dispatch(resetComposer(conversationId));
dispatch(setComposerDisabledState(false)); dispatch(setComposerDisabledState(conversationId, false));
}, },
} }
); );
@ -410,7 +473,7 @@ function sendMultiMediaMessage(
'Error pulling attached files before send', 'Error pulling attached files before send',
Errors.toLogFormat(error) Errors.toLogFormat(error)
); );
dispatch(setComposerDisabledState(false)); dispatch(setComposerDisabledState(conversationId, false));
} }
}; };
} }
@ -544,16 +607,16 @@ export function setQuoteByMessageId(
} }
dispatch( dispatch(
setQuotedMessage({ setQuotedMessage(conversationId, {
conversationId, conversationId,
quote, quote,
}) })
); );
dispatch(setComposerFocus(conversation.id)); dispatch(setComposerFocus(conversation.id));
dispatch(setComposerDisabledState(false)); dispatch(setComposerDisabledState(conversationId, false));
} else { } else {
dispatch(setQuotedMessage(undefined)); dispatch(setQuotedMessage(conversationId, undefined));
} }
}; };
} }
@ -567,11 +630,18 @@ function addAttachment(
// each other. // each other.
const onDisk = await writeDraftAttachment(attachment); const onDisk = await writeDraftAttachment(attachment);
const state = getState();
const isSelectedConversation = const isSelectedConversation =
getState().conversations.selectedConversationId === conversationId; state.conversations.selectedConversationId === conversationId;
const conversationComposerState = getComposerStateForConversation(
state.composer,
conversationId
);
const draftAttachments = isSelectedConversation const draftAttachments = isSelectedConversation
? getState().composer.attachments ? conversationComposerState.attachments
: getAttachmentsFromConversationModel(conversationId); : getAttachmentsFromConversationModel(conversationId);
// We expect there to either be a pending draft attachment or an existing // We expect there to either be a pending draft attachment or an existing
@ -619,18 +689,28 @@ function addPendingAttachment(
pendingAttachment: AttachmentDraftType pendingAttachment: AttachmentDraftType
): ThunkAction<void, RootStateType, unknown, ReplaceAttachmentsActionType> { ): ThunkAction<void, RootStateType, unknown, ReplaceAttachmentsActionType> {
return (dispatch, getState) => { return (dispatch, getState) => {
const state = getState();
const isSelectedConversation = const isSelectedConversation =
getState().conversations.selectedConversationId === conversationId; state.conversations.selectedConversationId === conversationId;
const conversationComposerState = getComposerStateForConversation(
state.composer,
conversationId
);
const draftAttachments = isSelectedConversation const draftAttachments = isSelectedConversation
? getState().composer.attachments ? conversationComposerState.attachments
: getAttachmentsFromConversationModel(conversationId); : getAttachmentsFromConversationModel(conversationId);
const nextAttachments = [...draftAttachments, pendingAttachment]; const nextAttachments = [...draftAttachments, pendingAttachment];
dispatch({ dispatch({
type: REPLACE_ATTACHMENTS, type: REPLACE_ATTACHMENTS,
payload: nextAttachments, payload: {
conversationId,
attachments: nextAttachments,
},
}); });
const conversation = window.ConversationController.get(conversationId); const conversation = window.ConversationController.get(conversationId);
@ -651,6 +731,9 @@ export function setComposerFocus(
dispatch({ dispatch({
type: SET_FOCUS, type: SET_FOCUS,
payload: {
conversationId,
},
}); });
}; };
} }
@ -686,6 +769,7 @@ function onEditorStateChange(
) { ) {
maybeGrabLinkPreview(messageText, LinkPreviewSourceType.Composer, { maybeGrabLinkPreview(messageText, LinkPreviewSourceType.Composer, {
caretLocation, caretLocation,
conversationId,
}); });
} }
@ -877,7 +961,12 @@ function removeAttachment(
filePath: string filePath: string
): ThunkAction<void, RootStateType, unknown, ReplaceAttachmentsActionType> { ): ThunkAction<void, RootStateType, unknown, ReplaceAttachmentsActionType> {
return async (dispatch, getState) => { return async (dispatch, getState) => {
const { attachments } = getState().composer; const state = getState();
const { attachments } = getComposerStateForConversation(
state.composer,
conversationId
);
const [targetAttachment] = attachments.filter( const [targetAttachment] = attachments.filter(
attachment => attachment.path === filePath attachment => attachment.path === filePath
@ -924,12 +1013,15 @@ export function replaceAttachments(
} }
if (hasDraftAttachments(attachments, { includePending: true })) { if (hasDraftAttachments(attachments, { includePending: true })) {
removeLinkPreview(); removeLinkPreview(conversationId);
} }
dispatch({ dispatch({
type: REPLACE_ATTACHMENTS, type: REPLACE_ATTACHMENTS,
payload: attachments.map(resolveDraftAttachmentOnDisk), payload: {
conversationId,
attachments: attachments.map(resolveDraftAttachmentOnDisk),
},
}); });
}; };
} }
@ -972,9 +1064,12 @@ function reactToMessage(
}; };
} }
export function resetComposer(): ResetComposerActionType { export function resetComposer(conversationId: string): ResetComposerActionType {
return { return {
type: RESET_COMPOSER, type: RESET_COMPOSER,
payload: {
conversationId,
},
}; };
} }
const debouncedSaveDraft = debounce(saveDraft); const debouncedSaveDraft = debounce(saveDraft);
@ -1024,29 +1119,41 @@ function saveDraft(
} }
function setComposerDisabledState( function setComposerDisabledState(
conversationId: string,
value: boolean value: boolean
): SetComposerDisabledStateActionType { ): SetComposerDisabledStateActionType {
return { return {
type: SET_COMPOSER_DISABLED, type: SET_COMPOSER_DISABLED,
payload: value, payload: {
conversationId,
value,
},
}; };
} }
function setMediaQualitySetting( function setMediaQualitySetting(
payload: boolean conversationId: string,
value: boolean
): SetHighQualitySettingActionType { ): SetHighQualitySettingActionType {
return { return {
type: SET_HIGH_QUALITY_SETTING, type: SET_HIGH_QUALITY_SETTING,
payload, payload: {
conversationId,
value,
},
}; };
} }
function setQuotedMessage( function setQuotedMessage(
payload?: Pick<MessageAttributesType, 'conversationId' | 'quote' | 'payment'> conversationId: string,
quotedMessage?: QuotedMessageType
): SetQuotedMessageActionType { ): SetQuotedMessageActionType {
return { return {
type: SET_QUOTED_MESSAGE, type: SET_QUOTED_MESSAGE,
payload, payload: {
conversationId,
quotedMessage,
},
}; };
} }
@ -1054,52 +1161,107 @@ function setQuotedMessage(
export function getEmptyState(): ComposerStateType { export function getEmptyState(): ComposerStateType {
return { return {
attachments: [], conversations: {},
focusCounter: 0,
isDisabled: false,
linkPreviewLoading: false,
messageCompositionId: UUID.generate().toString(),
}; };
} }
function updateComposerState(
state: Readonly<ComposerStateType>,
action: Readonly<ComposerActionType>,
getNextComposerState: (
prevState: ComposerStateByConversationType
) => Partial<ComposerStateByConversationType>
): ComposerStateType {
const { conversationId } = action.payload;
strictAssert(
conversationId,
'updateComposerState: no conversationId provided'
);
const prevComposerState = getComposerStateForConversation(
state,
conversationId
);
const nextComposerStateForConversation = assignWithNoUnnecessaryAllocation(
prevComposerState,
getNextComposerState(prevComposerState)
);
return assignWithNoUnnecessaryAllocation(state, {
conversations: assignWithNoUnnecessaryAllocation(state.conversations, {
[conversationId]: nextComposerStateForConversation,
}),
});
}
export function reducer( export function reducer(
state: Readonly<ComposerStateType> = getEmptyState(), state: Readonly<ComposerStateType> = getEmptyState(),
action: Readonly<ComposerActionType> action: Readonly<ComposerActionType>
): ComposerStateType { ): ComposerStateType {
if (action.type === RESET_COMPOSER) { if (action.type === CONVERSATION_UNLOADED) {
const nextConversations: Record<string, ComposerStateByConversationType> =
{};
Object.keys(state.conversations).forEach(conversationId => {
if (conversationId === action.payload.conversationId) {
return;
}
nextConversations[conversationId] = state.conversations[conversationId];
});
return {
...state,
conversations: nextConversations,
};
}
if (action.type === SELECTED_CONVERSATION_CHANGED) {
if (action.payload.conversationId) {
return {
...state,
conversations: {
[action.payload.conversationId]: getEmptyComposerState(),
},
};
}
return getEmptyState(); return getEmptyState();
} }
if (action.type === RESET_COMPOSER) {
return updateComposerState(state, action, () => ({}));
}
if (action.type === REPLACE_ATTACHMENTS) { if (action.type === REPLACE_ATTACHMENTS) {
const { payload: attachments } = action; const { attachments } = action.payload;
return {
...state, return updateComposerState(state, action, () => ({
attachments, attachments,
...(attachments.length ...(attachments.length
? {} ? {}
: { shouldSendHighQualityAttachments: undefined }), : { shouldSendHighQualityAttachments: undefined }),
}; }));
} }
if (action.type === SET_FOCUS) { if (action.type === SET_FOCUS) {
return { return updateComposerState(state, action, prevState => ({
...state, focusCounter: prevState.focusCounter + 1,
focusCounter: state.focusCounter + 1, }));
};
} }
if (action.type === SET_HIGH_QUALITY_SETTING) { if (action.type === SET_HIGH_QUALITY_SETTING) {
return { return updateComposerState(state, action, () => ({
...state, shouldSendHighQualityAttachments: action.payload.value,
shouldSendHighQualityAttachments: action.payload, }));
};
} }
if (action.type === SET_QUOTED_MESSAGE) { if (action.type === SET_QUOTED_MESSAGE) {
return { const { quotedMessage } = action.payload;
...state, return updateComposerState(state, action, () => ({
quotedMessage: action.payload, quotedMessage,
}; }));
} }
if (action.type === ADD_LINK_PREVIEW) { if (action.type === ADD_LINK_PREVIEW) {
@ -1107,32 +1269,29 @@ export function reducer(
return state; return state;
} }
return { return updateComposerState(state, action, () => ({
...state,
linkPreviewLoading: true, linkPreviewLoading: true,
linkPreviewResult: action.payload.linkPreview, linkPreviewResult: action.payload.linkPreview,
}; }));
} }
if (action.type === REMOVE_LINK_PREVIEW) { if (action.type === REMOVE_LINK_PREVIEW) {
return assignWithNoUnnecessaryAllocation(state, { return updateComposerState(state, action, () => ({
linkPreviewLoading: false, linkPreviewLoading: false,
linkPreviewResult: undefined, linkPreviewResult: undefined,
}); }));
} }
if (action.type === ADD_PENDING_ATTACHMENT) { if (action.type === ADD_PENDING_ATTACHMENT) {
return { return updateComposerState(state, action, prevState => ({
...state, attachments: [...prevState.attachments, action.payload.attachment],
attachments: [...state.attachments, action.payload], }));
};
} }
if (action.type === SET_COMPOSER_DISABLED) { if (action.type === SET_COMPOSER_DISABLED) {
return { return updateComposerState(state, action, () => ({
...state, isDisabled: action.payload.value,
isDisabled: action.payload, }));
};
} }
return state; return state;

View file

@ -612,7 +612,7 @@ export type ConversationRemovedActionType = {
export type ConversationUnloadedActionType = { export type ConversationUnloadedActionType = {
type: typeof CONVERSATION_UNLOADED; type: typeof CONVERSATION_UNLOADED;
payload: { payload: {
id: string; conversationId: string;
}; };
}; };
type CreateGroupPendingActionType = { type CreateGroupPendingActionType = {
@ -745,7 +745,7 @@ export type ClearUnreadMetricsActionType = {
export type SelectedConversationChangedActionType = { export type SelectedConversationChangedActionType = {
type: typeof SELECTED_CONVERSATION_CHANGED; type: typeof SELECTED_CONVERSATION_CHANGED;
payload: { payload: {
id?: string; conversationId?: string;
messageId?: string; messageId?: string;
switchToAssociatedView?: boolean; switchToAssociatedView?: boolean;
}; };
@ -3485,7 +3485,7 @@ function showConversation({
return { return {
type: SELECTED_CONVERSATION_CHANGED, type: SELECTED_CONVERSATION_CHANGED,
payload: { payload: {
id: conversationId, conversationId,
messageId, messageId,
switchToAssociatedView, switchToAssociatedView,
}, },
@ -3581,7 +3581,7 @@ function onConversationOpened(
conversation.get('id'), conversation.get('id'),
conversation.get('draftAttachments') || [] conversation.get('draftAttachments') || []
)(dispatch, getState, undefined); )(dispatch, getState, undefined);
dispatch(resetComposer()); dispatch(resetComposer(conversationId));
}; };
} }
@ -3625,13 +3625,13 @@ function onConversationClosed(
drop(conversation.updateLastMessage()); drop(conversation.updateLastMessage());
} }
removeLinkPreview(); removeLinkPreview(conversationId);
suspendLinkPreviews(); suspendLinkPreviews();
dispatch({ dispatch({
type: CONVERSATION_UNLOADED, type: CONVERSATION_UNLOADED,
payload: { payload: {
id: conversationId, conversationId,
}, },
}); });
}; };
@ -4186,15 +4186,15 @@ export function reducer(
} }
if (action.type === CONVERSATION_UNLOADED) { if (action.type === CONVERSATION_UNLOADED) {
const { payload } = action; const { payload } = action;
const { id } = payload; const { conversationId } = payload;
const existingConversation = state.messagesByConversation[id]; const existingConversation = state.messagesByConversation[conversationId];
if (!existingConversation) { if (!existingConversation) {
return state; return state;
} }
const { messageIds } = existingConversation; const { messageIds } = existingConversation;
const selectedConversationId = const selectedConversationId =
state.selectedConversationId !== id state.selectedConversationId !== conversationId
? state.selectedConversationId ? state.selectedConversationId
: undefined; : undefined;
@ -4203,7 +4203,9 @@ export function reducer(
selectedConversationId, selectedConversationId,
selectedConversationPanels: [], selectedConversationPanels: [],
messagesLookup: omit(state.messagesLookup, [...messageIds]), messagesLookup: omit(state.messagesLookup, [...messageIds]),
messagesByConversation: omit(state.messagesByConversation, [id]), messagesByConversation: omit(state.messagesByConversation, [
conversationId,
]),
}; };
} }
if (action.type === 'CONVERSATIONS_REMOVE_ALL') { if (action.type === 'CONVERSATIONS_REMOVE_ALL') {
@ -4993,17 +4995,17 @@ export function reducer(
} }
if (action.type === SELECTED_CONVERSATION_CHANGED) { if (action.type === SELECTED_CONVERSATION_CHANGED) {
const { payload } = action; const { payload } = action;
const { id, messageId, switchToAssociatedView } = payload; const { conversationId, messageId, switchToAssociatedView } = payload;
const nextState = { const nextState = {
...omit(state, 'contactSpoofingReview'), ...omit(state, 'contactSpoofingReview'),
selectedConversationId: id, selectedConversationId: conversationId,
selectedMessage: messageId, selectedMessage: messageId,
selectedMessageSource: SelectedMessageSource.NavigateToMessage, selectedMessageSource: SelectedMessageSource.NavigateToMessage,
}; };
if (switchToAssociatedView && id) { if (switchToAssociatedView && conversationId) {
const conversation = getOwn(state.conversationLookup, id); const conversation = getOwn(state.conversationLookup, conversationId);
if (!conversation) { if (!conversation) {
return nextState; return nextState;
} }

View file

@ -3,16 +3,15 @@
import type { ThunkAction } from 'redux-thunk'; import type { ThunkAction } from 'redux-thunk';
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
import type { LinkPreviewType } from '../../types/message/LinkPreviews';
import type { MaybeGrabLinkPreviewOptionsType } from '../../types/LinkPreview';
import type { NoopActionType } from './noop'; import type { NoopActionType } from './noop';
import type { StateType as RootStateType } from '../reducer'; import type { StateType as RootStateType } from '../reducer';
import type { LinkPreviewType } from '../../types/message/LinkPreviews'; import { LinkPreviewSourceType } from '../../types/LinkPreview';
import type {
LinkPreviewSourceType,
MaybeGrabLinkPreviewOptionsType,
} from '../../types/LinkPreview';
import { assignWithNoUnnecessaryAllocation } from '../../util/assignWithNoUnnecessaryAllocation'; import { assignWithNoUnnecessaryAllocation } from '../../util/assignWithNoUnnecessaryAllocation';
import { maybeGrabLinkPreview } from '../../services/LinkPreview'; import { maybeGrabLinkPreview } from '../../services/LinkPreview';
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions'; import { strictAssert } from '../../util/assert';
import { useBoundActions } from '../../hooks/useBoundActions'; import { useBoundActions } from '../../hooks/useBoundActions';
// State // State
@ -30,6 +29,7 @@ export const REMOVE_PREVIEW = 'linkPreviews/REMOVE_PREVIEW';
export type AddLinkPreviewActionType = { export type AddLinkPreviewActionType = {
type: 'linkPreviews/ADD_PREVIEW'; type: 'linkPreviews/ADD_PREVIEW';
payload: { payload: {
conversationId?: string;
linkPreview: LinkPreviewType; linkPreview: LinkPreviewType;
source: LinkPreviewSourceType; source: LinkPreviewSourceType;
}; };
@ -37,6 +37,9 @@ export type AddLinkPreviewActionType = {
export type RemoveLinkPreviewActionType = { export type RemoveLinkPreviewActionType = {
type: 'linkPreviews/REMOVE_PREVIEW'; type: 'linkPreviews/REMOVE_PREVIEW';
payload: {
conversationId?: string;
};
}; };
type LinkPreviewsActionType = type LinkPreviewsActionType =
@ -62,20 +65,31 @@ function debouncedMaybeGrabLinkPreview(
function addLinkPreview( function addLinkPreview(
linkPreview: LinkPreviewType, linkPreview: LinkPreviewType,
source: LinkPreviewSourceType source: LinkPreviewSourceType,
conversationId?: string
): AddLinkPreviewActionType { ): AddLinkPreviewActionType {
if (source === LinkPreviewSourceType.Composer) {
strictAssert(conversationId, 'no conversationId provided');
}
return { return {
type: ADD_PREVIEW, type: ADD_PREVIEW,
payload: { payload: {
conversationId,
linkPreview, linkPreview,
source, source,
}, },
}; };
} }
function removeLinkPreview(): RemoveLinkPreviewActionType { function removeLinkPreview(
conversationId?: string
): RemoveLinkPreviewActionType {
return { return {
type: REMOVE_PREVIEW, type: REMOVE_PREVIEW,
payload: {
conversationId,
},
}; };
} }

View file

@ -31,7 +31,10 @@ import {
getUserConversationId, getUserConversationId,
} from '../selectors/user'; } from '../selectors/user';
import { strictAssert } from '../../util/assert'; import { strictAssert } from '../../util/assert';
import { SELECTED_CONVERSATION_CHANGED } from './conversations'; import {
CONVERSATION_UNLOADED,
SELECTED_CONVERSATION_CHANGED,
} from './conversations';
const { const {
searchMessages: dataSearchMessages, searchMessages: dataSearchMessages,
@ -437,10 +440,10 @@ export function reducer(
if (action.type === SELECTED_CONVERSATION_CHANGED) { if (action.type === SELECTED_CONVERSATION_CHANGED) {
const { payload } = action; const { payload } = action;
const { id, messageId } = payload; const { conversationId, messageId } = payload;
const { searchConversationId } = state; const { searchConversationId } = state;
if (searchConversationId && searchConversationId !== id) { if (searchConversationId && searchConversationId !== conversationId) {
return getEmptyState(); return getEmptyState();
} }
@ -450,12 +453,12 @@ export function reducer(
}; };
} }
if (action.type === 'CONVERSATION_UNLOADED') { if (action.type === CONVERSATION_UNLOADED) {
const { payload } = action; const { payload } = action;
const { id } = payload; const { conversationId } = payload;
const { searchConversationId } = state; const { searchConversationId } = state;
if (searchConversationId && searchConversationId === id) { if (searchConversationId && searchConversationId === conversationId) {
return getEmptyState(); return getEmptyState();
} }

View file

@ -0,0 +1,24 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { createSelector } from 'reselect';
import type { StateType } from '../reducer';
import type { ComposerStateType, QuotedMessageType } from '../ducks/composer';
import { getComposerStateForConversation } from '../ducks/composer';
export const getComposerState = (state: StateType): ComposerStateType =>
state.composer;
export const getComposerStateForConversationIdSelector = createSelector(
getComposerState,
composer => (conversationId: string) =>
getComposerStateForConversation(composer, conversationId)
);
export const getQuotedMessageSelector = createSelector(
getComposerStateForConversationIdSelector,
composerStateForConversationIdSelector =>
(conversationId: string): QuotedMessageType | undefined =>
composerStateForConversationIdSelector(conversationId).quotedMessage
);

View file

@ -30,6 +30,7 @@ import {
getRecentStickers, getRecentStickers,
} from '../selectors/stickers'; } from '../selectors/stickers';
import { isSignalConversation } from '../../util/isSignalConversation'; import { isSignalConversation } from '../../util/isSignalConversation';
import { getComposerStateForConversationIdSelector } from '../selectors/composer';
type ExternalProps = { type ExternalProps = {
id: string; id: string;
@ -67,6 +68,9 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
receivedPacks.length > 0 receivedPacks.length > 0
); );
const composerStateForConversationIdSelector =
getComposerStateForConversationIdSelector(state);
const { const {
attachments: draftAttachments, attachments: draftAttachments,
focusCounter, focusCounter,
@ -76,7 +80,7 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
messageCompositionId, messageCompositionId,
quotedMessage, quotedMessage,
shouldSendHighQualityAttachments, shouldSendHighQualityAttachments,
} = state.composer; } = composerStateForConversationIdSelector(id);
const recentEmojis = selectRecentEmojis(state); const recentEmojis = selectRecentEmojis(state);

View file

@ -6,7 +6,12 @@ import * as sinon from 'sinon';
import { noop } from 'lodash'; import { noop } from 'lodash';
import type { ReduxActions } from '../../../state/types'; import type { ReduxActions } from '../../../state/types';
import { actions, getEmptyState, reducer } from '../../../state/ducks/composer'; import {
actions,
getComposerStateForConversation,
getEmptyState,
reducer,
} from '../../../state/ducks/composer';
import { noopAction } from '../../../state/ducks/noop'; import { noopAction } from '../../../state/ducks/noop';
import { reducer as rootReducer } from '../../../state/reducer'; import { reducer as rootReducer } from '../../../state/reducer';
@ -28,7 +33,7 @@ describe('both/state/ducks/composer', () => {
}, },
}; };
const getRootStateFunction = (selectedConversationId?: string) => { function getRootStateFunction(selectedConversationId?: string) {
const state = rootReducer(undefined, noopAction()); const state = rootReducer(undefined, noopAction());
return () => ({ return () => ({
...state, ...state,
@ -37,7 +42,7 @@ describe('both/state/ducks/composer', () => {
selectedConversationId, selectedConversationId,
}, },
}); });
}; }
describe('replaceAttachments', () => { describe('replaceAttachments', () => {
let oldReduxActions: ReduxActions; let oldReduxActions: ReduxActions;
@ -76,7 +81,8 @@ describe('both/state/ducks/composer', () => {
const action = dispatch.getCall(0).args[0]; const action = dispatch.getCall(0).args[0];
const state = reducer(getEmptyState(), action); const state = reducer(getEmptyState(), action);
assert.deepEqual(state.attachments, attachments); const composerState = getComposerStateForConversation(state, '123');
assert.deepEqual(composerState.attachments, attachments);
}); });
it('sets the high quality setting to false when there are no attachments', () => { it('sets the high quality setting to false when there are no attachments', () => {
@ -94,14 +100,20 @@ describe('both/state/ducks/composer', () => {
const state = reducer( const state = reducer(
{ {
...getEmptyState(), ...getEmptyState(),
shouldSendHighQualityAttachments: true, conversations: {
'123': {
...getComposerStateForConversation(getEmptyState(), '123'),
shouldSendHighQualityAttachments: true,
},
},
}, },
action action
); );
assert.deepEqual(state.attachments, attachments); const composerState = getComposerStateForConversation(state, '123');
assert.deepEqual(composerState.attachments, attachments);
assert.deepEqual(state.attachments, attachments); assert.deepEqual(composerState.attachments, attachments);
assert.isUndefined(state.shouldSendHighQualityAttachments); assert.isUndefined(composerState.shouldSendHighQualityAttachments);
}); });
it('does not update redux if the conversation is not selected', () => { it('does not update redux if the conversation is not selected', () => {
@ -122,23 +134,17 @@ describe('both/state/ducks/composer', () => {
describe('resetComposer', () => { describe('resetComposer', () => {
it('returns composer back to empty state', () => { it('returns composer back to empty state', () => {
const { resetComposer } = actions; const { resetComposer } = actions;
const emptyState = getEmptyState(); const nextState = reducer(getEmptyState(), resetComposer('456'));
const nextState = reducer(
{
attachments: [],
focusCounter: 0,
isDisabled: false,
linkPreviewLoading: true,
messageCompositionId: emptyState.messageCompositionId,
quotedMessage: QUOTED_MESSAGE,
shouldSendHighQualityAttachments: true,
},
resetComposer()
);
const composerState = getComposerStateForConversation(nextState, '456');
assert.deepEqual(nextState, { assert.deepEqual(nextState, {
...getEmptyState(), ...getEmptyState(),
messageCompositionId: nextState.messageCompositionId, conversations: {
'456': {
...composerState,
messageCompositionId: composerState.messageCompositionId,
},
},
}); });
}); });
}); });
@ -148,15 +154,40 @@ describe('both/state/ducks/composer', () => {
const { setMediaQualitySetting } = actions; const { setMediaQualitySetting } = actions;
const state = getEmptyState(); const state = getEmptyState();
assert.isUndefined(state.shouldSendHighQualityAttachments); const composerState = getComposerStateForConversation(state, '123');
assert.isUndefined(composerState.shouldSendHighQualityAttachments);
const nextState = reducer(state, setMediaQualitySetting(true)); const nextState = reducer(state, setMediaQualitySetting('123', true));
assert.isTrue(nextState.shouldSendHighQualityAttachments); const nextComposerState = getComposerStateForConversation(
nextState,
'123'
);
assert.isTrue(nextComposerState.shouldSendHighQualityAttachments);
const nextNextState = reducer(nextState, setMediaQualitySetting(false)); const nextNextState = reducer(
nextState,
setMediaQualitySetting('123', false)
);
const nextNextComposerState = getComposerStateForConversation(
nextNextState,
'123'
);
assert.isFalse(nextNextState.shouldSendHighQualityAttachments); assert.isFalse(nextNextComposerState.shouldSendHighQualityAttachments);
const notMyConvoState = reducer(
nextNextState,
setMediaQualitySetting('456', true)
);
const notMineComposerState = getComposerStateForConversation(
notMyConvoState,
'123'
);
assert.isFalse(
notMineComposerState.shouldSendHighQualityAttachments,
'still false for prev convo'
);
}); });
}); });
@ -164,10 +195,11 @@ describe('both/state/ducks/composer', () => {
it('sets the quoted message', () => { it('sets the quoted message', () => {
const { setQuotedMessage } = actions; const { setQuotedMessage } = actions;
const state = getEmptyState(); const state = getEmptyState();
const nextState = reducer(state, setQuotedMessage(QUOTED_MESSAGE)); const nextState = reducer(state, setQuotedMessage('123', QUOTED_MESSAGE));
assert.equal(nextState.quotedMessage?.conversationId, '123'); const composerState = getComposerStateForConversation(nextState, '123');
assert.equal(nextState.quotedMessage?.quote?.id, 456); assert.equal(composerState.quotedMessage?.conversationId, '123');
assert.equal(composerState.quotedMessage?.quote?.id, 456);
}); });
}); });
}); });

View file

@ -26,7 +26,7 @@ describe('both/state/ducks/linkPreviews', () => {
it('updates linkPreview', () => { it('updates linkPreview', () => {
const state = getEmptyState(); const state = getEmptyState();
const linkPreview = getMockLinkPreview(); const linkPreview = getMockLinkPreview();
const nextState = reducer(state, addLinkPreview(linkPreview, 0)); const nextState = reducer(state, addLinkPreview(linkPreview, 1));
assert.strictEqual(nextState.linkPreview, linkPreview); assert.strictEqual(nextState.linkPreview, linkPreview);
}); });

View file

@ -741,7 +741,7 @@ describe('both/state/ducks/conversations', () => {
sinon.assert.calledWith(dispatch, { sinon.assert.calledWith(dispatch, {
type: SELECTED_CONVERSATION_CHANGED, type: SELECTED_CONVERSATION_CHANGED,
payload: { payload: {
id: '9876', conversationId: '9876',
messageId: undefined, messageId: undefined,
switchToAssociatedView: true, switchToAssociatedView: true,
}, },

View file

@ -34,10 +34,12 @@ export enum LinkPreviewSourceType {
export type MaybeGrabLinkPreviewOptionsType = Readonly<{ export type MaybeGrabLinkPreviewOptionsType = Readonly<{
caretLocation?: number; caretLocation?: number;
conversationId?: string;
mode?: 'conversation' | 'story'; mode?: 'conversation' | 'story';
}>; }>;
export type AddLinkPreviewOptionsType = Readonly<{ export type AddLinkPreviewOptionsType = Readonly<{
conversationId?: string;
disableFetch?: boolean; disableFetch?: boolean;
}>; }>;