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
pull/272/head
Mikunj 6 years ago
parent 61b862b021
commit b8ef6c2cc6

@ -749,6 +749,10 @@
"message": "Conversations", "message": "Conversations",
"description": "Shown to separate the types of search results" "description": "Shown to separate the types of search results"
}, },
"friendsHeader": {
"message": "Friends",
"description": "Shown to separate the types of search results"
},
"contactsHeader": { "contactsHeader": {
"message": "Contacts", "message": "Contacts",
"description": "Shown to separate the types of search results" "description": "Shown to separate the types of search results"
@ -2000,5 +2004,23 @@
}, },
"remove": { "remove": {
"message": "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"
} }
} }

@ -54,6 +54,9 @@
<div class='gutter'> <div class='gutter'>
<div class='network-status-container'></div> <div class='network-status-container'></div>
<div class='left-pane-placeholder'></div> <div class='left-pane-placeholder'></div>
<div class='loki'>
<img src='images/loki/loki_icon_text.png' />
</div>
</div> </div>
<div class='conversation-stack'> <div class='conversation-stack'>
<div class='conversation placeholder'> <div class='conversation placeholder'>

@ -7,8 +7,7 @@
i18n, i18n,
Whisper, Whisper,
textsecure, textsecure,
Signal, Signal
clipboard
*/ */
// eslint-disable-next-line func-names // eslint-disable-next-line func-names
@ -87,13 +86,6 @@
this.render(); this.render();
this.$el.attr('tabindex', '1'); 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({ this.conversation_stack = new Whisper.ConversationStack({
el: this.$('.conversation-stack'), el: this.$('.conversation-stack'),
model: { window: options.window }, model: { window: options.window },
@ -145,7 +137,6 @@
conversation => conversation.cachedProps conversation => conversation.cachedProps
); );
// FIXME: Add our contacts here as well? getContactCollection
const initialState = { const initialState = {
conversations: { conversations: {
conversationLookup: Signal.Util.makeLookup(conversations, 'id'), conversationLookup: Signal.Util.makeLookup(conversations, 'id'),
@ -338,57 +329,6 @@
onClick(e) { onClick(e) {
this.closeRecording(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) { showToastMessageInGutter(message) {
const toast = new Whisper.MessageToastView({ const toast = new Whisper.MessageToastView({
message, message,

@ -78,9 +78,9 @@
"intl-tel-input": "12.1.15", "intl-tel-input": "12.1.15",
"jquery": "3.3.1", "jquery": "3.3.1",
"js-sha512": "0.8.0", "js-sha512": "0.8.0",
"js-yaml": "3.13.0",
"jsbn": "1.1.0", "jsbn": "1.1.0",
"libsodium-wrappers": "^0.7.4", "libsodium-wrappers": "^0.7.4",
"js-yaml": "3.13.0",
"linkify-it": "2.0.3", "linkify-it": "2.0.3",
"lodash": "4.17.11", "lodash": "4.17.11",
"mkdirp": "0.5.1", "mkdirp": "0.5.1",
@ -96,6 +96,7 @@
"react": "16.8.3", "react": "16.8.3",
"react-contextmenu": "2.11.0", "react-contextmenu": "2.11.0",
"react-dom": "16.8.3", "react-dom": "16.8.3",
"react-portal": "^4.2.0",
"react-redux": "6.0.1", "react-redux": "6.0.1",
"react-virtualized": "9.21.0", "react-virtualized": "9.21.0",
"read-last-lines": "1.3.0", "read-last-lines": "1.3.0",
@ -132,6 +133,7 @@
"@types/qs": "6.5.1", "@types/qs": "6.5.1",
"@types/react": "16.8.5", "@types/react": "16.8.5",
"@types/react-dom": "16.8.2", "@types/react-dom": "16.8.2",
"@types/react-portal": "^4.0.2",
"@types/react-redux": "7.0.1", "@types/react-redux": "7.0.1",
"@types/react-virtualized": "9.18.12", "@types/react-virtualized": "9.18.12",
"@types/redux-logger": "3.0.7", "@types/redux-logger": "3.0.7",

@ -316,10 +316,7 @@
bottom: 62px; bottom: 62px;
text-align: center; text-align: center;
padding-left: 16px; padding: 8px 16px;
padding-right: 16px;
padding-top: 8px;
padding-bottom: 8px;
border-radius: 4px; border-radius: 4px;
z-index: 100; z-index: 100;

@ -1,3 +1,7 @@
.conversation-stack {
position: relative;
}
.conversation-stack, .conversation-stack,
.new-conversation, .new-conversation,
.inbox, .inbox,
@ -183,10 +187,12 @@ h4.section-toggle,
} }
.left-pane-placeholder { .left-pane-placeholder {
height: 100%; flex-grow: 1;
display: flex;
} }
.left-pane-wrapper { .left-pane-wrapper {
height: 100%; flex: 1;
} }
.conversation-stack { .conversation-stack {

@ -5,7 +5,6 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
margin-right: 8px;
overflow-x: hidden; overflow-x: hidden;
} }
@ -17,7 +16,7 @@
user-select: none; user-select: none;
} }
.module-contact-name__profile-number { .module-contact-name__profile-number.italic {
font-style: italic; font-style: italic;
} }
@ -1852,10 +1851,6 @@
.module-avatar { .module-avatar {
background-color: $color-dark-85; background-color: $color-dark-85;
} }
.module-contact-name {
margin-right: 0px;
}
} }
.module-conversation-list-item--has-unread { .module-conversation-list-item--has-unread {
@ -2199,7 +2194,16 @@
// Module: Main Header // 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: 1;
flex-direction: row; flex-direction: row;
display: flex; display: flex;
@ -2211,10 +2215,20 @@
} }
} }
.main-header-content-wrapper { .module-main-header__menu {
color: $color-dark-05; 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; padding: 12px;
background-color: $color-dark-90; background-color: $color-dark-90;
user-select: none; user-select: none;
@ -2226,25 +2240,7 @@
} }
} }
.main-header-wrapper { .module-main-header-content-toggle {
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 {
width: 3em; width: 3em;
line-height: 3em; line-height: 3em;
font-weight: bold; font-weight: bold;
@ -2265,7 +2261,7 @@
} }
} }
.main-header-content-toggle-visible::after { .module-main-header-content-toggle-visible::after {
transform: rotate(180deg); transform: rotate(180deg);
} }
@ -2278,39 +2274,50 @@
} }
.module-main-header__search { .module-main-header__search {
margin-left: 12px; margin: 8px;
position: relative; position: relative;
} }
.module-main-header__search__input { .module-main-header__search__icon {
height: 28px; background-color: $color-light-35;
width: 228px; }
border-radius: 14px; .module-main-header__search__input {
border: solid 1px $color-gray-15; 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; position: relative;
padding-right: 30px; border-radius: 4px;
color: $color-gray-90; &:focus {
font-size: 14px; outline: solid 1px $blue;
}
&::placeholder { &::placeholder {
color: $color-gray-45; color: $color-gray-45;
} }
&:focus {
border: solid 1px blue;
outline: none;
}
} }
.module-main-header__search__icon { .module-main-header__search__icon {
content: '';
display: inline-block;
width: 18px;
height: 26px;
background-color: $color-light-35;
position: absolute; position: absolute;
left: 8px; left: 14px;
top: 6px; top: 3px;
height: 16px;
width: 16px;
cursor: text; cursor: text;
@include color-svg('../images/search.svg', $color-gray-60); @include color-svg('../images/search.svg', $color-gray-60);
@ -2318,8 +2325,8 @@
.module-main-header__search__cancel-icon { .module-main-header__search__cancel-icon {
position: absolute; position: absolute;
right: 8px; right: 16px;
top: 7px; top: 9px;
height: 14px; height: 14px;
width: 14px; width: 14px;
cursor: pointer; cursor: pointer;
@ -3157,7 +3164,6 @@
// Module: Left Pane // Module: Left Pane
.module-left-pane { .module-left-pane {
background-color: $color-dark-85;
border-right: 1px solid $color-dark-90; border-right: 1px solid $color-dark-90;
display: inline-flex; display: inline-flex;
@ -3172,6 +3178,28 @@
flex-grow: 0; 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 { .module-left-pane__archive-header {
height: 48px; height: 48px;
width: 100%; width: 100%;
@ -3249,24 +3277,43 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
padding: 8px 16px;
cursor: pointer; cursor: pointer;
padding: 8px 16px;
opacity: 0.7;
&:hover { &.valid {
background-color: $color-gray-05; 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 { .module-start-new-conversation__content {
overflow: hidden; overflow: hidden;
margin-left: 12px; margin-left: 12px;
flex: 1;
} }
.module-start-new-conversation__number { .module-start-new-conversation__number {
overflow-x: hidden; margin: 0;
font-size: 1em;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow-x: hidden;
text-align: left;
font-weight: 300; font-weight: 300;
} }

@ -1,6 +1,6 @@
[ [
{ {
"label": "Signal Desktop", "label": "Loki Messenger",
"submenu": [ "submenu": [
{ {
"label": "About Loki Messenger", "label": "About Loki Messenger",

@ -1,6 +1,6 @@
[ [
{ {
"label": "Signal Desktop", "label": "Loki Messenger",
"submenu": [ "submenu": [
{ {
"label": "About Loki Messenger", "label": "About Loki Messenger",

@ -42,6 +42,7 @@ describe('Contact', () => {
assert.deepEqual(result, message.contact[0]); assert.deepEqual(result, message.contact[0]);
}); });
// LOKI: Phone number stays the same
it('turns phone numbers to e164 format', async () => { it('turns phone numbers to e164 format', async () => {
const upgradeAttachment = sinon const upgradeAttachment = sinon
.stub() .stub()
@ -71,7 +72,7 @@ describe('Contact', () => {
number: [ number: [
{ {
type: 1, type: 1,
value: '+12025550099', value: '(202) 555-0099',
}, },
], ],
}; };

@ -1,5 +1,8 @@
import React from 'react'; import React from 'react';
import classNames from 'classnames'; 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 { Avatar } from './Avatar';
import { MessageBody } from './conversation/MessageBody'; import { MessageBody } from './conversation/MessageBody';
@ -8,7 +11,6 @@ import { ContactName } from './conversation/ContactName';
import { TypingAnimation } from './conversation/TypingAnimation'; import { TypingAnimation } from './conversation/TypingAnimation';
import { Colors, LocalizerType } from '../types/Util'; import { Colors, LocalizerType } from '../types/Util';
import { ContextMenu, ContextMenuTrigger, MenuItem } from 'react-contextmenu';
export type PropsData = { export type PropsData = {
id: string; id: string;
@ -34,6 +36,7 @@ export type PropsData = {
isBlocked?: boolean; isBlocked?: boolean;
isOnline?: boolean; isOnline?: boolean;
hasNickname?: boolean; hasNickname?: boolean;
isFriendItem?: boolean;
}; };
type PropsHousekeeping = { type PropsHousekeeping = {
@ -109,6 +112,7 @@ export class ConversationListItem extends React.PureComponent<Props> {
name, name,
phoneNumber, phoneNumber,
profileName, profileName,
isFriendItem,
} = this.props; } = this.props;
return ( return (
@ -132,21 +136,23 @@ export class ConversationListItem extends React.PureComponent<Props> {
/> />
)} )}
</div> </div>
<div {!isFriendItem && (
className={classNames( <div
'module-conversation-list-item__header__date', className={classNames(
unreadCount > 0 'module-conversation-list-item__header__date',
? 'module-conversation-list-item__header__date--has-unread' unreadCount > 0
: null ? 'module-conversation-list-item__header__date--has-unread'
)} : null
> )}
<Timestamp >
timestamp={lastUpdated} <Timestamp
extended={false} timestamp={lastUpdated}
module="module-conversation-list-item__header__timestamp" extended={false}
i18n={i18n} module="module-conversation-list-item__header__timestamp"
/> i18n={i18n}
</div> />
</div>
)}
</div> </div>
); );
} }
@ -192,12 +198,27 @@ export class ConversationListItem extends React.PureComponent<Props> {
} }
public renderMessage() { public renderMessage() {
const { lastMessage, isTyping, unreadCount, i18n } = this.props; const {
lastMessage,
isTyping,
unreadCount,
i18n,
isFriendItem,
} = this.props;
if (isFriendItem) {
return null;
}
if (!lastMessage && !isTyping) { if (!lastMessage && !isTyping) {
return null; return null;
} }
const text = lastMessage && lastMessage.text ? lastMessage.text : ''; const text = lastMessage && lastMessage.text ? lastMessage.text : '';
if (isEmpty(text)) {
return null;
}
return ( return (
<div className="module-conversation-list-item__message"> <div className="module-conversation-list-item__message">
<div <div
@ -277,7 +298,7 @@ export class ConversationListItem extends React.PureComponent<Props> {
</div> </div>
</div> </div>
</ContextMenuTrigger> </ContextMenuTrigger>
{this.renderContextMenu(triggerId)} <Portal>{this.renderContextMenu(triggerId)}</Portal>
</div> </div>
); );
} }

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import classNames from 'classnames';
import { AutoSizer, List } from 'react-virtualized'; import { AutoSizer, List } from 'react-virtualized';
import { import {
@ -13,6 +14,7 @@ import { LocalizerType } from '../types/Util';
export interface Props { export interface Props {
conversations?: Array<ConversationListItemPropsType>; conversations?: Array<ConversationListItemPropsType>;
friends?: Array<ConversationListItemPropsType>;
archivedConversations?: Array<ConversationListItemPropsType>; archivedConversations?: Array<ConversationListItemPropsType>;
searchResults?: SearchResultsProps; searchResults?: SearchResultsProps;
showArchived?: boolean; showArchived?: boolean;
@ -42,7 +44,52 @@ type RowRendererParamsType = {
style: Object; style: Object;
}; };
export class LeftPane extends React.Component<Props> { export class LeftPane extends React.Component<Props, any> {
public state = {
currentTab: 'conversations',
};
public getCurrentConversations():
| Array<ConversationListItemPropsType>
| 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 (
<div className="module-left-pane__tabs" key="tabs">
{tabs.map(tab => (
<div
role="button"
className={classNames('tab', tab.id === currentTab && 'selected')}
key={tab.id}
onClick={() => {
this.setState({ currentTab: tab.id });
}}
>
{tab.name}
</div>
))}
</div>
);
}
public renderRow = ({ public renderRow = ({
index, index,
key, key,
@ -50,11 +97,15 @@ export class LeftPane extends React.Component<Props> {
}: RowRendererParamsType): JSX.Element => { }: RowRendererParamsType): JSX.Element => {
const { const {
archivedConversations, archivedConversations,
conversations,
i18n, i18n,
openConversationInternal, openConversationInternal,
showArchived, showArchived,
} = this.props; } = this.props;
const { currentTab } = this.state;
const conversations = this.getCurrentConversations();
if (!conversations || !archivedConversations) { if (!conversations || !archivedConversations) {
throw new Error( throw new Error(
'renderRow: Tried to render without conversations or archivedConversations' 'renderRow: Tried to render without conversations or archivedConversations'
@ -76,6 +127,7 @@ export class LeftPane extends React.Component<Props> {
{...conversation} {...conversation}
onClick={openConversationInternal} onClick={openConversationInternal}
i18n={i18n} i18n={i18n}
isFriendItem={currentTab !== 'conversations'}
/> />
); );
}; };
@ -119,7 +171,6 @@ export class LeftPane extends React.Component<Props> {
const { const {
archivedConversations, archivedConversations,
i18n, i18n,
conversations,
openConversationInternal, openConversationInternal,
startNewConversation, startNewConversation,
searchResults, searchResults,
@ -137,6 +188,8 @@ export class LeftPane extends React.Component<Props> {
); );
} }
const conversations = this.getCurrentConversations();
if (!conversations || !archivedConversations) { if (!conversations || !archivedConversations) {
throw new Error( throw new Error(
'render: must provided conversations and archivedConverstions if no search results are provided' 'render: must provided conversations and archivedConverstions if no search results are provided'
@ -181,7 +234,7 @@ export class LeftPane extends React.Component<Props> {
</div> </div>
); );
return [archived, list]; return [this.renderTabs(), archived, list];
} }
public renderArchivedHeader(): JSX.Element { public renderArchivedHeader(): JSX.Element {

@ -1,5 +1,12 @@
import React from 'react'; import React from 'react';
import { debounce } from 'lodash'; 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 { Avatar } from './Avatar';
import { ContactName } from './conversation/ContactName'; import { ContactName } from './conversation/ContactName';
@ -7,6 +14,11 @@ import { ContactName } from './conversation/ContactName';
import { cleanSearchTerm } from '../util/cleanSearchTerm'; import { cleanSearchTerm } from '../util/cleanSearchTerm';
import { LocalizerType } from '../types/Util'; import { LocalizerType } from '../types/Util';
interface MenuItem {
id: string;
name: string;
onClick?: () => void;
}
export interface Props { export interface Props {
searchTerm: string; searchTerm: string;
@ -36,9 +48,10 @@ export interface Props {
clearSearch: () => void; clearSearch: () => void;
onClick?: () => void; onClick?: () => void;
onCopyPublicKey?: () => void;
} }
export class MainHeader extends React.Component<Props> { export class MainHeader extends React.Component<Props, any> {
private readonly updateSearchBound: ( private readonly updateSearchBound: (
event: React.FormEvent<HTMLInputElement> event: React.FormEvent<HTMLInputElement>
) => void; ) => void;
@ -53,6 +66,12 @@ export class MainHeader extends React.Component<Props> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
this.state = {
expanded: false,
hasPass: null,
menuItems: [],
};
this.updateSearchBound = this.updateSearch.bind(this); this.updateSearchBound = this.updateSearch.bind(this);
this.clearSearchBound = this.clearSearch.bind(this); this.clearSearchBound = this.clearSearch.bind(this);
this.handleKeyUpBound = this.handleKeyUp.bind(this); this.handleKeyUpBound = this.handleKeyUp.bind(this);
@ -62,6 +81,17 @@ export class MainHeader extends React.Component<Props> {
this.debouncedSearch = debounce(this.search.bind(this), 20); 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() { public search() {
const { searchTerm, search, i18n, ourNumber, regionCode } = this.props; const { searchTerm, search, i18n, ourNumber, regionCode } = this.props;
if (search) { if (search) {
@ -122,19 +152,39 @@ export class MainHeader extends React.Component<Props> {
} }
public render() { public render() {
const { onClick } = this.props;
return (
<div role="button" className="module-main-header" onClick={onClick}>
<div className="module-main-header__container">
{this.renderName()}
{this.renderMenu()}
</div>
{this.renderSearch()}
</div>
);
}
private renderName() {
const { const {
searchTerm,
avatarPath, avatarPath,
i18n, i18n,
color, color,
name, name,
phoneNumber, phoneNumber,
profileName, profileName,
onClick,
} = this.props; } = this.props;
const { expanded } = this.state;
return ( return (
<div role="button" className="module-main-header" onClick={onClick}> <div
role="button"
className="module-main-header__title"
onClick={() => {
this.setState({ expanded: !expanded });
}}
>
<Avatar <Avatar
avatarPath={avatarPath} avatarPath={avatarPath}
color={color} color={color}
@ -152,31 +202,121 @@ export class MainHeader extends React.Component<Props> {
i18n={i18n} i18n={i18n}
/> />
</div> </div>
<div className="module-main-header__search"> <div
<div className={classNames(
role="button" 'module-main-header-content-toggle',
className="module-main-header__search__icon" expanded && 'module-main-header-content-toggle-visible'
onClick={this.setFocusBound} )}
/> />
<input </div>
type="text" );
ref={this.inputRef} }
className="module-main-header__search__input"
placeholder={i18n('search')} private renderMenu() {
dir="auto" const { expanded, menuItems } = this.state;
onKeyUp={this.handleKeyUpBound}
value={searchTerm} return (
onChange={this.updateSearchBound} <div className="module-main-header__menu">
/> <div className={classNames('accordian', expanded && 'expanded')}>
{searchTerm ? ( {menuItems.map((item: MenuItem) => (
<div <div
role="button" role="button"
className="module-main-header__search__cancel-icon" className="menu-item"
onClick={this.clearSearchBound} key={item.id}
/> onClick={item.onClick}
) : null} >
{item.name}
</div>
))}
</div> </div>
</div> </div>
); );
} }
private renderSearch() {
const { searchTerm, i18n } = this.props;
return (
<div className="module-main-header__search">
<input
type="text"
ref={this.inputRef}
className="module-main-header__search__input"
placeholder={i18n('search')}
dir="auto"
onKeyUp={this.handleKeyUpBound}
value={searchTerm}
onChange={this.updateSearchBound}
/>
<span
role="button"
className="module-main-header__search__icon"
onClick={this.setFocusBound}
/>
{searchTerm ? (
<span
role="button"
className="module-main-header__search__cancel-icon"
onClick={this.clearSearchBound}
/>
) : null}
</div>
);
}
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 });
}
} }

@ -13,6 +13,7 @@ import { LocalizerType } from '../types/Util';
export type PropsData = { export type PropsData = {
contacts: Array<ConversationListItemPropsType>; contacts: Array<ConversationListItemPropsType>;
friends: Array<ConversationListItemPropsType>;
conversations: Array<ConversationListItemPropsType>; conversations: Array<ConversationListItemPropsType>;
hideMessagesHeader: boolean; hideMessagesHeader: boolean;
messages: Array<MessageSearchResultPropsType>; messages: Array<MessageSearchResultPropsType>;
@ -49,16 +50,19 @@ export class SearchResults extends React.Component<Props> {
openConversation, openConversation,
searchTerm, searchTerm,
showStartNewConversation, showStartNewConversation,
friends,
} = this.props; } = this.props;
const haveConversations = conversations && conversations.length; const haveConversations = conversations && conversations.length;
const haveContacts = contacts && contacts.length; const haveContacts = contacts && contacts.length;
const haveFriends = friends && friends.length;
const haveMessages = messages && messages.length; const haveMessages = messages && messages.length;
const noResults = const noResults =
!showStartNewConversation && !showStartNewConversation &&
!haveConversations && !haveConversations &&
!haveContacts && !haveContacts &&
!haveMessages; !haveMessages &&
!haveFriends;
return ( return (
<div className="module-search-results"> <div className="module-search-results">
@ -89,21 +93,12 @@ export class SearchResults extends React.Component<Props> {
))} ))}
</div> </div>
) : null} ) : null}
{haveContacts ? ( {haveFriends
<div className="module-search-results__contacts"> ? this.renderContacts(i18n('friendsHeader'), friends, true)
<div className="module-search-results__contacts-header"> : null}
{i18n('contactsHeader')} {haveContacts
</div> ? this.renderContacts(i18n('contactsHeader'), contacts)
{contacts.map(contact => ( : null}
<ConversationListItem
key={contact.phoneNumber}
{...contact}
onClick={openConversation}
i18n={i18n}
/>
))}
</div>
) : null}
{haveMessages ? ( {haveMessages ? (
<div className="module-search-results__messages"> <div className="module-search-results__messages">
{hideMessagesHeader ? null : ( {hideMessagesHeader ? null : (
@ -124,4 +119,27 @@ export class SearchResults extends React.Component<Props> {
</div> </div>
); );
} }
private renderContacts(
header: string,
items: Array<ConversationListItemPropsType>,
friends?: boolean
) {
const { i18n, openConversation } = this.props;
return (
<div className="module-search-results__contacts">
<div className="module-search-results__contacts-header">{header}</div>
{items.map(contact => (
<ConversationListItem
key={contact.phoneNumber}
isFriendItem={friends}
{...contact}
onClick={openConversation}
i18n={i18n}
/>
))}
</div>
);
}
} }

@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import classNames from 'classnames';
import { Avatar } from './Avatar';
import { LocalizerType } from '../types/Util'; import { LocalizerType } from '../types/Util';
import { validateNumber } from '../types/PhoneNumber';
export interface Props { export interface Props {
phoneNumber: string; phoneNumber: string;
@ -14,25 +14,26 @@ export class StartNewConversation extends React.PureComponent<Props> {
public render() { public render() {
const { phoneNumber, i18n, onClick } = this.props; const { phoneNumber, i18n, onClick } = this.props;
const error = validateNumber(phoneNumber, i18n);
const avatar = error ? '!' : '#';
const click = error ? undefined : onClick;
return ( return (
<div <div
role="button" role="button"
className="module-start-new-conversation" className={classNames(
onClick={onClick} 'module-start-new-conversation',
!error && 'valid'
)}
onClick={click}
> >
<Avatar <div className="module-start-new-conversation__avatar">{avatar}</div>
color="grey"
conversationType="direct"
i18n={i18n}
phoneNumber={phoneNumber}
size={48}
/>
<div className="module-start-new-conversation__content"> <div className="module-start-new-conversation__content">
<div className="module-start-new-conversation__number"> <div className="module-start-new-conversation__number">
{phoneNumber} {phoneNumber}
</div> </div>
<div className="module-start-new-conversation__text"> <div className="module-start-new-conversation__text">
{i18n('startConversation')} {error || i18n('startConversation')}
</div> </div>
</div> </div>
</div> </div>

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import classNames from 'classnames';
import { Emojify } from './Emojify'; import { Emojify } from './Emojify';
import { LocalizerType } from '../../types/Util'; import { LocalizerType } from '../../types/Util';
interface Props { interface Props {
@ -29,7 +29,14 @@ export class ContactName extends React.Component<Props> {
<span className={prefix} dir="auto"> <span className={prefix} dir="auto">
{profileElement} {profileElement}
{shouldShowProfile ? ' ' : null} {shouldShowProfile ? ' ' : null}
<Emojify text={title} i18n={i18n} /> <span
className={classNames(
`${prefix}__profile-number`,
shouldShowProfile && 'italic'
)}
>
<Emojify text={title} i18n={i18n} />
</span>
</span> </span>
); );
} }

@ -0,0 +1,6 @@
export async function hasPassword() {
// @ts-ignore
const hash = await window.Signal.Data.getPasswordHash();
return !!hash;
}

@ -96,27 +96,19 @@ export const _getLeftPaneLists = (
): { ): {
conversations: Array<ConversationType>; conversations: Array<ConversationType>;
archivedConversations: Array<ConversationType>; archivedConversations: Array<ConversationType>;
contacts: Array<ConversationType>; friends: Array<ConversationType>;
} => { } => {
const values = Object.values(lookup); const values = Object.values(lookup);
const sorted = values.sort(comparator); const sorted = values.sort(comparator);
const conversations: Array<ConversationType> = []; const conversations: Array<ConversationType> = [];
const archivedConversations: Array<ConversationType> = []; const archivedConversations: Array<ConversationType> = [];
const contacts: Array<ConversationType> = []; const friends: Array<ConversationType> = [];
const max = sorted.length; const max = sorted.length;
for (let i = 0; i < max; i += 1) { for (let i = 0; i < max; i += 1) {
let conversation = sorted[i]; let conversation = sorted[i];
if (conversation.isFriend) {
contacts.push(conversation);
}
if (!conversation.activeAt) {
continue;
}
if (selectedConversation === conversation.id) { if (selectedConversation === conversation.id) {
conversation = { conversation = {
...conversation, ...conversation,
@ -124,6 +116,14 @@ export const _getLeftPaneLists = (
}; };
} }
if (conversation.isFriend) {
friends.push(conversation);
}
if (!conversation.activeAt) {
continue;
}
if (conversation.isArchived) { if (conversation.isArchived) {
archivedConversations.push(conversation); archivedConversations.push(conversation);
} else { } else {
@ -131,7 +131,7 @@ export const _getLeftPaneLists = (
} }
} }
return { conversations, archivedConversations, contacts }; return { conversations, archivedConversations, friends };
}; };
export const getLeftPaneLists = createSelector( export const getLeftPaneLists = createSelector(

@ -50,8 +50,6 @@ export const getSearchResults = createSelector(
) => { ) => {
return { return {
contacts: compact( contacts: compact(
/*
LOKI: Unsure what signal does with this
state.contacts.map(id => { state.contacts.map(id => {
const value = lookup[id]; const value = lookup[id];
@ -64,34 +62,35 @@ export const getSearchResults = createSelector(
return value; return value;
}) })
*/ ),
conversations: compact(
state.conversations.map(id => { state.conversations.map(id => {
const value = lookup[id]; const value = lookup[id];
const friend = value && value.isFriend ? { ...value } : null; if (value && id === selectedConversation) {
if (friend && id === selectedConversation) {
return { return {
...friend, ...value,
isSelected: true, isSelected: true,
}; };
} }
return friend; return value;
}) })
), ),
conversations: compact( friends: compact(
state.conversations.map(id => { state.conversations.map(id => {
const value = lookup[id]; const value = lookup[id];
if (value && id === selectedConversation) { const friend = value && value.isFriend ? { ...value } : null;
if (friend && id === selectedConversation) {
return { return {
...value, ...friend,
isSelected: true, isSelected: true,
}; };
} }
return value; return friend;
}) })
), ),
hideMessagesHeader: false, hideMessagesHeader: false,
@ -107,9 +106,9 @@ export const getSearchResults = createSelector(
}), }),
regionCode: regionCode, regionCode: regionCode,
searchTerm: state.query, 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],
}; };
} }
); );

@ -1,3 +1,5 @@
import { LocalizerType } from './Util';
export function format( export function format(
phoneNumber: string, phoneNumber: string,
_options: { _options: {
@ -31,18 +33,30 @@ export function normalize(
} }
} }
function isValidNumber(number: string) { function validate(number: string) {
// Check if it's hex // Check if it's hex
const isHex = number.replace(/[\s]*/g, '').match(/^[0-9a-fA-F]+$/); const isHex = number.replace(/[\s]*/g, '').match(/^[0-9a-fA-F]+$/);
if (!isHex) { if (!isHex) {
return false; return 'invalidHexId';
} }
// Check if the pubkey length is 33 and leading with 05 or of length 32 // Check if the pubkey length is 33 and leading with 05 or of length 32
const len = number.length; const len = number.length;
if ((len !== 33 * 2 || !/^05/.test(number)) && len !== 32 * 2) { 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);
} }

@ -214,6 +214,13 @@
dependencies: dependencies:
"@types/react" "*" "@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": "@types/react-redux@7.0.1":
version "7.0.1" version "7.0.1"
resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.0.1.tgz#9dd2503be7a9861c5a092bf1c5050b7ade4dc62e" 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" loose-envify "^1.3.1"
object-assign "^4.1.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" version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== 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" resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== 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: react-redux@6.0.1:
version "6.0.1" version "6.0.1"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-6.0.1.tgz#0d423e2c1cb10ada87293d47e7de7c329623ba4d" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-6.0.1.tgz#0d423e2c1cb10ada87293d47e7de7c329623ba4d"

Loading…
Cancel
Save