add basic draft support (text only)

Relates #1791
pull/1833/head
audric 4 years ago
parent b6fcd59ec4
commit e2c26e9819

@ -27,7 +27,10 @@ import { SessionQuotedMessageComposition } from './SessionQuotedMessageCompositi
import { Mention, MentionsInput } from 'react-mentions'; import { Mention, MentionsInput } from 'react-mentions';
import { CaptionEditor } from '../../CaptionEditor'; import { CaptionEditor } from '../../CaptionEditor';
import { getConversationController } from '../../../session/conversations'; import { getConversationController } from '../../../session/conversations';
import { ReduxConversationType } from '../../../state/ducks/conversations'; import {
ReduxConversationType,
updateDraftForConversation,
} from '../../../state/ducks/conversations';
import { SessionMemberListItem } from '../SessionMemberListItem'; import { SessionMemberListItem } from '../SessionMemberListItem';
import autoBind from 'auto-bind'; import autoBind from 'auto-bind';
import { SessionSettingCategory } from '../settings/SessionSettings'; import { SessionSettingCategory } from '../settings/SessionSettings';
@ -44,6 +47,7 @@ import {
hasLinkPreviewPopupBeenDisplayed, hasLinkPreviewPopupBeenDisplayed,
} from '../../../data/data'; } from '../../../data/data';
import { import {
getDraftForCurrentConversation,
getMentionsInput, getMentionsInput,
getQuotedMessage, getQuotedMessage,
getSelectedConversation, getSelectedConversation,
@ -77,6 +81,7 @@ export interface StagedAttachmentType extends AttachmentType {
interface Props { interface Props {
sendMessage: any; sendMessage: any;
draft: string;
onLoadVoiceNoteView: any; onLoadVoiceNoteView: any;
onExitVoiceNoteView: any; onExitVoiceNoteView: any;
@ -90,7 +95,6 @@ interface Props {
} }
interface State { interface State {
message: string;
showRecordingView: boolean; showRecordingView: boolean;
showEmojiPanel: boolean; showEmojiPanel: boolean;
@ -393,7 +397,7 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
private renderTextArea() { private renderTextArea() {
const { i18n } = window; const { i18n } = window;
const { message } = this.state; const { draft } = this.props;
if (!this.props.selectedConversation) { if (!this.props.selectedConversation) {
return null; return null;
@ -414,7 +418,7 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
return ( return (
<MentionsInput <MentionsInput
value={message} value={draft}
onChange={this.onChange} onChange={this.onChange}
onKeyDown={this.onKeyDown} onKeyDown={this.onKeyDown}
onKeyUp={this.onKeyUp} onKeyUp={this.onKeyUp}
@ -545,7 +549,7 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
return <></>; return <></>;
} }
// we try to match the first link found in the current message // 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]) { if (!links || links.length === 0 || ignoredLink === links[0]) {
return <></>; return <></>;
} }
@ -766,18 +770,18 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
} }
private async onKeyUp(event: any) { private async onKeyUp(event: any) {
const { message } = this.state; const { draft } = this.props;
// Called whenever the user changes the message composition field. But only // Called whenever the user changes the message composition field. But only
// fires if there's content in the message field after the change. // 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 // Also, check for a message length change before firing it up, to avoid
// catching ESC, tab, or whatever which is not typing // 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); const conversationModel = getConversationController().get(this.props.selectedConversationKey);
if (!conversationModel) { if (!conversationModel) {
return; return;
} }
conversationModel.throttledBumpTyping(); conversationModel.throttledBumpTyping();
this.lastBumpTypingMessageLength = message.length; this.lastBumpTypingMessageLength = draft.length;
} }
} }
@ -809,7 +813,7 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
return replacedMentions; return replacedMentions;
}; };
const messagePlaintext = cleanMentions(this.parseEmojis(this.state.message)); const messagePlaintext = cleanMentions(this.parseEmojis(this.props.draft));
const { selectedConversation } = this.props; const { selectedConversation } = this.props;
@ -876,11 +880,16 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
// Empty composition box and stagedAttachments // Empty composition box and stagedAttachments
this.setState({ this.setState({
message: '',
showEmojiPanel: false, showEmojiPanel: false,
stagedLinkPreview: undefined, stagedLinkPreview: undefined,
ignoredLink: undefined, ignoredLink: undefined,
}); });
window.inboxStore?.dispatch(
updateDraftForConversation({
conversationKey: this.props.selectedConversationKey,
draft: '',
})
);
} catch (e) { } catch (e) {
// Message sending failed // Message sending failed
window?.log?.error(e); window?.log?.error(e);
@ -959,9 +968,13 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
} }
private onChange(event: any) { private onChange(event: any) {
const message = event.target.value ?? ''; const draft = event.target.value ?? '';
window.inboxStore?.dispatch(
this.setState({ message }); updateDraftForConversation({
conversationKey: this.props.selectedConversationKey,
draft,
})
);
} }
private getSelectionBasedOnMentions(index: number) { private getSelectionBasedOnMentions(index: number) {
@ -969,7 +982,7 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
// this is kind of a pain as the mentions box has two inputs, one with the real text, and one with the extracted mentions // 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 ᅲ...ᅭ // 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 lastMatchStartIndex = 0;
let lastMatchEndIndex = 0; let lastMatchEndIndex = 0;
@ -983,7 +996,7 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
const displayNameEnd = match.lastIndexOf('\uFFD2'); const displayNameEnd = match.lastIndexOf('\uFFD2');
const displayName = match.substring(displayNameStart, displayNameEnd); const displayName = match.substring(displayNameStart, displayNameEnd);
const currentMatchStartIndex = this.state.message.indexOf(match) + lastMatchStartIndex; const currentMatchStartIndex = this.props.draft.indexOf(match) + lastMatchStartIndex;
lastMatchStartIndex = currentMatchStartIndex; lastMatchStartIndex = currentMatchStartIndex;
lastMatchEndIndex = currentMatchStartIndex + match.length; lastMatchEndIndex = currentMatchStartIndex + match.length;
@ -1027,18 +1040,23 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
return; return;
} }
const { message } = this.state; const { draft } = this.props;
const currentSelectionStart = Number(messageBox.selectionStart); const currentSelectionStart = Number(messageBox.selectionStart);
const realSelectionStart = this.getSelectionBasedOnMentions(currentSelectionStart); const realSelectionStart = this.getSelectionBasedOnMentions(currentSelectionStart);
const before = message.slice(0, realSelectionStart); const before = draft.slice(0, realSelectionStart);
const end = message.slice(realSelectionStart); const end = draft.slice(realSelectionStart);
const newMessage = `${before}${colons}${end}`; const newMessage = `${before}${colons}${end}`;
window.inboxStore?.dispatch(
updateDraftForConversation({
conversationKey: this.props.selectedConversationKey,
draft: newMessage,
})
);
this.setState({ message: newMessage }, () => {
// update our selection because updating text programmatically // update our selection because updating text programmatically
// will put the selection at the end of the textarea // will put the selection at the end of the textarea
const selectionStart = currentSelectionStart + Number(colons.length); const selectionStart = currentSelectionStart + Number(colons.length);
@ -1050,7 +1068,6 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
messageBox.selectionStart = selectionStart; messageBox.selectionStart = selectionStart;
messageBox.selectionEnd = selectionStart; messageBox.selectionEnd = selectionStart;
}, 20); }, 20);
});
} }
private focusCompositionBox() { private focusCompositionBox() {
@ -1068,6 +1085,7 @@ const mapStateToProps = (state: StateType) => {
quotedMessageProps: getQuotedMessage(state), quotedMessageProps: getQuotedMessage(state),
selectedConversation: getSelectedConversation(state), selectedConversation: getSelectedConversation(state),
selectedConversationKey: getSelectedConversationKey(state), selectedConversationKey: getSelectedConversationKey(state),
draft: getDraftForCurrentConversation(state),
theme: getTheme(state), theme: getTheme(state),
}; };
}; };

