Open first search candidate on pressing "enter" key

This commit is contained in:
Vladislav Gorenkin 2022-04-30 11:24:20 +06:00 committed by Josh Perez
parent db523f0684
commit 01efed8ec3
9 changed files with 218 additions and 14 deletions

View file

@ -424,8 +424,8 @@ story.add('Search: all results', () => (
messageResults: { messageResults: {
isLoading: false, isLoading: false,
results: [ results: [
{ id: 'msg1', conversationId: 'foo' }, { id: 'msg1', type: 'outgoing', conversationId: 'foo' },
{ id: 'msg2', conversationId: 'bar' }, { id: 'msg2', type: 'incoming', conversationId: 'bar' },
], ],
}, },
primarySendsSms: false, primarySendsSms: false,

View file

@ -39,6 +39,7 @@ import {
getWidthFromPreferredWidth, getWidthFromPreferredWidth,
} from '../util/leftPaneWidth'; } from '../util/leftPaneWidth';
import type { LookupConversationWithoutUuidActionsType } from '../util/lookupConversationWithoutUuid'; import type { LookupConversationWithoutUuidActionsType } from '../util/lookupConversationWithoutUuid';
import type { OpenConversationInternalType } from '../state/ducks/conversations';
import { ConversationList } from './ConversationList'; import { ConversationList } from './ConversationList';
import { ContactCheckboxDisabledReason } from './conversationList/ContactCheckbox'; import { ContactCheckboxDisabledReason } from './conversationList/ContactCheckbox';
@ -99,11 +100,7 @@ export type PropsType = {
closeMaximumGroupSizeModal: () => void; closeMaximumGroupSizeModal: () => void;
closeRecommendedGroupSizeModal: () => void; closeRecommendedGroupSizeModal: () => void;
createGroup: () => void; createGroup: () => void;
openConversationInternal: (_: { openConversationInternal: OpenConversationInternalType;
conversationId: string;
messageId?: string;
switchToAssociatedView?: boolean;
}) => void;
savePreferredLeftPaneWidth: (_: number) => void; savePreferredLeftPaneWidth: (_: number) => void;
searchInConversation: (conversationId: string) => unknown; searchInConversation: (conversationId: string) => unknown;
setComposeSearchTerm: (composeSearchTerm: string) => void; setComposeSearchTerm: (composeSearchTerm: string) => void;
@ -332,7 +329,8 @@ export const LeftPane: React.FC<PropsType> = ({
}; };
const numericIndex = keyboardKeyToNumericIndex(event.key); const numericIndex = keyboardKeyToNumericIndex(event.key);
if (commandOrCtrl && isNumber(numericIndex)) { const openedByNumber = commandOrCtrl && isNumber(numericIndex);
if (openedByNumber) {
conversationToOpen = conversationToOpen =
helper.getConversationAndMessageAtIndex(numericIndex); helper.getConversationAndMessageAtIndex(numericIndex);
} else { } else {
@ -366,6 +364,9 @@ export const LeftPane: React.FC<PropsType> = ({
if (conversationToOpen) { if (conversationToOpen) {
const { conversationId, messageId } = conversationToOpen; const { conversationId, messageId } = conversationToOpen;
openConversationInternal({ conversationId, messageId }); openConversationInternal({ conversationId, messageId });
if (openedByNumber) {
clearSearch();
}
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
} }
@ -391,6 +392,7 @@ export const LeftPane: React.FC<PropsType> = ({
showInbox, showInbox,
startComposing, startComposing,
startSearch, startSearch,
clearSearch,
]); ]);
const requiresFullWidth = helper.requiresFullWidth(); const requiresFullWidth = helper.requiresFullWidth();
@ -558,6 +560,7 @@ export const LeftPane: React.FC<PropsType> = ({
setComposeSearchTerm(event.target.value); setComposeSearchTerm(event.target.value);
}, },
updateSearchTerm, updateSearchTerm,
openConversationInternal,
})} })}
<div className="module-left-pane__dialogs"> <div className="module-left-pane__dialogs">
{renderExpiredBuildDialog({ {renderExpiredBuildDialog({

View file

@ -2,7 +2,10 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React, { useEffect, useRef } from 'react'; 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 type { LocalizerType } from '../types/Util';
import { Avatar, AvatarSize } from './Avatar'; import { Avatar, AvatarSize } from './Avatar';
import { SearchInput } from './SearchInput'; import { SearchInput } from './SearchInput';
@ -17,6 +20,11 @@ type PropsType = {
searchTerm: string; searchTerm: string;
startSearchCounter: number; startSearchCounter: number;
updateSearchTerm: (searchTerm: string) => void; updateSearchTerm: (searchTerm: string) => void;
openConversationInternal: OpenConversationInternalType;
onEnterKeyDown?: (
clearSearch: () => void,
openConversationInternal: OpenConversationInternalType
) => void;
}; };
export const LeftPaneSearchInput = ({ export const LeftPaneSearchInput = ({
@ -28,6 +36,8 @@ export const LeftPaneSearchInput = ({
searchTerm, searchTerm,
startSearchCounter, startSearchCounter,
updateSearchTerm, updateSearchTerm,
openConversationInternal,
onEnterKeyDown,
}: PropsType): JSX.Element => { }: PropsType): JSX.Element => {
const inputRef = useRef<null | HTMLInputElement>(null); const inputRef = useRef<null | HTMLInputElement>(null);
@ -91,6 +101,13 @@ export const LeftPaneSearchInput = ({
clearSearch(); clearSearch();
} }
}} }}
onKeyDown={event => {
if (onEnterKeyDown && event.key === 'Enter') {
onEnterKeyDown(clearSearch, openConversationInternal);
event.preventDefault();
event.stopPropagation();
}
}}
onChange={event => { onChange={event => {
changeValue(event.currentTarget.value); changeValue(event.currentTarget.value);
}} }}

View file

@ -12,7 +12,10 @@ import type { Row } from '../ConversationList';
import { RowType } from '../ConversationList'; import { RowType } from '../ConversationList';
import type { PropsData as ConversationListItemPropsType } from '../conversationList/ConversationListItem'; import type { PropsData as ConversationListItemPropsType } from '../conversationList/ConversationListItem';
import type { LocalizerType } from '../../types/Util'; 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 { LeftPaneSearchInput } from '../LeftPaneSearchInput';
import type { LeftPaneSearchPropsType } from './LeftPaneSearchHelper'; import type { LeftPaneSearchPropsType } from './LeftPaneSearchHelper';
import { LeftPaneSearchHelper } from './LeftPaneSearchHelper'; import { LeftPaneSearchHelper } from './LeftPaneSearchHelper';
@ -81,11 +84,13 @@ export class LeftPaneArchiveHelper extends LeftPaneHelper<LeftPaneArchivePropsTy
clearSearch, clearSearch,
i18n, i18n,
updateSearchTerm, updateSearchTerm,
openConversationInternal,
}: Readonly<{ }: Readonly<{
clearConversationSearch: () => unknown; clearConversationSearch: () => unknown;
clearSearch: () => unknown; clearSearch: () => unknown;
i18n: LocalizerType; i18n: LocalizerType;
updateSearchTerm: (searchTerm: string) => unknown; updateSearchTerm: (searchTerm: string) => unknown;
openConversationInternal: OpenConversationInternalType;
}>): ReactChild | null { }>): ReactChild | null {
if (!this.searchConversation) { if (!this.searchConversation) {
return null; return null;
@ -100,6 +105,7 @@ export class LeftPaneArchiveHelper extends LeftPaneHelper<LeftPaneArchivePropsTy
searchTerm={this.searchTerm} searchTerm={this.searchTerm}
startSearchCounter={this.startSearchCounter} startSearchCounter={this.startSearchCounter}
updateSearchTerm={updateSearchTerm} updateSearchTerm={updateSearchTerm}
openConversationInternal={openConversationInternal}
/> />
); );
} }

View file

@ -10,6 +10,7 @@ import type {
ReplaceAvatarActionType, ReplaceAvatarActionType,
SaveAvatarToDiskActionType, SaveAvatarToDiskActionType,
} from '../../types/Avatar'; } from '../../types/Avatar';
import type { OpenConversationInternalType } from '../../state/ducks/conversations';
export enum FindDirection { export enum FindDirection {
Up, Up,
@ -42,6 +43,7 @@ export abstract class LeftPaneHelper<T> {
event: ChangeEvent<HTMLInputElement> event: ChangeEvent<HTMLInputElement>
) => unknown; ) => unknown;
updateSearchTerm: (searchTerm: string) => unknown; updateSearchTerm: (searchTerm: string) => unknown;
openConversationInternal: OpenConversationInternalType;
}> }>
): null | ReactChild { ): null | ReactChild {
return null; return null;

View file

@ -7,7 +7,10 @@ import React from 'react';
import { Intl } from '../Intl'; import { Intl } from '../Intl';
import type { ToFindType } from './LeftPaneHelper'; 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 { LeftPaneHelper } from './LeftPaneHelper';
import { getConversationInDirection } from './getConversationInDirection'; import { getConversationInDirection } from './getConversationInDirection';
import type { Row } from '../ConversationList'; import type { Row } from '../ConversationList';
@ -83,11 +86,13 @@ export class LeftPaneInboxHelper extends LeftPaneHelper<LeftPaneInboxPropsType>
clearSearch, clearSearch,
i18n, i18n,
updateSearchTerm, updateSearchTerm,
openConversationInternal,
}: Readonly<{ }: Readonly<{
clearConversationSearch: () => unknown; clearConversationSearch: () => unknown;
clearSearch: () => unknown; clearSearch: () => unknown;
i18n: LocalizerType; i18n: LocalizerType;
updateSearchTerm: (searchTerm: string) => unknown; updateSearchTerm: (searchTerm: string) => unknown;
openConversationInternal: OpenConversationInternalType;
}>): ReactChild { }>): ReactChild {
return ( return (
<LeftPaneSearchInput <LeftPaneSearchInput
@ -99,6 +104,7 @@ export class LeftPaneInboxHelper extends LeftPaneHelper<LeftPaneInboxPropsType>
searchTerm={this.searchTerm} searchTerm={this.searchTerm}
startSearchCounter={this.startSearchCounter} startSearchCounter={this.startSearchCounter}
updateSearchTerm={updateSearchTerm} updateSearchTerm={updateSearchTerm}
openConversationInternal={openConversationInternal}
/> />
); );
} }

