Use sentCounter in CompositionInput to drop old draft updates

This commit is contained in:
Scott Nonnenberg 2023-04-05 15:06:16 -07:00 committed by GitHub
parent 0e606c45b0
commit 4a18667ddf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 121 additions and 63 deletions

View file

@ -35,6 +35,7 @@ const useProps = (overrideProps: Partial<Props> = {}): Props => ({
addAttachment: action('addAttachment'), addAttachment: action('addAttachment'),
conversationId: '123', conversationId: '123',
focusCounter: 0, focusCounter: 0,
sendCounter: 0,
i18n, i18n,
isDisabled: false, isDisabled: false,
messageCompositionId: '456', messageCompositionId: '456',

View file

@ -162,14 +162,15 @@ export type OwnProps = Readonly<{
export type Props = Pick< export type Props = Pick<
CompositionInputProps, CompositionInputProps,
| 'sortedGroupMembers' | 'clearQuotedMessage'
| 'onEditorStateChange'
| 'onTextTooLong'
| 'draftText' | 'draftText'
| 'draftBodyRanges' | 'draftBodyRanges'
| 'clearQuotedMessage'
| 'getPreferredBadge' | 'getPreferredBadge'
| 'getQuotedMessage' | 'getQuotedMessage'
| 'onEditorStateChange'
| 'onTextTooLong'
| 'sendCounter'
| 'sortedGroupMembers'
> & > &
Pick< Pick<
EmojiButtonProps, EmojiButtonProps,
@ -231,13 +232,14 @@ export function CompositionArea({
setMediaQualitySetting, setMediaQualitySetting,
shouldSendHighQualityAttachments, shouldSendHighQualityAttachments,
// CompositionInput // CompositionInput
onEditorStateChange, clearQuotedMessage,
onTextTooLong,
draftText, draftText,
draftBodyRanges, draftBodyRanges,
clearQuotedMessage,
getPreferredBadge, getPreferredBadge,
getQuotedMessage, getQuotedMessage,
onEditorStateChange,
onTextTooLong,
sendCounter,
sortedGroupMembers, sortedGroupMembers,
// EmojiButton // EmojiButton
onPickEmoji, onPickEmoji,
@ -805,6 +807,7 @@ export function CompositionArea({
onPickEmoji={onPickEmoji} onPickEmoji={onPickEmoji}
onSubmit={handleSubmit} onSubmit={handleSubmit}
onTextTooLong={onTextTooLong} onTextTooLong={onTextTooLong}
sendCounter={sendCounter}
skinTone={skinTone} skinTone={skinTone}
sortedGroupMembers={sortedGroupMembers} sortedGroupMembers={sortedGroupMembers}
theme={theme} theme={theme}

View file

@ -33,6 +33,7 @@ const useProps = (overrideProps: Partial<Props> = {}): Props => ({
getQuotedMessage: action('getQuotedMessage'), getQuotedMessage: action('getQuotedMessage'),
onPickEmoji: action('onPickEmoji'), onPickEmoji: action('onPickEmoji'),
large: boolean('large', overrideProps.large || false), large: boolean('large', overrideProps.large || false),
sendCounter: 0,
sortedGroupMembers: overrideProps.sortedGroupMembers || [], sortedGroupMembers: overrideProps.sortedGroupMembers || [],
skinTone: select( skinTone: select(
'skinTone', 'skinTone',

View file

@ -79,6 +79,7 @@ export type Props = Readonly<{
getPreferredBadge: PreferredBadgeSelectorType; getPreferredBadge: PreferredBadgeSelectorType;
large?: boolean; large?: boolean;
inputApi?: React.MutableRefObject<InputApi | undefined>; inputApi?: React.MutableRefObject<InputApi | undefined>;
sendCounter: number;
skinTone?: EmojiPickDataType['skinTone']; skinTone?: EmojiPickDataType['skinTone'];
draftText?: string; draftText?: string;
draftBodyRanges?: DraftBodyRangesType; draftBodyRanges?: DraftBodyRangesType;
@ -88,12 +89,13 @@ export type Props = Readonly<{
sortedGroupMembers?: ReadonlyArray<ConversationType>; sortedGroupMembers?: ReadonlyArray<ConversationType>;
scrollerRef?: React.RefObject<HTMLDivElement>; scrollerRef?: React.RefObject<HTMLDivElement>;
onDirtyChange?(dirty: boolean): unknown; onDirtyChange?(dirty: boolean): unknown;
onEditorStateChange?( onEditorStateChange?(options: {
conversationId: string | undefined, bodyRanges: DraftBodyRangesType;
messageText: string, caretLocation?: number;
bodyRanges: DraftBodyRangesType, conversationId: string | undefined;
caretLocation?: number messageText: string;
): unknown; sendCounter: number;
}): unknown;
onTextTooLong(): unknown; onTextTooLong(): unknown;
onPickEmoji(o: EmojiPickDataType): unknown; onPickEmoji(o: EmojiPickDataType): unknown;
onSubmit( onSubmit(
@ -134,6 +136,7 @@ export function CompositionInput(props: Props): React.ReactElement {
onSubmit, onSubmit,
placeholder, placeholder,
skinTone, skinTone,
sendCounter,
sortedGroupMembers, sortedGroupMembers,
theme, theme,
} = props; } = props;
@ -458,12 +461,13 @@ export function CompositionInput(props: Props): React.ReactElement {
setTimeout(() => { setTimeout(() => {
const selection = quill.getSelection(); const selection = quill.getSelection();
onEditorStateChange( onEditorStateChange({
bodyRanges: mentions,
caretLocation: selection ? selection.index : undefined,
conversationId, conversationId,
text, messageText: text,
mentions, sendCounter,
selection ? selection.index : undefined });
);
}, 0); }, 0);
} }
} }

View file

@ -86,12 +86,7 @@ export function CompositionTextArea({
}, [inputApiRef]); }, [inputApiRef]);
const handleChange = React.useCallback( const handleChange = React.useCallback(
( ({ bodyRanges, caretLocation, messageText: newValue }) => {
_conversationId: string | undefined,
newValue: string,
bodyRanges: DraftBodyRangesType,
caretLocation?: number | undefined
) => {
const inputEl = inputApiRef.current; const inputEl = inputApiRef.current;
if (!inputEl) { if (!inputEl) {
return; return;
@ -124,21 +119,22 @@ export function CompositionTextArea({
return ( return (
<div className="CompositionTextArea"> <div className="CompositionTextArea">
<CompositionInput <CompositionInput
placeholder={placeholder}
clearQuotedMessage={shouldNeverBeCalled} clearQuotedMessage={shouldNeverBeCalled}
scrollerRef={scrollerRef} draftText={draftText}
getPreferredBadge={getPreferredBadge} getPreferredBadge={getPreferredBadge}
getQuotedMessage={noop} getQuotedMessage={noop}
i18n={i18n} i18n={i18n}
inputApi={inputApiRef} inputApi={inputApiRef}
large large
moduleClassName="CompositionTextArea__input" moduleClassName="CompositionTextArea__input"
onScroll={onScroll}
onEditorStateChange={handleChange} onEditorStateChange={handleChange}
onPickEmoji={onPickEmoji} onPickEmoji={onPickEmoji}
onScroll={onScroll}
onSubmit={onSubmit} onSubmit={onSubmit}
onTextTooLong={onTextTooLong} onTextTooLong={onTextTooLong}
draftText={draftText} placeholder={placeholder}
scrollerRef={scrollerRef}
sendCounter={0}
theme={theme} theme={theme}
/> />
<div className="CompositionTextArea__emoji"> <div className="CompositionTextArea__emoji">

View file

@ -225,7 +225,7 @@ export function StoryViewsNRepliesModal({
i18n={i18n} i18n={i18n}
inputApi={inputApiRef} inputApi={inputApiRef}
moduleClassName="StoryViewsNRepliesModal__input" moduleClassName="StoryViewsNRepliesModal__input"
onEditorStateChange={(_conversationId, messageText) => { onEditorStateChange={({ messageText }) => {
setMessageBodyText(messageText); setMessageBodyText(messageText);
}} }}
onPickEmoji={onUseEmoji} onPickEmoji={onUseEmoji}
@ -242,6 +242,7 @@ export function StoryViewsNRepliesModal({
firstName: authorTitle, firstName: authorTitle,
}) })
} }
sendCounter={0}
sortedGroupMembers={sortedGroupMembers} sortedGroupMembers={sortedGroupMembers}
theme={ThemeType.dark} theme={ThemeType.dark}
> >

View file

@ -100,6 +100,7 @@ type ComposerStateByConversationType = {
linkPreviewResult?: LinkPreviewType; linkPreviewResult?: LinkPreviewType;
messageCompositionId: UUIDStringType; messageCompositionId: UUIDStringType;
quotedMessage?: Pick<MessageAttributesType, 'conversationId' | 'quote'>; quotedMessage?: Pick<MessageAttributesType, 'conversationId' | 'quote'>;
sendCounter: number;
shouldSendHighQualityAttachments?: boolean; shouldSendHighQualityAttachments?: boolean;
}; };
@ -121,6 +122,7 @@ function getEmptyComposerState(): ComposerStateByConversationType {
isDisabled: false, isDisabled: false,
linkPreviewLoading: false, linkPreviewLoading: false,
messageCompositionId: UUID.generate().toString(), messageCompositionId: UUID.generate().toString(),
sendCounter: 0,
}; };
} }
@ -134,6 +136,7 @@ export function getComposerStateForConversation(
// Actions // Actions
const ADD_PENDING_ATTACHMENT = 'composer/ADD_PENDING_ATTACHMENT'; const ADD_PENDING_ATTACHMENT = 'composer/ADD_PENDING_ATTACHMENT';
const INCREMENT_SEND_COUNTER = 'composer/INCREMENT_SEND_COUNTER';
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_FOCUS = 'composer/SET_FOCUS';
@ -149,6 +152,13 @@ type AddPendingAttachmentActionType = ReadonlyDeep<{
}; };
}>; }>;
export type IncrementSendActionType = ReadonlyDeep<{
type: typeof INCREMENT_SEND_COUNTER;
payload: {
conversationId: string;
};
}>;
// eslint-disable-next-line local-rules/type-alias-readonlydeep // eslint-disable-next-line local-rules/type-alias-readonlydeep
export type ReplaceAttachmentsActionType = { export type ReplaceAttachmentsActionType = {
type: typeof REPLACE_ATTACHMENTS; type: typeof REPLACE_ATTACHMENTS;
@ -202,6 +212,7 @@ type ComposerActionType =
| AddLinkPreviewActionType | AddLinkPreviewActionType
| AddPendingAttachmentActionType | AddPendingAttachmentActionType
| ConversationUnloadedActionType | ConversationUnloadedActionType
| IncrementSendActionType
| RemoveLinkPreviewActionType | RemoveLinkPreviewActionType
| ReplaceAttachmentsActionType | ReplaceAttachmentsActionType
| ResetComposerActionType | ResetComposerActionType
@ -217,6 +228,7 @@ export const actions = {
addAttachment, addAttachment,
addPendingAttachment, addPendingAttachment,
cancelJoinRequest, cancelJoinRequest,
incrementSendCounter,
onClearAttachments, onClearAttachments,
onCloseLinkPreview, onCloseLinkPreview,
onEditorStateChange, onEditorStateChange,
@ -236,6 +248,15 @@ export const actions = {
setQuotedMessage, setQuotedMessage,
}; };
function incrementSendCounter(conversationId: string): IncrementSendActionType {
return {
type: INCREMENT_SEND_COUNTER,
payload: {
conversationId,
},
};
}
export const useComposerActions = (): BoundActionCreatorsMapObject< export const useComposerActions = (): BoundActionCreatorsMapObject<
typeof actions typeof actions
> => useBoundActions(actions); > => useBoundActions(actions);
@ -370,6 +391,7 @@ function sendMultiMediaMessage(
void, void,
RootStateType, RootStateType,
unknown, unknown,
| IncrementSendActionType
| NoopActionType | NoopActionType
| ResetComposerActionType | ResetComposerActionType
| SetComposerDisabledStateActionType | SetComposerDisabledStateActionType
@ -495,7 +517,7 @@ function sendMultiMediaMessage(
getState, getState,
undefined undefined
); );
dispatch(resetComposer(conversationId)); dispatch(incrementSendCounter(conversationId));
dispatch(setComposerDisabledState(conversationId, false)); dispatch(setComposerDisabledState(conversationId, false));
}, },
} }
@ -787,44 +809,66 @@ export function setComposerFocus(
}; };
} }
function onEditorStateChange( function onEditorStateChange({
conversationId: string | undefined, bodyRanges,
messageText: string, caretLocation,
bodyRanges: DraftBodyRangesType, conversationId,
caretLocation?: number messageText,
): NoopActionType { sendCounter,
if (!conversationId) { }: {
throw new Error( bodyRanges: DraftBodyRangesType;
'onEditorStateChange: Got falsey conversationId, needs local override' caretLocation?: number;
); conversationId: string | undefined;
} messageText: string;
sendCounter: number;
}): ThunkAction<void, RootStateType, unknown, NoopActionType> {
return (dispatch, getState) => {
if (!conversationId) {
throw new Error(
'onEditorStateChange: Got falsey conversationId, needs local override'
);
}
const conversation = window.ConversationController.get(conversationId); const conversation = window.ConversationController.get(conversationId);
if (!conversation) { if (!conversation) {
throw new Error('processAttachments: Unable to find conversation'); throw new Error('processAttachments: Unable to find conversation');
} }
if (messageText.length && conversation.throttledBumpTyping) { const state = getState().composer.conversations[conversationId];
conversation.throttledBumpTyping(); if (!state) {
} return;
}
debouncedSaveDraft(conversationId, messageText, bodyRanges); if (state.sendCounter !== sendCounter) {
log.warn(
`onEditorStateChange: Got update for conversation ${conversation.idForLogging()}`,
`but sendCounter doesnt match (old: ${state.sendCounter}, new: ${sendCounter})`
);
return;
}
// If we have attachments, don't add link preview if (messageText.length && conversation.throttledBumpTyping) {
if ( conversation.throttledBumpTyping();
!hasDraftAttachments(conversation.attributes.draftAttachments, { }
includePending: true,
}) debouncedSaveDraft(conversationId, messageText, bodyRanges);
) {
maybeGrabLinkPreview(messageText, LinkPreviewSourceType.Composer, { // If we have attachments, don't add link preview
caretLocation, if (
conversationId, !hasDraftAttachments(conversation.attributes.draftAttachments, {
includePending: true,
})
) {
maybeGrabLinkPreview(messageText, LinkPreviewSourceType.Composer, {
caretLocation,
conversationId,
});
}
dispatch({
type: 'NOOP',
payload: null,
}); });
}
return {
type: 'NOOP',
payload: null,
}; };
} }
@ -1296,6 +1340,12 @@ export function reducer(
})); }));
} }
if (action.type === INCREMENT_SEND_COUNTER) {
return updateComposerState(state, action, prevState => ({
sendCounter: prevState.sendCounter + 1,
}));
}
if (action.type === SET_FOCUS) { if (action.type === SET_FOCUS) {
return updateComposerState(state, action, prevState => ({ return updateComposerState(state, action, prevState => ({
focusCounter: prevState.focusCounter + 1, focusCounter: prevState.focusCounter + 1,

View file

@ -86,6 +86,7 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
linkPreviewResult, linkPreviewResult,
messageCompositionId, messageCompositionId,
quotedMessage, quotedMessage,
sendCounter,
shouldSendHighQualityAttachments, shouldSendHighQualityAttachments,
} = composerStateForConversationIdSelector(id); } = composerStateForConversationIdSelector(id);
@ -102,6 +103,7 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
i18n: getIntl(state), i18n: getIntl(state),
isDisabled, isDisabled,
messageCompositionId, messageCompositionId,
sendCounter,
theme: getTheme(state), theme: getTheme(state),
// AudioCapture // AudioCapture