fix: virtualize list of search result so input typing is fast
This commit is contained in:
parent
dc3e8450e9
commit
43badfa134
|
@ -70,9 +70,10 @@
|
|||
"show": "Show",
|
||||
"sessionMessenger": "Session",
|
||||
"noSearchResults": "No results found for \"$searchTerm$\"",
|
||||
"conversationsHeader": "Contacts and Groups",
|
||||
"conversationsHeader": "Contacts and Groups: $count$",
|
||||
"contactsHeader": "Contacts",
|
||||
"messagesHeader": "Conversations",
|
||||
"searchMessagesHeader": "Messages: $count$",
|
||||
"settingsHeader": "Settings",
|
||||
"typingAlt": "Typing animation for this conversation",
|
||||
"contactAvatarAlt": "Avatar for contact $name$",
|
||||
|
|
|
@ -75,6 +75,7 @@
|
|||
"@emoji-mart/data": "^1.0.6",
|
||||
"@emoji-mart/react": "^1.0.1",
|
||||
"@reduxjs/toolkit": "1.8.5",
|
||||
"@types/react-mentions": "^4.1.8",
|
||||
"abort-controller": "3.0.0",
|
||||
"auto-bind": "^4.0.0",
|
||||
"backbone": "1.3.3",
|
||||
|
@ -122,7 +123,7 @@
|
|||
"react-draggable": "^4.4.4",
|
||||
"react-h5-audio-player": "^3.2.0",
|
||||
"react-intersection-observer": "^8.30.3",
|
||||
"react-mentions": "^4.2.0",
|
||||
"react-mentions": "^4.4.9",
|
||||
"react-portal": "^4.2.0",
|
||||
"react-qr-svg": "^2.2.1",
|
||||
"react-redux": "8.0.4",
|
||||
|
@ -169,7 +170,6 @@
|
|||
"@types/rc-slider": "^8.6.5",
|
||||
"@types/react": "^17.0.2",
|
||||
"@types/react-dom": "^17.0.2",
|
||||
"@types/react-mentions": "^4.1.1",
|
||||
"@types/react-mic": "^12.4.1",
|
||||
"@types/react-portal": "^4.0.2",
|
||||
"@types/react-redux": "^7.1.24",
|
||||
|
|
|
@ -4,7 +4,7 @@ import { useSelector } from 'react-redux';
|
|||
import styled from 'styled-components';
|
||||
import { SectionType } from '../../state/ducks/section';
|
||||
import { getLeftPaneConversationIds } from '../../state/selectors/conversations';
|
||||
import { getSearchResultsIdsOnly, isSearching } from '../../state/selectors/search';
|
||||
import { getHasSearchResults } from '../../state/selectors/search';
|
||||
import { getFocusedSection, getOverlayMode } from '../../state/selectors/section';
|
||||
import { SessionTheme } from '../../themes/SessionTheme';
|
||||
import { SessionToastContainer } from '../SessionToastContainer';
|
||||
|
@ -22,18 +22,14 @@ const StyledLeftPane = styled.div`
|
|||
`;
|
||||
|
||||
const InnerLeftPaneMessageSection = () => {
|
||||
const showSearch = useSelector(isSearching);
|
||||
|
||||
const searchResults = useSelector(getSearchResultsIdsOnly);
|
||||
|
||||
const hasSearchResults = useSelector(getHasSearchResults);
|
||||
const conversationIds = useSelector(getLeftPaneConversationIds);
|
||||
const overlayMode = useSelector(getOverlayMode);
|
||||
|
||||
return (
|
||||
// tslint:disable-next-line: use-simple-attributes
|
||||
<LeftPaneMessageSection
|
||||
conversationIds={showSearch ? undefined : conversationIds || []}
|
||||
searchResults={showSearch ? searchResults : undefined}
|
||||
hasSearchResults={hasSearchResults}
|
||||
conversationIds={conversationIds}
|
||||
overlayMode={overlayMode}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import autoBind from 'auto-bind';
|
||||
import React from 'react';
|
||||
import { AutoSizer, List, ListRowProps } from 'react-virtualized';
|
||||
import { SearchResults, SearchResultsProps } from '../search/SearchResults';
|
||||
import { SearchResults } from '../search/SearchResults';
|
||||
import { LeftPaneSectionHeader } from './LeftPaneSectionHeader';
|
||||
import { MessageRequestsBanner } from './MessageRequestsBanner';
|
||||
|
||||
|
@ -21,7 +21,7 @@ import { assertUnreachable } from '../../types/sqlSharedTypes';
|
|||
|
||||
export interface Props {
|
||||
conversationIds?: Array<string>;
|
||||
searchResults?: SearchResultsProps;
|
||||
hasSearchResults: boolean;
|
||||
overlayMode: OverlayMode | undefined;
|
||||
}
|
||||
|
||||
|
@ -88,10 +88,10 @@ export class LeftPaneMessageSection extends React.Component<Props> {
|
|||
};
|
||||
|
||||
public renderList(): JSX.Element {
|
||||
const { conversationIds, searchResults } = this.props;
|
||||
const { conversationIds, hasSearchResults } = this.props;
|
||||
|
||||
if (searchResults) {
|
||||
return <SearchResults {...searchResults} />;
|
||||
if (hasSearchResults) {
|
||||
return <SearchResults />;
|
||||
}
|
||||
|
||||
if (!conversationIds) {
|
||||
|
|
|
@ -6,7 +6,7 @@ import { ContactName } from '../conversation/ContactName';
|
|||
import { Avatar, AvatarSize } from '../avatar/Avatar';
|
||||
import { Timestamp } from '../conversation/Timestamp';
|
||||
import { MessageBodyHighlight } from '../basic/MessageBodyHighlight';
|
||||
import styled from 'styled-components';
|
||||
import styled, { CSSProperties } from 'styled-components';
|
||||
import { MessageAttributes } from '../../models/messageType';
|
||||
import { useConversationUsername, useIsPrivate } from '../../hooks/useParamSelector';
|
||||
import { UserUtils } from '../../session/utils';
|
||||
|
@ -172,7 +172,9 @@ const StyledTimestampContaimer = styled.div`
|
|||
color: var(--conversation-tab-text-color);
|
||||
`;
|
||||
|
||||
export const MessageSearchResult = (props: MessageResultProps) => {
|
||||
type MessageSearchResultProps = MessageResultProps & { style: CSSProperties };
|
||||
|
||||
export const MessageSearchResult = (props: MessageSearchResultProps) => {
|
||||
const {
|
||||
id,
|
||||
conversationId,
|
||||
|
@ -183,6 +185,7 @@ export const MessageSearchResult = (props: MessageResultProps) => {
|
|||
serverTimestamp,
|
||||
timestamp,
|
||||
direction,
|
||||
style,
|
||||
} = props;
|
||||
|
||||
/** destination is only used for search results (showing the `from:` and `to`)
|
||||
|
@ -210,6 +213,7 @@ export const MessageSearchResult = (props: MessageResultProps) => {
|
|||
return (
|
||||
<StyledSearchResults
|
||||
key={`div-msg-searchresult-${id}`}
|
||||
style={style}
|
||||
role="button"
|
||||
onClick={() => {
|
||||
void openConversationToSpecificMessage({
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import styled, { CSSProperties } from 'styled-components';
|
||||
import { ConversationListItem } from '../leftpane/conversation-list-item/ConversationListItem';
|
||||
import { MessageResultProps, MessageSearchResult } from './MessageSearchResults';
|
||||
|
||||
export type SearchResultsProps = {
|
||||
contactsAndGroupsIds: Array<string>;
|
||||
messages: Array<MessageResultProps>;
|
||||
searchTerm: string;
|
||||
};
|
||||
import { MessageSearchResult } from './MessageSearchResults';
|
||||
import { AutoSizer, List } from 'react-virtualized';
|
||||
import { useSelector } from 'react-redux';
|
||||
import {
|
||||
SearchResultsMergedListItem,
|
||||
getHasSearchResults,
|
||||
getSearchResultsList,
|
||||
getSearchTerm,
|
||||
} from '../../state/selectors/search';
|
||||
import { isString } from 'lodash';
|
||||
|
||||
const StyledSeparatorSection = styled.div`
|
||||
height: 36px;
|
||||
|
@ -35,37 +38,58 @@ const NoResults = styled.div`
|
|||
text-align: center;
|
||||
`;
|
||||
|
||||
export const SearchResults = (props: SearchResultsProps) => {
|
||||
const { contactsAndGroupsIds, messages, searchTerm } = props;
|
||||
const SectionHeader = ({ title, style }: { title: string; style: CSSProperties }) => {
|
||||
return <StyledSeparatorSection style={style}>{title}</StyledSeparatorSection>;
|
||||
};
|
||||
|
||||
const haveContactsAndGroup = Boolean(contactsAndGroupsIds?.length);
|
||||
const haveMessages = Boolean(messages?.length);
|
||||
const noResults = !haveContactsAndGroup && !haveMessages;
|
||||
function isContact(item: SearchResultsMergedListItem): item is { contactConvoId: string } {
|
||||
return (item as any).contactConvoId !== undefined;
|
||||
}
|
||||
|
||||
const VirtualizedList = () => {
|
||||
const searchResultList = useSelector(getSearchResultsList);
|
||||
return (
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<List
|
||||
height={height}
|
||||
rowCount={searchResultList.length}
|
||||
rowHeight={rowPos => {
|
||||
return isString(searchResultList[rowPos.index]) ? 36 : 64;
|
||||
}}
|
||||
rowRenderer={({ index, key, style }) => {
|
||||
const row = searchResultList[index];
|
||||
if (!row) {
|
||||
return null;
|
||||
}
|
||||
if (isString(row)) {
|
||||
return <SectionHeader title={row} style={style} key={key} />;
|
||||
}
|
||||
if (isContact(row)) {
|
||||
return (
|
||||
<ConversationListItem conversationId={row.contactConvoId} style={style} key={key} />
|
||||
);
|
||||
}
|
||||
return <MessageSearchResult style={style} key={key} {...row} />;
|
||||
}}
|
||||
width={width}
|
||||
autoHeight={false}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
);
|
||||
};
|
||||
|
||||
export const SearchResults = () => {
|
||||
const searchTerm = useSelector(getSearchTerm);
|
||||
const hasSearchResults = useSelector(getHasSearchResults);
|
||||
|
||||
return (
|
||||
<SearchResultsContainer>
|
||||
{noResults ? <NoResults>{window.i18n('noSearchResults', [searchTerm])}</NoResults> : null}
|
||||
{haveContactsAndGroup ? (
|
||||
<>
|
||||
<StyledSeparatorSection>{window.i18n('conversationsHeader')}</StyledSeparatorSection>
|
||||
{contactsAndGroupsIds.map(conversationId => (
|
||||
<ConversationListItem
|
||||
conversationId={conversationId}
|
||||
key={`search-result-convo-${conversationId}`}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{haveMessages && (
|
||||
<>
|
||||
<StyledSeparatorSection>
|
||||
{`${window.i18n('messagesHeader')}: ${messages.length}`}
|
||||
</StyledSeparatorSection>
|
||||
{messages.map(message => (
|
||||
<MessageSearchResult key={`search-result-message-${message.id}`} {...message} />
|
||||
))}
|
||||
</>
|
||||
{!hasSearchResults ? (
|
||||
<NoResults>{window.i18n('noSearchResults', [searchTerm])}</NoResults>
|
||||
) : (
|
||||
<VirtualizedList />
|
||||
)}
|
||||
</SearchResultsContainer>
|
||||
);
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { compact } from 'lodash';
|
||||
import { compact, isEmpty } from 'lodash';
|
||||
|
||||
import { StateType } from '../reducer';
|
||||
|
||||
import { ConversationLookupType } from '../ducks/conversations';
|
||||
import { SearchStateType } from '../ducks/search';
|
||||
import { getConversationLookup } from './conversations';
|
||||
import { MessageResultProps } from '../../components/search/MessageSearchResults';
|
||||
|
||||
export const getSearch = (state: StateType): SearchStateType => state.search;
|
||||
|
||||
|
@ -41,6 +42,10 @@ const getSearchResults = createSelector(
|
|||
}
|
||||
);
|
||||
|
||||
export const getSearchTerm = createSelector([getSearchResults], searchResult => {
|
||||
return searchResult.searchTerm;
|
||||
});
|
||||
|
||||
export const getSearchResultsIdsOnly = createSelector([getSearchResults], searchState => {
|
||||
return {
|
||||
...searchState,
|
||||
|
@ -48,6 +53,32 @@ export const getSearchResultsIdsOnly = createSelector([getSearchResults], search
|
|||
};
|
||||
});
|
||||
|
||||
export const getHasSearchResults = createSelector([getSearchResults], searchState => {
|
||||
return !isEmpty(searchState.contactsAndGroups) || !isEmpty(searchState.messages);
|
||||
});
|
||||
|
||||
export const getSearchResultsContactOnly = createSelector([getSearchResults], searchState => {
|
||||
return searchState.contactsAndGroups.filter(m => m.isPrivate).map(m => m.id);
|
||||
});
|
||||
|
||||
/**
|
||||
*
|
||||
* When type is string, we render a sectionHeader.
|
||||
* When type just has a conversationId field, we render a ConversationListItem.
|
||||
* When type is MessageResultProps we render a MessageSearchResult
|
||||
*/
|
||||
export type SearchResultsMergedListItem = string | { contactConvoId: string } | MessageResultProps;
|
||||
|
||||
export const getSearchResultsList = createSelector([getSearchResults], searchState => {
|
||||
const { contactsAndGroups, messages } = searchState;
|
||||
const builtList: Array<SearchResultsMergedListItem> = [];
|
||||
if (contactsAndGroups.length) {
|
||||
builtList.push(window.i18n('conversationsHeader', [`${contactsAndGroups.length}`]));
|
||||
builtList.push(...contactsAndGroups.map(m => ({ contactConvoId: m.id })));
|
||||
}
|
||||
if (messages.length) {
|
||||
builtList.push(window.i18n('searchMessagesHeader', [`${messages.length}`]));
|
||||
builtList.push(...messages);
|
||||
}
|
||||
return builtList;
|
||||
});
|
||||
|
|
|
@ -73,6 +73,7 @@ export type LocalizerKeys =
|
|||
| 'conversationsHeader'
|
||||
| 'contactsHeader'
|
||||
| 'messagesHeader'
|
||||
| 'searchMessagesHeader'
|
||||
| 'settingsHeader'
|
||||
| 'typingAlt'
|
||||
| 'contactAvatarAlt'
|
||||
|
|
32
yarn.lock
32
yarn.lock
|
@ -411,13 +411,20 @@
|
|||
dependencies:
|
||||
regenerator-runtime "^0.13.2"
|
||||
|
||||
"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
|
||||
"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
|
||||
version "7.19.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.0.tgz#22b11c037b094d27a8a2504ea4dcff00f50e2259"
|
||||
integrity sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/runtime@^7.3.4":
|
||||
version "7.22.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.5.tgz#8564dd588182ce0047d55d7a75e93921107b57ec"
|
||||
integrity sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.11"
|
||||
|
||||
"@babel/template@^7.18.6":
|
||||
version "7.18.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.6.tgz#1283f4993e00b929d6e2d3c72fdc9168a2977a31"
|
||||
|
@ -1192,10 +1199,10 @@
|
|||
dependencies:
|
||||
"@types/react" "^17"
|
||||
|
||||
"@types/react-mentions@^4.1.1":
|
||||
version "4.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-mentions/-/react-mentions-4.1.6.tgz#0ecdb61785c22edbf9c7d6718505d4814ad3a65c"
|
||||
integrity sha512-f4/BdnjlMxT47q+WqlcYYwFABbBMVQrDoFFeMeljtFC5nnR9/x8TOFmN18BJKgNuWMgivy9uE5EKtsjlay751w==
|
||||
"@types/react-mentions@^4.1.8":
|
||||
version "4.1.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-mentions/-/react-mentions-4.1.8.tgz#4bebe54c5c74181d8eedf1e613a208d03b4a8d7e"
|
||||
integrity sha512-Go86ozdnh0FTNbiGiDPAcNqYqtab9iGzLOgZPYUKrnhI4539jGzfJtP6rFHcXgi9Koe58yhkeyKYib6Ucul/sQ==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
|
@ -6551,10 +6558,10 @@ react-lifecycles-compat@^3.0.4:
|
|||
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
|
||||
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
|
||||
|
||||
react-mentions@^4.2.0:
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/react-mentions/-/react-mentions-4.3.2.tgz#93a2a674648f31d2b40e8c90d30b94c377cbb582"
|
||||
integrity sha512-NV8ixuE5W9zuvBNWLpPlO+f4QYEkR+p6mR3Jfpfcbytrqqn2nbVb27YXE/M4qSP8N8C+ktgeMUV4jVhm86gt1A==
|
||||
react-mentions@^4.4.9:
|
||||
version "4.4.9"
|
||||
resolved "https://registry.yarnpkg.com/react-mentions/-/react-mentions-4.4.9.tgz#5f68c7978c107518646c5c34c47515c30259e23a"
|
||||
integrity sha512-CUDt8GOVbAmo3o+a8l1UxcJ/gJMdFdqeiJM3U5+krcNoUwyKv7Zcy67WfFZQJfChpJ8LTiD0FtCSRoyEzC6Ysw==
|
||||
dependencies:
|
||||
"@babel/runtime" "7.4.5"
|
||||
invariant "^2.2.4"
|
||||
|
@ -6759,7 +6766,12 @@ regenerator-runtime@^0.11.0:
|
|||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
|
||||
integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
|
||||
|
||||
regenerator-runtime@^0.13.2, regenerator-runtime@^0.13.4:
|
||||
regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.2:
|
||||
version "0.13.11"
|
||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
|
||||
integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
|
||||
|
||||
regenerator-runtime@^0.13.4:
|
||||
version "0.13.9"
|
||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
|
||||
integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==
|
||||
|
|
Loading…
Reference in New Issue