Make composer duck aware of the conversation it is in
This commit is contained in:
parent
7a076be0e7
commit
198d6f7e26
17 changed files with 449 additions and 170 deletions
|
@ -160,6 +160,7 @@ import { downloadOnboardingStory } from './util/downloadOnboardingStory';
|
|||
import { clearConversationDraftAttachments } from './util/clearConversationDraftAttachments';
|
||||
import { removeLinkPreview } from './services/LinkPreview';
|
||||
import { PanelType } from './types/Panels';
|
||||
import { getQuotedMessageSelector } from './state/selectors/composer';
|
||||
|
||||
const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000;
|
||||
|
||||
|
@ -1628,10 +1629,8 @@ export async function startApp(): Promise<void> {
|
|||
|
||||
const { selectedMessage } = state.conversations;
|
||||
|
||||
const composerState = window.reduxStore
|
||||
? window.reduxStore.getState().composer
|
||||
: undefined;
|
||||
const quote = composerState?.quotedMessage?.quote;
|
||||
const quotedMessageSelector = getQuotedMessageSelector(state);
|
||||
const quote = quotedMessageSelector(conversation.id);
|
||||
|
||||
window.reduxActions.composer.setQuoteByMessageId(
|
||||
conversation.id,
|
||||
|
@ -1708,7 +1707,7 @@ export async function startApp(): Promise<void> {
|
|||
!shiftKey &&
|
||||
(key === 'p' || key === 'P')
|
||||
) {
|
||||
removeLinkPreview();
|
||||
removeLinkPreview(conversation.id);
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
|
|
@ -102,12 +102,12 @@ export type OwnProps = Readonly<{
|
|||
linkPreviewResult?: LinkPreviewType;
|
||||
messageRequestsEnabled?: boolean;
|
||||
onClearAttachments(conversationId: string): unknown;
|
||||
onCloseLinkPreview(): unknown;
|
||||
onCloseLinkPreview(conversationId: string): unknown;
|
||||
processAttachments: (options: {
|
||||
conversationId: string;
|
||||
files: ReadonlyArray<File>;
|
||||
}) => unknown;
|
||||
setMediaQualitySetting(isHQ: boolean): unknown;
|
||||
setMediaQualitySetting(conversationId: string, isHQ: boolean): unknown;
|
||||
sendStickerMessage(
|
||||
id: string,
|
||||
opts: { packId: string; stickerId: number }
|
||||
|
@ -136,7 +136,7 @@ export type OwnProps = Readonly<{
|
|||
): unknown;
|
||||
shouldSendHighQualityAttachments: boolean;
|
||||
showConversation: ShowConversationType;
|
||||
startRecording: () => unknown;
|
||||
startRecording: (id: string) => unknown;
|
||||
theme: ThemeType;
|
||||
}>;
|
||||
|
||||
|
@ -366,6 +366,20 @@ export function CompositionArea({
|
|||
[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(() => {
|
||||
setLarge(l => !l);
|
||||
}, [setLarge]);
|
||||
|
@ -391,6 +405,7 @@ export function CompositionArea({
|
|||
{showMediaQualitySelector ? (
|
||||
<div className="CompositionArea__button-cell">
|
||||
<MediaQualitySelector
|
||||
conversationId={conversationId}
|
||||
i18n={i18n}
|
||||
isHighQuality={shouldSendHighQualityAttachments}
|
||||
onSelectQuality={setMediaQualitySetting}
|
||||
|
@ -672,7 +687,7 @@ export function CompositionArea({
|
|||
<StagedLinkPreview
|
||||
{...linkPreviewResult}
|
||||
i18n={i18n}
|
||||
onClose={onCloseLinkPreview}
|
||||
onClose={() => onCloseLinkPreview(conversationId)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -18,6 +18,7 @@ export default {
|
|||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
conversationId: 'abc123',
|
||||
i18n,
|
||||
isHighQuality: boolean('isHighQuality', Boolean(overrideProps.isHighQuality)),
|
||||
onSelectQuality: action('onSelectQuality'),
|
||||
|
|
|
@ -12,12 +12,14 @@ import { useRefMerger } from '../hooks/useRefMerger';
|
|||
import { handleOutsideClick } from '../util/handleOutsideClick';
|
||||
|
||||
export type PropsType = {
|
||||
conversationId: string;
|
||||
i18n: LocalizerType;
|
||||
isHighQuality: boolean;
|
||||
onSelectQuality: (isHQ: boolean) => unknown;
|
||||
onSelectQuality: (conversationId: string, isHQ: boolean) => unknown;
|
||||
};
|
||||
|
||||
export function MediaQualitySelector({
|
||||
conversationId,
|
||||
i18n,
|
||||
isHighQuality,
|
||||
onSelectQuality,
|
||||
|
@ -50,7 +52,7 @@ export function MediaQualitySelector({
|
|||
}
|
||||
|
||||
if (ev.key === 'Enter') {
|
||||
onSelectQuality(Boolean(focusedOption));
|
||||
onSelectQuality(conversationId, Boolean(focusedOption));
|
||||
setMenuShowing(false);
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
|
@ -136,7 +138,7 @@ export function MediaQualitySelector({
|
|||
})}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onSelectQuality(false);
|
||||
onSelectQuality(conversationId, false);
|
||||
setMenuShowing(false);
|
||||
}}
|
||||
>
|
||||
|
@ -169,7 +171,7 @@ export function MediaQualitySelector({
|
|||
})}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onSelectQuality(true);
|
||||
onSelectQuality(conversationId, true);
|
||||
setMenuShowing(false);
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -38,7 +38,7 @@ export type PropsType = {
|
|||
i18n: LocalizerType;
|
||||
recordingState: RecordingState;
|
||||
onSendAudioRecording: OnSendAudioRecordingType;
|
||||
startRecording: () => unknown;
|
||||
startRecording: (id: string) => unknown;
|
||||
};
|
||||
|
||||
enum ToastType {
|
||||
|
@ -96,7 +96,11 @@ export function AudioCapture({
|
|||
|
||||
useEscapeHandling(escapeRecording);
|
||||
|
||||
const startRecordingShortcut = useStartRecordingShortcut(startRecording);
|
||||
const recordConversation = useCallback(
|
||||
() => startRecording(conversationId),
|
||||
[conversationId, startRecording]
|
||||
);
|
||||
const startRecordingShortcut = useStartRecordingShortcut(recordConversation);
|
||||
useKeyboardShortcuts(startRecordingShortcut);
|
||||
|
||||
const closeToast = useCallback(() => {
|
||||
|
@ -240,7 +244,7 @@ export function AudioCapture({
|
|||
if (draftAttachments.length) {
|
||||
setToastType(ToastType.VoiceNoteMustBeOnlyAttachment);
|
||||
} else {
|
||||
startRecording();
|
||||
startRecording(conversationId);
|
||||
}
|
||||
}}
|
||||
title={i18n('voiceRecording--start')}
|
||||
|
|
|
@ -26,6 +26,7 @@ import { dropNull } from '../util/dropNull';
|
|||
import { fileToBytes } from '../util/fileToBytes';
|
||||
import { maybeParseUrl } from '../util/url';
|
||||
import { sniffImageMimeType } from '../util/sniffImageMimeType';
|
||||
import { drop } from '../util/drop';
|
||||
|
||||
const LINK_PREVIEW_TIMEOUT = 60 * SECOND;
|
||||
|
||||
|
@ -48,7 +49,11 @@ export const maybeGrabLinkPreview = debounce(_maybeGrabLinkPreview, 200);
|
|||
function _maybeGrabLinkPreview(
|
||||
message: string,
|
||||
source: LinkPreviewSourceType,
|
||||
{ caretLocation, mode = 'conversation' }: MaybeGrabLinkPreviewOptionsType = {}
|
||||
{
|
||||
caretLocation,
|
||||
conversationId,
|
||||
mode = 'conversation',
|
||||
}: MaybeGrabLinkPreviewOptionsType = {}
|
||||
): void {
|
||||
// Don't generate link previews if user has turned them off. When posting a
|
||||
// story we should return minimal (url-only) link previews.
|
||||
|
@ -67,7 +72,7 @@ function _maybeGrabLinkPreview(
|
|||
}
|
||||
|
||||
if (!message) {
|
||||
resetLinkPreview();
|
||||
resetLinkPreview(conversationId);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -88,22 +93,25 @@ function _maybeGrabLinkPreview(
|
|||
LinkPreview.shouldPreviewHref(item) && !excludedPreviewUrls.includes(item)
|
||||
);
|
||||
if (!link) {
|
||||
removeLinkPreview();
|
||||
removeLinkPreview(conversationId);
|
||||
return;
|
||||
}
|
||||
|
||||
void addLinkPreview(link, source, {
|
||||
disableFetch: !window.Events.getLinkPreviewSetting(),
|
||||
});
|
||||
drop(
|
||||
addLinkPreview(link, source, {
|
||||
conversationId,
|
||||
disableFetch: !window.Events.getLinkPreviewSetting(),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function resetLinkPreview(): void {
|
||||
export function resetLinkPreview(conversationId?: string): void {
|
||||
disableLinkPreviews = false;
|
||||
excludedPreviewUrls = [];
|
||||
removeLinkPreview();
|
||||
removeLinkPreview(conversationId);
|
||||
}
|
||||
|
||||
export function removeLinkPreview(): void {
|
||||
export function removeLinkPreview(conversationId?: string): void {
|
||||
(linkPreviewResult || []).forEach((item: LinkPreviewResult) => {
|
||||
if (item.url) {
|
||||
URL.revokeObjectURL(item.url);
|
||||
|
@ -114,13 +122,13 @@ export function removeLinkPreview(): void {
|
|||
linkPreviewAbortController?.abort();
|
||||
linkPreviewAbortController = undefined;
|
||||
|
||||
window.reduxActions.linkPreviews.removeLinkPreview();
|
||||
window.reduxActions.linkPreviews.removeLinkPreview(conversationId);
|
||||
}
|
||||
|
||||
export async function addLinkPreview(
|
||||
url: string,
|
||||
source: LinkPreviewSourceType,
|
||||
{ disableFetch }: AddLinkPreviewOptionsType = {}
|
||||
{ conversationId, disableFetch }: AddLinkPreviewOptionsType = {}
|
||||
): Promise<void> {
|
||||
if (currentlyMatchedLink === url) {
|
||||
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);
|
||||
}
|
||||
});
|
||||
window.reduxActions.linkPreviews.removeLinkPreview();
|
||||
window.reduxActions.linkPreviews.removeLinkPreview(conversationId);
|
||||
linkPreviewResult = undefined;
|
||||
|
||||
// Cancel other in-flight link preview requests.
|
||||
|
@ -156,7 +164,8 @@ export async function addLinkPreview(
|
|||
{
|
||||
url,
|
||||
},
|
||||
source
|
||||
source,
|
||||
conversationId
|
||||
);
|
||||
|
||||
try {
|
||||
|
@ -186,7 +195,7 @@ export async function addLinkPreview(
|
|||
const failedToFetch = currentlyMatchedLink === url;
|
||||
if (failedToFetch) {
|
||||
excludedPreviewUrls.push(url);
|
||||
removeLinkPreview();
|
||||
removeLinkPreview(conversationId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
@ -198,7 +207,7 @@ export async function addLinkPreview(
|
|||
result.image.url = URL.createObjectURL(blob);
|
||||
} else if (!result.title && !disableFetch) {
|
||||
// A link preview isn't worth showing unless we have either a title or an image
|
||||
removeLinkPreview();
|
||||
removeLinkPreview(conversationId);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -211,7 +220,8 @@ export async function addLinkPreview(
|
|||
domain: LinkPreview.getDomain(result.url),
|
||||
isStickerPack: LinkPreview.isStickerPack(result.url),
|
||||
},
|
||||
source
|
||||
source,
|
||||
conversationId
|
||||
);
|
||||
linkPreviewResult = [result];
|
||||
} catch (error) {
|
||||
|
@ -220,7 +230,7 @@ export async function addLinkPreview(
|
|||
Errors.toLogFormat(error)
|
||||
);
|
||||
disableLinkPreviews = true;
|
||||
removeLinkPreview();
|
||||
removeLinkPreview(conversationId);
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import { recorder } from '../../services/audioRecorder';
|
|||
import { stringToMIMEType } from '../../types/MIME';
|
||||
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
|
||||
import { useBoundActions } from '../../hooks/useBoundActions';
|
||||
import { getComposerStateForConversation } from './composer';
|
||||
|
||||
export enum ErrorDialogAudioRecorderType {
|
||||
Blur,
|
||||
|
@ -80,17 +81,24 @@ export const actions = {
|
|||
export const useActions = (): BoundActionCreatorsMapObject<typeof actions> =>
|
||||
useBoundActions(actions);
|
||||
|
||||
function startRecording(): ThunkAction<
|
||||
function startRecording(
|
||||
conversationId: string
|
||||
): ThunkAction<
|
||||
void,
|
||||
RootStateType,
|
||||
unknown,
|
||||
StartRecordingAction | NowRecordingAction | ErrorRecordingAction
|
||||
> {
|
||||
return async (dispatch, getState) => {
|
||||
if (getState().composer.attachments.length) {
|
||||
const state = getState();
|
||||
|
||||
if (
|
||||
getComposerStateForConversation(state.composer, conversationId)
|
||||
.attachments.length
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (getState().audioRecorder.recordingState !== RecordingState.Idle) {
|
||||
if (state.audioRecorder.recordingState !== RecordingState.Idle) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -71,14 +71,23 @@ import { getContactId } from '../../messages/helpers';
|
|||
import { getConversationSelector } from '../selectors/conversations';
|
||||
import { enqueueReactionForSend } from '../../reactions/enqueueReactionForSend';
|
||||
import { useBoundActions } from '../../hooks/useBoundActions';
|
||||
import { scrollToMessage } from './conversations';
|
||||
import type { ScrollToMessageActionType } from './conversations';
|
||||
import {
|
||||
CONVERSATION_UNLOADED,
|
||||
SELECTED_CONVERSATION_CHANGED,
|
||||
scrollToMessage,
|
||||
} from './conversations';
|
||||
import type {
|
||||
ConversationUnloadedActionType,
|
||||
SelectedConversationChangedActionType,
|
||||
ScrollToMessageActionType,
|
||||
} from './conversations';
|
||||
import { longRunningTaskWrapper } from '../../util/longRunningTaskWrapper';
|
||||
import { drop } from '../../util/drop';
|
||||
import { strictAssert } from '../../util/assert';
|
||||
|
||||
// State
|
||||
|
||||
export type ComposerStateType = {
|
||||
type ComposerStateByConversationType = {
|
||||
attachments: ReadonlyArray<AttachmentDraftType>;
|
||||
focusCounter: number;
|
||||
isDisabled: boolean;
|
||||
|
@ -89,6 +98,32 @@ export type ComposerStateType = {
|
|||
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
|
||||
|
||||
const ADD_PENDING_ATTACHMENT = 'composer/ADD_PENDING_ATTACHMENT';
|
||||
|
@ -101,43 +136,66 @@ const SET_COMPOSER_DISABLED = 'composer/SET_COMPOSER_DISABLED';
|
|||
|
||||
type AddPendingAttachmentActionType = {
|
||||
type: typeof ADD_PENDING_ATTACHMENT;
|
||||
payload: AttachmentDraftType;
|
||||
payload: {
|
||||
conversationId: string;
|
||||
attachment: AttachmentDraftType;
|
||||
};
|
||||
};
|
||||
|
||||
export type ReplaceAttachmentsActionType = {
|
||||
type: typeof REPLACE_ATTACHMENTS;
|
||||
payload: ReadonlyArray<AttachmentDraftType>;
|
||||
payload: {
|
||||
conversationId: string;
|
||||
attachments: ReadonlyArray<AttachmentDraftType>;
|
||||
};
|
||||
};
|
||||
|
||||
export type ResetComposerActionType = {
|
||||
type: typeof RESET_COMPOSER;
|
||||
payload: {
|
||||
conversationId: string;
|
||||
};
|
||||
};
|
||||
|
||||
type SetComposerDisabledStateActionType = {
|
||||
type: typeof SET_COMPOSER_DISABLED;
|
||||
payload: boolean;
|
||||
payload: {
|
||||
conversationId: string;
|
||||
value: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type SetFocusActionType = {
|
||||
type: typeof SET_FOCUS;
|
||||
payload: {
|
||||
conversationId: string;
|
||||
};
|
||||
};
|
||||
|
||||
type SetHighQualitySettingActionType = {
|
||||
type: typeof SET_HIGH_QUALITY_SETTING;
|
||||
payload: boolean;
|
||||
payload: {
|
||||
conversationId: string;
|
||||
value: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type SetQuotedMessageActionType = {
|
||||
type: typeof SET_QUOTED_MESSAGE;
|
||||
payload?: Pick<MessageAttributesType, 'conversationId' | 'quote'>;
|
||||
payload: {
|
||||
conversationId: string;
|
||||
quotedMessage?: QuotedMessageType;
|
||||
};
|
||||
};
|
||||
|
||||
type ComposerActionType =
|
||||
| AddLinkPreviewActionType
|
||||
| AddPendingAttachmentActionType
|
||||
| ConversationUnloadedActionType
|
||||
| RemoveLinkPreviewActionType
|
||||
| ReplaceAttachmentsActionType
|
||||
| ResetComposerActionType
|
||||
| SelectedConversationChangedActionType
|
||||
| SetComposerDisabledStateActionType
|
||||
| SetFocusActionType
|
||||
| SetHighQualitySettingActionType
|
||||
|
@ -207,9 +265,9 @@ function cancelJoinRequest(conversationId: string): NoopActionType {
|
|||
};
|
||||
}
|
||||
|
||||
function onCloseLinkPreview(): NoopActionType {
|
||||
function onCloseLinkPreview(conversationId: string): NoopActionType {
|
||||
suspendLinkPreviews();
|
||||
removeLinkPreview();
|
||||
removeLinkPreview(conversationId);
|
||||
|
||||
return {
|
||||
type: 'NOOP',
|
||||
|
@ -308,18 +366,18 @@ function sendMultiMediaMessage(
|
|||
]);
|
||||
|
||||
try {
|
||||
dispatch(setComposerDisabledState(true));
|
||||
dispatch(setComposerDisabledState(conversationId, true));
|
||||
|
||||
const sendAnyway = await blockSendUntilConversationsAreVerified(
|
||||
recipientsByConversation,
|
||||
SafetyNumberChangeSource.MessageSend
|
||||
);
|
||||
if (!sendAnyway) {
|
||||
dispatch(setComposerDisabledState(false));
|
||||
dispatch(setComposerDisabledState(conversationId, false));
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
dispatch(setComposerDisabledState(false));
|
||||
dispatch(setComposerDisabledState(conversationId, false));
|
||||
log.error('sendMessage error:', Errors.toLogFormat(error));
|
||||
return;
|
||||
}
|
||||
|
@ -334,7 +392,7 @@ function sendMultiMediaMessage(
|
|||
toastType,
|
||||
},
|
||||
});
|
||||
dispatch(setComposerDisabledState(false));
|
||||
dispatch(setComposerDisabledState(conversationId, false));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -345,7 +403,7 @@ function sendMultiMediaMessage(
|
|||
}) &&
|
||||
!voiceNoteAttachment
|
||||
) {
|
||||
dispatch(setComposerDisabledState(false));
|
||||
dispatch(setComposerDisabledState(conversationId, false));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -359,10 +417,15 @@ function sendMultiMediaMessage(
|
|||
).filter(isNotNil);
|
||||
}
|
||||
|
||||
const quote = state.composer.quotedMessage?.quote;
|
||||
const conversationComposerState = getComposerStateForConversation(
|
||||
state.composer,
|
||||
conversationId
|
||||
);
|
||||
|
||||
const quote = conversationComposerState.quotedMessage?.quote;
|
||||
|
||||
const shouldSendHighQualityAttachments = window.reduxStore
|
||||
? state.composer.shouldSendHighQualityAttachments
|
||||
? conversationComposerState.shouldSendHighQualityAttachments
|
||||
: undefined;
|
||||
|
||||
const sendHQImages =
|
||||
|
@ -388,7 +451,7 @@ function sendMultiMediaMessage(
|
|||
// We rely on enqueueMessageForSend to call these within redux's batch
|
||||
extraReduxActions: () => {
|
||||
conversation.setMarkedUnread(false);
|
||||
resetLinkPreview();
|
||||
resetLinkPreview(conversationId);
|
||||
drop(
|
||||
clearConversationDraftAttachments(
|
||||
conversationId,
|
||||
|
@ -400,8 +463,8 @@ function sendMultiMediaMessage(
|
|||
getState,
|
||||
undefined
|
||||
);
|
||||
dispatch(resetComposer());
|
||||
dispatch(setComposerDisabledState(false));
|
||||
dispatch(resetComposer(conversationId));
|
||||
dispatch(setComposerDisabledState(conversationId, false));
|
||||
},
|
||||
}
|
||||
);
|
||||
|
@ -410,7 +473,7 @@ function sendMultiMediaMessage(
|
|||
'Error pulling attached files before send',
|
||||
Errors.toLogFormat(error)
|
||||
);
|
||||
dispatch(setComposerDisabledState(false));
|
||||
dispatch(setComposerDisabledState(conversationId, false));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -544,16 +607,16 @@ export function setQuoteByMessageId(
|
|||
}
|
||||
|
||||
dispatch(
|
||||
setQuotedMessage({
|
||||
setQuotedMessage(conversationId, {
|
||||
conversationId,
|
||||
quote,
|
||||
})
|
||||
);
|
||||
|
||||
dispatch(setComposerFocus(conversation.id));
|
||||
dispatch(setComposerDisabledState(false));
|
||||
dispatch(setComposerDisabledState(conversationId, false));
|
||||
} else {
|
||||
dispatch(setQuotedMessage(undefined));
|
||||
dispatch(setQuotedMessage(conversationId, undefined));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -567,11 +630,18 @@ function addAttachment(
|
|||
// each other.
|
||||
const onDisk = await writeDraftAttachment(attachment);
|
||||
|
||||
const state = getState();
|
||||
|
||||
const isSelectedConversation =
|
||||
getState().conversations.selectedConversationId === conversationId;
|
||||
state.conversations.selectedConversationId === conversationId;
|
||||
|
||||
const conversationComposerState = getComposerStateForConversation(
|
||||
state.composer,
|
||||
conversationId
|
||||
);
|
||||
|
||||
const draftAttachments = isSelectedConversation
|
||||
? getState().composer.attachments
|
||||
? conversationComposerState.attachments
|
||||
: getAttachmentsFromConversationModel(conversationId);
|
||||
|
||||
// We expect there to either be a pending draft attachment or an existing
|
||||
|
@ -619,18 +689,28 @@ function addPendingAttachment(
|
|||
pendingAttachment: AttachmentDraftType
|
||||
): ThunkAction<void, RootStateType, unknown, ReplaceAttachmentsActionType> {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
||||
const isSelectedConversation =
|
||||
getState().conversations.selectedConversationId === conversationId;
|
||||
state.conversations.selectedConversationId === conversationId;
|
||||
|
||||
const conversationComposerState = getComposerStateForConversation(
|
||||
state.composer,
|
||||
conversationId
|
||||
);
|
||||
|
||||
const draftAttachments = isSelectedConversation
|
||||
? getState().composer.attachments
|
||||
? conversationComposerState.attachments
|
||||
: getAttachmentsFromConversationModel(conversationId);
|
||||
|
||||
const nextAttachments = [...draftAttachments, pendingAttachment];
|
||||
|
||||
dispatch({
|
||||
type: REPLACE_ATTACHMENTS,
|
||||
payload: nextAttachments,
|
||||
payload: {
|
||||
conversationId,
|
||||
attachments: nextAttachments,
|
||||
},
|
||||
});
|
||||
|
||||
const conversation = window.ConversationController.get(conversationId);
|
||||
|
@ -651,6 +731,9 @@ export function setComposerFocus(
|
|||
|
||||
dispatch({
|
||||
type: SET_FOCUS,
|
||||
payload: {
|
||||
conversationId,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@ -686,6 +769,7 @@ function onEditorStateChange(
|
|||
) {
|
||||
maybeGrabLinkPreview(messageText, LinkPreviewSourceType.Composer, {
|
||||
caretLocation,
|
||||
conversationId,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -877,7 +961,12 @@ function removeAttachment(
|
|||
filePath: string
|
||||
): ThunkAction<void, RootStateType, unknown, ReplaceAttachmentsActionType> {
|
||||
return async (dispatch, getState) => {
|
||||
const { attachments } = getState().composer;
|
||||
const state = getState();
|
||||
|
||||
const { attachments } = getComposerStateForConversation(
|
||||
state.composer,
|
||||
conversationId
|
||||
);
|
||||
|
||||
const [targetAttachment] = attachments.filter(
|
||||
attachment => attachment.path === filePath
|
||||
|
@ -924,12 +1013,15 @@ export function replaceAttachments(
|
|||
}
|
||||
|
||||
if (hasDraftAttachments(attachments, { includePending: true })) {
|
||||
removeLinkPreview();
|
||||
removeLinkPreview(conversationId);
|
||||
}
|
||||
|
||||
dispatch({
|
||||
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 {
|
||||
type: RESET_COMPOSER,
|
||||
payload: {
|
||||
conversationId,
|
||||
},
|
||||
};
|
||||
}
|
||||
const debouncedSaveDraft = debounce(saveDraft);
|
||||
|
@ -1024,29 +1119,41 @@ function saveDraft(
|
|||
}
|
||||
|
||||
function setComposerDisabledState(
|
||||
conversationId: string,
|
||||
value: boolean
|
||||
): SetComposerDisabledStateActionType {
|
||||
return {
|
||||
type: SET_COMPOSER_DISABLED,
|
||||
payload: value,
|
||||
payload: {
|
||||
conversationId,
|
||||
value,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function setMediaQualitySetting(
|
||||
payload: boolean
|
||||
conversationId: string,
|
||||
value: boolean
|
||||
): SetHighQualitySettingActionType {
|
||||
return {
|
||||
type: SET_HIGH_QUALITY_SETTING,
|
||||
payload,
|
||||
payload: {
|
||||
conversationId,
|
||||
value,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function setQuotedMessage(
|
||||
payload?: Pick<MessageAttributesType, 'conversationId' | 'quote' | 'payment'>
|
||||
conversationId: string,
|
||||
quotedMessage?: QuotedMessageType
|
||||
): SetQuotedMessageActionType {
|
||||
return {
|
||||
type: SET_QUOTED_MESSAGE,
|
||||
payload,
|
||||
payload: {
|
||||
conversationId,
|
||||
quotedMessage,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1054,52 +1161,107 @@ function setQuotedMessage(
|
|||
|
||||
export function getEmptyState(): ComposerStateType {
|
||||
return {
|
||||
attachments: [],
|
||||
focusCounter: 0,
|
||||
isDisabled: false,
|
||||
linkPreviewLoading: false,
|
||||
messageCompositionId: UUID.generate().toString(),
|
||||
conversations: {},
|
||||
};
|
||||
}
|
||||
|
||||
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(
|
||||
state: Readonly<ComposerStateType> = getEmptyState(),
|
||||
action: Readonly<ComposerActionType>
|
||||
): 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();
|
||||
}
|
||||
|
||||
if (action.type === RESET_COMPOSER) {
|
||||
return updateComposerState(state, action, () => ({}));
|
||||
}
|
||||
|
||||
if (action.type === REPLACE_ATTACHMENTS) {
|
||||
const { payload: attachments } = action;
|
||||
return {
|
||||
...state,
|
||||
const { attachments } = action.payload;
|
||||
|
||||
return updateComposerState(state, action, () => ({
|
||||
attachments,
|
||||
...(attachments.length
|
||||
? {}
|
||||
: { shouldSendHighQualityAttachments: undefined }),
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
if (action.type === SET_FOCUS) {
|
||||
return {
|
||||
...state,
|
||||
focusCounter: state.focusCounter + 1,
|
||||
};
|
||||
return updateComposerState(state, action, prevState => ({
|
||||
focusCounter: prevState.focusCounter + 1,
|
||||
}));
|
||||
}
|
||||
|
||||
if (action.type === SET_HIGH_QUALITY_SETTING) {
|
||||
return {
|
||||
...state,
|
||||
shouldSendHighQualityAttachments: action.payload,
|
||||
};
|
||||
return updateComposerState(state, action, () => ({
|
||||
shouldSendHighQualityAttachments: action.payload.value,
|
||||
}));
|
||||
}
|
||||
|
||||
if (action.type === SET_QUOTED_MESSAGE) {
|
||||
return {
|
||||
...state,
|
||||
quotedMessage: action.payload,
|
||||
};
|
||||
const { quotedMessage } = action.payload;
|
||||
return updateComposerState(state, action, () => ({
|
||||
quotedMessage,
|
||||
}));
|
||||
}
|
||||
|
||||
if (action.type === ADD_LINK_PREVIEW) {
|
||||
|
@ -1107,32 +1269,29 @@ export function reducer(
|
|||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
return updateComposerState(state, action, () => ({
|
||||
linkPreviewLoading: true,
|
||||
linkPreviewResult: action.payload.linkPreview,
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
if (action.type === REMOVE_LINK_PREVIEW) {
|
||||
return assignWithNoUnnecessaryAllocation(state, {
|
||||
return updateComposerState(state, action, () => ({
|
||||
linkPreviewLoading: false,
|
||||
linkPreviewResult: undefined,
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
if (action.type === ADD_PENDING_ATTACHMENT) {
|
||||
return {
|
||||
...state,
|
||||
attachments: [...state.attachments, action.payload],
|
||||
};
|
||||
return updateComposerState(state, action, prevState => ({
|
||||
attachments: [...prevState.attachments, action.payload.attachment],
|
||||
}));
|
||||
}
|
||||
|
||||
if (action.type === SET_COMPOSER_DISABLED) {
|
||||
return {
|
||||
...state,
|
||||
isDisabled: action.payload,
|
||||
};
|
||||
return updateComposerState(state, action, () => ({
|
||||
isDisabled: action.payload.value,
|
||||
}));
|
||||
}
|
||||
|
||||
return state;
|
||||
|
|
|
@ -612,7 +612,7 @@ export type ConversationRemovedActionType = {
|
|||
export type ConversationUnloadedActionType = {
|
||||
type: typeof CONVERSATION_UNLOADED;
|
||||
payload: {
|
||||
id: string;
|
||||
conversationId: string;
|
||||
};
|
||||
};
|
||||
type CreateGroupPendingActionType = {
|
||||
|
@ -745,7 +745,7 @@ export type ClearUnreadMetricsActionType = {
|
|||
export type SelectedConversationChangedActionType = {
|
||||
type: typeof SELECTED_CONVERSATION_CHANGED;
|
||||
payload: {
|
||||
id?: string;
|
||||
conversationId?: string;
|
||||
messageId?: string;
|
||||
switchToAssociatedView?: boolean;
|
||||
};
|
||||
|
@ -3485,7 +3485,7 @@ function showConversation({
|
|||
return {
|
||||
type: SELECTED_CONVERSATION_CHANGED,
|
||||
payload: {
|
||||
id: conversationId,
|
||||
conversationId,
|
||||
messageId,
|
||||
switchToAssociatedView,
|
||||
},
|
||||
|
@ -3581,7 +3581,7 @@ function onConversationOpened(
|
|||
conversation.get('id'),
|
||||
conversation.get('draftAttachments') || []
|
||||
)(dispatch, getState, undefined);
|
||||
dispatch(resetComposer());
|
||||
dispatch(resetComposer(conversationId));
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -3625,13 +3625,13 @@ function onConversationClosed(
|
|||
drop(conversation.updateLastMessage());
|
||||
}
|
||||
|
||||
removeLinkPreview();
|
||||
removeLinkPreview(conversationId);
|
||||
suspendLinkPreviews();
|
||||
|
||||
dispatch({
|
||||
type: CONVERSATION_UNLOADED,
|
||||
payload: {
|
||||
id: conversationId,
|
||||
conversationId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -4186,15 +4186,15 @@ export function reducer(
|
|||
}
|
||||
if (action.type === CONVERSATION_UNLOADED) {
|
||||
const { payload } = action;
|
||||
const { id } = payload;
|
||||
const existingConversation = state.messagesByConversation[id];
|
||||
const { conversationId } = payload;
|
||||
const existingConversation = state.messagesByConversation[conversationId];
|
||||
if (!existingConversation) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const { messageIds } = existingConversation;
|
||||
const selectedConversationId =
|
||||
state.selectedConversationId !== id
|
||||
state.selectedConversationId !== conversationId
|
||||
? state.selectedConversationId
|
||||
: undefined;
|
||||
|
||||
|
@ -4203,7 +4203,9 @@ export function reducer(
|
|||
selectedConversationId,
|
||||
selectedConversationPanels: [],
|
||||
messagesLookup: omit(state.messagesLookup, [...messageIds]),
|
||||
messagesByConversation: omit(state.messagesByConversation, [id]),
|
||||
messagesByConversation: omit(state.messagesByConversation, [
|
||||
conversationId,
|
||||
]),
|
||||
};
|
||||
}
|
||||
if (action.type === 'CONVERSATIONS_REMOVE_ALL') {
|
||||
|
@ -4993,17 +4995,17 @@ export function reducer(
|
|||
}
|
||||
if (action.type === SELECTED_CONVERSATION_CHANGED) {
|
||||
const { payload } = action;
|
||||
const { id, messageId, switchToAssociatedView } = payload;
|
||||
const { conversationId, messageId, switchToAssociatedView } = payload;
|
||||
|
||||
const nextState = {
|
||||
...omit(state, 'contactSpoofingReview'),
|
||||
selectedConversationId: id,
|
||||
selectedConversationId: conversationId,
|
||||
selectedMessage: messageId,
|
||||
selectedMessageSource: SelectedMessageSource.NavigateToMessage,
|
||||
};
|
||||
|
||||
if (switchToAssociatedView && id) {
|
||||
const conversation = getOwn(state.conversationLookup, id);
|
||||
if (switchToAssociatedView && conversationId) {
|
||||
const conversation = getOwn(state.conversationLookup, conversationId);
|
||||
if (!conversation) {
|
||||
return nextState;
|
||||
}
|
||||
|
|
|
@ -3,16 +3,15 @@
|
|||
|
||||
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 { StateType as RootStateType } from '../reducer';
|
||||
import type { LinkPreviewType } from '../../types/message/LinkPreviews';
|
||||
import type {
|
||||
LinkPreviewSourceType,
|
||||
MaybeGrabLinkPreviewOptionsType,
|
||||
} from '../../types/LinkPreview';
|
||||
import { LinkPreviewSourceType } from '../../types/LinkPreview';
|
||||
import { assignWithNoUnnecessaryAllocation } from '../../util/assignWithNoUnnecessaryAllocation';
|
||||
import { maybeGrabLinkPreview } from '../../services/LinkPreview';
|
||||
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
|
||||
import { strictAssert } from '../../util/assert';
|
||||
import { useBoundActions } from '../../hooks/useBoundActions';
|
||||
|
||||
// State
|
||||
|
@ -30,6 +29,7 @@ export const REMOVE_PREVIEW = 'linkPreviews/REMOVE_PREVIEW';
|
|||
export type AddLinkPreviewActionType = {
|
||||
type: 'linkPreviews/ADD_PREVIEW';
|
||||
payload: {
|
||||
conversationId?: string;
|
||||
linkPreview: LinkPreviewType;
|
||||
source: LinkPreviewSourceType;
|
||||
};
|
||||
|
@ -37,6 +37,9 @@ export type AddLinkPreviewActionType = {
|
|||
|
||||
export type RemoveLinkPreviewActionType = {
|
||||
type: 'linkPreviews/REMOVE_PREVIEW';
|
||||
payload: {
|
||||
conversationId?: string;
|
||||
};
|
||||
};
|
||||
|
||||
type LinkPreviewsActionType =
|
||||
|
@ -62,20 +65,31 @@ function debouncedMaybeGrabLinkPreview(
|
|||
|
||||
function addLinkPreview(
|
||||
linkPreview: LinkPreviewType,
|
||||
source: LinkPreviewSourceType
|
||||
source: LinkPreviewSourceType,
|
||||
conversationId?: string
|
||||
): AddLinkPreviewActionType {
|
||||
if (source === LinkPreviewSourceType.Composer) {
|
||||
strictAssert(conversationId, 'no conversationId provided');
|
||||
}
|
||||
|
||||
return {
|
||||
type: ADD_PREVIEW,
|
||||
payload: {
|
||||
conversationId,
|
||||
linkPreview,
|
||||
source,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function removeLinkPreview(): RemoveLinkPreviewActionType {
|
||||
function removeLinkPreview(
|
||||
conversationId?: string
|
||||
): RemoveLinkPreviewActionType {
|
||||
return {
|
||||
type: REMOVE_PREVIEW,
|
||||
payload: {
|
||||
conversationId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -31,7 +31,10 @@ import {
|
|||
getUserConversationId,
|
||||
} from '../selectors/user';
|
||||
import { strictAssert } from '../../util/assert';
|
||||
import { SELECTED_CONVERSATION_CHANGED } from './conversations';
|
||||
import {
|
||||
CONVERSATION_UNLOADED,
|
||||
SELECTED_CONVERSATION_CHANGED,
|
||||
} from './conversations';
|
||||
|
||||
const {
|
||||
searchMessages: dataSearchMessages,
|
||||
|
@ -437,10 +440,10 @@ export function reducer(
|
|||
|
||||
if (action.type === SELECTED_CONVERSATION_CHANGED) {
|
||||
const { payload } = action;
|
||||
const { id, messageId } = payload;
|
||||
const { conversationId, messageId } = payload;
|
||||
const { searchConversationId } = state;
|
||||
|
||||
if (searchConversationId && searchConversationId !== id) {
|
||||
if (searchConversationId && searchConversationId !== conversationId) {
|
||||
return getEmptyState();
|
||||
}
|
||||
|
||||
|
@ -450,12 +453,12 @@ export function reducer(
|
|||
};
|
||||
}
|
||||
|
||||
if (action.type === 'CONVERSATION_UNLOADED') {
|
||||
if (action.type === CONVERSATION_UNLOADED) {
|
||||
const { payload } = action;
|
||||
const { id } = payload;
|
||||
const { conversationId } = payload;
|
||||
const { searchConversationId } = state;
|
||||
|
||||
if (searchConversationId && searchConversationId === id) {
|
||||
if (searchConversationId && searchConversationId === conversationId) {
|
||||
return getEmptyState();
|
||||
}
|
||||
|
||||
|
|
24
ts/state/selectors/composer.ts
Normal file
24
ts/state/selectors/composer.ts
Normal 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
|
||||
);
|
|
@ -30,6 +30,7 @@ import {
|
|||
getRecentStickers,
|
||||
} from '../selectors/stickers';
|
||||
import { isSignalConversation } from '../../util/isSignalConversation';
|
||||
import { getComposerStateForConversationIdSelector } from '../selectors/composer';
|
||||
|
||||
type ExternalProps = {
|
||||
id: string;
|
||||
|
@ -67,6 +68,9 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
|||
receivedPacks.length > 0
|
||||
);
|
||||
|
||||
const composerStateForConversationIdSelector =
|
||||
getComposerStateForConversationIdSelector(state);
|
||||
|
||||
const {
|
||||
attachments: draftAttachments,
|
||||
focusCounter,
|
||||
|
@ -76,7 +80,7 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
|||
messageCompositionId,
|
||||
quotedMessage,
|
||||
shouldSendHighQualityAttachments,
|
||||
} = state.composer;
|
||||
} = composerStateForConversationIdSelector(id);
|
||||
|
||||
const recentEmojis = selectRecentEmojis(state);
|
||||
|
||||
|
|
|
@ -6,7 +6,12 @@ import * as sinon from 'sinon';
|
|||
import { noop } from 'lodash';
|
||||
|
||||
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 { 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());
|
||||
return () => ({
|
||||
...state,
|
||||
|
@ -37,7 +42,7 @@ describe('both/state/ducks/composer', () => {
|
|||
selectedConversationId,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
describe('replaceAttachments', () => {
|
||||
let oldReduxActions: ReduxActions;
|
||||
|
@ -76,7 +81,8 @@ describe('both/state/ducks/composer', () => {
|
|||
|
||||
const action = dispatch.getCall(0).args[0];
|
||||
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', () => {
|
||||
|
@ -94,14 +100,20 @@ describe('both/state/ducks/composer', () => {
|
|||
const state = reducer(
|
||||
{
|
||||
...getEmptyState(),
|
||||
shouldSendHighQualityAttachments: true,
|
||||
conversations: {
|
||||
'123': {
|
||||
...getComposerStateForConversation(getEmptyState(), '123'),
|
||||
shouldSendHighQualityAttachments: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
action
|
||||
);
|
||||
assert.deepEqual(state.attachments, attachments);
|
||||
const composerState = getComposerStateForConversation(state, '123');
|
||||
assert.deepEqual(composerState.attachments, attachments);
|
||||
|
||||
assert.deepEqual(state.attachments, attachments);
|
||||
assert.isUndefined(state.shouldSendHighQualityAttachments);
|
||||
assert.deepEqual(composerState.attachments, attachments);
|
||||
assert.isUndefined(composerState.shouldSendHighQualityAttachments);
|
||||
});
|
||||
|
||||
it('does not update redux if the conversation is not selected', () => {
|
||||
|
@ -122,23 +134,17 @@ describe('both/state/ducks/composer', () => {
|
|||
describe('resetComposer', () => {
|
||||
it('returns composer back to empty state', () => {
|
||||
const { resetComposer } = actions;
|
||||
const emptyState = getEmptyState();
|
||||
const nextState = reducer(
|
||||
{
|
||||
attachments: [],
|
||||
focusCounter: 0,
|
||||
isDisabled: false,
|
||||
linkPreviewLoading: true,
|
||||
messageCompositionId: emptyState.messageCompositionId,
|
||||
quotedMessage: QUOTED_MESSAGE,
|
||||
shouldSendHighQualityAttachments: true,
|
||||
},
|
||||
resetComposer()
|
||||
);
|
||||
const nextState = reducer(getEmptyState(), resetComposer('456'));
|
||||
|
||||
const composerState = getComposerStateForConversation(nextState, '456');
|
||||
assert.deepEqual(nextState, {
|
||||
...getEmptyState(),
|
||||
messageCompositionId: nextState.messageCompositionId,
|
||||
conversations: {
|
||||
'456': {
|
||||
...composerState,
|
||||
messageCompositionId: composerState.messageCompositionId,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -148,15 +154,40 @@ describe('both/state/ducks/composer', () => {
|
|||
const { setMediaQualitySetting } = actions;
|
||||
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', () => {
|
||||
const { setQuotedMessage } = actions;
|
||||
const state = getEmptyState();
|
||||
const nextState = reducer(state, setQuotedMessage(QUOTED_MESSAGE));
|
||||
const nextState = reducer(state, setQuotedMessage('123', QUOTED_MESSAGE));
|
||||
|
||||
assert.equal(nextState.quotedMessage?.conversationId, '123');
|
||||
assert.equal(nextState.quotedMessage?.quote?.id, 456);
|
||||
const composerState = getComposerStateForConversation(nextState, '123');
|
||||
assert.equal(composerState.quotedMessage?.conversationId, '123');
|
||||
assert.equal(composerState.quotedMessage?.quote?.id, 456);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -26,7 +26,7 @@ describe('both/state/ducks/linkPreviews', () => {
|
|||
it('updates linkPreview', () => {
|
||||
const state = getEmptyState();
|
||||
const linkPreview = getMockLinkPreview();
|
||||
const nextState = reducer(state, addLinkPreview(linkPreview, 0));
|
||||
const nextState = reducer(state, addLinkPreview(linkPreview, 1));
|
||||
|
||||
assert.strictEqual(nextState.linkPreview, linkPreview);
|
||||
});
|
||||
|
|
|
@ -741,7 +741,7 @@ describe('both/state/ducks/conversations', () => {
|
|||
sinon.assert.calledWith(dispatch, {
|
||||
type: SELECTED_CONVERSATION_CHANGED,
|
||||
payload: {
|
||||
id: '9876',
|
||||
conversationId: '9876',
|
||||
messageId: undefined,
|
||||
switchToAssociatedView: true,
|
||||
},
|
||||
|
|
|
@ -34,10 +34,12 @@ export enum LinkPreviewSourceType {
|
|||
|
||||
export type MaybeGrabLinkPreviewOptionsType = Readonly<{
|
||||
caretLocation?: number;
|
||||
conversationId?: string;
|
||||
mode?: 'conversation' | 'story';
|
||||
}>;
|
||||
|
||||
export type AddLinkPreviewOptionsType = Readonly<{
|
||||
conversationId?: string;
|
||||
disableFetch?: boolean;
|
||||
}>;
|
||||
|
||||
|
|
Loading…
Reference in a new issue