From e4b36fe7f7f47bffdf489adfc97a324068006b23 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 23 Dec 2019 11:56:42 +1100 Subject: [PATCH] add start of user search dropdown in compose view --- .../_session_theme_dark_left_pane.scss | 29 +++++- ts/components/UserSearchResults.tsx | 73 ++++++++++++++ .../session/LeftPaneMessageSection.tsx | 13 ++- ts/components/session/SessionSearchInput.tsx | 13 +++ ts/components/session/UserSearchDropdown.tsx | 96 +++++++++++++++++++ ts/state/ducks/search.ts | 7 +- 6 files changed, 223 insertions(+), 8 deletions(-) create mode 100644 ts/components/UserSearchResults.tsx create mode 100644 ts/components/session/UserSearchDropdown.tsx diff --git a/stylesheets/_session_theme_dark_left_pane.scss b/stylesheets/_session_theme_dark_left_pane.scss index cfc68ac4f..7b2bdb7e7 100644 --- a/stylesheets/_session_theme_dark_left_pane.scss +++ b/stylesheets/_session_theme_dark_left_pane.scss @@ -1,3 +1,5 @@ +$session-compose-margin: 20px; + .dark-theme { .module-conversation-list-item { @include session-dark-background-lighter; @@ -144,6 +146,7 @@ margin-left: -65px; top: 50%; margin-top: 6px; + border: none; } .white { @@ -161,7 +164,11 @@ } .session-search-input { - margin: 10px 20px; + margin: 10px $session-compose-margin 0 $session-compose-margin; + width: -webkit-fill-available; + } + + .module-search-results { width: -webkit-fill-available; } @@ -219,3 +226,23 @@ .app-loading-screen { @include session-dark-background; } + +.contacts-dropdown { + width: -webkit-fill-available; + + &-row { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding: 5px 20px; + margin: 0px $session-compose-margin; + background: $session-shade-4; + color: $session-color-light-grey; + + &-selected, + &:hover { + font-weight: bold; + color: $session-color-white; + } + } +} diff --git a/ts/components/UserSearchResults.tsx b/ts/components/UserSearchResults.tsx new file mode 100644 index 000000000..4b8fa9ade --- /dev/null +++ b/ts/components/UserSearchResults.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { PropsData as ConversationListItemPropsType } from './ConversationListItem'; + +import { LocalizerType } from '../types/Util'; +import classNames from 'classnames'; + +export type PropsData = { + contacts: Array; + regionCode: string; + searchTerm: string; + selectedContact: Number; + onContactSelected: any; +}; + +type PropsHousekeeping = { + i18n: LocalizerType; +}; + +type Props = PropsData & PropsHousekeeping; + +export class UserSearchResults extends React.Component { + public constructor(props: Props) { + super(props); + } + + public render() { + const { contacts, i18n, searchTerm } = this.props; + + const haveContacts = contacts && contacts.length; + const noResults = !haveContacts; + + return ( +
+ {noResults ? ( +
+ {i18n('noSearchResults', [searchTerm])} +
+ ) : null} + {haveContacts ? this.renderContacts(contacts) : null} +
+ ); + } + + private renderContacts(items: Array) { + return ( +
+ {items.map((contact, index) => this.renderContact(contact, index))} +
+ ); + } + + private renderContact(contact: ConversationListItemPropsType, index: Number) { + const { profileName, phoneNumber } = contact; + const { selectedContact } = this.props; + + const shortenedPubkey = window.shortenPubkey(phoneNumber); + const rowContent = `${profileName} ${shortenedPubkey}`; + + return ( +
this.props.onContactSelected(contact.phoneNumber)} + role="button" + > + {rowContent} +
+ ); + } +} diff --git a/ts/components/session/LeftPaneMessageSection.tsx b/ts/components/session/LeftPaneMessageSection.tsx index db69b5d93..a03820538 100644 --- a/ts/components/session/LeftPaneMessageSection.tsx +++ b/ts/components/session/LeftPaneMessageSection.tsx @@ -20,6 +20,7 @@ import { cleanSearchTerm } from '../../util/cleanSearchTerm'; import { SearchOptions } from '../../types/Search'; import { SessionIconButton, SessionIconSize, SessionIconType } from './icon'; import { SessionIdEditable } from './SessionIdEditable'; +import { UserSearchDropdown } from './UserSearchDropdown'; export interface Props { searchTerm: string; @@ -185,6 +186,8 @@ export class LeftPaneMessageSection extends React.Component { } public renderCompose(): JSX.Element { + const { searchResults } = this.props; + return (
@@ -210,10 +213,12 @@ export class LeftPaneMessageSection extends React.Component { {window.i18n('usersCanShareTheir...')}

{window.i18n('or')}

- { this.setState((state: any) => { return { showComposeView: !state.showComposeView }; }); + // empty our generalized searchedString (one for the whole app) + this.updateSearch(''); } private handleOnPasteSessionID() { diff --git a/ts/components/session/SessionSearchInput.tsx b/ts/components/session/SessionSearchInput.tsx index 07c637b3b..c92b0cb1b 100644 --- a/ts/components/session/SessionSearchInput.tsx +++ b/ts/components/session/SessionSearchInput.tsx @@ -4,12 +4,14 @@ import { SessionIconButton, SessionIconSize, SessionIconType } from './icon'; interface Props { searchString: string; onChange: any; + handleNavigation?: any; placeholder: string; } export class SessionSearchInput extends React.Component { public constructor(props: Props) { super(props); + this.handleKeyDown = this.handleKeyDown.bind(this); } public render() { @@ -24,9 +26,20 @@ export class SessionSearchInput extends React.Component { this.props.onChange(e.target.value)} + onKeyDown={this.handleKeyDown} placeholder={this.props.placeholder} />
); } + + public handleKeyDown(e: any) { + if (e.keyCode === 38 || e.keyCode === 40 || e.key === 'Enter') { + // Up or Bottom arrow pressed + if (this.props.handleNavigation) { + e.stopPropagation(); + this.props.handleNavigation(e); + } + } + } } diff --git a/ts/components/session/UserSearchDropdown.tsx b/ts/components/session/UserSearchDropdown.tsx new file mode 100644 index 000000000..1720988a3 --- /dev/null +++ b/ts/components/session/UserSearchDropdown.tsx @@ -0,0 +1,96 @@ +import React from 'react'; + +import { UserSearchResults } from '../UserSearchResults'; +import { SessionSearchInput } from './SessionSearchInput'; + +import { PropsData as SearchResultsProps } from '../SearchResults'; + +export interface Props { + searchTerm: string; + placeholder: string; + searchResults?: SearchResultsProps; + updateSearch: (searchTerm: string) => void; +} + +interface State { + selectedContact: number; +} + +export class UserSearchDropdown extends React.Component { + private readonly updateSearchBound: (searchedString: string) => void; + + public constructor(props: Props) { + super(props); + this.updateSearchBound = this.updateSearch.bind(this); + this.handleNavigation = this.handleNavigation.bind(this); + this.handleContactSelected = this.handleContactSelected.bind(this); + this.state = { + selectedContact: 0, + }; + } + + public handleNavigation(e: any) { + const { selectedContact } = this.state; + const { searchResults } = this.props; + // arrow up/down button should select next/previous list element + if ( + e.keyCode === 38 && + selectedContact > 0 && + searchResults && + searchResults.contacts.length > 0 + ) { + this.setState(prevState => ({ + selectedContact: +prevState.selectedContact - 1, + })); + } else if ( + e.keyCode === 40 && + searchResults && + selectedContact < searchResults.contacts.length - 1 + ) { + this.setState(prevState => ({ + selectedContact: +prevState.selectedContact + 1, + })); + } else if ( + e.key === 'Enter' && + searchResults && + searchResults.contacts.length > 0 + ) { + this.handleContactSelected( + searchResults.contacts[selectedContact].phoneNumber + ); + } + } + + public render() { + const { searchResults, placeholder } = this.props; + const { selectedContact } = this.state; + + return ( +
+ + {searchResults && ( + + )} +
+ ); + } + + public updateSearch(data: string) { + this.setState({ selectedContact: 0 }); + this.props.updateSearch(data); + } + + public handleContactSelected(key: string) { + this.updateSearch(key); + } +} diff --git a/ts/state/ducks/search.ts b/ts/state/ducks/search.ts index ffd4e85e0..b1bc407b8 100644 --- a/ts/state/ducks/search.ts +++ b/ts/state/ducks/search.ts @@ -204,10 +204,9 @@ async function queryConversationsAndContacts( } else { conversations.push(primaryDevice); } - } else if ( - conversation.type === 'direct' && - !Boolean(conversation.lastMessage) - ) { + } else if (conversation.type === 'direct') { + contacts.push(conversation.id); + } else if (conversation.type !== 'group') { contacts.push(conversation.id); } else { conversations.push(conversation.id);