Skip to content

Commit

Permalink
add basic draft support (text only)
Browse files Browse the repository at this point in the history
Relates oxen-io#1791
  • Loading branch information
Bilb committed Aug 9, 2021
1 parent b6fcd59 commit e2c26e9
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 31 deletions.
76 changes: 47 additions & 29 deletions ts/components/session/conversation/SessionCompositionBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -44,6 +47,7 @@ import {
hasLinkPreviewPopupBeenDisplayed,
} from '../../../data/data';
import {
getDraftForCurrentConversation,
getMentionsInput,
getQuotedMessage,
getSelectedConversation,
Expand Down Expand Up @@ -77,6 +81,7 @@ export interface StagedAttachmentType extends AttachmentType {

interface Props {
sendMessage: any;
draft: string;

onLoadVoiceNoteView: any;
onExitVoiceNoteView: any;
Expand All @@ -90,7 +95,6 @@ interface Props {
}

interface State {
message: string;
showRecordingView: boolean;

showEmojiPanel: boolean;
Expand Down Expand Up @@ -393,7 +397,7 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {

private renderTextArea() {
const { i18n } = window;
const { message } = this.state;
const { draft } = this.props;

if (!this.props.selectedConversation) {
return null;
Expand All @@ -414,7 +418,7 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {

return (
<MentionsInput
value={message}
value={draft}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
onKeyUp={this.onKeyUp}
Expand Down Expand Up @@ -545,7 +549,7 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
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 <></>;
}
Expand Down Expand Up @@ -766,18 +770,18 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
}

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

Expand Down Expand Up @@ -809,7 +813,7 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
return replacedMentions;
};

const messagePlaintext = cleanMentions(this.parseEmojis(this.state.message));
const messagePlaintext = cleanMentions(this.parseEmojis(this.props.draft));

const { selectedConversation } = this.props;

Expand Down Expand Up @@ -876,11 +880,16 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {

// 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);
Expand Down Expand Up @@ -959,17 +968,21 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
}

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) {
// we have to get the real selectionStart/end of an index in the mentions box.
// 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;
Expand All @@ -983,7 +996,7 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
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;

Expand Down Expand Up @@ -1027,30 +1040,34 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
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() {
Expand All @@ -1068,6 +1085,7 @@ const mapStateToProps = (state: StateType) => {
quotedMessageProps: getQuotedMessage(state),
selectedConversation: getSelectedConversation(state),
selectedConversationKey: getSelectedConversationKey(state),
draft: getDraftForCurrentConversation(state),
theme: getTheme(state),
};
};
Expand Down
20 changes: 19 additions & 1 deletion ts/state/ducks/conversations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ export type ConversationsStateType = {
animateQuotedMessageId?: string;
nextMessageToPlayId?: string;
mentionMembers: MentionsMembersType;
draftsForConversations: Array<{ conversationKey: string; draft: string }>;
};

export type MentionsMembersType = Array<{
Expand Down Expand Up @@ -355,6 +356,7 @@ export function getEmptyConversationState(): ConversationsStateType {
mentionMembers: [],
firstUnreadMessageId: undefined,
haveDoneFirstScroll: false,
draftsForConversations: new Array(),
};
}

Expand Down Expand Up @@ -686,6 +688,7 @@ const conversationsSlice = createSlice({
firstUnreadMessageId: action.payload.firstUnreadIdOnOpen,

haveDoneFirstScroll: false,
draftsForConversations: state.draftsForConversations,
};
},
updateHaveDoneFirstScroll(state: ConversationsStateType) {
Expand Down Expand Up @@ -728,10 +731,24 @@ const conversationsSlice = createSlice({
state: ConversationsStateType,
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;
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
Expand Down Expand Up @@ -791,6 +808,7 @@ export const {
quotedMessageToAnimate,
setNextMessageToPlayId,
updateMentionsMembers,
updateDraftForConversation,
} = actions;

export async function openConversationWithMessages(args: {
Expand Down
14 changes: 13 additions & 1 deletion ts/state/selectors/conversations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand Down

0 comments on commit e2c26e9

Please sign in to comment.