From e2c26e9819b7452d06e39c99ddcba93ff605959d Mon Sep 17 00:00:00 2001 From: audric Date: Mon, 9 Aug 2021 12:17:57 +1000 Subject: [PATCH] add basic draft support (text only) Relates #1791 --- .../conversation/SessionCompositionBox.tsx | 76 ++++++++++++------- ts/state/ducks/conversations.ts | 20 ++++- ts/state/selectors/conversations.ts | 14 +++- 3 files changed, 79 insertions(+), 31 deletions(-) diff --git a/ts/components/session/conversation/SessionCompositionBox.tsx b/ts/components/session/conversation/SessionCompositionBox.tsx index 4465b2208..f1f679ce2 100644 --- a/ts/components/session/conversation/SessionCompositionBox.tsx +++ b/ts/components/session/conversation/SessionCompositionBox.tsx @@ -27,7 +27,10 @@ import { SessionQuotedMessageComposition } from './SessionQuotedMessageCompositi import { Mention, MentionsInput } from 'react-mentions'; import { CaptionEditor } from '../../CaptionEditor'; import { getConversationController } from '../../../session/conversations'; -import { ReduxConversationType } from '../../../state/ducks/conversations'; +import { + ReduxConversationType, + updateDraftForConversation, +} from '../../../state/ducks/conversations'; import { SessionMemberListItem } from '../SessionMemberListItem'; import autoBind from 'auto-bind'; import { SessionSettingCategory } from '../settings/SessionSettings'; @@ -44,6 +47,7 @@ import { hasLinkPreviewPopupBeenDisplayed, } from '../../../data/data'; import { + getDraftForCurrentConversation, getMentionsInput, getQuotedMessage, getSelectedConversation, @@ -77,6 +81,7 @@ export interface StagedAttachmentType extends AttachmentType { interface Props { sendMessage: any; + draft: string; onLoadVoiceNoteView: any; onExitVoiceNoteView: any; @@ -90,7 +95,6 @@ interface Props { } interface State { - message: string; showRecordingView: boolean; showEmojiPanel: boolean; @@ -393,7 +397,7 @@ class SessionCompositionBoxInner extends React.Component { private renderTextArea() { const { i18n } = window; - const { message } = this.state; + const { draft } = this.props; if (!this.props.selectedConversation) { return null; @@ -414,7 +418,7 @@ class SessionCompositionBoxInner extends React.Component { return ( { return <>; } // we try to match the first link found in the current message - const links = window.Signal.LinkPreviews.findLinks(this.state.message, undefined); + const links = window.Signal.LinkPreviews.findLinks(this.props.draft, undefined); if (!links || links.length === 0 || ignoredLink === links[0]) { return <>; } @@ -766,18 +770,18 @@ class SessionCompositionBoxInner extends React.Component { } private async onKeyUp(event: any) { - const { message } = this.state; + const { draft } = this.props; // Called whenever the user changes the message composition field. But only // fires if there's content in the message field after the change. // Also, check for a message length change before firing it up, to avoid // catching ESC, tab, or whatever which is not typing - if (message.length && message.length !== this.lastBumpTypingMessageLength) { + if (draft.length && draft.length !== this.lastBumpTypingMessageLength) { const conversationModel = getConversationController().get(this.props.selectedConversationKey); if (!conversationModel) { return; } conversationModel.throttledBumpTyping(); - this.lastBumpTypingMessageLength = message.length; + this.lastBumpTypingMessageLength = draft.length; } } @@ -809,7 +813,7 @@ class SessionCompositionBoxInner extends React.Component { return replacedMentions; }; - const messagePlaintext = cleanMentions(this.parseEmojis(this.state.message)); + const messagePlaintext = cleanMentions(this.parseEmojis(this.props.draft)); const { selectedConversation } = this.props; @@ -876,11 +880,16 @@ class SessionCompositionBoxInner extends React.Component { // Empty composition box and stagedAttachments this.setState({ - message: '', showEmojiPanel: false, stagedLinkPreview: undefined, ignoredLink: undefined, }); + window.inboxStore?.dispatch( + updateDraftForConversation({ + conversationKey: this.props.selectedConversationKey, + draft: '', + }) + ); } catch (e) { // Message sending failed window?.log?.error(e); @@ -959,9 +968,13 @@ class SessionCompositionBoxInner extends React.Component { } private onChange(event: any) { - const message = event.target.value ?? ''; - - this.setState({ message }); + const draft = event.target.value ?? ''; + window.inboxStore?.dispatch( + updateDraftForConversation({ + conversationKey: this.props.selectedConversationKey, + draft, + }) + ); } private getSelectionBasedOnMentions(index: number) { @@ -969,7 +982,7 @@ class SessionCompositionBoxInner extends React.Component { // this is kind of a pain as the mentions box has two inputs, one with the real text, and one with the extracted mentions // the index shown to the user is actually just the visible part of the mentions (so the part between ᅲ...ᅭ - const matches = this.state.message.match(this.mentionsRegex); + const matches = this.props.draft.match(this.mentionsRegex); let lastMatchStartIndex = 0; let lastMatchEndIndex = 0; @@ -983,7 +996,7 @@ class SessionCompositionBoxInner extends React.Component { const displayNameEnd = match.lastIndexOf('\uFFD2'); const displayName = match.substring(displayNameStart, displayNameEnd); - const currentMatchStartIndex = this.state.message.indexOf(match) + lastMatchStartIndex; + const currentMatchStartIndex = this.props.draft.indexOf(match) + lastMatchStartIndex; lastMatchStartIndex = currentMatchStartIndex; lastMatchEndIndex = currentMatchStartIndex + match.length; @@ -1027,30 +1040,34 @@ class SessionCompositionBoxInner extends React.Component { return; } - const { message } = this.state; + const { draft } = this.props; const currentSelectionStart = Number(messageBox.selectionStart); const realSelectionStart = this.getSelectionBasedOnMentions(currentSelectionStart); - const before = message.slice(0, realSelectionStart); - const end = message.slice(realSelectionStart); + const before = draft.slice(0, realSelectionStart); + const end = draft.slice(realSelectionStart); const newMessage = `${before}${colons}${end}`; + window.inboxStore?.dispatch( + updateDraftForConversation({ + conversationKey: this.props.selectedConversationKey, + draft: newMessage, + }) + ); + + // update our selection because updating text programmatically + // will put the selection at the end of the textarea + const selectionStart = currentSelectionStart + Number(colons.length); + messageBox.selectionStart = selectionStart; + messageBox.selectionEnd = selectionStart; - this.setState({ message: newMessage }, () => { - // update our selection because updating text programmatically - // will put the selection at the end of the textarea - const selectionStart = currentSelectionStart + Number(colons.length); + // Sometimes, we have to repeat the set of the selection position with a timeout to be effective + setTimeout(() => { messageBox.selectionStart = selectionStart; messageBox.selectionEnd = selectionStart; - - // Sometimes, we have to repeat the set of the selection position with a timeout to be effective - setTimeout(() => { - messageBox.selectionStart = selectionStart; - messageBox.selectionEnd = selectionStart; - }, 20); - }); + }, 20); } private focusCompositionBox() { @@ -1068,6 +1085,7 @@ const mapStateToProps = (state: StateType) => { quotedMessageProps: getQuotedMessage(state), selectedConversation: getSelectedConversation(state), selectedConversationKey: getSelectedConversationKey(state), + draft: getDraftForCurrentConversation(state), theme: getTheme(state), }; }; diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index ac13fe817..07ac9238d 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -269,6 +269,7 @@ export type ConversationsStateType = { animateQuotedMessageId?: string; nextMessageToPlayId?: string; mentionMembers: MentionsMembersType; + draftsForConversations: Array<{ conversationKey: string; draft: string }>; }; export type MentionsMembersType = Array<{ @@ -355,6 +356,7 @@ export function getEmptyConversationState(): ConversationsStateType { mentionMembers: [], firstUnreadMessageId: undefined, haveDoneFirstScroll: false, + draftsForConversations: new Array(), }; } @@ -686,6 +688,7 @@ const conversationsSlice = createSlice({ firstUnreadMessageId: action.payload.firstUnreadIdOnOpen, haveDoneFirstScroll: false, + draftsForConversations: state.draftsForConversations, }; }, updateHaveDoneFirstScroll(state: ConversationsStateType) { @@ -728,10 +731,24 @@ const conversationsSlice = createSlice({ state: ConversationsStateType, action: PayloadAction ) { - window?.log?.warn('updating mentions input members length', action.payload?.length); + window?.log?.info('updating mentions input members length', action.payload?.length); state.mentionMembers = action.payload; return state; }, + updateDraftForConversation( + state: ConversationsStateType, + action: PayloadAction<{ conversationKey: string; draft: string }> + ) { + window?.log?.info('updating draft for conversation'); + const { conversationKey, draft } = action.payload; + const foundAtIndex = state.draftsForConversations.findIndex( + c => c.conversationKey === conversationKey + ); + foundAtIndex === -1 + ? state.draftsForConversations.push({ conversationKey, draft }) + : (state.draftsForConversations[foundAtIndex] = action.payload); + return state; + }, }, extraReducers: (builder: any) => { // Add reducers for additional action types here, and handle loading state as needed @@ -791,6 +808,7 @@ export const { quotedMessageToAnimate, setNextMessageToPlayId, updateMentionsMembers, + updateDraftForConversation, } = actions; export async function openConversationWithMessages(args: { diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index 93c8e3da1..c38ee8140 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -21,7 +21,6 @@ import { } from '../../components/conversation/ConversationHeader'; import { LightBoxOptions } from '../../components/session/conversation/SessionConversation'; import { ReplyingToMessageProps } from '../../components/session/conversation/SessionCompositionBox'; -import { createSlice } from '@reduxjs/toolkit'; import { getConversationController } from '../../session/conversations'; export const getConversations = (state: StateType): ConversationsStateType => state.conversations; @@ -367,6 +366,19 @@ export const getMentionsInput = createSelector( (state: ConversationsStateType): MentionsMembersType => state.mentionMembers ); +export const getDraftForCurrentConversation = createSelector( + getConversations, + (state: ConversationsStateType): string => { + if (state.selectedConversation) { + return ( + state.draftsForConversations.find(c => c.conversationKey === state.selectedConversation) + ?.draft || '' + ); + } + return ''; + } +); + /// Those calls are just related to ordering messages in the redux store. function updateFirstMessageOfSeries(