diff --git a/_locales/en/messages.json b/_locales/en/messages.json index d1dd6a40b..3e7b73e02 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -749,6 +749,10 @@ "message": "Conversations", "description": "Shown to separate the types of search results" }, + "friendsHeader": { + "message": "Friends", + "description": "Shown to separate the types of search results" + }, "contactsHeader": { "message": "Contacts", "description": "Shown to separate the types of search results" @@ -2000,5 +2004,23 @@ }, "remove": { "message": "Remove" + }, + "invalidHexId": { + "message": "Invalid Hex ID", + "description": + "Error string shown when user type an invalid pubkey hex string" + }, + "invalidPubkeyFormat": { + "message": "Invalid Pubkey Format", + "description": "Error string shown when user types an invalid pubkey format" + }, + + "conversationsTab": { + "message": "Conversations", + "description": "conversation tab title" + }, + "friendsTab": { + "message": "Friends", + "description": "friend tab title" } } diff --git a/background.html b/background.html index def813225..fd5c46bc4 100644 --- a/background.html +++ b/background.html @@ -54,6 +54,9 @@
+
+ +
diff --git a/js/views/inbox_view.js b/js/views/inbox_view.js index 980a6c17d..757b6db18 100644 --- a/js/views/inbox_view.js +++ b/js/views/inbox_view.js @@ -7,8 +7,7 @@ i18n, Whisper, textsecure, - Signal, - clipboard + Signal */ // eslint-disable-next-line func-names @@ -87,13 +86,6 @@ this.render(); this.$el.attr('tabindex', '1'); - this.mainHeaderView = new Whisper.MainHeaderView({ - el: this.$('.main-header-placeholder'), - items: this.getMainHeaderItems(), - }); - this.onPasswordUpdated(); - this.on('password-updated', () => this.onPasswordUpdated()); - this.conversation_stack = new Whisper.ConversationStack({ el: this.$('.conversation-stack'), model: { window: options.window }, @@ -145,7 +137,6 @@ conversation => conversation.cachedProps ); - // FIXME: Add our contacts here as well? getContactCollection const initialState = { conversations: { conversationLookup: Signal.Util.makeLookup(conversations, 'id'), @@ -338,57 +329,6 @@ onClick(e) { this.closeRecording(e); }, - getMainHeaderItems() { - return [ - this._mainHeaderItem('copyPublicKey', () => { - const ourNumber = textsecure.storage.user.getNumber(); - clipboard.writeText(ourNumber); - - this.showToastMessageInGutter(i18n('copiedPublicKey')); - }), - this._mainHeaderItem('editDisplayName', () => { - window.Whisper.events.trigger('onEditProfile'); - }), - this._mainHeaderItem('showSeed', () => { - window.Whisper.events.trigger('showSeedDialog'); - }), - ]; - }, - async onPasswordUpdated() { - const hasPassword = await Signal.Data.getPasswordHash(); - const items = this.getMainHeaderItems(); - - const showPasswordDialog = (type, resolve) => - Whisper.events.trigger('showPasswordDialog', { - type, - resolve, - }); - - const passwordItem = (textKey, type) => - this._mainHeaderItem(textKey, () => - showPasswordDialog(type, () => { - this.showToastMessageInGutter(i18n(`${textKey}Success`)); - }) - ); - - if (hasPassword) { - items.push( - passwordItem('changePassword', 'change'), - passwordItem('removePassword', 'remove') - ); - } else { - items.push(passwordItem('setPassword', 'set')); - } - - this.mainHeaderView.updateItems(items); - }, - _mainHeaderItem(textKey, onClick) { - return { - id: textKey, - text: i18n(textKey), - onClick, - }; - }, showToastMessageInGutter(message) { const toast = new Whisper.MessageToastView({ message, diff --git a/package.json b/package.json index bd6c5d27d..4dc3398c0 100644 --- a/package.json +++ b/package.json @@ -78,9 +78,9 @@ "intl-tel-input": "12.1.15", "jquery": "3.3.1", "js-sha512": "0.8.0", + "js-yaml": "3.13.0", "jsbn": "1.1.0", "libsodium-wrappers": "^0.7.4", - "js-yaml": "3.13.0", "linkify-it": "2.0.3", "lodash": "4.17.11", "mkdirp": "0.5.1", @@ -96,6 +96,7 @@ "react": "16.8.3", "react-contextmenu": "2.11.0", "react-dom": "16.8.3", + "react-portal": "^4.2.0", "react-redux": "6.0.1", "react-virtualized": "9.21.0", "read-last-lines": "1.3.0", @@ -132,6 +133,7 @@ "@types/qs": "6.5.1", "@types/react": "16.8.5", "@types/react-dom": "16.8.2", + "@types/react-portal": "^4.0.2", "@types/react-redux": "7.0.1", "@types/react-virtualized": "9.18.12", "@types/redux-logger": "3.0.7", diff --git a/stylesheets/_conversation.scss b/stylesheets/_conversation.scss index 2f1fee424..8495b38f5 100644 --- a/stylesheets/_conversation.scss +++ b/stylesheets/_conversation.scss @@ -316,10 +316,7 @@ bottom: 62px; text-align: center; - padding-left: 16px; - padding-right: 16px; - padding-top: 8px; - padding-bottom: 8px; + padding: 8px 16px; border-radius: 4px; z-index: 100; diff --git a/stylesheets/_index.scss b/stylesheets/_index.scss index 6a09e840d..a0bf73772 100644 --- a/stylesheets/_index.scss +++ b/stylesheets/_index.scss @@ -1,3 +1,7 @@ +.conversation-stack { + position: relative; +} + .conversation-stack, .new-conversation, .inbox, @@ -183,10 +187,12 @@ h4.section-toggle, } .left-pane-placeholder { - height: 100%; + flex-grow: 1; + display: flex; } + .left-pane-wrapper { - height: 100%; + flex: 1; } .conversation-stack { diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index e9075dd81..aa3050134 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -5,7 +5,6 @@ display: flex; flex-direction: column; align-items: flex-start; - margin-right: 8px; overflow-x: hidden; } @@ -17,7 +16,7 @@ user-select: none; } -.module-contact-name__profile-number { +.module-contact-name__profile-number.italic { font-style: italic; } @@ -1852,10 +1851,6 @@ .module-avatar { background-color: $color-dark-85; } - - .module-contact-name { - margin-right: 0px; - } } .module-conversation-list-item--has-unread { @@ -2199,7 +2194,16 @@ // Module: Main Header -.main-header-title-wrapper { +.module-main-header { + display: flex; + flex-direction: column; + border-bottom: 1px solid $color-dark-90; + color: $color-dark-05; +} + +.module-main-header__title { + height: 55px; + padding-left: 16px; flex: 1; flex-direction: row; display: flex; @@ -2211,10 +2215,20 @@ } } -.main-header-content-wrapper { +.module-main-header__menu { color: $color-dark-05; + overflow: hidden; + + .accordian { + margin-top: -100%; + transition: margin-top 0.35s ease-out; + + &.expanded { + margin-top: 0; + } + } - div { + .menu-item { padding: 12px; background-color: $color-dark-90; user-select: none; @@ -2226,25 +2240,7 @@ } } -.main-header-wrapper { - overflow-x: hidden; - flex: 1; -} - -.module-main-header { - height: $header-height; - width: 300px; - - padding-left: 16px; - - display: flex; - flex-direction: row; - align-items: center; - - border-bottom: 1px solid $color-gray-15; -} - -.main-header-content-toggle { +.module-main-header-content-toggle { width: 3em; line-height: 3em; font-weight: bold; @@ -2265,7 +2261,7 @@ } } -.main-header-content-toggle-visible::after { +.module-main-header-content-toggle-visible::after { transform: rotate(180deg); } @@ -2278,39 +2274,50 @@ } .module-main-header__search { - margin-left: 12px; + margin: 8px; position: relative; } -.module-main-header__search__input { - height: 28px; - width: 228px; +.module-main-header__search__icon { + background-color: $color-light-35; +} - border-radius: 14px; - border: solid 1px $color-gray-15; +.module-main-header__search__input { + color: $color-dark-05; + background-color: $color-gray-95; + border: 1px solid $color-light-60; + padding: 0 26px 0 30px; + margin-left: 8px; + margin-right: 8px; + outline: 0; + height: 32px; + width: calc(100% - 16px); + outline-offset: -2px; + font-size: 14px; + line-height: 18px; + font-weight: normal; - padding-left: 30px; - padding-right: 30px; + position: relative; + border-radius: 4px; - color: $color-gray-90; - font-size: 14px; + &:focus { + outline: solid 1px $blue; + } &::placeholder { color: $color-gray-45; } - - &:focus { - border: solid 1px blue; - outline: none; - } } .module-main-header__search__icon { + content: ''; + display: inline-block; + width: 18px; + height: 26px; + background-color: $color-light-35; position: absolute; - left: 8px; - top: 6px; - height: 16px; - width: 16px; + left: 14px; + top: 3px; cursor: text; @include color-svg('../images/search.svg', $color-gray-60); @@ -2318,8 +2325,8 @@ .module-main-header__search__cancel-icon { position: absolute; - right: 8px; - top: 7px; + right: 16px; + top: 9px; height: 14px; width: 14px; cursor: pointer; @@ -3157,7 +3164,6 @@ // Module: Left Pane .module-left-pane { - background-color: $color-dark-85; border-right: 1px solid $color-dark-90; display: inline-flex; @@ -3172,6 +3178,28 @@ flex-grow: 0; } +.module-left-pane__tabs { + color: $color-dark-05; + background-color: $color-dark-75; + display: flex; + flex-direction: row; + + .tab { + width: 50%; + padding: 16px; + text-align: center; + cursor: pointer; + + &:hover { + background-color: $color-dark-72; + } + } + + .tab.selected { + background-color: #383c46; + } +} + .module-left-pane__archive-header { height: 48px; width: 100%; @@ -3249,24 +3277,43 @@ display: flex; flex-direction: row; align-items: center; - - padding: 8px 16px; - cursor: pointer; + padding: 8px 16px; + opacity: 0.7; - &:hover { - background-color: $color-gray-05; + &.valid { + opacity: 1; } } +.module-start-new-conversation__avatar { + display: inline-block; + height: 48px; + width: 48px; + border-radius: 50%; + background-size: cover; + vertical-align: middle; + text-align: center; + line-height: 48px; + overflow-x: hidden; + text-overflow: ellipsis; + color: #ffffff; + font-size: 18px; + background-color: #616161; +} + .module-start-new-conversation__content { overflow: hidden; margin-left: 12px; + flex: 1; } .module-start-new-conversation__number { - overflow-x: hidden; + margin: 0; + font-size: 1em; text-overflow: ellipsis; + overflow-x: hidden; + text-align: left; font-weight: 300; } diff --git a/test/app/fixtures/menu-mac-os-setup.json b/test/app/fixtures/menu-mac-os-setup.json index 324178c72..18936a5b1 100644 --- a/test/app/fixtures/menu-mac-os-setup.json +++ b/test/app/fixtures/menu-mac-os-setup.json @@ -1,6 +1,6 @@ [ { - "label": "Signal Desktop", + "label": "Loki Messenger", "submenu": [ { "label": "About Loki Messenger", diff --git a/test/app/fixtures/menu-mac-os.json b/test/app/fixtures/menu-mac-os.json index c7d2a1587..d00db642d 100644 --- a/test/app/fixtures/menu-mac-os.json +++ b/test/app/fixtures/menu-mac-os.json @@ -1,6 +1,6 @@ [ { - "label": "Signal Desktop", + "label": "Loki Messenger", "submenu": [ { "label": "About Loki Messenger", diff --git a/test/modules/types/contact_test.js b/test/modules/types/contact_test.js index e9732be10..c835f38d9 100644 --- a/test/modules/types/contact_test.js +++ b/test/modules/types/contact_test.js @@ -42,6 +42,7 @@ describe('Contact', () => { assert.deepEqual(result, message.contact[0]); }); + // LOKI: Phone number stays the same it('turns phone numbers to e164 format', async () => { const upgradeAttachment = sinon .stub() @@ -71,7 +72,7 @@ describe('Contact', () => { number: [ { type: 1, - value: '+12025550099', + value: '(202) 555-0099', }, ], }; diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index 65c7d60f0..c158a5372 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -1,5 +1,8 @@ import React from 'react'; import classNames from 'classnames'; +import { isEmpty } from 'lodash'; +import { ContextMenu, ContextMenuTrigger, MenuItem } from 'react-contextmenu'; +import { Portal } from 'react-portal'; import { Avatar } from './Avatar'; import { MessageBody } from './conversation/MessageBody'; @@ -8,7 +11,6 @@ import { ContactName } from './conversation/ContactName'; import { TypingAnimation } from './conversation/TypingAnimation'; import { Colors, LocalizerType } from '../types/Util'; -import { ContextMenu, ContextMenuTrigger, MenuItem } from 'react-contextmenu'; export type PropsData = { id: string; @@ -34,6 +36,7 @@ export type PropsData = { isBlocked?: boolean; isOnline?: boolean; hasNickname?: boolean; + isFriendItem?: boolean; }; type PropsHousekeeping = { @@ -109,6 +112,7 @@ export class ConversationListItem extends React.PureComponent { name, phoneNumber, profileName, + isFriendItem, } = this.props; return ( @@ -132,21 +136,23 @@ export class ConversationListItem extends React.PureComponent { /> )}
-
0 - ? 'module-conversation-list-item__header__date--has-unread' - : null - )} - > - -
+ {!isFriendItem && ( +
0 + ? 'module-conversation-list-item__header__date--has-unread' + : null + )} + > + +
+ )}
); } @@ -192,12 +198,27 @@ export class ConversationListItem extends React.PureComponent { } public renderMessage() { - const { lastMessage, isTyping, unreadCount, i18n } = this.props; + const { + lastMessage, + isTyping, + unreadCount, + i18n, + isFriendItem, + } = this.props; + + if (isFriendItem) { + return null; + } + if (!lastMessage && !isTyping) { return null; } const text = lastMessage && lastMessage.text ? lastMessage.text : ''; + if (isEmpty(text)) { + return null; + } + return (
{
- {this.renderContextMenu(triggerId)} + {this.renderContextMenu(triggerId)} ); } diff --git a/ts/components/LeftPane.tsx b/ts/components/LeftPane.tsx index b3c0ab1d3..6541c8601 100644 --- a/ts/components/LeftPane.tsx +++ b/ts/components/LeftPane.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import classNames from 'classnames'; import { AutoSizer, List } from 'react-virtualized'; import { @@ -13,6 +14,7 @@ import { LocalizerType } from '../types/Util'; export interface Props { conversations?: Array; + friends?: Array; archivedConversations?: Array; searchResults?: SearchResultsProps; showArchived?: boolean; @@ -42,7 +44,52 @@ type RowRendererParamsType = { style: Object; }; -export class LeftPane extends React.Component { +export class LeftPane extends React.Component { + public state = { + currentTab: 'conversations', + }; + + public getCurrentConversations(): + | Array + | undefined { + const { conversations, friends } = this.props; + const { currentTab } = this.state; + + return currentTab === 'conversations' ? conversations : friends; + } + + public renderTabs(): JSX.Element { + const { i18n } = this.props; + const { currentTab } = this.state; + const tabs = [ + { + id: 'conversations', + name: i18n('conversationsTab'), + }, + { + id: 'friends', + name: i18n('friendsTab'), + }, + ]; + + return ( +
+ {tabs.map(tab => ( +
{ + this.setState({ currentTab: tab.id }); + }} + > + {tab.name} +
+ ))} +
+ ); + } + public renderRow = ({ index, key, @@ -50,11 +97,15 @@ export class LeftPane extends React.Component { }: RowRendererParamsType): JSX.Element => { const { archivedConversations, - conversations, i18n, openConversationInternal, showArchived, } = this.props; + + const { currentTab } = this.state; + + const conversations = this.getCurrentConversations(); + if (!conversations || !archivedConversations) { throw new Error( 'renderRow: Tried to render without conversations or archivedConversations' @@ -76,6 +127,7 @@ export class LeftPane extends React.Component { {...conversation} onClick={openConversationInternal} i18n={i18n} + isFriendItem={currentTab !== 'conversations'} /> ); }; @@ -119,7 +171,6 @@ export class LeftPane extends React.Component { const { archivedConversations, i18n, - conversations, openConversationInternal, startNewConversation, searchResults, @@ -137,6 +188,8 @@ export class LeftPane extends React.Component { ); } + const conversations = this.getCurrentConversations(); + if (!conversations || !archivedConversations) { throw new Error( 'render: must provided conversations and archivedConverstions if no search results are provided' @@ -181,7 +234,7 @@ export class LeftPane extends React.Component { ); - return [archived, list]; + return [this.renderTabs(), archived, list]; } public renderArchivedHeader(): JSX.Element { diff --git a/ts/components/MainHeader.tsx b/ts/components/MainHeader.tsx index 4d2d806f4..bedd2f394 100644 --- a/ts/components/MainHeader.tsx +++ b/ts/components/MainHeader.tsx @@ -1,5 +1,12 @@ import React from 'react'; import { debounce } from 'lodash'; +import classNames from 'classnames'; + +// Use this to trigger whisper events +import { trigger } from '../shims/events'; + +// Use this to check for password +import { hasPassword } from '../shims/Signal'; import { Avatar } from './Avatar'; import { ContactName } from './conversation/ContactName'; @@ -7,6 +14,11 @@ import { ContactName } from './conversation/ContactName'; import { cleanSearchTerm } from '../util/cleanSearchTerm'; import { LocalizerType } from '../types/Util'; +interface MenuItem { + id: string; + name: string; + onClick?: () => void; +} export interface Props { searchTerm: string; @@ -36,9 +48,10 @@ export interface Props { clearSearch: () => void; onClick?: () => void; + onCopyPublicKey?: () => void; } -export class MainHeader extends React.Component { +export class MainHeader extends React.Component { private readonly updateSearchBound: ( event: React.FormEvent ) => void; @@ -53,6 +66,12 @@ export class MainHeader extends React.Component { constructor(props: Props) { super(props); + this.state = { + expanded: false, + hasPass: null, + menuItems: [], + }; + this.updateSearchBound = this.updateSearch.bind(this); this.clearSearchBound = this.clearSearch.bind(this); this.handleKeyUpBound = this.handleKeyUp.bind(this); @@ -62,6 +81,17 @@ export class MainHeader extends React.Component { this.debouncedSearch = debounce(this.search.bind(this), 20); } + public componentWillMount() { + // tslint:disable-next-line + this.updateHasPass(); + } + + public componentDidUpdate(_prevProps: Props, prevState: any) { + if (prevState.hasPass !== this.state.hasPass) { + this.updateMenuItems(); + } + } + public search() { const { searchTerm, search, i18n, ourNumber, regionCode } = this.props; if (search) { @@ -122,19 +152,39 @@ export class MainHeader extends React.Component { } public render() { + const { onClick } = this.props; + + return ( +
+
+ {this.renderName()} + {this.renderMenu()} +
+ {this.renderSearch()} +
+ ); + } + + private renderName() { const { - searchTerm, avatarPath, i18n, color, name, phoneNumber, profileName, - onClick, } = this.props; + const { expanded } = this.state; + return ( -
+
{ + this.setState({ expanded: !expanded }); + }} + > { i18n={i18n} />
-
-
- - {searchTerm ? ( +
+
+ ); + } + + private renderMenu() { + const { expanded, menuItems } = this.state; + + return ( +
+
+ {menuItems.map((item: MenuItem) => (
- ) : null} + className="menu-item" + key={item.id} + onClick={item.onClick} + > + {item.name} +
+ ))}
); } + + private renderSearch() { + const { searchTerm, i18n } = this.props; + + return ( +
+ + + {searchTerm ? ( + + ) : null} +
+ ); + } + + private async updateHasPass() { + const hasPass = await hasPassword(); + this.setState({ hasPass }); + } + + private updateMenuItems() { + const { i18n, onCopyPublicKey } = this.props; + const { hasPass } = this.state; + + const menuItems = [ + { + id: 'copyPublicKey', + name: i18n('copyPublicKey'), + onClick: onCopyPublicKey, + }, + { + id: 'editDisplayName', + name: i18n('editDisplayName'), + onClick: () => { + trigger('onEditProfile'); + }, + }, + { + id: 'showSeed', + name: i18n('showSeed'), + onClick: () => { + trigger('showSeedDialog'); + }, + }, + ]; + + const passItem = (type: string) => ({ + id: `${type}Password`, + name: i18n(`${type}Password`), + onClick: () => { + trigger('showPasswordDialog', { + type, + resolve: () => { + trigger('showToast', { + message: i18n(`${type}PasswordSuccess`), + }); + setTimeout(async () => this.updateHasPass(), 100); + }, + }); + }, + }); + + if (hasPass) { + menuItems.push(passItem('change'), passItem('remove')); + } else { + menuItems.push(passItem('set')); + } + + this.setState({ menuItems }); + } } diff --git a/ts/components/SearchResults.tsx b/ts/components/SearchResults.tsx index 5314c108a..ae5d4b6b5 100644 --- a/ts/components/SearchResults.tsx +++ b/ts/components/SearchResults.tsx @@ -13,6 +13,7 @@ import { LocalizerType } from '../types/Util'; export type PropsData = { contacts: Array; + friends: Array; conversations: Array; hideMessagesHeader: boolean; messages: Array; @@ -49,16 +50,19 @@ export class SearchResults extends React.Component { openConversation, searchTerm, showStartNewConversation, + friends, } = this.props; const haveConversations = conversations && conversations.length; const haveContacts = contacts && contacts.length; + const haveFriends = friends && friends.length; const haveMessages = messages && messages.length; const noResults = !showStartNewConversation && !haveConversations && !haveContacts && - !haveMessages; + !haveMessages && + !haveFriends; return (
@@ -89,21 +93,12 @@ export class SearchResults extends React.Component { ))}
) : null} - {haveContacts ? ( -
-
- {i18n('contactsHeader')} -
- {contacts.map(contact => ( - - ))} -
- ) : null} + {haveFriends + ? this.renderContacts(i18n('friendsHeader'), friends, true) + : null} + {haveContacts + ? this.renderContacts(i18n('contactsHeader'), contacts) + : null} {haveMessages ? (
{hideMessagesHeader ? null : ( @@ -124,4 +119,27 @@ export class SearchResults extends React.Component {
); } + + private renderContacts( + header: string, + items: Array, + friends?: boolean + ) { + const { i18n, openConversation } = this.props; + + return ( +
+
{header}
+ {items.map(contact => ( + + ))} +
+ ); + } } diff --git a/ts/components/StartNewConversation.tsx b/ts/components/StartNewConversation.tsx index 40b11b008..69650cd06 100644 --- a/ts/components/StartNewConversation.tsx +++ b/ts/components/StartNewConversation.tsx @@ -1,8 +1,8 @@ import React from 'react'; - -import { Avatar } from './Avatar'; +import classNames from 'classnames'; import { LocalizerType } from '../types/Util'; +import { validateNumber } from '../types/PhoneNumber'; export interface Props { phoneNumber: string; @@ -14,25 +14,26 @@ export class StartNewConversation extends React.PureComponent { public render() { const { phoneNumber, i18n, onClick } = this.props; + const error = validateNumber(phoneNumber, i18n); + const avatar = error ? '!' : '#'; + const click = error ? undefined : onClick; + return (
- +
{avatar}
{phoneNumber}
- {i18n('startConversation')} + {error || i18n('startConversation')}
diff --git a/ts/components/conversation/ContactName.tsx b/ts/components/conversation/ContactName.tsx index 641039566..d2d14d7a8 100644 --- a/ts/components/conversation/ContactName.tsx +++ b/ts/components/conversation/ContactName.tsx @@ -1,7 +1,7 @@ import React from 'react'; +import classNames from 'classnames'; import { Emojify } from './Emojify'; - import { LocalizerType } from '../../types/Util'; interface Props { @@ -29,7 +29,14 @@ export class ContactName extends React.Component { {profileElement} {shouldShowProfile ? ' ' : null} - + + + ); } diff --git a/ts/shims/Signal.ts b/ts/shims/Signal.ts new file mode 100644 index 000000000..6478e75d4 --- /dev/null +++ b/ts/shims/Signal.ts @@ -0,0 +1,6 @@ +export async function hasPassword() { + // @ts-ignore + const hash = await window.Signal.Data.getPasswordHash(); + + return !!hash; +} diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index ad817b382..bc718a53a 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -96,27 +96,19 @@ export const _getLeftPaneLists = ( ): { conversations: Array; archivedConversations: Array; - contacts: Array; + friends: Array; } => { const values = Object.values(lookup); const sorted = values.sort(comparator); const conversations: Array = []; const archivedConversations: Array = []; - const contacts: Array = []; + const friends: Array = []; const max = sorted.length; for (let i = 0; i < max; i += 1) { let conversation = sorted[i]; - if (conversation.isFriend) { - contacts.push(conversation); - } - - if (!conversation.activeAt) { - continue; - } - if (selectedConversation === conversation.id) { conversation = { ...conversation, @@ -124,6 +116,14 @@ export const _getLeftPaneLists = ( }; } + if (conversation.isFriend) { + friends.push(conversation); + } + + if (!conversation.activeAt) { + continue; + } + if (conversation.isArchived) { archivedConversations.push(conversation); } else { @@ -131,7 +131,7 @@ export const _getLeftPaneLists = ( } } - return { conversations, archivedConversations, contacts }; + return { conversations, archivedConversations, friends }; }; export const getLeftPaneLists = createSelector( diff --git a/ts/state/selectors/search.ts b/ts/state/selectors/search.ts index 4d6154246..22d8dbbe1 100644 --- a/ts/state/selectors/search.ts +++ b/ts/state/selectors/search.ts @@ -50,8 +50,6 @@ export const getSearchResults = createSelector( ) => { return { contacts: compact( - /* - LOKI: Unsure what signal does with this state.contacts.map(id => { const value = lookup[id]; @@ -64,34 +62,35 @@ export const getSearchResults = createSelector( return value; }) - */ + ), + conversations: compact( state.conversations.map(id => { const value = lookup[id]; - const friend = value && value.isFriend ? { ...value } : null; - - if (friend && id === selectedConversation) { + if (value && id === selectedConversation) { return { - ...friend, + ...value, isSelected: true, }; } - return friend; + return value; }) ), - conversations: compact( + friends: compact( state.conversations.map(id => { const value = lookup[id]; - if (value && id === selectedConversation) { + const friend = value && value.isFriend ? { ...value } : null; + + if (friend && id === selectedConversation) { return { - ...value, + ...friend, isSelected: true, }; } - return value; + return friend; }) ), hideMessagesHeader: false, @@ -107,9 +106,9 @@ export const getSearchResults = createSelector( }), regionCode: regionCode, searchTerm: state.query, - showStartNewConversation: Boolean( - state.normalizedPhoneNumber && !lookup[state.normalizedPhoneNumber] - ), + + // We only want to show the start conversation if we don't have the query in our lookup + showStartNewConversation: !lookup[state.query], }; } ); diff --git a/ts/types/PhoneNumber.ts b/ts/types/PhoneNumber.ts index 444a7fd1c..4c9cc7810 100644 --- a/ts/types/PhoneNumber.ts +++ b/ts/types/PhoneNumber.ts @@ -1,3 +1,5 @@ +import { LocalizerType } from './Util'; + export function format( phoneNumber: string, _options: { @@ -31,18 +33,30 @@ export function normalize( } } -function isValidNumber(number: string) { +function validate(number: string) { // Check if it's hex const isHex = number.replace(/[\s]*/g, '').match(/^[0-9a-fA-F]+$/); if (!isHex) { - return false; + return 'invalidHexId'; } // Check if the pubkey length is 33 and leading with 05 or of length 32 const len = number.length; if ((len !== 33 * 2 || !/^05/.test(number)) && len !== 32 * 2) { - return false; + return 'invalidPubkeyFormat'; } - return true; + return null; +} + +function isValidNumber(number: string) { + const error = validate(number); + + return !error; +} + +export function validateNumber(number: string, i18n: LocalizerType) { + const error = validate(number); + + return error && i18n(error); } diff --git a/yarn.lock b/yarn.lock index 1a3a908f2..d11f8725e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -214,6 +214,13 @@ dependencies: "@types/react" "*" +"@types/react-portal@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/react-portal/-/react-portal-4.0.2.tgz#57a7f4c8ad48097c5a2d0cbbd09187831b91afdf" + integrity sha512-8tOaQHURcZ9j5lg9laFRu5/7+ol71WvVs10VXuIp7IuoIwR2iXQB8+BOEASMRgc/+L1omgANCy+WyXDTmc1/iQ== + dependencies: + "@types/react" "*" + "@types/react-redux@7.0.1": version "7.0.1" resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.0.1.tgz#9dd2503be7a9861c5a092bf1c5050b7ade4dc62e" @@ -7317,7 +7324,7 @@ prop-types@^15.5.10, prop-types@^15.6.0, prop-types@^15.6.1: loose-envify "^1.3.1" object-assign "^4.1.1" -prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -7663,6 +7670,13 @@ 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-portal@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/react-portal/-/react-portal-4.2.0.tgz#5400831cdb0ae64dccb8128121cf076089ab1afd" + integrity sha512-Zf+vGQ/VEAb5XAy+muKEn48yhdCNYPZaB1BWg1xc8sAZWD8pXTgPtQT4ihBdmWzsfCq8p8/kqf0GWydSBqc+Eg== + dependencies: + prop-types "^15.5.8" + react-redux@6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-6.0.1.tgz#0d423e2c1cb10ada87293d47e7de7c329623ba4d"