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",
"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"
}
}

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

@ -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,

@ -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",

@ -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;

@ -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 {

@ -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;
}

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

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

@ -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',
},
],
};

@ -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<Props> {
name,
phoneNumber,
profileName,
isFriendItem,
} = this.props;
return (
@ -132,21 +136,23 @@ export class ConversationListItem extends React.PureComponent<Props> {
/>
)}
</div>
<div
className={classNames(
'module-conversation-list-item__header__date',
unreadCount > 0
? 'module-conversation-list-item__header__date--has-unread'
: null
)}
>
<Timestamp
timestamp={lastUpdated}
extended={false}
module="module-conversation-list-item__header__timestamp"
i18n={i18n}
/>
</div>
{!isFriendItem && (
<div
className={classNames(
'module-conversation-list-item__header__date',
unreadCount > 0
? 'module-conversation-list-item__header__date--has-unread'
: null
)}
>
<Timestamp
timestamp={lastUpdated}
extended={false}
module="module-conversation-list-item__header__timestamp"
i18n={i18n}
/>
</div>
)}
</div>
);
}
@ -192,12 +198,27 @@ export class ConversationListItem extends React.PureComponent<Props> {
}
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 (
<div className="module-conversation-list-item__message">
<div
@ -277,7 +298,7 @@ export class ConversationListItem extends React.PureComponent<Props> {
</div>
</div>
</ContextMenuTrigger>
{this.renderContextMenu(triggerId)}
<Portal>{this.renderContextMenu(triggerId)}</Portal>
</div>
);
}

@ -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<ConversationListItemPropsType>;
friends?: Array<ConversationListItemPropsType>;
archivedConversations?: Array<ConversationListItemPropsType>;
searchResults?: SearchResultsProps;
showArchived?: boolean;
@ -42,7 +44,52 @@ type RowRendererParamsType = {
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 = ({
index,
key,
@ -50,11 +97,15 @@ export class LeftPane extends React.Component<Props> {
}: 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<Props> {
{...conversation}
onClick={openConversationInternal}
i18n={i18n}
isFriendItem={currentTab !== 'conversations'}
/>
);
};
@ -119,7 +171,6 @@ export class LeftPane extends React.Component<Props> {
const {
archivedConversations,
i18n,
conversations,
openConversationInternal,
startNewConversation,
searchResults,
@ -137,6 +188,8 @@ export class LeftPane extends React.Component<Props> {
);
}
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<Props> {
</div>
);
return [archived, list];
return [this.renderTabs(), archived, list];
}
public renderArchivedHeader(): JSX.Element {

@ -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<Props> {
export class MainHeader extends React.Component<Props, any> {
private readonly updateSearchBound: (
event: React.FormEvent<HTMLInputElement>
) => void;
@ -53,6 +66,12 @@ export class MainHeader extends React.Component<Props> {
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<Props> {
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<Props> {
}
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 {
searchTerm,
avatarPath,
i18n,
color,
name,
phoneNumber,
profileName,
onClick,
} = this.props;
const { expanded } = this.state;
return (
<div role="button" className="module-main-header" onClick={onClick}>
<div
role="button"
className="module-main-header__title"
onClick={() => {
this.setState({ expanded: !expanded });
}}
>
<Avatar
avatarPath={avatarPath}
color={color}
@ -152,31 +202,121 @@ export class MainHeader extends React.Component<Props> {
i18n={i18n}
/>
</div>
<div className="module-main-header__search">
<div
role="button"
className="module-main-header__search__icon"
onClick={this.setFocusBound}
/>
<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}
/>
{searchTerm ? (
<div
className={classNames(
'module-main-header-content-toggle',
expanded && 'module-main-header-content-toggle-visible'
)}
/>
</div>
);
}
private renderMenu() {
const { expanded, menuItems } = this.state;
return (
<div className="module-main-header__menu">
<div className={classNames('accordian', expanded && 'expanded')}>
{menuItems.map((item: MenuItem) => (
<div
role="button"
className="module-main-header__search__cancel-icon"
onClick={this.clearSearchBound}
/>
) : null}
className="menu-item"
key={item.id}
onClick={item.onClick}
>
{item.name}
</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 = {
contacts: Array<ConversationListItemPropsType>;
friends: Array<ConversationListItemPropsType>;
conversations: Array<ConversationListItemPropsType>;
hideMessagesHeader: boolean;
messages: Array<MessageSearchResultPropsType>;
@ -49,16 +50,19 @@ export class SearchResults extends React.Component<Props> {
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 (
<div className="module-search-results">
@ -89,21 +93,12 @@ export class SearchResults extends React.Component<Props> {
))}
</div>
) : null}
{haveContacts ? (
<div className="module-search-results__contacts">
<div className="module-search-results__contacts-header">
{i18n('contactsHeader')}
</div>
{contacts.map(contact => (
<ConversationListItem
key={contact.phoneNumber}
{...contact}
onClick={openConversation}
i18n={i18n}
/>
))}
</div>
) : null}
{haveFriends
? this.renderContacts(i18n('friendsHeader'), friends, true)
: null}
{haveContacts
? this.renderContacts(i18n('contactsHeader'), contacts)
: null}
{haveMessages ? (
<div className="module-search-results__messages">
{hideMessagesHeader ? null : (
@ -124,4 +119,27 @@ export class SearchResults extends React.Component<Props> {
</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 { 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<Props> {
public render() {
const { phoneNumber, i18n, onClick } = this.props;
const error = validateNumber(phoneNumber, i18n);
const avatar = error ? '!' : '#';
const click = error ? undefined : onClick;
return (
<div
role="button"
className="module-start-new-conversation"
onClick={onClick}
className={classNames(
'module-start-new-conversation',
!error && 'valid'
)}
onClick={click}
>
<Avatar
color="grey"
conversationType="direct"
i18n={i18n}
phoneNumber={phoneNumber}
size={48}
/>
<div className="module-start-new-conversation__avatar">{avatar}</div>
<div className="module-start-new-conversation__content">
<div className="module-start-new-conversation__number">
{phoneNumber}
</div>
<div className="module-start-new-conversation__text">
{i18n('startConversation')}
{error || i18n('startConversation')}
</div>
</div>
</div>

@ -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<Props> {
<span className={prefix} dir="auto">
{profileElement}
{shouldShowProfile ? ' ' : null}
<Emojify text={title} i18n={i18n} />
<span
className={classNames(
`${prefix}__profile-number`,
shouldShowProfile && 'italic'
)}
>
<Emojify text={title} i18n={i18n} />
</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>;
archivedConversations: Array<ConversationType>;
contacts: Array<ConversationType>;
friends: Array<ConversationType>;
} => {
const values = Object.values(lookup);
const sorted = values.sort(comparator);
const conversations: Array<ConversationType> = [];
const archivedConversations: Array<ConversationType> = [];
const contacts: Array<ConversationType> = [];
const friends: Array<ConversationType> = [];
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(

@ -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],
};
}
);

@ -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);
}

@ -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"

Loading…
Cancel
Save