@ -269,6 +269,7 @@ export type ConversationsStateType = {
animateQuotedMessageId?: string; animateQuotedMessageId?: string;
nextMessageToPlayId?: string; nextMessageToPlayId?: string;
mentionMembers: MentionsMembersType; mentionMembers: MentionsMembersType;
draftsForConversations: Array<{ conversationKey: string; draft: string }>;
}; };
export type MentionsMembersType = Array<{ export type MentionsMembersType = Array<{
@ -355,6 +356,7 @@ export function getEmptyConversationState(): ConversationsStateType {
mentionMembers: [], mentionMembers: [],
firstUnreadMessageId: undefined, firstUnreadMessageId: undefined,
haveDoneFirstScroll: false, haveDoneFirstScroll: false,
draftsForConversations: new Array(),
}; };
} }
@ -686,6 +688,7 @@ const conversationsSlice = createSlice({
firstUnreadMessageId: action.payload.firstUnreadIdOnOpen, firstUnreadMessageId: action.payload.firstUnreadIdOnOpen,
haveDoneFirstScroll: false, haveDoneFirstScroll: false,
draftsForConversations: state.draftsForConversations,
}; };
}, },
updateHaveDoneFirstScroll(state: ConversationsStateType) { updateHaveDoneFirstScroll(state: ConversationsStateType) {
@ -728,10 +731,24 @@ const conversationsSlice = createSlice({
state: ConversationsStateType, state: ConversationsStateType,
action: PayloadAction<MentionsMembersType> action: PayloadAction<MentionsMembersType>
) { ) {
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; state.mentionMembers = action.payload;
return state; 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) => { extraReducers: (builder: any) => {
// Add reducers for additional action types here, and handle loading state as needed // Add reducers for additional action types here, and handle loading state as needed
@ -791,6 +808,7 @@ export const {
quotedMessageToAnimate, quotedMessageToAnimate,
setNextMessageToPlayId, setNextMessageToPlayId,
updateMentionsMembers, updateMentionsMembers,
updateDraftForConversation,
} = actions; } = actions;
export async function openConversationWithMessages(args: { export async function openConversationWithMessages(args: {

@ -21,7 +21,6 @@ import {
} from '../../components/conversation/ConversationHeader'; } from '../../components/conversation/ConversationHeader';
import { LightBoxOptions } from '../../components/session/conversation/SessionConversation'; import { LightBoxOptions } from '../../components/session/conversation/SessionConversation';
import { ReplyingToMessageProps } from '../../components/session/conversation/SessionCompositionBox'; import { ReplyingToMessageProps } from '../../components/session/conversation/SessionCompositionBox';
import { createSlice } from '@reduxjs/toolkit';
import { getConversationController } from '../../session/conversations'; import { getConversationController } from '../../session/conversations';
export const getConversations = (state: StateType): ConversationsStateType => state.conversations; export const getConversations = (state: StateType): ConversationsStateType => state.conversations;
@ -367,6 +366,19 @@ export const getMentionsInput = createSelector(
(state: ConversationsStateType): MentionsMembersType => state.mentionMembers (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. /// Those calls are just related to ordering messages in the redux store.
function updateFirstMessageOfSeries( function updateFirstMessageOfSeries(

Loading…
Cancel
Save