From 0f121427fb7538fcf9b89df63feb7670f95e6e66 Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Mon, 31 Aug 2026 09:37:55 -0700 Subject: [PATCH] Preserve composer focus when adding thread posts (#11605) --- src/view/com/composer/Composer.tsx | 2 +- src/view/com/composer/state/composer.test.ts | 60 ++++++++++++++++++++ src/view/com/composer/state/composer.ts | 2 + 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 src/view/com/composer/state/composer.test.ts diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 465110c45a..ea288830a3 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -1700,7 +1700,7 @@ let ComposerPost = memo(function ComposerPost({ style={[a.pt_xs]} richtext={richtext} placeholder={selectTextInputPlaceholder} - autoFocus={isLastPost} + autoFocus={isActive} webForceMinHeight={forceMinHeight} // To avoid overlap with the close button: hasRightPadding={isPartOfThread} diff --git a/src/view/com/composer/state/composer.test.ts b/src/view/com/composer/state/composer.test.ts new file mode 100644 index 0000000000..fc91e4b913 --- /dev/null +++ b/src/view/com/composer/state/composer.test.ts @@ -0,0 +1,60 @@ +import {composerReducer, createComposerState} from './composer' + +jest.mock('#/state/gallery', () => ({ + createInitialImages: jest.fn(), +})) +jest.mock('#/logger', () => ({ + logger: { + warn: jest.fn(), + }, +})) +jest.mock('#/state/queries/postgate/util', () => ({ + createPostgateRecord: jest.fn(() => ({})), +})) +jest.mock('#/state/queries/threadgate', () => ({ + threadgateRecordToAllowUISetting: jest.fn(() => []), +})) + +function createState() { + return createComposerState({ + initText: undefined, + initMention: undefined, + initImageUris: undefined, + initQuoteUri: undefined, + initInteractionSettings: undefined, + }) +} + +describe('composerReducer', () => { + describe('add_post', () => { + it('selects the appended post and requests focus', () => { + const state = createState() + + const nextState = composerReducer(state, {type: 'add_post'}) + + expect(nextState.thread.posts).toHaveLength(2) + expect(nextState.activePostIndex).toBe(1) + expect(nextState.mutableNeedsFocusActive).toBe(true) + }) + + it('selects a post inserted in the middle of a thread', () => { + let state = createState() + state = composerReducer(state, {type: 'add_post'}) + state = composerReducer(state, {type: 'add_post'}) + + const lastPostId = state.thread.posts[2].id + state = composerReducer(state, { + type: 'focus_post', + postId: state.thread.posts[0].id, + }) + + const nextState = composerReducer(state, {type: 'add_post'}) + + expect(nextState.thread.posts).toHaveLength(4) + expect(nextState.activePostIndex).toBe(1) + expect(nextState.thread.posts[2].id).not.toBe(lastPostId) + expect(nextState.thread.posts[3].id).toBe(lastPostId) + expect(nextState.mutableNeedsFocusActive).toBe(true) + }) + }) +}) diff --git a/src/view/com/composer/state/composer.ts b/src/view/com/composer/state/composer.ts index 5a013a830b..5e56bab21f 100644 --- a/src/view/com/composer/state/composer.ts +++ b/src/view/com/composer/state/composer.ts @@ -253,6 +253,8 @@ export function composerReducer( return { ...state, isDirty: true, + activePostIndex: activePostIndex + 1, + mutableNeedsFocusActive: true, thread: { ...state.thread, posts: nextPosts,