Open first search candidate on pressing "enter" key
This commit is contained in:
parent
db523f0684
commit
01efed8ec3
9 changed files with 218 additions and 14 deletions
|
@ -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,
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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);
|
||||
}}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -347,6 +347,12 @@ export type ConversationsStateType = {
|
|||
messagesByConversation: MessagesByConversationType;
|
||||
};
|
||||
|
||||
export type OpenConversationInternalType = (_: {
|
||||
conversationId: string;
|
||||
messageId?: string;
|
||||
switchToAssociatedView?: boolean;
|
||||
}) => void;
|
||||
|
||||
// Helpers
|
||||
|
||||
export const getConversationCallMode = (
|
||||
|
|
|
@ -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
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue