move search results to styled components

and cleanup search logic and rendering of message results
pull/2142/head
Audric Ackermann 3 years ago
parent 108d810fde
commit 44f61073dc
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4

@ -85,7 +85,7 @@
"sessionMessenger": "Session", "sessionMessenger": "Session",
"search": "Search", "search": "Search",
"noSearchResults": "No results found for \"$searchTerm$\"", "noSearchResults": "No results found for \"$searchTerm$\"",
"conversationsHeader": "Conversations", "conversationsHeader": "Contacts and Groups",
"contactsHeader": "Contacts", "contactsHeader": "Contacts",
"messagesHeader": "Messages", "messagesHeader": "Messages",
"settingsHeader": "Settings", "settingsHeader": "Settings",

@ -1359,129 +1359,6 @@
@include color-svg('../images/x-16.svg', $color-gray-60); @include color-svg('../images/x-16.svg', $color-gray-60);
} }
// Module: Search Results
.module-search-results {
overflow-y: auto;
max-height: 100%;
color: var(--color-text);
}
.module-search-results__conversations-header {
height: 36px;
line-height: 36px;
margin-inline-start: 16px;
font-size: 14px;
font-weight: 300;
letter-spacing: 0;
}
.module-search-results__no-results {
margin-top: 27px;
width: 100%;
text-align: center;
}
.module-search-results__contacts-header {
height: 36px;
line-height: 36px;
margin-inline-start: 16px;
font-size: 14px;
font-weight: 300;
letter-spacing: 0;
}
.module-search-results__messages-header {
height: 36px;
line-height: 36px;
margin-inline-start: 16px;
font-size: 14px;
font-weight: 300;
letter-spacing: 0;
}
// Module: Message Search Result
.module-message-search-result {
padding: 8px;
padding-inline-start: 16px;
padding-inline-end: 16px;
min-height: 64px;
max-width: 300px;
display: flex;
flex-direction: row;
align-items: flex-start;
cursor: pointer;
&:hover {
background-color: var(--color-clickable-hovered);
}
}
.module-message-search-result__text {
flex-grow: 1;
margin-inline-start: 12px;
// parent - 48px (for avatar) - 16px (our right margin)
max-width: calc(100% - 64px);
display: inline-flex;
flex-direction: column;
align-items: stretch;
}
.module-message-search-result__header {
display: flex;
flex-direction: row;
align-items: center;
}
.module-message-search-result__header__from {
flex-grow: 1;
flex-shrink: 1;
font-size: 14px;
line-height: 18px;
overflow-x: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: var(--color-text);
}
.module-message-search-result__header__timestamp {
flex-shrink: 0;
margin-inline-start: 6px;
font-size: 11px;
line-height: 16px;
letter-spacing: 0.3px;
overflow-x: hidden;
white-space: nowrap;
text-overflow: ellipsis;
text-transform: uppercase;
color: $color-gray-60;
}
.module-message-search-result__header__name {
font-weight: 300;
}
.module-messages-search-result__header__group {
font-weight: 300;
.module-contact-name {
display: initial;
}
}
// Module: Left Pane // Module: Left Pane
.module-left-pane { .module-left-pane {

@ -184,10 +184,6 @@ $session-compose-margin: 20px;
width: -webkit-fill-available; width: -webkit-fill-available;
} }
.module-search-results {
width: -webkit-fill-available;
}
.session-description-long { .session-description-long {
font-size: $session-font-sm; font-size: $session-font-sm;
line-height: $session-font-h3; line-height: $session-font-h3;
@ -217,9 +213,6 @@ $session-compose-margin: 20px;
} }
} }
} }
.module-search-results {
flex-grow: 1;
}
.module-conversations-list-content { .module-conversations-list-content {
overflow-x: hidden; overflow-x: hidden;

@ -383,18 +383,6 @@
@include color-svg('../images/x-16.svg', $color-gray-25); @include color-svg('../images/x-16.svg', $color-gray-25);
} }
// Module: Spinner
.module-search-results__conversations-header {
color: $color-gray-05;
}
.module-search-results__contacts-header {
color: $color-gray-05;
}
.module-search-results__messages-header {
color: $color-gray-05;
}
// Module: Message Search Result // Module: Message Search Result
.module-message-search-result { .module-message-search-result {
@ -403,14 +391,6 @@
} }
} }
.module-message-search-result__header__from {
color: $color-gray-05;
}
.module-message-search-result__header__timestamp {
color: $color-gray-25;
}
.module-message__link-preview__icon-container__circle-background { .module-message__link-preview__icon-container__circle-background {
background-color: $color-gray-25; background-color: $color-gray-25;
} }

