Eliminate resetEmojiResults, move onEditorStateChanged to redux
This commit is contained in:
parent
6d868030ae
commit
5c059c54d5
13 changed files with 162 additions and 155 deletions
|
@ -1506,7 +1506,7 @@ export async function startApp(): Promise<void> {
|
||||||
shiftKey &&
|
shiftKey &&
|
||||||
(key === 't' || key === 'T')
|
(key === 't' || key === 'T')
|
||||||
) {
|
) {
|
||||||
conversation.trigger('focus-composer');
|
window.reduxActions.composer.setComposerFocus(conversation.id);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -34,6 +34,7 @@ export default {
|
||||||
const useProps = (overrideProps: Partial<Props> = {}): Props => ({
|
const useProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||||
addAttachment: action('addAttachment'),
|
addAttachment: action('addAttachment'),
|
||||||
conversationId: '123',
|
conversationId: '123',
|
||||||
|
focusCounter: 0,
|
||||||
i18n,
|
i18n,
|
||||||
isDisabled: false,
|
isDisabled: false,
|
||||||
messageCompositionId: '456',
|
messageCompositionId: '456',
|
||||||
|
@ -41,6 +42,7 @@ const useProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||||
processAttachments: action('processAttachments'),
|
processAttachments: action('processAttachments'),
|
||||||
removeAttachment: action('removeAttachment'),
|
removeAttachment: action('removeAttachment'),
|
||||||
theme: React.useContext(StorybookThemeContext),
|
theme: React.useContext(StorybookThemeContext),
|
||||||
|
setComposerFocus: action('setComposerFocus'),
|
||||||
|
|
||||||
// AttachmentList
|
// AttachmentList
|
||||||
draftAttachments: overrideProps.draftAttachments || [],
|
draftAttachments: overrideProps.draftAttachments || [],
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
// Copyright 2019-2022 Signal Messenger, LLC
|
// Copyright 2019-2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import type { MutableRefObject } from 'react';
|
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { get } from 'lodash';
|
import { get } from 'lodash';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
@ -60,14 +59,6 @@ import { isImageTypeSupported } from '../util/GoogleChrome';
|
||||||
import * as KeyboardLayout from '../services/keyboardLayout';
|
import * as KeyboardLayout from '../services/keyboardLayout';
|
||||||
import { usePrevious } from '../hooks/usePrevious';
|
import { usePrevious } from '../hooks/usePrevious';
|
||||||
|
|
||||||
export type CompositionAPIType =
|
|
||||||
| {
|
|
||||||
focusInput: () => void;
|
|
||||||
isDirty: () => boolean;
|
|
||||||
resetEmojiResults: InputApi['resetEmojiResults'];
|
|
||||||
}
|
|
||||||
| undefined;
|
|
||||||
|
|
||||||
export type OwnProps = Readonly<{
|
export type OwnProps = Readonly<{
|
||||||
acceptedMessageRequest?: boolean;
|
acceptedMessageRequest?: boolean;
|
||||||
addAttachment: (
|
addAttachment: (
|
||||||
|
@ -83,12 +74,12 @@ export type OwnProps = Readonly<{
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
onSendAudioRecording?: (rec: InMemoryAttachmentDraftType) => unknown
|
onSendAudioRecording?: (rec: InMemoryAttachmentDraftType) => unknown
|
||||||
) => unknown;
|
) => unknown;
|
||||||
compositionApi?: MutableRefObject<CompositionAPIType>;
|
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
uuid?: string;
|
uuid?: string;
|
||||||
draftAttachments: ReadonlyArray<AttachmentDraftType>;
|
draftAttachments: ReadonlyArray<AttachmentDraftType>;
|
||||||
errorDialogAudioRecorderType?: ErrorDialogAudioRecorderType;
|
errorDialogAudioRecorderType?: ErrorDialogAudioRecorderType;
|
||||||
errorRecording: (e: ErrorDialogAudioRecorderType) => unknown;
|
errorRecording: (e: ErrorDialogAudioRecorderType) => unknown;
|
||||||
|
focusCounter: number;
|
||||||
groupAdmins: Array<ConversationType>;
|
groupAdmins: Array<ConversationType>;
|
||||||
groupVersion?: 1 | 2;
|
groupVersion?: 1 | 2;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
@ -133,6 +124,7 @@ export type OwnProps = Readonly<{
|
||||||
'i18n' | 'onClick' | 'onClose' | 'withContentAbove'
|
'i18n' | 'onClick' | 'onClose' | 'withContentAbove'
|
||||||
>;
|
>;
|
||||||
removeAttachment: (conversationId: string, filePath: string) => unknown;
|
removeAttachment: (conversationId: string, filePath: string) => unknown;
|
||||||
|
setComposerFocus: (conversationId: string) => unknown;
|
||||||
setQuotedMessage(message: undefined): unknown;
|
setQuotedMessage(message: undefined): unknown;
|
||||||
shouldSendHighQualityAttachments: boolean;
|
shouldSendHighQualityAttachments: boolean;
|
||||||
startRecording: () => unknown;
|
startRecording: () => unknown;
|
||||||
|
@ -177,14 +169,16 @@ export function CompositionArea({
|
||||||
// Base props
|
// Base props
|
||||||
addAttachment,
|
addAttachment,
|
||||||
conversationId,
|
conversationId,
|
||||||
|
focusCounter,
|
||||||
i18n,
|
i18n,
|
||||||
imageToBlurHash,
|
imageToBlurHash,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
isSignalConversation,
|
isSignalConversation,
|
||||||
|
messageCompositionId,
|
||||||
processAttachments,
|
processAttachments,
|
||||||
removeAttachment,
|
removeAttachment,
|
||||||
messageCompositionId,
|
|
||||||
sendMultiMediaMessage,
|
sendMultiMediaMessage,
|
||||||
|
setComposerFocus,
|
||||||
theme,
|
theme,
|
||||||
|
|
||||||
// AttachmentList
|
// AttachmentList
|
||||||
|
@ -209,7 +203,6 @@ export function CompositionArea({
|
||||||
onSelectMediaQuality,
|
onSelectMediaQuality,
|
||||||
shouldSendHighQualityAttachments,
|
shouldSendHighQualityAttachments,
|
||||||
// CompositionInput
|
// CompositionInput
|
||||||
compositionApi,
|
|
||||||
onEditorStateChange,
|
onEditorStateChange,
|
||||||
onTextTooLong,
|
onTextTooLong,
|
||||||
draftText,
|
draftText,
|
||||||
|
@ -315,11 +308,22 @@ export function CompositionArea({
|
||||||
const attachFileShortcut = useAttachFileShortcut(launchAttachmentPicker);
|
const attachFileShortcut = useAttachFileShortcut(launchAttachmentPicker);
|
||||||
useKeyboardShortcuts(attachFileShortcut);
|
useKeyboardShortcuts(attachFileShortcut);
|
||||||
|
|
||||||
const focusInput = useCallback(() => {
|
// Focus input on first mount
|
||||||
|
const previousFocusCounter = usePrevious<number | undefined>(
|
||||||
|
focusCounter,
|
||||||
|
focusCounter
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
if (inputApiRef.current) {
|
if (inputApiRef.current) {
|
||||||
inputApiRef.current.focus();
|
inputApiRef.current.focus();
|
||||||
}
|
}
|
||||||
}, [inputApiRef]);
|
});
|
||||||
|
// Focus input whenever explicitly requested
|
||||||
|
useEffect(() => {
|
||||||
|
if (focusCounter !== previousFocusCounter && inputApiRef.current) {
|
||||||
|
inputApiRef.current.focus();
|
||||||
|
}
|
||||||
|
}, [inputApiRef, focusCounter, previousFocusCounter]);
|
||||||
|
|
||||||
const withStickers =
|
const withStickers =
|
||||||
countStickers({
|
countStickers({
|
||||||
|
@ -329,20 +333,6 @@ export function CompositionArea({
|
||||||
receivedPacks,
|
receivedPacks,
|
||||||
}) > 0;
|
}) > 0;
|
||||||
|
|
||||||
if (compositionApi) {
|
|
||||||
// Using a React.MutableRefObject, so we need to reassign this prop.
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
compositionApi.current = {
|
|
||||||
isDirty: () => dirty,
|
|
||||||
focusInput,
|
|
||||||
resetEmojiResults: () => {
|
|
||||||
if (inputApiRef.current) {
|
|
||||||
inputApiRef.current.resetEmojiResults();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const previousMessageCompositionId = usePrevious(
|
const previousMessageCompositionId = usePrevious(
|
||||||
messageCompositionId,
|
messageCompositionId,
|
||||||
messageCompositionId
|
messageCompositionId
|
||||||
|
@ -382,7 +372,7 @@ export function CompositionArea({
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
doSend={handleForceSend}
|
doSend={handleForceSend}
|
||||||
onPickEmoji={insertEmoji}
|
onPickEmoji={insertEmoji}
|
||||||
onClose={focusInput}
|
onClose={() => setComposerFocus(conversationId)}
|
||||||
recentEmojis={recentEmojis}
|
recentEmojis={recentEmojis}
|
||||||
skinTone={skinTone}
|
skinTone={skinTone}
|
||||||
onSetSkinTone={onSetSkinTone}
|
onSetSkinTone={onSetSkinTone}
|
||||||
|
@ -706,6 +696,7 @@ export function CompositionArea({
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<CompositionInput
|
<CompositionInput
|
||||||
|
conversationId={conversationId}
|
||||||
clearQuotedMessage={clearQuotedMessage}
|
clearQuotedMessage={clearQuotedMessage}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
draftBodyRanges={draftBodyRanges}
|
draftBodyRanges={draftBodyRanges}
|
||||||
|
|
|
@ -62,12 +62,12 @@ export type InputApi = {
|
||||||
insertEmoji: (e: EmojiPickDataType) => void;
|
insertEmoji: (e: EmojiPickDataType) => void;
|
||||||
setText: (text: string, cursorToEnd?: boolean) => void;
|
setText: (text: string, cursorToEnd?: boolean) => void;
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
resetEmojiResults: () => void;
|
|
||||||
submit: () => void;
|
submit: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Props = Readonly<{
|
export type Props = Readonly<{
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
conversationId?: string;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
getPreferredBadge: PreferredBadgeSelectorType;
|
getPreferredBadge: PreferredBadgeSelectorType;
|
||||||
|
@ -83,6 +83,7 @@ export type Props = Readonly<{
|
||||||
scrollerRef?: React.RefObject<HTMLDivElement>;
|
scrollerRef?: React.RefObject<HTMLDivElement>;
|
||||||
onDirtyChange?(dirty: boolean): unknown;
|
onDirtyChange?(dirty: boolean): unknown;
|
||||||
onEditorStateChange?(
|
onEditorStateChange?(
|
||||||
|
conversationId: string | undefined,
|
||||||
messageText: string,
|
messageText: string,
|
||||||
bodyRanges: DraftBodyRangesType,
|
bodyRanges: DraftBodyRangesType,
|
||||||
caretLocation?: number
|
caretLocation?: number
|
||||||
|
@ -105,6 +106,7 @@ const BASE_CLASS_NAME = 'module-composition-input';
|
||||||
export function CompositionInput(props: Props): React.ReactElement {
|
export function CompositionInput(props: Props): React.ReactElement {
|
||||||
const {
|
const {
|
||||||
children,
|
children,
|
||||||
|
conversationId,
|
||||||
i18n,
|
i18n,
|
||||||
disabled,
|
disabled,
|
||||||
large,
|
large,
|
||||||
|
@ -246,16 +248,6 @@ export function CompositionInput(props: Props): React.ReactElement {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetEmojiResults = () => {
|
|
||||||
const emojiCompletion = emojiCompletionRef.current;
|
|
||||||
|
|
||||||
if (emojiCompletion === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
emojiCompletion.reset();
|
|
||||||
};
|
|
||||||
|
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
const timestamp = Date.now();
|
const timestamp = Date.now();
|
||||||
const quill = quillRef.current;
|
const quill = quillRef.current;
|
||||||
|
@ -286,7 +278,6 @@ export function CompositionInput(props: Props): React.ReactElement {
|
||||||
insertEmoji,
|
insertEmoji,
|
||||||
setText,
|
setText,
|
||||||
reset,
|
reset,
|
||||||
resetEmojiResults,
|
|
||||||
submit,
|
submit,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -446,6 +437,7 @@ export function CompositionInput(props: Props): React.ReactElement {
|
||||||
const selection = quill.getSelection();
|
const selection = quill.getSelection();
|
||||||
|
|
||||||
onEditorStateChange(
|
onEditorStateChange(
|
||||||
|
conversationId,
|
||||||
text,
|
text,
|
||||||
mentions,
|
mentions,
|
||||||
selection ? selection.index : undefined
|
selection ? selection.index : undefined
|
||||||
|
|
|
@ -87,6 +87,7 @@ export function CompositionTextArea({
|
||||||
|
|
||||||
const handleChange = React.useCallback(
|
const handleChange = React.useCallback(
|
||||||
(
|
(
|
||||||
|
_conversationId: string | undefined,
|
||||||
newValue: string,
|
newValue: string,
|
||||||
bodyRanges: DraftBodyRangesType,
|
bodyRanges: DraftBodyRangesType,
|
||||||
caretLocation?: number | undefined
|
caretLocation?: number | undefined
|
||||||
|
|
|
@ -56,6 +56,7 @@ export type DataPropsType = {
|
||||||
messageBody?: string;
|
messageBody?: string;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onEditorStateChange: (
|
onEditorStateChange: (
|
||||||
|
conversationId: string | undefined,
|
||||||
messageText: string,
|
messageText: string,
|
||||||
bodyRanges: DraftBodyRangesType,
|
bodyRanges: DraftBodyRangesType,
|
||||||
caretLocation?: number
|
caretLocation?: number
|
||||||
|
@ -332,7 +333,12 @@ export function ForwardMessageModal({
|
||||||
draftText={messageBodyText}
|
draftText={messageBodyText}
|
||||||
onChange={(messageText, bodyRanges, caretLocation?) => {
|
onChange={(messageText, bodyRanges, caretLocation?) => {
|
||||||
setMessageBodyText(messageText);
|
setMessageBodyText(messageText);
|
||||||
onEditorStateChange(messageText, bodyRanges, caretLocation);
|
onEditorStateChange(
|
||||||
|
undefined,
|
||||||
|
messageText,
|
||||||
|
bodyRanges,
|
||||||
|
caretLocation
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
onSubmit={forwardMessage}
|
onSubmit={forwardMessage}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
|
|
@ -245,7 +245,7 @@ export function StoryViewsNRepliesModal({
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
inputApi={inputApiRef}
|
inputApi={inputApiRef}
|
||||||
moduleClassName="StoryViewsNRepliesModal__input"
|
moduleClassName="StoryViewsNRepliesModal__input"
|
||||||
onEditorStateChange={messageText => {
|
onEditorStateChange={(_conversationId, messageText) => {
|
||||||
setMessageBodyText(messageText);
|
setMessageBodyText(messageText);
|
||||||
}}
|
}}
|
||||||
onPickEmoji={onUseEmoji}
|
onPickEmoji={onUseEmoji}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
|
import { debounce } from 'lodash';
|
||||||
import type { ThunkAction } from 'redux-thunk';
|
import type { ThunkAction } from 'redux-thunk';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
@ -40,8 +41,9 @@ import { blockSendUntilConversationsAreVerified } from '../../util/blockSendUnti
|
||||||
import { clearConversationDraftAttachments } from '../../util/clearConversationDraftAttachments';
|
import { clearConversationDraftAttachments } from '../../util/clearConversationDraftAttachments';
|
||||||
import { deleteDraftAttachment } from '../../util/deleteDraftAttachment';
|
import { deleteDraftAttachment } from '../../util/deleteDraftAttachment';
|
||||||
import {
|
import {
|
||||||
hasLinkPreviewLoaded,
|
|
||||||
getLinkPreviewForSend,
|
getLinkPreviewForSend,
|
||||||
|
hasLinkPreviewLoaded,
|
||||||
|
maybeGrabLinkPreview,
|
||||||
resetLinkPreview,
|
resetLinkPreview,
|
||||||
} from '../../services/LinkPreview';
|
} from '../../services/LinkPreview';
|
||||||
import { getMaximumAttachmentSize } from '../../util/attachments';
|
import { getMaximumAttachmentSize } from '../../util/attachments';
|
||||||
|
@ -64,6 +66,7 @@ import { writeDraftAttachment } from '../../util/writeDraftAttachment';
|
||||||
|
|
||||||
export type ComposerStateType = {
|
export type ComposerStateType = {
|
||||||
attachments: ReadonlyArray<AttachmentDraftType>;
|
attachments: ReadonlyArray<AttachmentDraftType>;
|
||||||
|
focusCounter: number;
|
||||||
isDisabled: boolean;
|
isDisabled: boolean;
|
||||||
linkPreviewLoading: boolean;
|
linkPreviewLoading: boolean;
|
||||||
linkPreviewResult?: LinkPreviewType;
|
linkPreviewResult?: LinkPreviewType;
|
||||||
|
@ -77,6 +80,7 @@ export type ComposerStateType = {
|
||||||
const ADD_PENDING_ATTACHMENT = 'composer/ADD_PENDING_ATTACHMENT';
|
const ADD_PENDING_ATTACHMENT = 'composer/ADD_PENDING_ATTACHMENT';
|
||||||
const REPLACE_ATTACHMENTS = 'composer/REPLACE_ATTACHMENTS';
|
const REPLACE_ATTACHMENTS = 'composer/REPLACE_ATTACHMENTS';
|
||||||
const RESET_COMPOSER = 'composer/RESET_COMPOSER';
|
const RESET_COMPOSER = 'composer/RESET_COMPOSER';
|
||||||
|
const SET_FOCUS = 'composer/SET_FOCUS';
|
||||||
const SET_HIGH_QUALITY_SETTING = 'composer/SET_HIGH_QUALITY_SETTING';
|
const SET_HIGH_QUALITY_SETTING = 'composer/SET_HIGH_QUALITY_SETTING';
|
||||||
const SET_QUOTED_MESSAGE = 'composer/SET_QUOTED_MESSAGE';
|
const SET_QUOTED_MESSAGE = 'composer/SET_QUOTED_MESSAGE';
|
||||||
const SET_COMPOSER_DISABLED = 'composer/SET_COMPOSER_DISABLED';
|
const SET_COMPOSER_DISABLED = 'composer/SET_COMPOSER_DISABLED';
|
||||||
|
@ -100,6 +104,10 @@ type SetComposerDisabledStateActionType = {
|
||||||
payload: boolean;
|
payload: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type SetFocusActionType = {
|
||||||
|
type: typeof SET_FOCUS;
|
||||||
|
};
|
||||||
|
|
||||||
type SetHighQualitySettingActionType = {
|
type SetHighQualitySettingActionType = {
|
||||||
type: typeof SET_HIGH_QUALITY_SETTING;
|
type: typeof SET_HIGH_QUALITY_SETTING;
|
||||||
payload: boolean;
|
payload: boolean;
|
||||||
|
@ -117,6 +125,7 @@ type ComposerActionType =
|
||||||
| ReplaceAttachmentsActionType
|
| ReplaceAttachmentsActionType
|
||||||
| ResetComposerActionType
|
| ResetComposerActionType
|
||||||
| SetComposerDisabledStateActionType
|
| SetComposerDisabledStateActionType
|
||||||
|
| SetFocusActionType
|
||||||
| SetHighQualitySettingActionType
|
| SetHighQualitySettingActionType
|
||||||
| SetQuotedMessageActionType;
|
| SetQuotedMessageActionType;
|
||||||
|
|
||||||
|
@ -125,13 +134,15 @@ type ComposerActionType =
|
||||||
export const actions = {
|
export const actions = {
|
||||||
addAttachment,
|
addAttachment,
|
||||||
addPendingAttachment,
|
addPendingAttachment,
|
||||||
|
onEditorStateChange,
|
||||||
processAttachments,
|
processAttachments,
|
||||||
removeAttachment,
|
removeAttachment,
|
||||||
replaceAttachments,
|
replaceAttachments,
|
||||||
resetComposer,
|
resetComposer,
|
||||||
setComposerDisabledState,
|
|
||||||
sendMultiMediaMessage,
|
sendMultiMediaMessage,
|
||||||
sendStickerMessage,
|
sendStickerMessage,
|
||||||
|
setComposerDisabledState,
|
||||||
|
setComposerFocus,
|
||||||
setMediaQualitySetting,
|
setMediaQualitySetting,
|
||||||
setQuotedMessage,
|
setQuotedMessage,
|
||||||
};
|
};
|
||||||
|
@ -421,6 +432,56 @@ function addPendingAttachment(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setComposerFocus(
|
||||||
|
conversationId: string
|
||||||
|
): ThunkAction<void, RootStateType, unknown, SetFocusActionType> {
|
||||||
|
return async (dispatch, getState) => {
|
||||||
|
if (getState().conversations.selectedConversationId !== conversationId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: SET_FOCUS,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEditorStateChange(
|
||||||
|
conversationId: string | undefined,
|
||||||
|
messageText: string,
|
||||||
|
bodyRanges: DraftBodyRangesType,
|
||||||
|
caretLocation?: number
|
||||||
|
): NoopActionType {
|
||||||
|
if (!conversationId) {
|
||||||
|
throw new Error(
|
||||||
|
'onEditorStateChange: Got falsey conversationId, needs local override'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const conversation = window.ConversationController.get(conversationId);
|
||||||
|
if (!conversation) {
|
||||||
|
throw new Error('processAttachments: Unable to find conversation');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messageText.length && conversation.throttledBumpTyping) {
|
||||||
|
conversation.throttledBumpTyping();
|
||||||
|
}
|
||||||
|
|
||||||
|
debouncedSaveDraft(conversationId, messageText, bodyRanges);
|
||||||
|
|
||||||
|
// If we have attachments, don't add link preview
|
||||||
|
if (!hasDraftAttachments(conversation.attributes, { includePending: true })) {
|
||||||
|
maybeGrabLinkPreview(messageText, LinkPreviewSourceType.Composer, {
|
||||||
|
caretLocation,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'NOOP',
|
||||||
|
payload: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function processAttachments({
|
function processAttachments({
|
||||||
conversationId,
|
conversationId,
|
||||||
files,
|
files,
|
||||||
|
@ -661,6 +722,51 @@ function resetComposer(): ResetComposerActionType {
|
||||||
type: RESET_COMPOSER,
|
type: RESET_COMPOSER,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
const debouncedSaveDraft = debounce(saveDraft);
|
||||||
|
|
||||||
|
function saveDraft(
|
||||||
|
conversationId: string,
|
||||||
|
messageText: string,
|
||||||
|
bodyRanges: DraftBodyRangesType
|
||||||
|
) {
|
||||||
|
const conversation = window.ConversationController.get(conversationId);
|
||||||
|
if (!conversation) {
|
||||||
|
throw new Error('saveDraft: Unable to find conversation');
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmed =
|
||||||
|
messageText && messageText.length > 0 ? messageText.trim() : '';
|
||||||
|
|
||||||
|
if (conversation.get('draft') && (!messageText || trimmed.length === 0)) {
|
||||||
|
conversation.set({
|
||||||
|
draft: null,
|
||||||
|
draftChanged: true,
|
||||||
|
draftBodyRanges: [],
|
||||||
|
});
|
||||||
|
window.Signal.Data.updateConversation(conversation.attributes);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messageText !== conversation.get('draft')) {
|
||||||
|
const now = Date.now();
|
||||||
|
let activeAt = conversation.get('active_at');
|
||||||
|
let timestamp = conversation.get('timestamp');
|
||||||
|
|
||||||
|
if (!activeAt) {
|
||||||
|
activeAt = now;
|
||||||
|
timestamp = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
conversation.set({
|
||||||
|
active_at: activeAt,
|
||||||
|
draft: messageText,
|
||||||
|
draftBodyRanges: bodyRanges,
|
||||||
|
draftChanged: true,
|
||||||
|
timestamp,
|
||||||
|
});
|
||||||
|
window.Signal.Data.updateConversation(conversation.attributes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function setComposerDisabledState(
|
function setComposerDisabledState(
|
||||||
value: boolean
|
value: boolean
|
||||||
|
@ -694,6 +800,7 @@ function setQuotedMessage(
|
||||||
export function getEmptyState(): ComposerStateType {
|
export function getEmptyState(): ComposerStateType {
|
||||||
return {
|
return {
|
||||||
attachments: [],
|
attachments: [],
|
||||||
|
focusCounter: 0,
|
||||||
isDisabled: false,
|
isDisabled: false,
|
||||||
linkPreviewLoading: false,
|
linkPreviewLoading: false,
|
||||||
messageCompositionId: UUID.generate().toString(),
|
messageCompositionId: UUID.generate().toString(),
|
||||||
|
@ -719,6 +826,13 @@ export function reducer(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action.type === SET_FOCUS) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
focusCounter: state.focusCounter + 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (action.type === SET_HIGH_QUALITY_SETTING) {
|
if (action.type === SET_HIGH_QUALITY_SETTING) {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|
|
@ -70,6 +70,7 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||||
|
|
||||||
const {
|
const {
|
||||||
attachments: draftAttachments,
|
attachments: draftAttachments,
|
||||||
|
focusCounter,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
linkPreviewLoading,
|
linkPreviewLoading,
|
||||||
linkPreviewResult,
|
linkPreviewResult,
|
||||||
|
@ -83,11 +84,13 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||||
return {
|
return {
|
||||||
// Base
|
// Base
|
||||||
conversationId: id,
|
conversationId: id,
|
||||||
|
focusCounter,
|
||||||
getPreferredBadge: getPreferredBadgeSelector(state),
|
getPreferredBadge: getPreferredBadgeSelector(state),
|
||||||
i18n: getIntl(state),
|
i18n: getIntl(state),
|
||||||
isDisabled,
|
isDisabled,
|
||||||
messageCompositionId,
|
messageCompositionId,
|
||||||
theme: getTheme(state),
|
theme: getTheme(state),
|
||||||
|
|
||||||
// AudioCapture
|
// AudioCapture
|
||||||
errorDialogAudioRecorderType:
|
errorDialogAudioRecorderType:
|
||||||
state.audioRecorder.errorDialogAudioRecorderType,
|
state.audioRecorder.errorDialogAudioRecorderType,
|
||||||
|
|
|
@ -18,7 +18,6 @@ export type PropsType = {
|
||||||
compositionAreaProps: Pick<
|
compositionAreaProps: Pick<
|
||||||
CompositionAreaPropsType,
|
CompositionAreaPropsType,
|
||||||
| 'clearQuotedMessage'
|
| 'clearQuotedMessage'
|
||||||
| 'compositionApi'
|
|
||||||
| 'getQuotedMessage'
|
| 'getQuotedMessage'
|
||||||
| 'handleClickQuotedMessage'
|
| 'handleClickQuotedMessage'
|
||||||
| 'id'
|
| 'id'
|
||||||
|
|
|
@ -121,6 +121,7 @@ export function SmartForwardMessageModal(): JSX.Element | null {
|
||||||
messageBody={cleanedBody}
|
messageBody={cleanedBody}
|
||||||
onClose={closeModal}
|
onClose={closeModal}
|
||||||
onEditorStateChange={(
|
onEditorStateChange={(
|
||||||
|
_conversationId: string | undefined,
|
||||||
messageText: string,
|
messageText: string,
|
||||||
_: DraftBodyRangesType,
|
_: DraftBodyRangesType,
|
||||||
caretLocation?: number
|
caretLocation?: number
|
||||||
|
|
|
@ -107,6 +107,7 @@ describe('both/state/ducks/composer', () => {
|
||||||
const nextState = reducer(
|
const nextState = reducer(
|
||||||
{
|
{
|
||||||
attachments: [],
|
attachments: [],
|
||||||
|
focusCounter: 0,
|
||||||
isDisabled: false,
|
isDisabled: false,
|
||||||
linkPreviewLoading: true,
|
linkPreviewLoading: true,
|
||||||
messageCompositionId: emptyState.messageCompositionId,
|
messageCompositionId: emptyState.messageCompositionId,
|
||||||
|
|
|
@ -6,13 +6,12 @@
|
||||||
import type * as Backbone from 'backbone';
|
import type * as Backbone from 'backbone';
|
||||||
import type { ComponentProps } from 'react';
|
import type { ComponentProps } from 'react';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { debounce, flatten } from 'lodash';
|
import { flatten } from 'lodash';
|
||||||
import { render } from 'mustache';
|
import { render } from 'mustache';
|
||||||
|
|
||||||
import type { AttachmentType } from '../types/Attachment';
|
import type { AttachmentType } from '../types/Attachment';
|
||||||
import { isGIF } from '../types/Attachment';
|
import { isGIF } from '../types/Attachment';
|
||||||
import * as Stickers from '../types/Stickers';
|
import * as Stickers from '../types/Stickers';
|
||||||
import type { DraftBodyRangesType } from '../types/Util';
|
|
||||||
import type { MIMEType } from '../types/MIME';
|
import type { MIMEType } from '../types/MIME';
|
||||||
import type { ConversationModel } from '../models/conversations';
|
import type { ConversationModel } from '../models/conversations';
|
||||||
import type { MessageAttributesType } from '../model-types.d';
|
import type { MessageAttributesType } from '../model-types.d';
|
||||||
|
@ -43,7 +42,6 @@ import { ConversationDetailsMembershipList } from '../components/conversation/co
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
import type { EmbeddedContactType } from '../types/EmbeddedContact';
|
import type { EmbeddedContactType } from '../types/EmbeddedContact';
|
||||||
import { createConversationView } from '../state/roots/createConversationView';
|
import { createConversationView } from '../state/roots/createConversationView';
|
||||||
import type { CompositionAPIType } from '../components/CompositionArea';
|
|
||||||
import { ToastConversationArchived } from '../components/ToastConversationArchived';
|
import { ToastConversationArchived } from '../components/ToastConversationArchived';
|
||||||
import { ToastConversationMarkedUnread } from '../components/ToastConversationMarkedUnread';
|
import { ToastConversationMarkedUnread } from '../components/ToastConversationMarkedUnread';
|
||||||
import { ToastConversationUnarchived } from '../components/ToastConversationUnarchived';
|
import { ToastConversationUnarchived } from '../components/ToastConversationUnarchived';
|
||||||
|
@ -67,16 +65,15 @@ import { ContactDetail } from '../components/conversation/ContactDetail';
|
||||||
import { MediaGallery } from '../components/conversation/media-gallery/MediaGallery';
|
import { MediaGallery } from '../components/conversation/media-gallery/MediaGallery';
|
||||||
import type { ItemClickEvent } from '../components/conversation/media-gallery/types/ItemClickEvent';
|
import type { ItemClickEvent } from '../components/conversation/media-gallery/types/ItemClickEvent';
|
||||||
import {
|
import {
|
||||||
maybeGrabLinkPreview,
|
|
||||||
removeLinkPreview,
|
removeLinkPreview,
|
||||||
suspendLinkPreviews,
|
suspendLinkPreviews,
|
||||||
} from '../services/LinkPreview';
|
} from '../services/LinkPreview';
|
||||||
import { LinkPreviewSourceType } from '../types/LinkPreview';
|
|
||||||
import { closeLightbox, showLightbox } from '../util/showLightbox';
|
import { closeLightbox, showLightbox } from '../util/showLightbox';
|
||||||
import { saveAttachment } from '../util/saveAttachment';
|
import { saveAttachment } from '../util/saveAttachment';
|
||||||
import { SECOND } from '../util/durations';
|
import { SECOND } from '../util/durations';
|
||||||
import { startConversation } from '../util/startConversation';
|
import { startConversation } from '../util/startConversation';
|
||||||
import { longRunningTaskWrapper } from '../util/longRunningTaskWrapper';
|
import { longRunningTaskWrapper } from '../util/longRunningTaskWrapper';
|
||||||
|
import { hasDraftAttachments } from '../util/hasDraftAttachments';
|
||||||
|
|
||||||
type AttachmentOptions = {
|
type AttachmentOptions = {
|
||||||
messageId: string;
|
messageId: string;
|
||||||
|
@ -160,16 +157,6 @@ type MediaType = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export class ConversationView extends window.Backbone.View<ConversationModel> {
|
export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
private debouncedSaveDraft: (
|
|
||||||
messageText: string,
|
|
||||||
bodyRanges: DraftBodyRangesType
|
|
||||||
) => Promise<void>;
|
|
||||||
|
|
||||||
// Composing messages
|
|
||||||
private compositionApi: {
|
|
||||||
current: CompositionAPIType;
|
|
||||||
} = { current: undefined };
|
|
||||||
|
|
||||||
// Sub-views
|
// Sub-views
|
||||||
private contactModalView?: Backbone.View;
|
private contactModalView?: Backbone.View;
|
||||||
private conversationView?: Backbone.View;
|
private conversationView?: Backbone.View;
|
||||||
|
@ -184,8 +171,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
constructor(...args: Array<any>) {
|
constructor(...args: Array<any>) {
|
||||||
super(...args);
|
super(...args);
|
||||||
|
|
||||||
this.debouncedSaveDraft = debounce(this.saveDraft.bind(this), 200);
|
|
||||||
|
|
||||||
// Events on Conversation model
|
// Events on Conversation model
|
||||||
this.listenTo(this.model, 'destroy', this.stopListening);
|
this.listenTo(this.model, 'destroy', this.stopListening);
|
||||||
|
|
||||||
|
@ -197,7 +182,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
);
|
);
|
||||||
|
|
||||||
// These are triggered by background.ts for keyboard handling
|
// These are triggered by background.ts for keyboard handling
|
||||||
this.listenTo(this.model, 'focus-composer', this.focusMessageField);
|
|
||||||
this.listenTo(this.model, 'open-all-media', this.showAllMedia);
|
this.listenTo(this.model, 'open-all-media', this.showAllMedia);
|
||||||
this.listenTo(this.model, 'escape-pressed', this.resetPanel);
|
this.listenTo(this.model, 'escape-pressed', this.resetPanel);
|
||||||
this.listenTo(this.model, 'show-message-details', this.showMessageDetail);
|
this.listenTo(this.model, 'show-message-details', this.showMessageDetail);
|
||||||
|
@ -416,13 +400,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
|
|
||||||
const compositionAreaProps = {
|
const compositionAreaProps = {
|
||||||
id: this.model.id,
|
id: this.model.id,
|
||||||
compositionApi: this.compositionApi,
|
|
||||||
onClickAddPack: () => this.showStickerManager(),
|
onClickAddPack: () => this.showStickerManager(),
|
||||||
onEditorStateChange: (
|
|
||||||
msg: string,
|
|
||||||
bodyRanges: DraftBodyRangesType,
|
|
||||||
caretLocation?: number
|
|
||||||
) => this.onEditorStateChange(msg, bodyRanges, caretLocation),
|
|
||||||
onTextTooLong: () => showToast(ToastMessageBodyTooLong),
|
onTextTooLong: () => showToast(ToastMessageBodyTooLong),
|
||||||
getQuotedMessage: () => this.model.get('quotedMessageId'),
|
getQuotedMessage: () => this.model.get('quotedMessageId'),
|
||||||
clearQuotedMessage: () => this.setQuoteMessage(undefined),
|
clearQuotedMessage: () => this.setQuoteMessage(undefined),
|
||||||
|
@ -736,7 +714,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
|
|
||||||
loadAndUpdate();
|
loadAndUpdate();
|
||||||
|
|
||||||
this.focusMessageField();
|
window.reduxActions.composer.setComposerFocus(this.model.id);
|
||||||
|
|
||||||
const quotedMessageId = this.model.get('quotedMessageId');
|
const quotedMessageId = this.model.get('quotedMessageId');
|
||||||
if (quotedMessageId) {
|
if (quotedMessageId) {
|
||||||
|
@ -1619,78 +1597,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
focusMessageField(): void {
|
|
||||||
if (this.panels && this.panels.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.compositionApi.current?.focusInput();
|
|
||||||
}
|
|
||||||
|
|
||||||
resetEmojiResults(): void {
|
|
||||||
this.compositionApi.current?.resetEmojiResults();
|
|
||||||
}
|
|
||||||
|
|
||||||
onEditorStateChange(
|
|
||||||
messageText: string,
|
|
||||||
bodyRanges: DraftBodyRangesType,
|
|
||||||
caretLocation?: number
|
|
||||||
): void {
|
|
||||||
if (messageText.length && this.model.throttledBumpTyping) {
|
|
||||||
this.model.throttledBumpTyping();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.debouncedSaveDraft(messageText, bodyRanges);
|
|
||||||
|
|
||||||
// If we have attachments, don't add link preview
|
|
||||||
if (!this.hasFiles({ includePending: true })) {
|
|
||||||
maybeGrabLinkPreview(messageText, LinkPreviewSourceType.Composer, {
|
|
||||||
caretLocation,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async saveDraft(
|
|
||||||
messageText: string,
|
|
||||||
bodyRanges: DraftBodyRangesType
|
|
||||||
): Promise<void> {
|
|
||||||
const { model }: { model: ConversationModel } = this;
|
|
||||||
|
|
||||||
const trimmed =
|
|
||||||
messageText && messageText.length > 0 ? messageText.trim() : '';
|
|
||||||
|
|
||||||
if (model.get('draft') && (!messageText || trimmed.length === 0)) {
|
|
||||||
this.model.set({
|
|
||||||
draft: null,
|
|
||||||
draftChanged: true,
|
|
||||||
draftBodyRanges: [],
|
|
||||||
});
|
|
||||||
await this.saveModel();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (messageText !== model.get('draft')) {
|
|
||||||
const now = Date.now();
|
|
||||||
let active_at = this.model.get('active_at');
|
|
||||||
let timestamp = this.model.get('timestamp');
|
|
||||||
|
|
||||||
if (!active_at) {
|
|
||||||
active_at = now;
|
|
||||||
timestamp = now;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.model.set({
|
|
||||||
active_at,
|
|
||||||
draft: messageText,
|
|
||||||
draftBodyRanges: bodyRanges,
|
|
||||||
draftChanged: true,
|
|
||||||
timestamp,
|
|
||||||
});
|
|
||||||
await this.saveModel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async setQuoteMessage(messageId: string | undefined): Promise<void> {
|
async setQuoteMessage(messageId: string | undefined): Promise<void> {
|
||||||
const { model } = this;
|
const { model } = this;
|
||||||
const message = messageId ? await getMessageById(messageId) : undefined;
|
const message = messageId ? await getMessageById(messageId) : undefined;
|
||||||
|
@ -1738,8 +1644,8 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
quote,
|
quote,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.reduxActions.composer.setComposerFocus(this.model.id);
|
||||||
window.reduxActions.composer.setComposerDisabledState(false);
|
window.reduxActions.composer.setComposerDisabledState(false);
|
||||||
this.focusMessageField();
|
|
||||||
} else {
|
} else {
|
||||||
window.reduxActions.composer.setQuotedMessage(undefined);
|
window.reduxActions.composer.setQuotedMessage(undefined);
|
||||||
}
|
}
|
||||||
|
@ -1763,22 +1669,13 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
hasFiles(options: { includePending: boolean }): boolean {
|
|
||||||
const draftAttachments = this.model.get('draftAttachments') || [];
|
|
||||||
if (options.includePending) {
|
|
||||||
return draftAttachments.length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return draftAttachments.some(item => !item.pending);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateAttachmentsView(): void {
|
updateAttachmentsView(): void {
|
||||||
const draftAttachments = this.model.get('draftAttachments') || [];
|
const draftAttachments = this.model.get('draftAttachments') || [];
|
||||||
window.reduxActions.composer.replaceAttachments(
|
window.reduxActions.composer.replaceAttachments(
|
||||||
this.model.get('id'),
|
this.model.get('id'),
|
||||||
draftAttachments
|
draftAttachments
|
||||||
);
|
);
|
||||||
if (this.hasFiles({ includePending: true })) {
|
if (hasDraftAttachments(this.model.attributes, { includePending: true })) {
|
||||||
removeLinkPreview();
|
removeLinkPreview();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue