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: {
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,

View File

@ -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<PropsType> = ({
};
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<PropsType> = ({
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<PropsType> = ({
showInbox,
startComposing,
startSearch,
clearSearch,
]);
const requiresFullWidth = helper.requiresFullWidth();
@ -558,6 +560,7 @@ export const LeftPane: React.FC<PropsType> = ({
setComposeSearchTerm(event.target.value);
},
updateSearchTerm,
openConversationInternal,
})}
<div className="module-left-pane__dialogs">
{renderExpiredBuildDialog({

View File

@ -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 | HTMLInputElement>(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);
}}

View File

@ -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<LeftPaneArchivePropsTy
clearSearch,
i18n,
updateSearchTerm,
openConversationInternal,
}: Readonly<{
clearConversationSearch: () => 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<LeftPaneArchivePropsTy
searchTerm={this.searchTerm}
startSearchCounter={this.startSearchCounter}
updateSearchTerm={updateSearchTerm}
openConversationInternal={openConversationInternal}
/>
);
}

View File

@ -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<T> {
event: ChangeEvent<HTMLInputElement>
) => unknown;
updateSearchTerm: (searchTerm: string) => unknown;
openConversationInternal: OpenConversationInternalType;
}>
): null | ReactChild {
return null;

View File

@ -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<LeftPaneInboxPropsType>
clearSearch,
i18n,
updateSearchTerm,
openConversationInternal,
}: Readonly<{
clearConversationSearch: () => unknown;
clearSearch: () => unknown;
i18n: LocalizerType;
updateSearchTerm: (searchTerm: string) => unknown;
openConversationInternal: OpenConversationInternalType;
}>): ReactChild {
return (
<LeftPaneSearchInput
@ -99,6 +104,7 @@ export class LeftPaneInboxHelper extends LeftPaneHelper<LeftPaneInboxPropsType>
searchTerm={this.searchTerm}
startSearchCounter={this.startSearchCounter}
updateSearchTerm={updateSearchTerm}
openConversationInternal={openConversationInternal}
/>
);
}

View File

@ -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<LeftPaneSearchPropsType
private readonly messageResults: MaybeLoadedSearchResultsType<{
id: string;
conversationId: string;
type: string;
}>;
private readonly searchConversationName?: string;
@ -94,6 +99,7 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
this.searchDisabled = searchDisabled;
this.searchTerm = searchTerm;
this.startSearchCounter = startSearchCounter;
this.onEnterKeyDown = this.onEnterKeyDown.bind(this);
}
override getSearchInput({
@ -101,11 +107,13 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
clearSearch,
i18n,
updateSearchTerm,
openConversationInternal,
}: Readonly<{
clearConversationSearch: () => unknown;
clearSearch: () => unknown;
i18n: LocalizerType;
updateSearchTerm: (searchTerm: string) => unknown;
openConversationInternal: OpenConversationInternalType;
}>): ReactChild {
return (
<LeftPaneSearchInput
@ -117,6 +125,8 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
searchTerm={this.searchTerm}
startSearchCounter={this.startSearchCounter}
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(
_conversationIndex: number
conversationIndex: number
): 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;
}
@ -332,6 +360,18 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
private isLoading(): boolean {
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(

View File

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

View File

@ -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
);
});
});
});