View file

@ -11,7 +11,10 @@ import type { Row } from '../ConversationList';
import { RowType } from '../ConversationList'; import { RowType } from '../ConversationList';
import type { PropsData as ConversationListItemPropsType } from '../conversationList/ConversationListItem'; import type { PropsData as ConversationListItemPropsType } from '../conversationList/ConversationListItem';
import { handleKeydownForSearch } from './handleKeydownForSearch'; import { handleKeydownForSearch } from './handleKeydownForSearch';
import type { ConversationType } from '../../state/ducks/conversations'; import type {
ConversationType,
OpenConversationInternalType,
} from '../../state/ducks/conversations';
import { LeftPaneSearchInput } from '../LeftPaneSearchInput'; import { LeftPaneSearchInput } from '../LeftPaneSearchInput';
import { Intl } from '../Intl'; import { Intl } from '../Intl';
@ -35,6 +38,7 @@ export type LeftPaneSearchPropsType = {
messageResults: MaybeLoadedSearchResultsType<{ messageResults: MaybeLoadedSearchResultsType<{
id: string; id: string;
conversationId: string; conversationId: string;
type: string;
}>; }>;
searchConversationName?: string; searchConversationName?: string;
primarySendsSms: boolean; primarySendsSms: boolean;
@ -58,6 +62,7 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
private readonly messageResults: MaybeLoadedSearchResultsType<{ private readonly messageResults: MaybeLoadedSearchResultsType<{
id: string; id: string;
conversationId: string; conversationId: string;
type: string;
}>; }>;
private readonly searchConversationName?: string; private readonly searchConversationName?: string;
@ -94,6 +99,7 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
this.searchDisabled = searchDisabled; this.searchDisabled = searchDisabled;
this.searchTerm = searchTerm; this.searchTerm = searchTerm;
this.startSearchCounter = startSearchCounter; this.startSearchCounter = startSearchCounter;
this.onEnterKeyDown = this.onEnterKeyDown.bind(this);
} }
override getSearchInput({ override getSearchInput({
@ -101,11 +107,13 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
clearSearch, clearSearch,
i18n, i18n,
updateSearchTerm, updateSearchTerm,
openConversationInternal,
}: Readonly<{ }: Readonly<{
clearConversationSearch: () => unknown; clearConversationSearch: () => unknown;
clearSearch: () => unknown; clearSearch: () => unknown;
i18n: LocalizerType; i18n: LocalizerType;
updateSearchTerm: (searchTerm: string) => unknown; updateSearchTerm: (searchTerm: string) => unknown;
openConversationInternal: OpenConversationInternalType;
}>): ReactChild { }>): ReactChild {
return ( return (
<LeftPaneSearchInput <LeftPaneSearchInput
@ -117,6 +125,8 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
searchTerm={this.searchTerm} searchTerm={this.searchTerm}
startSearchCounter={this.startSearchCounter} startSearchCounter={this.startSearchCounter}
updateSearchTerm={updateSearchTerm} updateSearchTerm={updateSearchTerm}
openConversationInternal={openConversationInternal}
onEnterKeyDown={this.onEnterKeyDown}
/> />
); );
} }
@ -298,10 +308,28 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
); );
} }
// This is currently unimplemented. See DESKTOP-1170.
getConversationAndMessageAtIndex( getConversationAndMessageAtIndex(
_conversationIndex: number conversationIndex: number
): undefined | { conversationId: string; messageId?: string } { ): undefined | { conversationId: string; messageId?: string } {
if (conversationIndex < 0) {
return undefined;
}
let pointer = conversationIndex;
for (const list of this.allResults()) {
if (list.isLoading) {
continue;
}
if (pointer < list.results.length) {
const result = list.results[pointer];
return result.type === 'incoming' || result.type === 'outgoing' // message
? {
conversationId: result.conversationId,
messageId: result.id,
}
: { conversationId: result.id };
}
pointer -= list.results.length;
}
return undefined; return undefined;
} }
@ -332,6 +360,18 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
private isLoading(): boolean { private isLoading(): boolean {
return this.allResults().some(results => results.isLoading); return this.allResults().some(results => results.isLoading);
} }
private onEnterKeyDown(
clearSearch: () => unknown,
openConversationInternal: OpenConversationInternalType
): void {
const conversation = this.getConversationAndMessageAtIndex(0);
if (!conversation) {
return;
}
openConversationInternal(conversation);
clearSearch();
}
} }
function getRowCountForLoadedSearchResults( function getRowCountForLoadedSearchResults(

View file

@ -347,6 +347,12 @@ export type ConversationsStateType = {
messagesByConversation: MessagesByConversationType; messagesByConversation: MessagesByConversationType;
}; };
export type OpenConversationInternalType = (_: {
conversationId: string;
messageId?: string;
switchToAssociatedView?: boolean;
}) => void;
// Helpers // Helpers
export const getConversationCallMode = ( export const getConversationCallMode = (

View file

@ -12,6 +12,7 @@ import { LeftPaneSearchHelper } from '../../../components/leftPane/LeftPaneSearc
describe('LeftPaneSearchHelper', () => { describe('LeftPaneSearchHelper', () => {
const fakeMessage = () => ({ const fakeMessage = () => ({
id: uuid(), id: uuid(),
type: 'outgoing',
conversationId: uuid(), 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
);
});
});
}); });