move search results to styled components

and cleanup search logic and rendering of message results
pull/2142/head
Audric Ackermann 2 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",
"search": "Search",
"noSearchResults": "No results found for \"$searchTerm$\"",
"conversationsHeader": "Conversations",
"conversationsHeader": "Contacts and Groups",
"contactsHeader": "Contacts",
"messagesHeader": "Messages",
"settingsHeader": "Settings",

@ -1359,129 +1359,6 @@
@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 {

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

@ -383,18 +383,6 @@
@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 {
@ -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 {
background-color: $color-gray-25;
}

@ -7,25 +7,10 @@ import styled from 'styled-components';
type Props = {
timestamp?: number;
module?: string;
withImageNoCaption?: boolean;
isConversationListItem?: 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 TimestampContainerNotListItem = styled.div`
@ -34,7 +19,15 @@ const TimestampContainerNotListItem = styled.div`
letter-spacing: 0.3px;
text-transform: uppercase;
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) => {

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

@ -41,7 +41,7 @@ export class LeftPaneMessageSection extends React.Component<Props> {
super(props);
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 => {
@ -62,10 +62,9 @@ export class LeftPaneMessageSection extends React.Component<Props> {
public renderList(): JSX.Element | Array<JSX.Element | null> {
const { conversations, searchResults } = this.props;
const contacts = searchResults?.contacts || [];
if (searchResults) {
return <SearchResults {...searchResults} contacts={contacts} />;
return <SearchResults {...searchResults} />;
}
if (!conversations) {
@ -160,11 +159,11 @@ export class LeftPaneMessageSection extends React.Component<Props> {
this.debouncedSearch(cleanedTerm);
}
public clearSearch() {
private clearSearch() {
window.inboxStore?.dispatch(clearSearch());
}
public search() {
private search() {
const { searchTerm } = this.props;
window.inboxStore?.dispatch(
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 styled from 'styled-components';

@ -1,5 +1,4 @@
import React from 'react';
import classNames from 'classnames';
import { getOurPubKeyStrFromCache } from '../../session/utils/User';
import { openConversationToSpecificMessage } from '../../state/ducks/conversations';
@ -30,10 +29,45 @@ const StyledConversationFromUserInGroup = styled(StyledConversationTitleResults)
font-size: 12px;
line-height: 14px;
overflow-x: hidden;
font-weight: 400;
font-weight: 700;
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 { conversationId, source } = props;
@ -41,14 +75,12 @@ const FromName = (props: { source: string; conversationId: string }) => {
if (isNoteToSelf) {
return (
<span className="module-message-search-result__header__name">
{window.i18n('noteToSelf')}
</span>
<StyledMessageResultsHeaderName>{window.i18n('noteToSelf')}</StyledMessageResultsHeaderName>
);
}
if (source === getOurPubKeyStrFromCache()) {
return <span className="module-message-search-result__header__name">{window.i18n('you')}</span>;
return <StyledMessageResultsHeaderName>{window.i18n('you')}</StyledMessageResultsHeaderName>;
}
return (
@ -68,9 +100,9 @@ const ConversationHeader = (props: { source: string; conversationId: string }) =
if (conversationId !== ourKey) {
return (
<StyledConversationTitleResults>
<span className="module-messages-search-result__header__group">
<StyledMessageResultsHeaderName>
<ContactName pubkey={conversationId} shouldShowPubkey={false} boldProfileName={true} />
</span>
</StyledMessageResultsHeaderName>
</StyledConversationTitleResults>
);
}
@ -121,6 +153,23 @@ const ResultBody = styled.div`
-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) => {
const {
id,
@ -156,7 +205,7 @@ export const MessageSearchResult = (props: MessageResultProps) => {
}
return (
<div
<StyledSearchResulsts
key={`div-msg-searchresult-${id}`}
role="button"
onClick={() => {
@ -166,24 +215,23 @@ export const MessageSearchResult = (props: MessageResultProps) => {
shouldHighlightMessage: true,
});
}}
className={classNames('module-message-search-result')}
>
<AvatarItem source={conversationId} />
<div className="module-message-search-result__text">
<div className="module-message-search-result__header">
<StyledResultText>
<ResultsHeader>
<ConversationHeader source={destination} conversationId={conversationId} />
<div className="module-message-search-result__header__timestamp">
<StyledTimestampContaimer>
<Timestamp
timestamp={serverTimestamp || timestamp || sent_at || received_at}
momentFromNow={false}
/>
</div>
</div>
</StyledTimestampContaimer>
</ResultsHeader>
<ResultBody>
<FromUserInGroup authorPubkey={source} conversationId={conversationId} />
<MessageBodyHighlight text={snippet || ''} />
</ResultBody>
</div>
</div>
</StyledResultText>
</StyledSearchResulsts>
);
};

@ -1,4 +1,5 @@
import React from 'react';
import styled from 'styled-components';
import {
ConversationListItemProps,
MemoConversationListItemWithDetails,
@ -6,69 +7,70 @@ import {
import { MessageResultProps, MessageSearchResult } from './MessageSearchResults';
export type SearchResultsProps = {
contacts: Array<ConversationListItemProps>;
conversations: Array<ConversationListItemProps>;
contactsAndGroups: Array<ConversationListItemProps>;
messages: Array<MessageResultProps>;
searchTerm: string;
};
const ContactsItem = (props: { header: string; items: Array<ConversationListItemProps> }) => {
return (
<div className="module-search-results__contacts">
<div className="module-search-results__contacts-header">{props.header}</div>
{props.items.map(contact => (
<MemoConversationListItemWithDetails
{...contact}
key={`search-result-contact-${contact.id}`}
/>
))}
</div>
);
};
const StyledSeparatorSection = styled.div`
height: 36px;
line-height: 36px;
margin-inline-start: 16px;
color: var(--color-text);
font-size: 14px;
font-weight: 700;
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) => {
const { conversations, contacts, messages, searchTerm } = props;
const { contactsAndGroups, messages, searchTerm } = props;
const haveConversations = conversations && conversations.length;
const haveContacts = contacts && contacts.length;
const haveMessages = messages && messages.length;
const noResults = !haveConversations && !haveContacts && !haveMessages;
const haveContactsAndGroup = contactsAndGroups?.length;
const haveMessages = Boolean(messages?.length);
const noResults = !haveContactsAndGroup && !haveMessages;
return (
<div className="module-search-results">
{noResults ? (
<div className="module-search-results__no-results">
{window.i18n('noSearchResults', [searchTerm])}
</div>
) : null}
{haveConversations ? (
<div className="module-search-results__conversations">
<div className="module-search-results__conversations-header">
{window.i18n('conversationsHeader')}
</div>
{conversations.map(conversation => (
<SearchResultsContainer>
{noResults ? <NoResults>{window.i18n('noSearchResults', [searchTerm])}</NoResults> : null}
{haveContactsAndGroup ? (
<div>
<StyledSeparatorSection>{window.i18n('conversationsHeader')}</StyledSeparatorSection>
{contactsAndGroups.map(contactOrGroup => (
<MemoConversationListItemWithDetails
{...conversation}
{...contactOrGroup}
mentionedUs={false}
key={`search-result-convo-${conversation.id}`}
key={`search-result-convo-${contactOrGroup.id}`}
/>
))}
</div>
) : null}
{haveContacts ? (
<ContactsItem header={window.i18n('contactsHeader')} items={contacts} />
) : null}
{haveMessages ? (
<div className="module-search-results__messages">
<div className="module-search-results__messages-header">
{haveMessages && (
<div>
<StyledSeparatorSection>
{`${window.i18n('messagesHeader')}: ${messages.length}`}
</div>
</StyledSeparatorSection>
{messages.map(message => (
<MessageSearchResult key={`search-result-message-${message.id}`} {...message} />
))}
</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 = {
query: 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
conversations: Array<string>;
contacts: Array<string>;
contactsAndGroups: Array<string>;
messages?: Array<MessageResultProps>;
};
@ -27,8 +23,7 @@ export type SearchStateType = {
type SearchResultsPayloadType = {
query: string;
normalizedPhoneNumber?: string;
conversations: Array<string>;
contacts: Array<string>;
contactsAndGroups: Array<string>;
messages?: Array<MessageResultProps>;
};
@ -81,6 +76,7 @@ async function doSearch(query: string, options: SearchOptions): Promise<SearchRe
queryMessages(processedQuery),
]);
const { conversations, contacts } = discussions;
const contactsAndGroups = _.uniq([...conversations, ...contacts]);
const filteredMessages = _.compact(messages);
// if (isAdvancedQuery) {
// const senderFilterQuery =
@ -96,8 +92,7 @@ async function doSearch(query: string, options: SearchOptions): Promise<SearchRe
return {
query,
normalizedPhoneNumber: PubKey.normalize(query),
conversations,
contacts,
contactsAndGroups,
messages: filteredMessages,
};
}
@ -251,8 +246,7 @@ async function queryConversationsAndContacts(providedQuery: string, options: Sea
export const initialSearchState: SearchStateType = {
query: '',
conversations: [],
contacts: [],
contactsAndGroups: [],
messages: [],
};
@ -281,7 +275,7 @@ export function reducer(state: SearchStateType | undefined, action: SEARCH_TYPES
if (action.type === 'SEARCH_RESULTS_FULFILLED') {
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
if (state.query !== query) {
return state;
@ -291,8 +285,7 @@ export function reducer(state: SearchStateType | undefined, action: SEARCH_TYPES
...state,
query,
normalizedPhoneNumber,
conversations,
contacts,
contactsAndGroups,
messages,
};
}

@ -21,22 +21,8 @@ export const getSearchResults = createSelector(
[getSearch, getConversationLookup, getSelectedConversationKey],
(searchState: SearchStateType, lookup: ConversationLookupType, selectedConversation?: string) => {
return {
contacts: compact(
searchState.contacts.map(id => {
const value = lookup[id];
if (value && id === selectedConversation) {
return {
...value,
isSelected: true,
};
}
return value;
})
),
conversations: compact(
searchState.conversations.map(id => {
contactsAndGroups: compact(
searchState.contactsAndGroups.map(id => {
const value = lookup[id];
// 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 whiteSpaceNormalized = withoutSpecialCharacters.replace(/\s+/g, ' ');
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(
token =>
token &&

Loading…
Cancel
Save