Improve left pane UI when loading search results
This commit is contained in:
parent
f05d45ac9b
commit
d81aaf654f
15 changed files with 420 additions and 93 deletions
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016-2020 Signal Messenger, LLC
|
||||
// Copyright 2016-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// Fonts
|
||||
|
@ -116,6 +116,42 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Search results loading
|
||||
|
||||
@mixin search-results-loading-pulsating-background {
|
||||
animation: search-results-loading-pulsating-background-animation 2s infinite;
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@include light-theme {
|
||||
background: $color-gray-05;
|
||||
}
|
||||
@include dark-theme {
|
||||
background: $color-gray-65;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes search-results-loading-pulsating-background-animation {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.55;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin search-results-loading-box($width) {
|
||||
width: $width;
|
||||
height: 12px;
|
||||
border-radius: 4px;
|
||||
@include search-results-loading-pulsating-background;
|
||||
}
|
||||
|
||||
// Icons
|
||||
|
||||
@mixin color-svg($svg, $color, $stretch: true) {
|
||||
|
|
|
@ -6911,7 +6911,11 @@ button.module-image__border-overlay:focus {
|
|||
// Module: conversation list
|
||||
|
||||
.module-conversation-list {
|
||||
@include smooth-scroll;
|
||||
&--scroll-behavior {
|
||||
&-default {
|
||||
@include smooth-scroll;
|
||||
}
|
||||
}
|
||||
|
||||
&__item {
|
||||
&--archive-button {
|
||||
|
|
16
stylesheets/components/SearchResultsLoadingFakeHeader.scss
Normal file
16
stylesheets/components/SearchResultsLoadingFakeHeader.scss
Normal file
|
@ -0,0 +1,16 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// These styles should match the "real" header.
|
||||
.module-SearchResultsLoadingFakeHeader {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding-left: 16px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
@include search-results-loading-box(25%);
|
||||
}
|
||||
}
|
35
stylesheets/components/SearchResultsLoadingFakeRow.scss
Normal file
35
stylesheets/components/SearchResultsLoadingFakeRow.scss
Normal file
|
@ -0,0 +1,35 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// These styles should match the "real" contact/conversation row.
|
||||
.module-SearchResultsLoadingFakeRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
|
||||
&__avatar {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 100%;
|
||||
@include search-results-loading-pulsating-background;
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
margin-left: 12px;
|
||||
|
||||
&__header {
|
||||
@include search-results-loading-box(50%);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&__message {
|
||||
@include search-results-loading-box(90%);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -38,3 +38,5 @@
|
|||
@import './components/GroupDialog.scss';
|
||||
@import './components/GroupTitleInput.scss';
|
||||
@import './components/MessageAudio.scss';
|
||||
@import './components/SearchResultsLoadingFakeHeader.scss';
|
||||
@import './components/SearchResultsLoadingFakeRow.scss';
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { omit } from 'lodash';
|
||||
import { times, omit } from 'lodash';
|
||||
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
@ -413,7 +413,7 @@ Line 4, well.`,
|
|||
});
|
||||
|
||||
story.add('Conversations: Various Times', () => {
|
||||
const times: Array<[number, string]> = [
|
||||
const pairs: Array<[number, string]> = [
|
||||
[Date.now() - 5 * 60 * 60 * 1000, 'Five hours ago'],
|
||||
[Date.now() - 24 * 60 * 60 * 1000, 'One day ago'],
|
||||
[Date.now() - 7 * 24 * 60 * 60 * 1000, 'One week ago'],
|
||||
|
@ -423,7 +423,7 @@ Line 4, well.`,
|
|||
return (
|
||||
<ConversationList
|
||||
{...createProps(
|
||||
times.map(([lastUpdated, messageText]) => ({
|
||||
pairs.map(([lastUpdated, messageText]) => ({
|
||||
type: RowType.Conversation,
|
||||
conversation: createConversation({
|
||||
lastUpdated,
|
||||
|
@ -510,6 +510,18 @@ story.add('Start new conversation', () => (
|
|||
/>
|
||||
));
|
||||
|
||||
story.add('Search results loading skeleton', () => (
|
||||
<ConversationList
|
||||
scrollable={false}
|
||||
{...createProps([
|
||||
{ type: RowType.SearchResultsLoadingFakeHeader },
|
||||
...times(99, () => ({
|
||||
type: RowType.SearchResultsLoadingFakeRow as const,
|
||||
})),
|
||||
])}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Kitchen sink', () => (
|
||||
<ConversationList
|
||||
{...createProps([
|
||||
|
@ -533,7 +545,6 @@ story.add('Kitchen sink', () => (
|
|||
type: RowType.MessageSearchResult,
|
||||
messageId: '123',
|
||||
},
|
||||
{ type: RowType.Spinner },
|
||||
{
|
||||
type: RowType.ArchiveButton,
|
||||
archivedConversationsCount: 123,
|
||||
|
|
|
@ -3,10 +3,11 @@
|
|||
|
||||
import React, { useRef, useEffect, useCallback, CSSProperties } from 'react';
|
||||
import { List, ListRowRenderer } from 'react-virtualized';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
import { assert } from '../util/assert';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
import { LocalizerType, ScrollBehavior } from '../types/Util';
|
||||
|
||||
import {
|
||||
ConversationListItem,
|
||||
|
@ -21,8 +22,9 @@ import {
|
|||
ContactCheckboxDisabledReason,
|
||||
} from './conversationList/ContactCheckbox';
|
||||
import { CreateNewGroupButton } from './conversationList/CreateNewGroupButton';
|
||||
import { Spinner as SpinnerComponent } from './Spinner';
|
||||
import { StartNewConversation as StartNewConversationComponent } from './conversationList/StartNewConversation';
|
||||
import { SearchResultsLoadingFakeHeader as SearchResultsLoadingFakeHeaderComponent } from './conversationList/SearchResultsLoadingFakeHeader';
|
||||
import { SearchResultsLoadingFakeRow as SearchResultsLoadingFakeRowComponent } from './conversationList/SearchResultsLoadingFakeRow';
|
||||
|
||||
export enum RowType {
|
||||
ArchiveButton,
|
||||
|
@ -33,7 +35,8 @@ export enum RowType {
|
|||
CreateNewGroup,
|
||||
Header,
|
||||
MessageSearchResult,
|
||||
Spinner,
|
||||
SearchResultsLoadingFakeHeader,
|
||||
SearchResultsLoadingFakeRow,
|
||||
StartNewConversation,
|
||||
}
|
||||
|
||||
|
@ -76,7 +79,13 @@ type HeaderRowType = {
|
|||
i18nKey: string;
|
||||
};
|
||||
|
||||
type SpinnerRowType = { type: RowType.Spinner };
|
||||
type SearchResultsLoadingFakeHeaderType = {
|
||||
type: RowType.SearchResultsLoadingFakeHeader;
|
||||
};
|
||||
|
||||
type SearchResultsLoadingFakeRowType = {
|
||||
type: RowType.SearchResultsLoadingFakeRow;
|
||||
};
|
||||
|
||||
type StartNewConversationRowType = {
|
||||
type: RowType.StartNewConversation;
|
||||
|
@ -92,7 +101,8 @@ export type Row =
|
|||
| CreateNewGroupRowType
|
||||
| MessageRowType
|
||||
| HeaderRowType
|
||||
| SpinnerRowType
|
||||
| SearchResultsLoadingFakeHeaderType
|
||||
| SearchResultsLoadingFakeRowType
|
||||
| StartNewConversationRowType;
|
||||
|
||||
export type PropsType = {
|
||||
|
@ -105,8 +115,10 @@ export type PropsType = {
|
|||
// this should only happen if there is a bug somewhere. For example, an inaccurate
|
||||
// `rowCount`.
|
||||
getRow: (index: number) => undefined | Row;
|
||||
scrollBehavior?: ScrollBehavior;
|
||||
scrollToRowIndex?: number;
|
||||
shouldRecomputeRowHeights: boolean;
|
||||
scrollable?: boolean;
|
||||
|
||||
i18n: LocalizerType;
|
||||
|
||||
|
@ -121,6 +133,9 @@ export type PropsType = {
|
|||
startNewConversationFromPhoneNumber: (e164: string) => void;
|
||||
};
|
||||
|
||||
const NORMAL_ROW_HEIGHT = 68;
|
||||
const HEADER_ROW_HEIGHT = 40;
|
||||
|
||||
export const ConversationList: React.FC<PropsType> = ({
|
||||
dimensions,
|
||||
getRow,
|
||||
|
@ -130,7 +145,9 @@ export const ConversationList: React.FC<PropsType> = ({
|
|||
onSelectConversation,
|
||||
renderMessageSearchResult,
|
||||
rowCount,
|
||||
scrollBehavior = ScrollBehavior.Default,
|
||||
scrollToRowIndex,
|
||||
scrollable = true,
|
||||
shouldRecomputeRowHeights,
|
||||
showChooseGroupMembers,
|
||||
startNewConversationFromPhoneNumber,
|
||||
|
@ -149,9 +166,15 @@ export const ConversationList: React.FC<PropsType> = ({
|
|||
const row = getRow(index);
|
||||
if (!row) {
|
||||
assert(false, `Expected a row at index ${index}`);
|
||||
return 68;
|
||||
return NORMAL_ROW_HEIGHT;
|
||||
}
|
||||
switch (row.type) {
|
||||
case RowType.Header:
|
||||
case RowType.SearchResultsLoadingFakeHeader:
|
||||
return HEADER_ROW_HEIGHT;
|
||||
default:
|
||||
return NORMAL_ROW_HEIGHT;
|
||||
}
|
||||
return row.type === RowType.Header ? 40 : 68;
|
||||
},
|
||||
[getRow]
|
||||
);
|
||||
|
@ -235,22 +258,20 @@ export const ConversationList: React.FC<PropsType> = ({
|
|||
{i18n(row.i18nKey)}
|
||||
</div>
|
||||
);
|
||||
case RowType.Spinner:
|
||||
return (
|
||||
<div
|
||||
className="module-conversation-list__item--spinner"
|
||||
key={key}
|
||||
style={style}
|
||||
>
|
||||
<SpinnerComponent size="24px" svgSize="small" />
|
||||
</div>
|
||||
);
|
||||
case RowType.MessageSearchResult:
|
||||
return (
|
||||
<React.Fragment key={key}>
|
||||
{renderMessageSearchResult(row.messageId, style)}
|
||||
</React.Fragment>
|
||||
);
|
||||
case RowType.SearchResultsLoadingFakeHeader:
|
||||
return (
|
||||
<SearchResultsLoadingFakeHeaderComponent key={key} style={style} />
|
||||
);
|
||||
case RowType.SearchResultsLoadingFakeRow:
|
||||
return (
|
||||
<SearchResultsLoadingFakeRowComponent key={key} style={style} />
|
||||
);
|
||||
case RowType.StartNewConversation:
|
||||
return (
|
||||
<StartNewConversationComponent
|
||||
|
@ -288,13 +309,17 @@ export const ConversationList: React.FC<PropsType> = ({
|
|||
|
||||
return (
|
||||
<List
|
||||
className="module-conversation-list"
|
||||
className={classNames(
|
||||
'module-conversation-list',
|
||||
`module-conversation-list--scroll-behavior-${scrollBehavior}`
|
||||
)}
|
||||
height={height}
|
||||
ref={listRef}
|
||||
rowCount={rowCount}
|
||||
rowHeight={calculateRowHeight}
|
||||
rowRenderer={renderRow}
|
||||
scrollToIndex={scrollToRowIndex}
|
||||
style={{ overflow: scrollable ? 'auto' : 'hidden' }}
|
||||
tabIndex={-1}
|
||||
width={width}
|
||||
/>
|
||||
|
|
|
@ -36,7 +36,7 @@ import {
|
|||
} from './leftPane/LeftPaneSetGroupMetadataHelper';
|
||||
|
||||
import * as OS from '../OS';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
import { LocalizerType, ScrollBehavior } from '../types/Util';
|
||||
import { usePrevious } from '../util/hooks';
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
|
||||
|
@ -337,10 +337,21 @@ export const LeftPane: React.FC<PropsType> = ({
|
|||
selectedConversationId,
|
||||
selectedConversationId
|
||||
);
|
||||
const rowIndexToScrollTo: undefined | number =
|
||||
previousSelectedConversationId === selectedConversationId
|
||||
? undefined
|
||||
: helper.getRowIndexToScrollTo(selectedConversationId);
|
||||
|
||||
const isScrollable = helper.isScrollable();
|
||||
|
||||
let rowIndexToScrollTo: undefined | number;
|
||||
let scrollBehavior: ScrollBehavior;
|
||||
if (isScrollable) {
|
||||
rowIndexToScrollTo =
|
||||
previousSelectedConversationId === selectedConversationId
|
||||
? undefined
|
||||
: helper.getRowIndexToScrollTo(selectedConversationId);
|
||||
scrollBehavior = ScrollBehavior.Default;
|
||||
} else {
|
||||
rowIndexToScrollTo = 0;
|
||||
scrollBehavior = ScrollBehavior.Hard;
|
||||
}
|
||||
|
||||
// We ensure that the listKey differs between some modes (e.g. inbox/archived), ensuring
|
||||
// that AutoSizer properly detects the new size of its slot in the flexbox. The
|
||||
|
@ -436,7 +447,9 @@ export const LeftPane: React.FC<PropsType> = ({
|
|||
}}
|
||||
renderMessageSearchResult={renderMessageSearchResult}
|
||||
rowCount={helper.getRowCount()}
|
||||
scrollBehavior={scrollBehavior}
|
||||
scrollToRowIndex={rowIndexToScrollTo}
|
||||
scrollable={isScrollable}
|
||||
shouldRecomputeRowHeights={shouldRecomputeRowHeights}
|
||||
showChooseGroupMembers={showChooseGroupMembers}
|
||||
startNewConversationFromPhoneNumber={
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { CSSProperties, FunctionComponent } from 'react';
|
||||
|
||||
type PropsType = {
|
||||
style: CSSProperties;
|
||||
};
|
||||
|
||||
export const SearchResultsLoadingFakeHeader: FunctionComponent<PropsType> = ({
|
||||
style,
|
||||
}) => <div className="module-SearchResultsLoadingFakeHeader" style={style} />;
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { CSSProperties, FunctionComponent } from 'react';
|
||||
|
||||
type PropsType = {
|
||||
style: CSSProperties;
|
||||
};
|
||||
|
||||
export const SearchResultsLoadingFakeRow: FunctionComponent<PropsType> = ({
|
||||
style,
|
||||
}) => (
|
||||
<div className="module-SearchResultsLoadingFakeRow" style={style}>
|
||||
<div className="module-SearchResultsLoadingFakeRow__avatar" />
|
||||
<div className="module-SearchResultsLoadingFakeRow__content">
|
||||
<div className="module-SearchResultsLoadingFakeRow__content__header" />
|
||||
<div className="module-SearchResultsLoadingFakeRow__content__message" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
|
@ -83,6 +83,10 @@ export abstract class LeftPaneHelper<T> {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
isScrollable(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
abstract getConversationAndMessageAtIndex(
|
||||
conversationIndex: number
|
||||
): undefined | { conversationId: string; messageId?: string };
|
||||
|
|
|
@ -10,6 +10,14 @@ import { PropsData as ConversationListItemPropsType } from '../conversationList/
|
|||
|
||||
import { Intl } from '../Intl';
|
||||
import { Emojify } from '../conversation/Emojify';
|
||||
import { assert } from '../../util/assert';
|
||||
|
||||
// The "correct" thing to do is to measure the size of the left pane and render enough
|
||||
// search results for the container height. But (1) that's slow (2) the list is
|
||||
// virtualized (3) 99 rows is over 6000px tall, taller than most monitors (4) it's fine
|
||||
// if, in some extremely tall window, we have some empty space. So we just hard-code a
|
||||
// fairly big number.
|
||||
const SEARCH_RESULTS_FAKE_ROW_COUNT = 99;
|
||||
|
||||
type MaybeLoadedSearchResultsType<T> =
|
||||
| { isLoading: true }
|
||||
|
@ -106,9 +114,14 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<
|
|||
}
|
||||
|
||||
getRowCount(): number {
|
||||
if (this.isLoading()) {
|
||||
// 1 for the header.
|
||||
return 1 + SEARCH_RESULTS_FAKE_ROW_COUNT;
|
||||
}
|
||||
|
||||
return this.allResults().reduce(
|
||||
(result: number, searchResults) =>
|
||||
result + getRowCountForSearchResult(searchResults),
|
||||
result + getRowCountForLoadedSearchResults(searchResults),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
@ -124,11 +137,21 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<
|
|||
getRow(rowIndex: number): undefined | Row {
|
||||
const { conversationResults, contactResults, messageResults } = this;
|
||||
|
||||
const conversationRowCount = getRowCountForSearchResult(
|
||||
if (this.isLoading()) {
|
||||
if (rowIndex === 0) {
|
||||
return { type: RowType.SearchResultsLoadingFakeHeader };
|
||||
}
|
||||
if (rowIndex + 1 <= SEARCH_RESULTS_FAKE_ROW_COUNT) {
|
||||
return { type: RowType.SearchResultsLoadingFakeRow };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const conversationRowCount = getRowCountForLoadedSearchResults(
|
||||
conversationResults
|
||||
);
|
||||
const contactRowCount = getRowCountForSearchResult(contactResults);
|
||||
const messageRowCount = getRowCountForSearchResult(messageResults);
|
||||
const contactRowCount = getRowCountForLoadedSearchResults(contactResults);
|
||||
const messageRowCount = getRowCountForLoadedSearchResults(messageResults);
|
||||
|
||||
if (rowIndex < conversationRowCount) {
|
||||
if (rowIndex === 0) {
|
||||
|
@ -137,9 +160,10 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<
|
|||
i18nKey: 'conversationsHeader',
|
||||
};
|
||||
}
|
||||
if (conversationResults.isLoading) {
|
||||
return { type: RowType.Spinner };
|
||||
}
|
||||
assert(
|
||||
!conversationResults.isLoading,
|
||||
"We shouldn't get here with conversation results still loading"
|
||||
);
|
||||
const conversation = conversationResults.results[rowIndex - 1];
|
||||
return conversation
|
||||
? {
|
||||
|
@ -157,9 +181,10 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<
|
|||
i18nKey: 'contactsHeader',
|
||||
};
|
||||
}
|
||||
if (contactResults.isLoading) {
|
||||
return { type: RowType.Spinner };
|
||||
}
|
||||
assert(
|
||||
!contactResults.isLoading,
|
||||
"We shouldn't get here with contact results still loading"
|
||||
);
|
||||
const conversation = contactResults.results[localIndex - 1];
|
||||
return conversation
|
||||
? {
|
||||
|
@ -180,9 +205,10 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<
|
|||
i18nKey: 'messagesHeader',
|
||||
};
|
||||
}
|
||||
if (messageResults.isLoading) {
|
||||
return { type: RowType.Spinner };
|
||||
}
|
||||
assert(
|
||||
!messageResults.isLoading,
|
||||
"We shouldn't get here with message results still loading"
|
||||
);
|
||||
const message = messageResults.results[localIndex - 1];
|
||||
return message
|
||||
? {
|
||||
|
@ -192,11 +218,23 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<
|
|||
: undefined;
|
||||
}
|
||||
|
||||
isScrollable(): boolean {
|
||||
return !this.isLoading();
|
||||
}
|
||||
|
||||
shouldRecomputeRowHeights(old: Readonly<LeftPaneSearchPropsType>): boolean {
|
||||
const oldIsLoading = new LeftPaneSearchHelper(old).isLoading();
|
||||
const newIsLoading = this.isLoading();
|
||||
if (oldIsLoading && newIsLoading) {
|
||||
return false;
|
||||
}
|
||||
if (oldIsLoading !== newIsLoading) {
|
||||
return true;
|
||||
}
|
||||
return searchResultKeys.some(
|
||||
key =>
|
||||
getRowCountForSearchResult(old[key]) !==
|
||||
getRowCountForSearchResult(this[key])
|
||||
getRowCountForLoadedSearchResults(old[key]) !==
|
||||
getRowCountForLoadedSearchResults(this[key])
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -221,20 +259,27 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<
|
|||
private allResults() {
|
||||
return [this.conversationResults, this.contactResults, this.messageResults];
|
||||
}
|
||||
|
||||
private isLoading(): boolean {
|
||||
return this.allResults().some(results => results.isLoading);
|
||||
}
|
||||
}
|
||||
|
||||
function getRowCountForSearchResult(
|
||||
function getRowCountForLoadedSearchResults(
|
||||
searchResults: Readonly<MaybeLoadedSearchResultsType<unknown>>
|
||||
): number {
|
||||
let hasHeader: boolean;
|
||||
let resultRows: number;
|
||||
// It's possible to call this helper with invalid results (e.g., ones that are loading).
|
||||
// We could change the parameter of this function, but that adds a bunch of redundant
|
||||
// checks that are, in the author's opinion, less clear.
|
||||
if (searchResults.isLoading) {
|
||||
hasHeader = true;
|
||||
resultRows = 1; // For the spinner.
|
||||
} else {
|
||||
const resultCount = searchResults.results.length;
|
||||
hasHeader = Boolean(resultCount);
|
||||
resultRows = resultCount;
|
||||
assert(
|
||||
false,
|
||||
'getRowCountForLoadedSearchResults: Expected this to be called with loaded search results. Returning 0'
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const resultRows = searchResults.results.length;
|
||||
const hasHeader = Boolean(resultRows);
|
||||
return (hasHeader ? 1 : 0) + resultRows;
|
||||
}
|
||||
|
|
|
@ -40,6 +40,39 @@ describe('LeftPaneSearchHelper', () => {
|
|||
});
|
||||
|
||||
describe('getRowCount', () => {
|
||||
it('returns 100 if any results are loading', () => {
|
||||
assert.strictEqual(
|
||||
new LeftPaneSearchHelper({
|
||||
conversationResults: { isLoading: true },
|
||||
contactResults: { isLoading: true },
|
||||
messageResults: { isLoading: true },
|
||||
searchTerm: 'foo',
|
||||
}).getRowCount(),
|
||||
100
|
||||
);
|
||||
assert.strictEqual(
|
||||
new LeftPaneSearchHelper({
|
||||
conversationResults: {
|
||||
isLoading: false,
|
||||
results: [fakeConversation(), fakeConversation()],
|
||||
},
|
||||
contactResults: { isLoading: true },
|
||||
messageResults: { isLoading: true },
|
||||
searchTerm: 'foo',
|
||||
}).getRowCount(),
|
||||
100
|
||||
);
|
||||
assert.strictEqual(
|
||||
new LeftPaneSearchHelper({
|
||||
conversationResults: { isLoading: true },
|
||||
contactResults: { isLoading: true },
|
||||
messageResults: { isLoading: false, results: [fakeMessage()] },
|
||||
searchTerm: 'foo',
|
||||
}).getRowCount(),
|
||||
100
|
||||
);
|
||||
});
|
||||
|
||||
it('returns 0 when there are no search results', () => {
|
||||
const helper = new LeftPaneSearchHelper({
|
||||
conversationResults: { isLoading: false, results: [] },
|
||||
|
@ -51,17 +84,6 @@ describe('LeftPaneSearchHelper', () => {
|
|||
assert.strictEqual(helper.getRowCount(), 0);
|
||||
});
|
||||
|
||||
it("returns 2 rows for each section of search results that's loading", () => {
|
||||
const helper = new LeftPaneSearchHelper({
|
||||
conversationResults: { isLoading: true },
|
||||
contactResults: { isLoading: false, results: [] },
|
||||
messageResults: { isLoading: true },
|
||||
searchTerm: 'foo',
|
||||
});
|
||||
|
||||
assert.strictEqual(helper.getRowCount(), 4);
|
||||
});
|
||||
|
||||
it('returns 1 + the number of results, dropping empty sections', () => {
|
||||
const helper = new LeftPaneSearchHelper({
|
||||
conversationResults: {
|
||||
|
@ -78,34 +100,41 @@ describe('LeftPaneSearchHelper', () => {
|
|||
});
|
||||
|
||||
describe('getRow', () => {
|
||||
it('returns header + spinner for loading sections', () => {
|
||||
const helper = new LeftPaneSearchHelper({
|
||||
conversationResults: { isLoading: true },
|
||||
contactResults: { isLoading: true },
|
||||
messageResults: { isLoading: true },
|
||||
searchTerm: 'foo',
|
||||
});
|
||||
it('returns a "loading search results" row if any results are loading', () => {
|
||||
const helpers = [
|
||||
new LeftPaneSearchHelper({
|
||||
conversationResults: { isLoading: true },
|
||||
contactResults: { isLoading: true },
|
||||
messageResults: { isLoading: true },
|
||||
searchTerm: 'foo',
|
||||
}),
|
||||
new LeftPaneSearchHelper({
|
||||
conversationResults: {
|
||||
isLoading: false,
|
||||
results: [fakeConversation(), fakeConversation()],
|
||||
},
|
||||
contactResults: { isLoading: true },
|
||||
messageResults: { isLoading: true },
|
||||
searchTerm: 'foo',
|
||||
}),
|
||||
new LeftPaneSearchHelper({
|
||||
conversationResults: { isLoading: true },
|
||||
contactResults: { isLoading: true },
|
||||
messageResults: { isLoading: false, results: [fakeMessage()] },
|
||||
searchTerm: 'foo',
|
||||
}),
|
||||
];
|
||||
|
||||
assert.deepEqual(helper.getRow(0), {
|
||||
type: RowType.Header,
|
||||
i18nKey: 'conversationsHeader',
|
||||
});
|
||||
assert.deepEqual(helper.getRow(1), {
|
||||
type: RowType.Spinner,
|
||||
});
|
||||
assert.deepEqual(helper.getRow(2), {
|
||||
type: RowType.Header,
|
||||
i18nKey: 'contactsHeader',
|
||||
});
|
||||
assert.deepEqual(helper.getRow(3), {
|
||||
type: RowType.Spinner,
|
||||
});
|
||||
assert.deepEqual(helper.getRow(4), {
|
||||
type: RowType.Header,
|
||||
i18nKey: 'messagesHeader',
|
||||
});
|
||||
assert.deepEqual(helper.getRow(5), {
|
||||
type: RowType.Spinner,
|
||||
helpers.forEach(helper => {
|
||||
assert.deepEqual(helper.getRow(0), {
|
||||
type: RowType.SearchResultsLoadingFakeHeader,
|
||||
});
|
||||
for (let i = 1; i < 99; i += 1) {
|
||||
assert.deepEqual(helper.getRow(i), {
|
||||
type: RowType.SearchResultsLoadingFakeRow,
|
||||
});
|
||||
}
|
||||
assert.isUndefined(helper.getRow(100));
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -272,6 +301,54 @@ describe('LeftPaneSearchHelper', () => {
|
|||
assert.isUndefined(helper.getRow(5));
|
||||
});
|
||||
|
||||
describe('isScrollable', () => {
|
||||
it('returns false if any results are loading', () => {
|
||||
const helpers = [
|
||||
new LeftPaneSearchHelper({
|
||||
conversationResults: { isLoading: true },
|
||||
contactResults: { isLoading: true },
|
||||
messageResults: { isLoading: true },
|
||||
searchTerm: 'foo',
|
||||
}),
|
||||
new LeftPaneSearchHelper({
|
||||
conversationResults: {
|
||||
isLoading: false,
|
||||
results: [fakeConversation(), fakeConversation()],
|
||||
},
|
||||
contactResults: { isLoading: true },
|
||||
messageResults: { isLoading: true },
|
||||
searchTerm: 'foo',
|
||||
}),
|
||||
new LeftPaneSearchHelper({
|
||||
conversationResults: { isLoading: true },
|
||||
contactResults: { isLoading: true },
|
||||
messageResults: { isLoading: false, results: [fakeMessage()] },
|
||||
searchTerm: 'foo',
|
||||
}),
|
||||
];
|
||||
|
||||
helpers.forEach(helper => {
|
||||
assert.isFalse(helper.isScrollable());
|
||||
});
|
||||
});
|
||||
|
||||
it('returns true if all results have loaded', () => {
|
||||
const helper = new LeftPaneSearchHelper({
|
||||
conversationResults: {
|
||||
isLoading: false,
|
||||
results: [fakeConversation(), fakeConversation()],
|
||||
},
|
||||
contactResults: { isLoading: false, results: [] },
|
||||
messageResults: {
|
||||
isLoading: false,
|
||||
results: [fakeMessage(), fakeMessage(), fakeMessage()],
|
||||
},
|
||||
searchTerm: 'foo',
|
||||
});
|
||||
assert.isTrue(helper.isScrollable());
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldRecomputeRowHeights', () => {
|
||||
it("returns false if the number of results doesn't change", () => {
|
||||
const helper = new LeftPaneSearchHelper({
|
||||
|
@ -303,7 +380,7 @@ describe('LeftPaneSearchHelper', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('returns false when a section goes from loading to loaded with 1 result', () => {
|
||||
it('returns false when a section completes loading, but not all sections are done (because the pane is still loading overall)', () => {
|
||||
const helper = new LeftPaneSearchHelper({
|
||||
conversationResults: { isLoading: true },
|
||||
contactResults: { isLoading: true },
|
||||
|
@ -324,6 +401,27 @@ describe('LeftPaneSearchHelper', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('returns true when all sections finish loading', () => {
|
||||
const helper = new LeftPaneSearchHelper({
|
||||
conversationResults: { isLoading: true },
|
||||
contactResults: { isLoading: true },
|
||||
messageResults: { isLoading: false, results: [fakeMessage()] },
|
||||
searchTerm: 'foo',
|
||||
});
|
||||
|
||||
assert.isTrue(
|
||||
helper.shouldRecomputeRowHeights({
|
||||
conversationResults: {
|
||||
isLoading: false,
|
||||
results: [fakeConversation(), fakeConversation()],
|
||||
},
|
||||
contactResults: { isLoading: false, results: [] },
|
||||
messageResults: { isLoading: false, results: [fakeMessage()] },
|
||||
searchTerm: 'foo',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('returns true if the number of results in a section changes', () => {
|
||||
const helper = new LeftPaneSearchHelper({
|
||||
conversationResults: {
|
||||
|
|
|
@ -29,3 +29,9 @@ export enum ThemeType {
|
|||
'light' = 'light',
|
||||
'dark' = 'dark',
|
||||
}
|
||||
|
||||
// These are strings so they can be interpolated into class names.
|
||||
export enum ScrollBehavior {
|
||||
Default = 'default',
|
||||
Hard = 'hard',
|
||||
}
|
||||
|
|
|
@ -14384,7 +14384,7 @@
|
|||
"rule": "React-useRef",
|
||||
"path": "ts/components/ConversationList.js",
|
||||
"line": " const listRef = react_1.useRef(null);",
|
||||
"lineNumber": 49,
|
||||
"lineNumber": 58,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-02-12T16:25:08.285Z",
|
||||
"reasonDetail": "Used for scroll calculations"
|
||||
|
|
Loading…
Reference in a new issue