From b8ef6c2cc66ffb856e7a9f57c5115b0e69e79839 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Thu, 18 Apr 2019 13:48:13 +1000 Subject: [PATCH] Loki changes Added friends section in search. This is because contacts is now used in signal for something else and we don't want to clash meanings. Styling fixes Add dropdown options into mainheader Updated styling Restore StartNewConversation to the old ui style of loki messenger. Fix friend search display. Fix header expand animation. Hooked up menu actions. Linting. More styling changes. Fix tests. Added back in the loki logo below the gutter. Fix toast positioning. Fix context menu showing incorrectly on virtual lists. Added tabs. Linting --- _locales/en/messages.json | 22 +++ background.html | 3 + js/views/inbox_view.js | 62 +------ package.json | 4 +- stylesheets/_conversation.scss | 5 +- stylesheets/_index.scss | 10 +- stylesheets/_modules.scss | 161 ++++++++++------- test/app/fixtures/menu-mac-os-setup.json | 2 +- test/app/fixtures/menu-mac-os.json | 2 +- test/modules/types/contact_test.js | 3 +- ts/components/ConversationListItem.tsx | 57 +++++-- ts/components/LeftPane.tsx | 61 ++++++- ts/components/MainHeader.tsx | 190 ++++++++++++++++++--- ts/components/SearchResults.tsx | 50 ++++-- ts/components/StartNewConversation.tsx | 25 +-- ts/components/conversation/ContactName.tsx | 11 +- ts/shims/Signal.ts | 6 + ts/state/selectors/conversations.ts | 22 +-- ts/state/selectors/search.ts | 29 ++-- ts/types/PhoneNumber.ts | 22 ++- yarn.lock | 16 +- 21 files changed, 527 insertions(+), 236 deletions(-) create mode 100644 ts/shims/Signal.ts 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"