Speedup body update composition box (#1911)
* disable sending on enter while composing Fixes #1899 #1497 * ask confirmation before deleting account * move drafts outside of redux to speedup body message writing
This commit is contained in:
parent
a1f5706fea
commit
25453ee807
|
@ -428,6 +428,9 @@
|
|||
"dialogClearAllDataDeletionQuestion": "Would you like to clear only this device, or delete your entire account?",
|
||||
"deviceOnly": "Device Only",
|
||||
"entireAccount": "Entire Account",
|
||||
"areYouSureDeleteDeviceOnly": "Are you sure you want to delete your device data only?",
|
||||
"areYouSureDeleteEntireAccount": "Are you sure you want to delete your entire account, including the network data?",
|
||||
"iAmSure": "I am sure",
|
||||
"recoveryPhraseSecureTitle": "You're almost finished!",
|
||||
"recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.",
|
||||
"recoveryPhraseRevealButtonText": "Reveal Recovery Phrase",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useCallback, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { ed25519Str } from '../../session/onions/onionPath';
|
||||
import { forceNetworkDeletion } from '../../session/snode_api/SNodeAPI';
|
||||
import { forceSyncConfigurationNowIfNeeded } from '../../session/utils/syncUtils';
|
||||
|
@ -127,31 +128,54 @@ async function deleteEverythingAndNetworkData() {
|
|||
|
||||
export const DeleteAccountModal = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onDeleteEverythingLocallyOnly = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
window.log.warn('Deleting everything excluding network data');
|
||||
const onDeleteEverythingLocallyOnly = () => {
|
||||
dispatch(
|
||||
updateConfirmModal({
|
||||
message: window.i18n('areYouSureDeleteDeviceOnly'),
|
||||
okText: window.i18n('iAmSure'),
|
||||
okTheme: SessionButtonColor.Danger,
|
||||
onClickOk: async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
window.log.warn('Deleting everything on device but keeping network data');
|
||||
|
||||
await sendConfigMessageAndDeleteEverything();
|
||||
} catch (e) {
|
||||
window.log.warn(e);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
window.inboxStore?.dispatch(updateConfirmModal(null));
|
||||
await sendConfigMessageAndDeleteEverything();
|
||||
} catch (e) {
|
||||
window.log.warn(e);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
onClickClose: () => {
|
||||
window.inboxStore?.dispatch(updateConfirmModal(null));
|
||||
},
|
||||
})
|
||||
);
|
||||
};
|
||||
const onDeleteEverythingAndNetworkData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
window.log.warn('Deleting everything including network data');
|
||||
await deleteEverythingAndNetworkData();
|
||||
} catch (e) {
|
||||
window.log.warn(e);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
const onDeleteEverythingAndNetworkData = () => {
|
||||
dispatch(
|
||||
updateConfirmModal({
|
||||
message: window.i18n('areYouSureDeleteEntireAccount'),
|
||||
okText: window.i18n('iAmSure'),
|
||||
okTheme: SessionButtonColor.Danger,
|
||||
onClickOk: async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
window.log.warn('Deleting everything including network data');
|
||||
await deleteEverythingAndNetworkData();
|
||||
} catch (e) {
|
||||
window.log.warn(e);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
onClickClose: () => {
|
||||
window.inboxStore?.dispatch(updateConfirmModal(null));
|
||||
},
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -49,7 +49,6 @@ export const ModalContainer = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
{confirmModalState && <SessionConfirm {...confirmModalState} />}
|
||||
{inviteModalState && <InviteContactsDialog {...inviteModalState} />}
|
||||
{addModeratorsModalState && <AddModeratorsDialog {...addModeratorsModalState} />}
|
||||
{removeModeratorsModalState && <RemoveModeratorsDialog {...removeModeratorsModalState} />}
|
||||
|
@ -67,6 +66,7 @@ export const ModalContainer = () => {
|
|||
)}
|
||||
{sessionPasswordModalState && <SessionPasswordDialog {...sessionPasswordModalState} />}
|
||||
{deleteAccountModalState && <DeleteAccountModal {...deleteAccountModalState} />}
|
||||
{confirmModalState && <SessionConfirm {...confirmModalState} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -25,10 +25,7 @@ import { SessionQuotedMessageComposition } from './SessionQuotedMessageCompositi
|
|||
import { Mention, MentionsInput } from 'react-mentions';
|
||||
import { CaptionEditor } from '../../CaptionEditor';
|
||||
import { getConversationController } from '../../../session/conversations';
|
||||
import {
|
||||
ReduxConversationType,
|
||||
updateDraftForConversation,
|
||||
} from '../../../state/ducks/conversations';
|
||||
import { ReduxConversationType } from '../../../state/ducks/conversations';
|
||||
import { SessionMemberListItem } from '../SessionMemberListItem';
|
||||
import autoBind from 'auto-bind';
|
||||
import { SessionSettingCategory } from '../settings/SessionSettings';
|
||||
|
@ -45,7 +42,6 @@ import {
|
|||
hasLinkPreviewPopupBeenDisplayed,
|
||||
} from '../../../data/data';
|
||||
import {
|
||||
getDraftForCurrentConversation,
|
||||
getMentionsInput,
|
||||
getQuotedMessage,
|
||||
getSelectedConversation,
|
||||
|
@ -142,10 +138,17 @@ const SendMessageButton = (props: { onClick: () => void }) => {
|
|||
);
|
||||
};
|
||||
|
||||
// keep this draft state local to not have to do a redux state update (a bit slow with our large state for soem computers)
|
||||
const draftsForConversations: Array<{ conversationKey: string; draft: string }> = new Array();
|
||||
function updateDraftForConversation(action: { conversationKey: string; draft: string }) {
|
||||
const { conversationKey, draft } = action;
|
||||
const foundAtIndex = draftsForConversations.findIndex(c => c.conversationKey === conversationKey);
|
||||
foundAtIndex === -1
|
||||
? draftsForConversations.push({ conversationKey, draft })
|
||||
: (draftsForConversations[foundAtIndex] = action);
|
||||
}
|
||||
interface Props {
|
||||
sendMessage: (msg: SendMessageType) => void;
|
||||
draft: string;
|
||||
|
||||
onLoadVoiceNoteView: any;
|
||||
onExitVoiceNoteView: any;
|
||||
selectedConversationKey: string;
|
||||
|
@ -157,7 +160,7 @@ interface Props {
|
|||
|
||||
interface State {
|
||||
showRecordingView: boolean;
|
||||
|
||||
draft: string;
|
||||
showEmojiPanel: boolean;
|
||||
voiceRecording?: Blob;
|
||||
ignoredLink?: string; // set the the ignored url when users closed the link preview
|
||||
|
@ -185,10 +188,11 @@ const sendMessageStyle = {
|
|||
minHeight: '24px',
|
||||
width: '100%',
|
||||
};
|
||||
|
||||
const getDefaultState = () => {
|
||||
const getDefaultState = (newConvoId?: string) => {
|
||||
return {
|
||||
message: '',
|
||||
draft:
|
||||
(newConvoId && draftsForConversations.find(c => c.conversationKey === newConvoId)?.draft) ||
|
||||
'',
|
||||
voiceRecording: undefined,
|
||||
showRecordingView: false,
|
||||
showEmojiPanel: false,
|
||||
|
@ -238,7 +242,7 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
|
|||
public componentDidUpdate(prevProps: Props, _prevState: State) {
|
||||
// reset the state on new conversation key
|
||||
if (prevProps.selectedConversationKey !== this.props.selectedConversationKey) {
|
||||
this.setState(getDefaultState(), this.focusCompositionBox);
|
||||
this.setState(getDefaultState(this.props.selectedConversationKey), this.focusCompositionBox);
|
||||
this.lastBumpTypingMessageLength = 0;
|
||||
} else if (this.props.stagedAttachments?.length !== prevProps.stagedAttachments?.length) {
|
||||
// if number of staged attachment changed, focus the composition box for a more natural UI
|
||||
|
@ -433,7 +437,7 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
|
|||
|
||||
private renderTextArea() {
|
||||
const { i18n } = window;
|
||||
const { draft } = this.props;
|
||||
const { draft } = this.state;
|
||||
|
||||
if (!this.props.selectedConversation) {
|
||||
return null;
|
||||
|
@ -585,7 +589,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.props.draft, undefined);
|
||||
const links = window.Signal.LinkPreviews.findLinks(this.state.draft, undefined);
|
||||
if (!links || links.length === 0 || ignoredLink === links[0]) {
|
||||
if (this.state.stagedLinkPreview) {
|
||||
this.setState({
|
||||
|
@ -809,12 +813,12 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
private async onKeyUp() {
|
||||
const { draft } = this.props;
|
||||
const { draft } = this.state;
|
||||
// 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 (draft.length && draft.length !== this.lastBumpTypingMessageLength) {
|
||||
if (draft && draft.length && draft.length !== this.lastBumpTypingMessageLength) {
|
||||
const conversationModel = getConversationController().get(this.props.selectedConversationKey);
|
||||
if (!conversationModel) {
|
||||
return;
|
||||
|
@ -852,7 +856,7 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
|
|||
return replacedMentions;
|
||||
};
|
||||
|
||||
const messagePlaintext = cleanMentions(this.parseEmojis(this.props.draft));
|
||||
const messagePlaintext = cleanMentions(this.parseEmojis(this.state.draft));
|
||||
|
||||
const { selectedConversation } = this.props;
|
||||
|
||||
|
@ -924,13 +928,12 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
|
|||
showEmojiPanel: false,
|
||||
stagedLinkPreview: undefined,
|
||||
ignoredLink: undefined,
|
||||
draft: '',
|
||||
});
|
||||
updateDraftForConversation({
|
||||
conversationKey: this.props.selectedConversationKey,
|
||||
draft: '',
|
||||
});
|
||||
window.inboxStore?.dispatch(
|
||||
updateDraftForConversation({
|
||||
conversationKey: this.props.selectedConversationKey,
|
||||
draft: '',
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
// Message sending failed
|
||||
window?.log?.error(e);
|
||||
|
@ -1022,12 +1025,8 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
|
|||
|
||||
private onChange(event: any) {
|
||||
const draft = event.target.value ?? '';
|
||||
window.inboxStore?.dispatch(
|
||||
updateDraftForConversation({
|
||||
conversationKey: this.props.selectedConversationKey,
|
||||
draft,
|
||||
})
|
||||
);
|
||||
this.setState({ draft });
|
||||
updateDraftForConversation({ conversationKey: this.props.selectedConversationKey, draft });
|
||||
}
|
||||
|
||||
private getSelectionBasedOnMentions(index: number) {
|
||||
|
@ -1035,7 +1034,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
|
||||
|
||||
// the index shown to the user is actually just the visible part of the mentions (so the part between ᅲ...ᅭ
|
||||
const matches = this.props.draft.match(this.mentionsRegex);
|
||||
const matches = this.state.draft.match(this.mentionsRegex);
|
||||
|
||||
let lastMatchStartIndex = 0;
|
||||
let lastMatchEndIndex = 0;
|
||||
|
@ -1049,7 +1048,7 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
|
|||
const displayNameEnd = match.lastIndexOf('\uFFD2');
|
||||
const displayName = match.substring(displayNameStart, displayNameEnd);
|
||||
|
||||
const currentMatchStartIndex = this.props.draft.indexOf(match) + lastMatchStartIndex;
|
||||
const currentMatchStartIndex = this.state.draft.indexOf(match) + lastMatchStartIndex;
|
||||
lastMatchStartIndex = currentMatchStartIndex;
|
||||
lastMatchEndIndex = currentMatchStartIndex + match.length;
|
||||
|
||||
|
@ -1093,7 +1092,7 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
|
|||
return;
|
||||
}
|
||||
|
||||
const { draft } = this.props;
|
||||
const { draft } = this.state;
|
||||
|
||||
const currentSelectionStart = Number(messageBox.selectionStart);
|
||||
|
||||
|
@ -1103,12 +1102,11 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
|
|||
const end = draft.slice(realSelectionStart);
|
||||
|
||||
const newMessage = `${before}${colons}${end}`;
|
||||
window.inboxStore?.dispatch(
|
||||
updateDraftForConversation({
|
||||
conversationKey: this.props.selectedConversationKey,
|
||||
draft: newMessage,
|
||||
})
|
||||
);
|
||||
this.setState({ draft: newMessage });
|
||||
updateDraftForConversation({
|
||||
conversationKey: this.props.selectedConversationKey,
|
||||
draft: newMessage,
|
||||
});
|
||||
|
||||
// update our selection because updating text programmatically
|
||||
// will put the selection at the end of the textarea
|
||||
|
@ -1138,7 +1136,6 @@ const mapStateToProps = (state: StateType) => {
|
|||
quotedMessageProps: getQuotedMessage(state),
|
||||
selectedConversation: getSelectedConversation(state),
|
||||
selectedConversationKey: getSelectedConversationKey(state),
|
||||
draft: getDraftForCurrentConversation(state),
|
||||
theme: getTheme(state),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -266,7 +266,6 @@ export type ConversationsStateType = {
|
|||
animateQuotedMessageId?: string;
|
||||
nextMessageToPlayId?: string;
|
||||
mentionMembers: MentionsMembersType;
|
||||
draftsForConversations: Array<{ conversationKey: string; draft: string }>;
|
||||
};
|
||||
|
||||
export type MentionsMembersType = Array<{
|
||||
|
@ -356,7 +355,6 @@ export function getEmptyConversationState(): ConversationsStateType {
|
|||
mentionMembers: [],
|
||||
firstUnreadMessageId: undefined,
|
||||
haveDoneFirstScroll: false,
|
||||
draftsForConversations: new Array(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -698,7 +696,6 @@ const conversationsSlice = createSlice({
|
|||
firstUnreadMessageId: action.payload.firstUnreadIdOnOpen,
|
||||
|
||||
haveDoneFirstScroll: false,
|
||||
draftsForConversations: state.draftsForConversations,
|
||||
};
|
||||
},
|
||||
updateHaveDoneFirstScroll(state: ConversationsStateType) {
|
||||
|
@ -745,19 +742,6 @@ const conversationsSlice = createSlice({
|
|||
state.mentionMembers = action.payload;
|
||||
return state;
|
||||
},
|
||||
updateDraftForConversation(
|
||||
state: ConversationsStateType,
|
||||
action: PayloadAction<{ conversationKey: string; draft: string }>
|
||||
) {
|
||||
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
|
||||
|
@ -817,7 +801,6 @@ export const {
|
|||
quotedMessageToAnimate,
|
||||
setNextMessageToPlayId,
|
||||
updateMentionsMembers,
|
||||
updateDraftForConversation,
|
||||
} = actions;
|
||||
|
||||
export async function openConversationWithMessages(args: {
|
||||
|
|
|
@ -507,19 +507,6 @@ 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(
|
||||
|
|
Loading…
Reference in New Issue