@ -7,25 +7,10 @@ import styled from 'styled-components';
type Props = { type Props = {
timestamp?: number; timestamp?: number;
module?: string;
withImageNoCaption?: boolean;
isConversationListItem?: boolean; isConversationListItem?: boolean;
momentFromNow: boolean; momentFromNow: boolean;
}; };
const TimestampContainerListItem = styled.div`
flex-shrink: 0;
margin-inline-start: 6px;
font-size: 11px;
line-height: 16px;
letter-spacing: 0.3px;
overflow-x: hidden;
white-space: nowrap;
text-overflow: ellipsis;
text-transform: uppercase;
color: var(--color-text);
`;
const UPDATE_FREQUENCY = 60 * 1000; const UPDATE_FREQUENCY = 60 * 1000;
const TimestampContainerNotListItem = styled.div` const TimestampContainerNotListItem = styled.div`
@ -34,7 +19,15 @@ const TimestampContainerNotListItem = styled.div`
letter-spacing: 0.3px; letter-spacing: 0.3px;
text-transform: uppercase; text-transform: uppercase;
user-select: none; user-select: none;
color: var(--color-text); color: var(--color-text-subtle);
`;
const TimestampContainerListItem = styled(TimestampContainerNotListItem)`
flex-shrink: 0;
margin-inline-start: 6px;
overflow-x: hidden;
white-space: nowrap;
text-overflow: ellipsis;
`; `;
export const Timestamp = (props: Props) => { export const Timestamp = (props: Props) => {

@ -750,6 +750,7 @@ class CompositionBoxInner extends React.Component<Props, State> {
} else if (event.key === 'PageUp' || event.key === 'PageDown') { } else if (event.key === 'PageUp' || event.key === 'PageDown') {
// swallow pageUp events if they occurs on the composition box (it breaks the app layout) // swallow pageUp events if they occurs on the composition box (it breaks the app layout)
event.preventDefault(); event.preventDefault();
event.stopPropagation();
} }
} }

@ -41,7 +41,7 @@ export class LeftPaneMessageSection extends React.Component<Props> {
super(props); super(props);
autoBind(this); autoBind(this);
this.debouncedSearch = _.debounce(this.search.bind(this), 20); this.debouncedSearch = _.debounce(this.search.bind(this), 50);
} }
public renderRow = ({ index, key, style }: RowRendererParamsType): JSX.Element | null => { public renderRow = ({ index, key, style }: RowRendererParamsType): JSX.Element | null => {
@ -62,10 +62,9 @@ export class LeftPaneMessageSection extends React.Component<Props> {
public renderList(): JSX.Element | Array<JSX.Element | null> { public renderList(): JSX.Element | Array<JSX.Element | null> {
const { conversations, searchResults } = this.props; const { conversations, searchResults } = this.props;
const contacts = searchResults?.contacts || [];
if (searchResults) { if (searchResults) {
return <SearchResults {...searchResults} contacts={contacts} />; return <SearchResults {...searchResults} />;
} }
if (!conversations) { if (!conversations) {
@ -160,11 +159,11 @@ export class LeftPaneMessageSection extends React.Component<Props> {
this.debouncedSearch(cleanedTerm); this.debouncedSearch(cleanedTerm);
} }
public clearSearch() { private clearSearch() {
window.inboxStore?.dispatch(clearSearch()); window.inboxStore?.dispatch(clearSearch());
} }
public search() { private search() {
const { searchTerm } = this.props; const { searchTerm } = this.props;
window.inboxStore?.dispatch( window.inboxStore?.dispatch(
search(searchTerm, { search(searchTerm, {

@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import styled from 'styled-components'; import styled from 'styled-components';

@ -1,5 +1,4 @@
import React from 'react'; import React from 'react';
import classNames from 'classnames';
import { getOurPubKeyStrFromCache } from '../../session/utils/User'; import { getOurPubKeyStrFromCache } from '../../session/utils/User';
import { openConversationToSpecificMessage } from '../../state/ducks/conversations'; import { openConversationToSpecificMessage } from '../../state/ducks/conversations';
@ -30,10 +29,45 @@ const StyledConversationFromUserInGroup = styled(StyledConversationTitleResults)
font-size: 12px; font-size: 12px;
line-height: 14px; line-height: 14px;
overflow-x: hidden; overflow-x: hidden;
font-weight: 400; font-weight: 700;
color: var(--color-text-subtle); color: var(--color-text-subtle);
`; `;
const StyledSearchResulsts = styled.div`
padding: 8px;
padding-inline-start: 16px;
padding-inline-end: 16px;
min-height: 64px;
max-width: 300px;
display: flex;
flex-direction: row;
align-items: flex-start;
cursor: pointer;
&:hover {
background-color: var(--color-clickable-hovered);
}
`;
const StyledResultText = styled.div`
flex-grow: 1;
margin-inline-start: 12px;
display: inline-flex;
flex-direction: column;
align-items: stretch;
`;
const ResultsHeader = styled.div`
display: flex;
flex-direction: row;
align-items: center;
`;
const StyledMessageResultsHeaderName = styled.span`
font-weight: 300;
`;
const FromName = (props: { source: string; conversationId: string }) => { const FromName = (props: { source: string; conversationId: string }) => {
const { conversationId, source } = props; const { conversationId, source } = props;
@ -41,14 +75,12 @@ const FromName = (props: { source: string; conversationId: string }) => {
if (isNoteToSelf) { if (isNoteToSelf) {
return ( return (
<span className="module-message-search-result__header__name"> <StyledMessageResultsHeaderName>{window.i18n('noteToSelf')}</StyledMessageResultsHeaderName>
{window.i18n('noteToSelf')}
</span>
); );
} }
if (source === getOurPubKeyStrFromCache()) { if (source === getOurPubKeyStrFromCache()) {
return <span className="module-message-search-result__header__name">{window.i18n('you')}</span>; return <StyledMessageResultsHeaderName>{window.i18n('you')}</StyledMessageResultsHeaderName>;
} }
return ( return (
@ -68,9 +100,9 @@ const ConversationHeader = (props: { source: string; conversationId: string }) =
if (conversationId !== ourKey) { if (conversationId !== ourKey) {
return ( return (
<StyledConversationTitleResults> <StyledConversationTitleResults>
<span className="module-messages-search-result__header__group"> <StyledMessageResultsHeaderName>
<ContactName pubkey={conversationId} shouldShowPubkey={false} boldProfileName={true} /> <ContactName pubkey={conversationId} shouldShowPubkey={false} boldProfileName={true} />
</span> </StyledMessageResultsHeaderName>
</StyledConversationTitleResults> </StyledConversationTitleResults>
); );
} }
@ -121,6 +153,23 @@ const ResultBody = styled.div`
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
`; `;
const StyledTimestampContaimer = styled.div`
flex-shrink: 0;
margin-inline-start: 6px;
font-size: 11px;
line-height: 16px;
letter-spacing: 0.3px;
overflow-x: hidden;
white-space: nowrap;
text-overflow: ellipsis;
text-transform: uppercase;
color: var(--color-text-subtle);
`;
export const MessageSearchResult = (props: MessageResultProps) => { export const MessageSearchResult = (props: MessageResultProps) => {
const { const {
id, id,
@ -156,7 +205,7 @@ export const MessageSearchResult = (props: MessageResultProps) => {
} }
return ( return (
<div <StyledSearchResulsts
key={`div-msg-searchresult-${id}`} key={`div-msg-searchresult-${id}`}
role="button" role="button"
onClick={() => { onClick={() => {
@ -166,24 +215,23 @@ export const MessageSearchResult = (props: MessageResultProps) => {
shouldHighlightMessage: true, shouldHighlightMessage: true,
}); });
}} }}
className={classNames('module-message-search-result')}
> >
<AvatarItem source={conversationId} /> <AvatarItem source={conversationId} />
<div className="module-message-search-result__text"> <StyledResultText>
<div className="module-message-search-result__header"> <ResultsHeader>
<ConversationHeader source={destination} conversationId={conversationId} /> <ConversationHeader source={destination} conversationId={conversationId} />
<div className="module-message-search-result__header__timestamp"> <StyledTimestampContaimer>
<Timestamp <Timestamp
timestamp={serverTimestamp || timestamp || sent_at || received_at} timestamp={serverTimestamp || timestamp || sent_at || received_at}
momentFromNow={false} momentFromNow={false}
/> />
</div> </StyledTimestampContaimer>
</div> </ResultsHeader>
<ResultBody> <ResultBody>
<FromUserInGroup authorPubkey={source} conversationId={conversationId} /> <FromUserInGroup authorPubkey={source} conversationId={conversationId} />
<MessageBodyHighlight text={snippet || ''} /> <MessageBodyHighlight text={snippet || ''} />
</ResultBody> </ResultBody>
</div> </StyledResultText>
</div> </StyledSearchResulsts>
); );
}; };

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import styled from 'styled-components';
import { import {
ConversationListItemProps, ConversationListItemProps,
MemoConversationListItemWithDetails, MemoConversationListItemWithDetails,
@ -6,69 +7,70 @@ import {
import { MessageResultProps, MessageSearchResult } from './MessageSearchResults'; import { MessageResultProps, MessageSearchResult } from './MessageSearchResults';
export type SearchResultsProps = { export type SearchResultsProps = {
contacts: Array<ConversationListItemProps>; contactsAndGroups: Array<ConversationListItemProps>;
conversations: Array<ConversationListItemProps>;
messages: Array<MessageResultProps>; messages: Array<MessageResultProps>;
searchTerm: string; searchTerm: string;
}; };
const ContactsItem = (props: { header: string; items: Array<ConversationListItemProps> }) => { const StyledSeparatorSection = styled.div`
return ( height: 36px;
<div className="module-search-results__contacts"> line-height: 36px;
<div className="module-search-results__contacts-header">{props.header}</div>
{props.items.map(contact => ( margin-inline-start: 16px;
<MemoConversationListItemWithDetails
{...contact} color: var(--color-text);
key={`search-result-contact-${contact.id}`}
/> font-size: 14px;
))} font-weight: 700;
</div> letter-spacing: 0;
); `;
};
const SearchResultsContainer = styled.div`
overflow-y: auto;
max-height: 100%;
color: var(--color-text);
flex-grow: 1;
width: -webkit-fill-available;
`;
const NoResults = styled.div`
margin-top: 27px;
width: 100%;
text-align: center;
`;
export const SearchResults = (props: SearchResultsProps) => { export const SearchResults = (props: SearchResultsProps) => {
const { conversations, contacts, messages, searchTerm } = props; const { contactsAndGroups, messages, searchTerm } = props;
const haveConversations = conversations && conversations.length; const haveContactsAndGroup = contactsAndGroups?.length;
const haveContacts = contacts && contacts.length; const haveMessages = Boolean(messages?.length);
const haveMessages = messages && messages.length; const noResults = !haveContactsAndGroup && !haveMessages;
const noResults = !haveConversations && !haveContacts && !haveMessages;
return ( return (
<div className="module-search-results"> <SearchResultsContainer>
{noResults ? ( {noResults ? <NoResults>{window.i18n('noSearchResults', [searchTerm])}</NoResults> : null}
<div className="module-search-results__no-results"> {haveContactsAndGroup ? (
{window.i18n('noSearchResults', [searchTerm])} <div>
</div> <StyledSeparatorSection>{window.i18n('conversationsHeader')}</StyledSeparatorSection>
) : null} {contactsAndGroups.map(contactOrGroup => (
{haveConversations ? (
<div className="module-search-results__conversations">
<div className="module-search-results__conversations-header">
{window.i18n('conversationsHeader')}
</div>
{conversations.map(conversation => (
<MemoConversationListItemWithDetails <MemoConversationListItemWithDetails
{...conversation} {...contactOrGroup}
mentionedUs={false} mentionedUs={false}
key={`search-result-convo-${conversation.id}`} key={`search-result-convo-${contactOrGroup.id}`}
/> />
))} ))}
</div> </div>
) : null} ) : null}
{haveContacts ? (
<ContactsItem header={window.i18n('contactsHeader')} items={contacts} />
) : null}
{haveMessages ? ( {haveMessages && (
<div className="module-search-results__messages"> <div>
<div className="module-search-results__messages-header"> <StyledSeparatorSection>
{`${window.i18n('messagesHeader')}: ${messages.length}`} {`${window.i18n('messagesHeader')}: ${messages.length}`}
</div> </StyledSeparatorSection>
{messages.map(message => ( {messages.map(message => (
<MessageSearchResult key={`search-result-message-${message.id}`} {...message} /> <MessageSearchResult key={`search-result-message-${message.id}`} {...message} />
))} ))}
</div> </div>
) : null} )}
</div> </SearchResultsContainer>
); );
}; };

@ -1,66 +0,0 @@
import React from 'react';
import classNames from 'classnames';
import { PubKey } from '../../session/types';
import { ConversationListItemProps } from '../leftpane/conversation-list-item/ConversationListItem';
export type Props = {
contacts: Array<ConversationListItemProps>;
searchTerm: string;
selectedContact: number;
onContactSelected: any;
};
export class UserSearchResults extends React.Component<Props> {
public constructor(props: Props) {
super(props);
}
public render() {
const { contacts, searchTerm } = this.props;
const noResults = !contacts || contacts.length <= 0;
return (
<div className="module-search-results">
{noResults ? (
<div className="module-search-results__no-results">
{window.i18n('noSearchResults', [searchTerm])}
</div>
) : (
this.renderContacts(contacts)
)}
</div>
);
}
private renderContacts(items: Array<ConversationListItemProps>) {
return (
<div className="contacts-dropdown">
{items.map((contact, index) => this.renderContact(contact, index))}
</div>
);
}
private renderContact(contact: ConversationListItemProps, index: Number) {
const { profileName, id } = contact;
const { selectedContact } = this.props;
const shortenedPubkey = PubKey.shorten(id);
const rowContent = `${profileName} ${shortenedPubkey}`;
return (
<div
className={classNames(
'contacts-dropdown-row',
selectedContact === index && 'contacts-dropdown-row-selected'
)}
key={contact.id}
onClick={() => this.props.onContactSelected(contact.id)}
role="button"
>
{rowContent}
</div>
);
}
}

@ -14,12 +14,8 @@ import { MessageResultProps } from '../../components/search/MessageSearchResults
export type SearchStateType = { export type SearchStateType = {
query: string; query: string;
normalizedPhoneNumber?: string; normalizedPhoneNumber?: string;
// We need to store messages here, because they aren't anywhere else in state
selectedMessage?: string;
// For conversations we store just the id, and pull conversation props in the selector // For conversations we store just the id, and pull conversation props in the selector
conversations: Array<string>; contactsAndGroups: Array<string>;
contacts: Array<string>;
messages?: Array<MessageResultProps>; messages?: Array<MessageResultProps>;
}; };
@ -27,8 +23,7 @@ export type SearchStateType = {
type SearchResultsPayloadType = { type SearchResultsPayloadType = {
query: string; query: string;
normalizedPhoneNumber?: string; normalizedPhoneNumber?: string;
conversations: Array<string>; contactsAndGroups: Array<string>;
contacts: Array<string>;
messages?: Array<MessageResultProps>; messages?: Array<MessageResultProps>;
}; };
@ -81,6 +76,7 @@ async function doSearch(query: string, options: SearchOptions): Promise<SearchRe
queryMessages(processedQuery), queryMessages(processedQuery),
]); ]);
const { conversations, contacts } = discussions; const { conversations, contacts } = discussions;
const contactsAndGroups = _.uniq([...conversations, ...contacts]);
const filteredMessages = _.compact(messages); const filteredMessages = _.compact(messages);
// if (isAdvancedQuery) { // if (isAdvancedQuery) {
// const senderFilterQuery = // const senderFilterQuery =
@ -96,8 +92,7 @@ async function doSearch(query: string, options: SearchOptions): Promise<SearchRe
return { return {
query, query,
normalizedPhoneNumber: PubKey.normalize(query), normalizedPhoneNumber: PubKey.normalize(query),
conversations, contactsAndGroups,
contacts,
messages: filteredMessages, messages: filteredMessages,
}; };
} }
@ -251,8 +246,7 @@ async function queryConversationsAndContacts(providedQuery: string, options: Sea
export const initialSearchState: SearchStateType = { export const initialSearchState: SearchStateType = {
query: '', query: '',
conversations: [], contactsAndGroups: [],
contacts: [],
messages: [], messages: [],
}; };
@ -281,7 +275,7 @@ export function reducer(state: SearchStateType | undefined, action: SEARCH_TYPES
if (action.type === 'SEARCH_RESULTS_FULFILLED') { if (action.type === 'SEARCH_RESULTS_FULFILLED') {
const { payload } = action; const { payload } = action;
const { query, normalizedPhoneNumber, conversations, contacts, messages } = payload; const { query, normalizedPhoneNumber, contactsAndGroups, messages } = payload;
// Reject if the associated query is not the most recent user-provided query // Reject if the associated query is not the most recent user-provided query
if (state.query !== query) { if (state.query !== query) {
return state; return state;
@ -291,8 +285,7 @@ export function reducer(state: SearchStateType | undefined, action: SEARCH_TYPES
...state, ...state,
query, query,
normalizedPhoneNumber, normalizedPhoneNumber,
conversations, contactsAndGroups,
contacts,
messages, messages,
}; };
} }

@ -21,22 +21,8 @@ export const getSearchResults = createSelector(
[getSearch, getConversationLookup, getSelectedConversationKey], [getSearch, getConversationLookup, getSelectedConversationKey],
(searchState: SearchStateType, lookup: ConversationLookupType, selectedConversation?: string) => { (searchState: SearchStateType, lookup: ConversationLookupType, selectedConversation?: string) => {
return { return {
contacts: compact( contactsAndGroups: compact(
searchState.contacts.map(id => { searchState.contactsAndGroups.map(id => {
const value = lookup[id];
if (value && id === selectedConversation) {
return {
...value,
isSelected: true,
};
}
return value;
})
),
conversations: compact(
searchState.conversations.map(id => {
const value = lookup[id]; const value = lookup[id];
// Don't return anything when activeAt is unset (i.e. no current conversations with this user) // Don't return anything when activeAt is unset (i.e. no current conversations with this user)

@ -3,6 +3,7 @@ export function cleanSearchTerm(searchTerm: string) {
const withoutSpecialCharacters = lowercase.replace(/([!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~])/g, ' '); const withoutSpecialCharacters = lowercase.replace(/([!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~])/g, ' ');
const whiteSpaceNormalized = withoutSpecialCharacters.replace(/\s+/g, ' '); const whiteSpaceNormalized = withoutSpecialCharacters.replace(/\s+/g, ' ');
const byToken = whiteSpaceNormalized.split(' '); const byToken = whiteSpaceNormalized.split(' ');
// be aware that a user typing Note To Self will have an issue when the `not` part of it is typed as the not word is reserved
const withoutSpecialTokens = byToken.filter( const withoutSpecialTokens = byToken.filter(
token => token =>
token && token &&

Loading…
Cancel
Save