diff --git a/ts/components/LeftPane.stories.tsx b/ts/components/LeftPane.stories.tsx index 62dd90c2ec..42d11ba7c1 100644 --- a/ts/components/LeftPane.stories.tsx +++ b/ts/components/LeftPane.stories.tsx @@ -424,8 +424,8 @@ story.add('Search: all results', () => ( messageResults: { isLoading: false, results: [ - { id: 'msg1', conversationId: 'foo' }, - { id: 'msg2', conversationId: 'bar' }, + { id: 'msg1', type: 'outgoing', conversationId: 'foo' }, + { id: 'msg2', type: 'incoming', conversationId: 'bar' }, ], }, primarySendsSms: false, diff --git a/ts/components/LeftPane.tsx b/ts/components/LeftPane.tsx index 5e4d41bce7..2f1a6f58cf 100644 --- a/ts/components/LeftPane.tsx +++ b/ts/components/LeftPane.tsx @@ -39,6 +39,7 @@ import { getWidthFromPreferredWidth, } from '../util/leftPaneWidth'; import type { LookupConversationWithoutUuidActionsType } from '../util/lookupConversationWithoutUuid'; +import type { OpenConversationInternalType } from '../state/ducks/conversations'; import { ConversationList } from './ConversationList'; import { ContactCheckboxDisabledReason } from './conversationList/ContactCheckbox'; @@ -99,11 +100,7 @@ export type PropsType = { closeMaximumGroupSizeModal: () => void; closeRecommendedGroupSizeModal: () => void; createGroup: () => void; - openConversationInternal: (_: { - conversationId: string; - messageId?: string; - switchToAssociatedView?: boolean; - }) => void; + openConversationInternal: OpenConversationInternalType; savePreferredLeftPaneWidth: (_: number) => void; searchInConversation: (conversationId: string) => unknown; setComposeSearchTerm: (composeSearchTerm: string) => void; @@ -332,7 +329,8 @@ export const LeftPane: React.FC = ({ }; const numericIndex = keyboardKeyToNumericIndex(event.key); - if (commandOrCtrl && isNumber(numericIndex)) { + const openedByNumber = commandOrCtrl && isNumber(numericIndex); + if (openedByNumber) { conversationToOpen = helper.getConversationAndMessageAtIndex(numericIndex); } else { @@ -366,6 +364,9 @@ export const LeftPane: React.FC = ({ if (conversationToOpen) { const { conversationId, messageId } = conversationToOpen; openConversationInternal({ conversationId, messageId }); + if (openedByNumber) { + clearSearch(); + } event.preventDefault(); event.stopPropagation(); } @@ -391,6 +392,7 @@ export const LeftPane: React.FC = ({ showInbox, startComposing, startSearch, + clearSearch, ]); const requiresFullWidth = helper.requiresFullWidth(); @@ -558,6 +560,7 @@ export const LeftPane: React.FC = ({ setComposeSearchTerm(event.target.value); }, updateSearchTerm, + openConversationInternal, })}
{renderExpiredBuildDialog({ diff --git a/ts/components/LeftPaneSearchInput.tsx b/ts/components/LeftPaneSearchInput.tsx index 3762adf2fd..77bee1e665 100644 --- a/ts/components/LeftPaneSearchInput.tsx +++ b/ts/components/LeftPaneSearchInput.tsx @@ -2,7 +2,10 @@ // SPDX-License-Identifier: AGPL-3.0-only import React, { useEffect, useRef } from 'react'; -import type { ConversationType } from '../state/ducks/conversations'; +import type { + ConversationType, + OpenConversationInternalType, +} from '../state/ducks/conversations'; import type { LocalizerType } from '../types/Util'; import { Avatar, AvatarSize } from './Avatar'; import { SearchInput } from './SearchInput'; @@ -17,6 +20,11 @@ type PropsType = { searchTerm: string; startSearchCounter: number; updateSearchTerm: (searchTerm: string) => void; + openConversationInternal: OpenConversationInternalType; + onEnterKeyDown?: ( + clearSearch: () => void, + openConversationInternal: OpenConversationInternalType + ) => void; }; export const LeftPaneSearchInput = ({ @@ -28,6 +36,8 @@ export const LeftPaneSearchInput = ({ searchTerm, startSearchCounter, updateSearchTerm, + openConversationInternal, + onEnterKeyDown, }: PropsType): JSX.Element => { const inputRef = useRef(null); @@ -91,6 +101,13 @@ export const LeftPaneSearchInput = ({ clearSearch(); } }} + onKeyDown={event => { + if (onEnterKeyDown && event.key === 'Enter') { + onEnterKeyDown(clearSearch, openConversationInternal); + event.preventDefault(); + event.stopPropagation(); + } + }} onChange={event => { changeValue(event.currentTarget.value); }} diff --git a/ts/components/leftPane/LeftPaneArchiveHelper.tsx b/ts/components/leftPane/LeftPaneArchiveHelper.tsx index 971a279de0..27977c42db 100644 --- a/ts/components/leftPane/LeftPaneArchiveHelper.tsx +++ b/ts/components/leftPane/LeftPaneArchiveHelper.tsx @@ -12,7 +12,10 @@ import type { Row } from '../ConversationList'; import { RowType } from '../ConversationList'; import type { PropsData as ConversationListItemPropsType } from '../conversationList/ConversationListItem'; import type { LocalizerType } from '../../types/Util'; -import type { ConversationType } from '../../state/ducks/conversations'; +import type { + ConversationType, + OpenConversationInternalType, +} from '../../state/ducks/conversations'; import { LeftPaneSearchInput } from '../LeftPaneSearchInput'; import type { LeftPaneSearchPropsType } from './LeftPaneSearchHelper'; import { LeftPaneSearchHelper } from './LeftPaneSearchHelper'; @@ -81,11 +84,13 @@ export class LeftPaneArchiveHelper extends LeftPaneHelper unknown; clearSearch: () => unknown; i18n: LocalizerType; updateSearchTerm: (searchTerm: string) => unknown; + openConversationInternal: OpenConversationInternalType; }>): ReactChild | null { if (!this.searchConversation) { return null; @@ -100,6 +105,7 @@ export class LeftPaneArchiveHelper extends LeftPaneHelper ); } diff --git a/ts/components/leftPane/LeftPaneHelper.tsx b/ts/components/leftPane/LeftPaneHelper.tsx index 4284f0fcc5..0dd4cc09bf 100644 --- a/ts/components/leftPane/LeftPaneHelper.tsx +++ b/ts/components/leftPane/LeftPaneHelper.tsx @@ -10,6 +10,7 @@ import type { ReplaceAvatarActionType, SaveAvatarToDiskActionType, } from '../../types/Avatar'; +import type { OpenConversationInternalType } from '../../state/ducks/conversations'; export enum FindDirection { Up, @@ -42,6 +43,7 @@ export abstract class LeftPaneHelper { event: ChangeEvent ) => unknown; updateSearchTerm: (searchTerm: string) => unknown; + openConversationInternal: OpenConversationInternalType; }> ): null | ReactChild { return null; diff --git a/ts/components/leftPane/LeftPaneInboxHelper.tsx b/ts/components/leftPane/LeftPaneInboxHelper.tsx index 0784cd1e25..d296ff3e7e 100644 --- a/ts/components/leftPane/LeftPaneInboxHelper.tsx +++ b/ts/components/leftPane/LeftPaneInboxHelper.tsx @@ -7,7 +7,10 @@ import React from 'react'; import { Intl } from '../Intl'; import type { ToFindType } from './LeftPaneHelper'; -import type { ConversationType } from '../../state/ducks/conversations'; +import type { + ConversationType, + OpenConversationInternalType, +} from '../../state/ducks/conversations'; import { LeftPaneHelper } from './LeftPaneHelper'; import { getConversationInDirection } from './getConversationInDirection'; import type { Row } from '../ConversationList'; @@ -83,11 +86,13 @@ export class LeftPaneInboxHelper extends LeftPaneHelper clearSearch, i18n, updateSearchTerm, + openConversationInternal, }: Readonly<{ clearConversationSearch: () => unknown; clearSearch: () => unknown; i18n: LocalizerType; updateSearchTerm: (searchTerm: string) => unknown; + openConversationInternal: OpenConversationInternalType; }>): ReactChild { return ( searchTerm={this.searchTerm} startSearchCounter={this.startSearchCounter} updateSearchTerm={updateSearchTerm} + openConversationInternal={openConversationInternal} /> ); } diff --git a/ts/components/leftPane/LeftPaneSearchHelper.tsx b/ts/components/leftPane/LeftPaneSearchHelper.tsx index 9141c990b2..fe3f0cf485 100644 --- a/ts/components/leftPane/LeftPaneSearchHelper.tsx +++ b/ts/components/leftPane/LeftPaneSearchHelper.tsx @@ -11,7 +11,10 @@ import type { Row } from '../ConversationList'; import { RowType } from '../ConversationList'; import type { PropsData as ConversationListItemPropsType } from '../conversationList/ConversationListItem'; import { handleKeydownForSearch } from './handleKeydownForSearch'; -import type { ConversationType } from '../../state/ducks/conversations'; +import type { + ConversationType, + OpenConversationInternalType, +} from '../../state/ducks/conversations'; import { LeftPaneSearchInput } from '../LeftPaneSearchInput'; import { Intl } from '../Intl'; @@ -35,6 +38,7 @@ export type LeftPaneSearchPropsType = { messageResults: MaybeLoadedSearchResultsType<{ id: string; conversationId: string; + type: string; }>; searchConversationName?: string; primarySendsSms: boolean; @@ -58,6 +62,7 @@ export class LeftPaneSearchHelper extends LeftPaneHelper; private readonly searchConversationName?: string; @@ -94,6 +99,7 @@ export class LeftPaneSearchHelper extends LeftPaneHelper unknown; clearSearch: () => unknown; i18n: LocalizerType; updateSearchTerm: (searchTerm: string) => unknown; + openConversationInternal: OpenConversationInternalType; }>): ReactChild { return ( ); } @@ -298,10 +308,28 @@ export class LeftPaneSearchHelper extends LeftPaneHelper results.isLoading); } + + private onEnterKeyDown( + clearSearch: () => unknown, + openConversationInternal: OpenConversationInternalType + ): void { + const conversation = this.getConversationAndMessageAtIndex(0); + if (!conversation) { + return; + } + openConversationInternal(conversation); + clearSearch(); + } } function getRowCountForLoadedSearchResults( diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index d48d89cacf..be4d8043ad 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -347,6 +347,12 @@ export type ConversationsStateType = { messagesByConversation: MessagesByConversationType; }; +export type OpenConversationInternalType = (_: { + conversationId: string; + messageId?: string; + switchToAssociatedView?: boolean; +}) => void; + // Helpers export const getConversationCallMode = ( diff --git a/ts/test-node/components/leftPane/LeftPaneSearchHelper_test.ts b/ts/test-node/components/leftPane/LeftPaneSearchHelper_test.ts index 3cf81a8ce7..b1ed5fe9f7 100644 --- a/ts/test-node/components/leftPane/LeftPaneSearchHelper_test.ts +++ b/ts/test-node/components/leftPane/LeftPaneSearchHelper_test.ts @@ -12,6 +12,7 @@ import { LeftPaneSearchHelper } from '../../../components/leftPane/LeftPaneSearc describe('LeftPaneSearchHelper', () => { const fakeMessage = () => ({ id: uuid(), + type: 'outgoing', conversationId: uuid(), }); @@ -547,4 +548,127 @@ describe('LeftPaneSearchHelper', () => { ); }); }); + + describe('getConversationAndMessageAtIndex', () => { + it('returns correct conversation at given index', () => { + const expected = getDefaultConversation(); + const helper = new LeftPaneSearchHelper({ + conversationResults: { + isLoading: false, + results: [expected, getDefaultConversation()], + }, + contactResults: { isLoading: false, results: [] }, + messageResults: { + isLoading: false, + results: [fakeMessage(), fakeMessage(), fakeMessage()], + }, + searchTerm: 'foo', + primarySendsSms: false, + searchConversation: undefined, + searchDisabled: false, + startSearchCounter: 0, + }); + assert.strictEqual( + helper.getConversationAndMessageAtIndex(0)?.conversationId, + expected.id + ); + }); + + it('returns correct contact at given index', () => { + const expected = getDefaultConversation(); + const helper = new LeftPaneSearchHelper({ + conversationResults: { + isLoading: false, + results: [getDefaultConversation(), getDefaultConversation()], + }, + contactResults: { + isLoading: false, + results: [expected], + }, + messageResults: { + isLoading: false, + results: [fakeMessage(), fakeMessage(), fakeMessage()], + }, + searchTerm: 'foo', + primarySendsSms: false, + searchConversation: undefined, + searchDisabled: false, + startSearchCounter: 0, + }); + assert.strictEqual( + helper.getConversationAndMessageAtIndex(2)?.conversationId, + expected.id + ); + }); + + it('returns correct message at given index', () => { + const expected = fakeMessage(); + const helper = new LeftPaneSearchHelper({ + conversationResults: { + isLoading: false, + results: [getDefaultConversation(), getDefaultConversation()], + }, + contactResults: { isLoading: false, results: [] }, + messageResults: { + isLoading: false, + results: [fakeMessage(), fakeMessage(), expected], + }, + searchTerm: 'foo', + primarySendsSms: false, + searchConversation: undefined, + searchDisabled: false, + startSearchCounter: 0, + }); + assert.strictEqual( + helper.getConversationAndMessageAtIndex(4)?.messageId, + expected.id + ); + }); + + it('returns correct message at given index skipping not loaded results', () => { + const expected = fakeMessage(); + const helper = new LeftPaneSearchHelper({ + conversationResults: { isLoading: true }, + contactResults: { isLoading: true }, + messageResults: { + isLoading: false, + results: [fakeMessage(), expected, fakeMessage()], + }, + searchTerm: 'foo', + primarySendsSms: false, + searchConversation: undefined, + searchDisabled: false, + startSearchCounter: 0, + }); + assert.strictEqual( + helper.getConversationAndMessageAtIndex(1)?.messageId, + expected.id + ); + }); + + it('returns undefined if search candidate with given index does not exist', () => { + const helper = new LeftPaneSearchHelper({ + conversationResults: { + isLoading: false, + results: [getDefaultConversation(), getDefaultConversation()], + }, + contactResults: { isLoading: false, results: [] }, + messageResults: { + isLoading: false, + results: [fakeMessage(), fakeMessage(), fakeMessage()], + }, + searchTerm: 'foo', + primarySendsSms: false, + searchConversation: undefined, + searchDisabled: false, + startSearchCounter: 0, + }); + assert.isUndefined( + helper.getConversationAndMessageAtIndex(100)?.messageId + ); + assert.isUndefined( + helper.getConversationAndMessageAtIndex(-100)?.messageId + ); + }); + }); });