Eliminate resetEmojiResults, move onEditorStateChanged to redux

This commit is contained in:
Scott Nonnenberg 2022-12-08 15:56:17 -08:00 committed by GitHub
parent 6d868030ae
commit 5c059c54d5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 162 additions and 155 deletions

View file

@ -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;

View file

@ -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 || [],

View file

@ -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}

View file

@ -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

View file

@ -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

View file

@ -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}

View file

@ -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}

View file

@ -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,

View file

@ -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,

View file

@ -18,7 +18,6 @@ export type PropsType = {
compositionAreaProps: Pick< compositionAreaProps: Pick<
CompositionAreaPropsType, CompositionAreaPropsType,
| 'clearQuotedMessage' | 'clearQuotedMessage'
| 'compositionApi'
| 'getQuotedMessage' | 'getQuotedMessage'
| 'handleClickQuotedMessage' | 'handleClickQuotedMessage'
| 'id' | 'id'

View file

@ -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

View file

@ -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,

View file

@ -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();
} }
} }