Fix conflicts
This commit is contained in:
commit
74fd0159eb
|
@ -182,7 +182,7 @@ module.exports = grunt => {
|
|||
tasks: ['sass'],
|
||||
},
|
||||
transpile: {
|
||||
files: ['./ts/**/*.ts', './ts/**/*.tsx'],
|
||||
files: ['./ts/**/*.ts', './ts/**/*.tsx', './ts/**/**/*.tsx'],
|
||||
tasks: ['exec:transpile'],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -778,7 +778,7 @@
|
|||
"description": "Shown to separate the types of search results"
|
||||
},
|
||||
"welcomeToSignal": {
|
||||
"message": "Welcome to Loki Messenger"
|
||||
"message": "Welcome to Session"
|
||||
},
|
||||
"selectAContact": {
|
||||
"message": "Select a contact or group to start chatting."
|
||||
|
@ -2405,7 +2405,7 @@
|
|||
"or": {
|
||||
"message": "or"
|
||||
},
|
||||
"ByUsingThiService...": {
|
||||
"ByUsingThisService...": {
|
||||
"message":
|
||||
"By using this service, you agree to our <a>Terms and Conditions</a> and <a>Privacy Statement</a>"
|
||||
},
|
||||
|
@ -2417,5 +2417,51 @@
|
|||
},
|
||||
"completeSignUp": {
|
||||
"message": "Complete Sign Up"
|
||||
},
|
||||
"compose": {
|
||||
"message": "Compose"
|
||||
},
|
||||
"searchForAKeyPhrase": {
|
||||
"message": "Search for a key phrase or contact"
|
||||
},
|
||||
"enterRecipient": {
|
||||
"message": "Enter Recipient"
|
||||
},
|
||||
"enterSessionID": {
|
||||
"message": "Enter Session ID"
|
||||
},
|
||||
"pasteSessionIDRecipient": {
|
||||
"message": "Paste Session ID of recipient"
|
||||
},
|
||||
"usersCanShareTheir...": {
|
||||
"message":
|
||||
"Users can share their Session ID by going into their account settings and clicking \"Share Public Key\"."
|
||||
},
|
||||
"searchByIDOrDisplayName": {
|
||||
"message": "Search by ID # or Display Name"
|
||||
},
|
||||
"message": {
|
||||
"message": "Message"
|
||||
},
|
||||
"lists": {
|
||||
"message": "Lists"
|
||||
},
|
||||
"edit": {
|
||||
"message": "Edit"
|
||||
},
|
||||
"addContact": {
|
||||
"message": "Add Contact"
|
||||
},
|
||||
"createGroup": {
|
||||
"message": "Create Group"
|
||||
},
|
||||
"yourPublicKey": {
|
||||
"message": "Your Public Key"
|
||||
},
|
||||
"accept": {
|
||||
"message": "Accept"
|
||||
},
|
||||
"decline": {
|
||||
"message": "Decline"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,24 +29,15 @@
|
|||
|
||||
<script type='text/x-tmpl-mustache' id='app-loading-screen'>
|
||||
<div class='content'>
|
||||
<img src='images/loki/loki_icon_128.png'>
|
||||
<div class='container'>
|
||||
<span class='dot'></span>
|
||||
<span class='dot'></span>
|
||||
<span class='dot'></span>
|
||||
</div>
|
||||
<div class='message'>{{ message }}</div>
|
||||
<img src='images/session/brand.svg' class='session-filter-color-green session-logo-128' />
|
||||
<p class='session-logo-text'>SESSION</p>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type='text/x-tmpl-mustache' id='conversation-loading-screen'>
|
||||
<div class='content'>
|
||||
<img src='images/loki/loki_icon_128.png'>
|
||||
<div class='container'>
|
||||
<span class='dot'></span>
|
||||
<span class='dot'></span>
|
||||
<span class='dot'></span>
|
||||
</div>
|
||||
<img src='images/session/brand.svg' class='session-filter-color-green session-logo-128' />
|
||||
<p class='session-logo-text'>SESSION</p>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
|
@ -56,34 +47,20 @@
|
|||
<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'>
|
||||
<div class='conversation-header'></div>
|
||||
<div class='container'>
|
||||
<div class='content'>
|
||||
<img src='images/loki/loki_icon_128.png' />
|
||||
<h3>{{ welcomeToSignal }}</h3>
|
||||
<p>{{ selectAContact }}</p>
|
||||
<img src='images/session/brand.svg' class='session-filter-color-green session-logo-128' />
|
||||
<p class='session-logo-text'>SESSION</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='lightbox-container'></div>
|
||||
</script>
|
||||
<script type='text/x-tmpl-mustache' id='main-header-placeholder'>
|
||||
<div class='main-header-title-wrapper'>
|
||||
<div class='main-header-content-toggle'/>
|
||||
</div>
|
||||
<div class='main-header-content-wrapper'>
|
||||
{{ #items }}
|
||||
<div role='button' id='{{ id }}'>{{ text }}</div>
|
||||
{{ /items }}
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type='text/x-tmpl-mustache' id='scroll-down-button-view'>
|
||||
<button class='text module-scroll-down__button {{ buttonClass }}' alt='{{ moreBelow }}'>
|
||||
|
@ -709,7 +686,6 @@
|
|||
<script type='text/javascript' src='js/views/file_input_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/list_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/contact_list_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/main_header_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/message_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/key_verification_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/message_list_view.js'></script>
|
||||
|
@ -751,14 +727,8 @@
|
|||
<body>
|
||||
<div class='app-loading-screen'>
|
||||
<div class='content'>
|
||||
<img src='images/loki/loki_icon_128.png'>
|
||||
<div class='container'>
|
||||
<span class='dot'></span>
|
||||
<span class='dot'></span>
|
||||
<span class='dot'></span>
|
||||
</div>
|
||||
<div class='message'></div>
|
||||
</div>
|
||||
<img src='images/session/brand.svg' class='session-filter-color-green session-logo-128' />
|
||||
<p class='session-logo-text'>SESSION</p>
|
||||
</div>
|
||||
|
||||
<script type='text/javascript' src='js/background.js'></script>
|
||||
|
|
|
@ -14,13 +14,8 @@
|
|||
<body id="signal-container" class='signal index'>
|
||||
<div class='app-loading-screen'>
|
||||
<div class='content'>
|
||||
<img src='/images/loki/loki_icon_128.png'>
|
||||
<div class='container'>
|
||||
<span class='dot'></span>
|
||||
<span class='dot'></span>
|
||||
<span class='dot'></span>
|
||||
</div>
|
||||
<div class='message'></div>
|
||||
<img src='images/session/brand.svg' class='session-filter-color-green session-logo-128' />
|
||||
<p class='session-logo-text'>SESSION</p>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript" src="js/index.js"></script>
|
||||
|
|
|
@ -231,7 +231,28 @@
|
|||
this.trigger('change');
|
||||
this.messageCollection.forEach(m => m.trigger('change'));
|
||||
},
|
||||
async acceptFriendRequest() {
|
||||
const messages = await window.Signal.Data.getMessagesByConversation(
|
||||
this.id,
|
||||
{ limit: 1, MessageCollection: Whisper.MessageCollection }
|
||||
);
|
||||
|
||||
const lastMessageModel = messages.at(0);
|
||||
if (lastMessageModel) {
|
||||
lastMessageModel.acceptFriendRequest();
|
||||
}
|
||||
},
|
||||
async declineFriendRequest() {
|
||||
const messages = await window.Signal.Data.getMessagesByConversation(
|
||||
this.id,
|
||||
{ limit: 1, MessageCollection: Whisper.MessageCollection }
|
||||
);
|
||||
|
||||
const lastMessageModel = messages.at(0);
|
||||
if (lastMessageModel) {
|
||||
lastMessageModel.declineFriendRequest();
|
||||
}
|
||||
},
|
||||
setMessageSelectionBackdrop() {
|
||||
const messageSelected = this.selectedMessages.size > 0;
|
||||
|
||||
|
@ -544,10 +565,11 @@
|
|||
title: this.getTitle(),
|
||||
unreadCount: this.get('unreadCount') || 0,
|
||||
mentionedUs: this.get('mentionedUs') || false,
|
||||
showFriendRequestIndicator: this.isPendingFriendRequest(),
|
||||
isPendingFriendRequest: this.isPendingFriendRequest(),
|
||||
hasReceivedFriendRequest: this.hasReceivedFriendRequest(),
|
||||
hasSentFriendRequest: this.hasSentFriendRequest(),
|
||||
isBlocked: this.isBlocked(),
|
||||
isSecondary: !!this.get('secondaryStatus'),
|
||||
|
||||
phoneNumber: format(this.id, {
|
||||
ourRegionCode: regionCode,
|
||||
}),
|
||||
|
@ -571,6 +593,8 @@
|
|||
onDeleteContact: () => this.deleteContact(),
|
||||
onDeleteMessages: () => this.deleteMessages(),
|
||||
onCloseOverlay: () => this.resetMessageSelection(),
|
||||
acceptFriendRequest: () => this.acceptFriendRequest(),
|
||||
declineFriendRequest: () => this.declineFriendRequest(),
|
||||
};
|
||||
|
||||
this.updateAsyncPropsCache();
|
||||
|
@ -954,6 +978,12 @@
|
|||
direction: 'incoming',
|
||||
});
|
||||
await window.libloki.storage.removeContactPreKeyBundle(this.id);
|
||||
await this.destroyMessages();
|
||||
window.pushToast({
|
||||
title: i18n('friendRequestDeclined'),
|
||||
type: 'success',
|
||||
id: 'declineFriendRequest',
|
||||
});
|
||||
},
|
||||
// We have accepted an incoming friend request
|
||||
async onAcceptFriendRequest(options = {}) {
|
||||
|
@ -962,6 +992,7 @@
|
|||
}
|
||||
if (this.hasReceivedFriendRequest()) {
|
||||
this.setFriendRequestStatus(FriendRequestStatusEnum.friends, options);
|
||||
|
||||
await this.respondToAllFriendRequests({
|
||||
response: 'accepted',
|
||||
direction: 'incoming',
|
||||
|
|
|
@ -398,7 +398,6 @@
|
|||
return;
|
||||
}
|
||||
const conversation = await this.getSourceDeviceConversation();
|
||||
|
||||
// If we somehow received an old friend request (e.g. after having restored
|
||||
// from seed, we won't be able to accept it, we should initiate our own
|
||||
// friend request to reset the session:
|
||||
|
@ -408,7 +407,6 @@
|
|||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.set({ friendStatus: 'accepted' });
|
||||
await window.Signal.Data.saveMessage(this.attributes, {
|
||||
Message: Whisper.Message,
|
||||
|
|
|
@ -42,7 +42,6 @@ const {
|
|||
} = require('../../ts/components/conversation/GroupNotification');
|
||||
const { Lightbox } = require('../../ts/components/Lightbox');
|
||||
const { LightboxGallery } = require('../../ts/components/LightboxGallery');
|
||||
const { MainHeader } = require('../../ts/components/MainHeader');
|
||||
const { MemberList } = require('../../ts/components/conversation/MemberList');
|
||||
const { BulkEdit } = require('../../ts/components/conversation/BulkEdit');
|
||||
const {
|
||||
|
@ -270,7 +269,6 @@ exports.setup = (options = {}) => {
|
|||
GroupNotification,
|
||||
Lightbox,
|
||||
LightboxGallery,
|
||||
MainHeader,
|
||||
MemberList,
|
||||
CreateGroupDialog,
|
||||
EditProfileDialog,
|
||||
|
|
|
@ -66,12 +66,7 @@
|
|||
Whisper.AppLoadingScreen = Whisper.View.extend({
|
||||
templateName: 'app-loading-screen',
|
||||
className: 'app-loading-screen',
|
||||
updateProgress(count) {
|
||||
if (count > 0) {
|
||||
const message = i18n('loadingMessages', count.toString());
|
||||
this.$('.message').text(message);
|
||||
}
|
||||
},
|
||||
updateProgress() {},
|
||||
render_attributes: {
|
||||
message: i18n('loading'),
|
||||
},
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
/* global Whisper, textsecure, ConversationController, Signal */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
Whisper.MainHeaderView = Whisper.View.extend({
|
||||
templateName: 'main-header-placeholder',
|
||||
events: {
|
||||
'click .main-header-title-wrapper': 'onClick',
|
||||
},
|
||||
initialize(options) {
|
||||
this.ourNumber = textsecure.storage.user.getNumber();
|
||||
const me = ConversationController.getOrCreate(this.ourNumber, 'private');
|
||||
|
||||
this.mainHeaderView = new Whisper.ReactWrapperView({
|
||||
className: 'main-header-wrapper',
|
||||
Component: Signal.Components.MainHeader,
|
||||
props: me.format(),
|
||||
});
|
||||
const update = () => this.mainHeaderView.update(me.format());
|
||||
this.listenTo(me, 'change', update);
|
||||
|
||||
this.render();
|
||||
this.$('.main-header-title-wrapper').prepend(this.mainHeaderView.el);
|
||||
|
||||
this.$toggle = this.$('.main-header-content-toggle');
|
||||
this.$content = this.$('.main-header-content-wrapper');
|
||||
this.$content.hide();
|
||||
|
||||
this.updateItems(options.items);
|
||||
},
|
||||
updateItems(items) {
|
||||
this.$content.html('');
|
||||
(items || []).forEach(item => {
|
||||
// Add the item
|
||||
this.$content.append(
|
||||
`<div role='button' id='${item.id}'>${item.text}</div>`
|
||||
);
|
||||
|
||||
// Register its callback
|
||||
if (item.onClick) {
|
||||
this.$(`#${item.id}`).click(item.onClick);
|
||||
}
|
||||
});
|
||||
},
|
||||
render_attributes() {
|
||||
return {
|
||||
items: this.items,
|
||||
};
|
||||
},
|
||||
onClick() {
|
||||
// Toggle section visibility
|
||||
this.$content.slideToggle('fast');
|
||||
this.$toggle.toggleClass('main-header-content-toggle-visible');
|
||||
},
|
||||
});
|
||||
})();
|
|
@ -121,7 +121,7 @@
|
|||
window.Session = window.Session || {};
|
||||
|
||||
window.Session.setNewSessionID = sessionID => {
|
||||
const el = document.querySelector('.session-signin-enter-session-id');
|
||||
const el = document.querySelector('.session-id-editable');
|
||||
const fx = new TextScramble(el);
|
||||
el.innerHTML = sessionID;
|
||||
fx.setText(sessionID);
|
||||
|
|
|
@ -77,13 +77,10 @@
|
|||
<body>
|
||||
<div class='app-loading-screen'>
|
||||
<div class='content'>
|
||||
<img src='images/loki/loki_icon_128.png'>
|
||||
<div class='container'>
|
||||
<span class='dot'></span>
|
||||
<span class='dot'></span>
|
||||
<span class='dot'></span>
|
||||
<img src='images/session/brand.svg' class='session-filter-color-green session-logo-128' />
|
||||
<div class='session-content-accent-text'>
|
||||
<p class='title'>SESSION</p>
|
||||
</div>
|
||||
<div class='message'></div>
|
||||
</div>
|
||||
</div>
|
||||
<script type='text/javascript' src='js/password_start.js'></script>
|
||||
|
|
|
@ -470,7 +470,7 @@
|
|||
line-height: 16px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.module-conversation__user,
|
||||
.module-message__author {
|
||||
color: $color-white;
|
||||
font-size: 13px;
|
||||
|
@ -482,7 +482,7 @@
|
|||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.module-conversation__user__profile-name,
|
||||
.module-message__author__profile-name {
|
||||
font-style: italic;
|
||||
}
|
||||
|
@ -3233,8 +3233,7 @@
|
|||
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
|
||||
width: 300px;
|
||||
width: 380px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
|
|
@ -52,6 +52,7 @@ $session-shade-14: #535865;
|
|||
$session-shade-15: #5b6c72;
|
||||
$session-shade-16: #979797;
|
||||
$session-shade-17: #d8d8d8;
|
||||
$session-shade-18: #141414;
|
||||
|
||||
$session-opaque-dark-1: rgba(0, 0, 0, 0.25);
|
||||
$session-opaque-dark-2: rgba(0, 0, 0, 0.37);
|
||||
|
@ -63,6 +64,8 @@ $session-color-black: #000;
|
|||
$session-color-danger: #ff453a;
|
||||
$session-color-primary: $session-shade-13;
|
||||
$session-color-secondary: $session-shade-16;
|
||||
$session-background-overlay: #191919;
|
||||
$session-background: #121212;
|
||||
|
||||
$session-color-info: $session-shade-11;
|
||||
$session-color-success: #35d388;
|
||||
|
@ -89,6 +92,11 @@ div.spacer-lg {
|
|||
height: $session-margin-lg;
|
||||
}
|
||||
|
||||
@mixin session-filter-color-green {
|
||||
filter: brightness(0) saturate(100%) invert(67%) sepia(69%) saturate(2367%)
|
||||
hue-rotate(101deg) brightness(107%) contrast(101%);
|
||||
}
|
||||
|
||||
@mixin session-color-subtle($color) {
|
||||
color: rgba($color, 0.6);
|
||||
}
|
||||
|
@ -116,6 +124,16 @@ div.spacer-lg {
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
@mixin session-dark-background {
|
||||
background-color: $session-background !important;
|
||||
}
|
||||
@mixin session-dark-background-lighter {
|
||||
background-color: $session-background-overlay !important;
|
||||
}
|
||||
@mixin session-dark-background-hover {
|
||||
background-color: $session-shade-7 !important;
|
||||
}
|
||||
|
||||
$session-transition-duration: 0.25s;
|
||||
|
||||
$session-icon-size-sm: 15px;
|
||||
|
@ -138,6 +156,10 @@ div,
|
|||
span,
|
||||
label {
|
||||
user-select: none;
|
||||
|
||||
&::selection {
|
||||
background: $session-shade-17;
|
||||
}
|
||||
}
|
||||
|
||||
$session-gradient-green: linear-gradient(
|
||||
|
@ -312,12 +334,23 @@ $session_message-container-border-radius: 5px;
|
|||
}
|
||||
}
|
||||
.session-icon-button {
|
||||
opacity: 0.4;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
transition: opacity $session-transition-duration;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
position: relative;
|
||||
|
||||
svg {
|
||||
transition: opacity $session-transition-duration;
|
||||
opacity: 0.4;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.no-opacity {
|
||||
svg {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.small.padded {
|
||||
|
@ -331,6 +364,23 @@ $session_message-container-border-radius: 5px;
|
|||
&.large.padded {
|
||||
@include set-icon-margin($session-icon-size-lg);
|
||||
}
|
||||
|
||||
.notification-count {
|
||||
position: absolute;
|
||||
font-size: 12px;
|
||||
font-family: $session-font-family;
|
||||
top: -10px;
|
||||
right: -10px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding: 3px;
|
||||
border-radius: 50%;
|
||||
font-weight: 700;
|
||||
background: red;
|
||||
color: $session-color-white;
|
||||
text-align: center;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.session-icon {
|
||||
|
@ -360,7 +410,7 @@ $session_message-container-border-radius: 5px;
|
|||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.module-conversation__user__profile-name,
|
||||
.module-message__author__profile-name {
|
||||
font-style: normal;
|
||||
}
|
||||
|
@ -422,6 +472,10 @@ label {
|
|||
visibility: hidden;
|
||||
}
|
||||
|
||||
.session-button div[role='button'] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.input-with-label-container {
|
||||
height: 46.5px;
|
||||
width: 280px;
|
||||
|
@ -840,4 +894,9 @@ label {
|
|||
&__description {
|
||||
max-width: $session-modal-size-lg;
|
||||
}
|
||||
|
||||
|
||||
.messages li {
|
||||
transition: $session-transition-duration !important;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
.session {
|
||||
&-fullscreen {
|
||||
@include session-dark-background;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #121212 100%, #171717 0%);
|
||||
}
|
||||
|
||||
&-content {
|
||||
|
@ -102,17 +102,10 @@
|
|||
|
||||
@mixin registration-label-mixin {
|
||||
color: $session-color-white;
|
||||
text-align: center;
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
line-height: 17px;
|
||||
font-weight: bold;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
&__or {
|
||||
@include registration-label-mixin;
|
||||
}
|
||||
|
||||
&__welcome-session {
|
||||
@include registration-label-mixin;
|
||||
font-size: 12px;
|
||||
|
@ -124,6 +117,7 @@
|
|||
&__unique-session-id {
|
||||
@include registration-label-mixin;
|
||||
padding-top: 3em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__entry-fields {
|
||||
|
@ -210,7 +204,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
&-signup-header,
|
||||
&-description-long,
|
||||
&-signin-device-pairing-header {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
|
@ -220,9 +214,9 @@
|
|||
line-height: 20px;
|
||||
}
|
||||
|
||||
&-signin-enter-session-id {
|
||||
&-id-editable {
|
||||
height: 94px;
|
||||
width: 289px;
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
border: 2px solid $session-color-dark-grey;
|
||||
outline: 0;
|
||||
|
@ -236,10 +230,12 @@
|
|||
padding: 20px 5px 20px 5px;
|
||||
display: inline-block;
|
||||
font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
|
||||
user-select: all;
|
||||
}
|
||||
}
|
||||
|
||||
[contenteditable='true']:empty::before {
|
||||
content: attr(placeholder);
|
||||
color: $session-color-light-grey;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
// Messages
|
||||
.dark-theme {
|
||||
.conversation {
|
||||
background: none !important;
|
||||
}
|
||||
.module-conversation-header {
|
||||
border-bottom: none;
|
||||
background-color: $session-shade-4;
|
||||
}
|
||||
.discussion-container {
|
||||
background-image: $session-gradient-black;
|
||||
}
|
||||
|
||||
.module-message {
|
||||
&__author,
|
||||
|
@ -53,4 +53,51 @@
|
|||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.inbox {
|
||||
background-image: $session-gradient-black;
|
||||
}
|
||||
}
|
||||
.conversation {
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
.discussion-container {
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
@mixin session-h-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@include session-h-title;
|
||||
color: $session-shade-16;
|
||||
font-size: 25px;
|
||||
margin: 0;
|
||||
|
||||
&.active {
|
||||
color: $session-color-white;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
@include session-h-title;
|
||||
color: $session-color-white;
|
||||
font-size: 22px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@include session-h-title;
|
||||
color: $session-color-white;
|
||||
font-size: 18px;
|
||||
padding-top: 22px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
@include session-h-title;
|
||||
color: $session-color-white;
|
||||
font-size: 17px;
|
||||
text-align: center;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,349 @@
|
|||
$session-compose-margin: 20px;
|
||||
|
||||
.dark-theme {
|
||||
.module-conversation-list-item {
|
||||
@include session-dark-background-lighter;
|
||||
&:hover {
|
||||
@include session-dark-background-hover;
|
||||
}
|
||||
}
|
||||
.gutter {
|
||||
width: 380px !important;
|
||||
background: none !important;
|
||||
padding-right: 5px !important;
|
||||
}
|
||||
|
||||
.module-conversation {
|
||||
&-list-item {
|
||||
&--has-unread {
|
||||
border-left: 4px solid $session-color-green !important;
|
||||
}
|
||||
|
||||
&__unread-count {
|
||||
color: $color-white !important;
|
||||
background-color: $session-shade-10 !important;
|
||||
position: static !important;
|
||||
font-weight: 700 !important;
|
||||
box-shadow: none !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
&__header__date,
|
||||
&__header__date--has-unread {
|
||||
flex-grow: 1 !important;
|
||||
text-align: end !important;
|
||||
}
|
||||
|
||||
&__message__text {
|
||||
color: $session-shade-17;
|
||||
&--has-unread {
|
||||
color: $session-color-white !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__header__name {
|
||||
flex-grow: 0 !important;
|
||||
padding-right: 5px !important;
|
||||
color: $session-shade-17;
|
||||
}
|
||||
|
||||
&__header__name--with-unread .module-conversation__user__profile-number,
|
||||
&__header__name--with-unread .module-conversation__user__profile-name {
|
||||
color: $session-color-white;
|
||||
}
|
||||
}
|
||||
|
||||
&__user__profile {
|
||||
&-number,
|
||||
&-name {
|
||||
color: $session-shade-17;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.module-conversation-list-item--is-selected {
|
||||
background-color: $color-dark-70;
|
||||
.module-conversation__user__profile-number,
|
||||
.module-conversation__user__profile-name,
|
||||
.module-conversation-list-item__message__text {
|
||||
color: $session-color-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
.inbox {
|
||||
@include session-dark-background;
|
||||
}
|
||||
|
||||
.module-left-pane {
|
||||
border-right: none !important;
|
||||
width: 300px;
|
||||
height: -webkit-fill-available;
|
||||
background: none !important;
|
||||
|
||||
&-session {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&__sections-container {
|
||||
height: -webkit-fill-available;
|
||||
width: 80px;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
|
||||
.module-avatar,
|
||||
.session-icon-button {
|
||||
margin: 30px auto 30px auto;
|
||||
&:last-child {
|
||||
margin: auto auto 30px auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 28px 7px 28px 0px;
|
||||
|
||||
.session-button {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
cursor: pointer;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
&__list {
|
||||
height: -webkit-fill-available;
|
||||
}
|
||||
|
||||
&-compose {
|
||||
@include session-dark-background-lighter;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: -webkit-fill-available;
|
||||
.session-icon .exit {
|
||||
padding: 13px;
|
||||
}
|
||||
|
||||
h3,
|
||||
h2 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h3 {
|
||||
padding-top: 22px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
&-border-container {
|
||||
width: -webkit-fill-available;
|
||||
position: relative;
|
||||
margin-bottom: 50px;
|
||||
.green {
|
||||
position: absolute;
|
||||
color: $session-color-green;
|
||||
background-color: $session-color-green;
|
||||
height: 6px;
|
||||
width: 130px;
|
||||
left: 50%;
|
||||
margin-left: -65px;
|
||||
top: 50%;
|
||||
margin-top: 6px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.white {
|
||||
position: absolute;
|
||||
color: none;
|
||||
height: 1px;
|
||||
width: -webkit-fill-available;
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
.exit {
|
||||
margin-top: 10px;
|
||||
margin-left: 13px;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.session-search-input {
|
||||
margin: 10px $session-compose-margin 0 $session-compose-margin;
|
||||
width: -webkit-fill-available;
|
||||
}
|
||||
|
||||
.module-search-results {
|
||||
width: -webkit-fill-available;
|
||||
}
|
||||
|
||||
.session-description-long {
|
||||
font-size: 14px;
|
||||
margin: 0px 20px;
|
||||
}
|
||||
|
||||
.session-id-editable {
|
||||
margin: 0px 20px;
|
||||
width: -webkit-fill-available;
|
||||
flex-shrink: 0;
|
||||
user-select: all;
|
||||
}
|
||||
|
||||
.session-button {
|
||||
width: fit-content;
|
||||
margin-top: auto;
|
||||
margin-bottom: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-search-dropdown {
|
||||
width: 100%;
|
||||
flex-grow: 1;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.session-search-input {
|
||||
height: 34px;
|
||||
width: 100%;
|
||||
margin-right: 1px;
|
||||
margin-bottom: 10px;
|
||||
background: $session-shade-4;
|
||||
color: $session-color-white;
|
||||
display: inline-flex;
|
||||
|
||||
.session-icon-button {
|
||||
margin: auto 10px;
|
||||
}
|
||||
|
||||
input {
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
background: $session-shade-4;
|
||||
color: $session-color-white;
|
||||
border: none;
|
||||
flex-grow: 1;
|
||||
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.session-logo {
|
||||
&-128 {
|
||||
@include session-filter-color-green();
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
}
|
||||
|
||||
&-text {
|
||||
font-size: 70px;
|
||||
font-weight: bold;
|
||||
margin: 0px;
|
||||
color: $session-color-white;
|
||||
font-family: $session-font-family;
|
||||
}
|
||||
}
|
||||
|
||||
.app-loading-screen {
|
||||
@include session-dark-background;
|
||||
}
|
||||
|
||||
.contacts-dropdown {
|
||||
width: -webkit-fill-available;
|
||||
|
||||
&-row {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding: 8px 20px;
|
||||
margin: 0px $session-compose-margin;
|
||||
background: $session-shade-4;
|
||||
color: $session-color-light-grey;
|
||||
|
||||
&-selected,
|
||||
&:hover {
|
||||
font-weight: bold;
|
||||
color: $session-color-white;
|
||||
background: $session-shade-8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.contact-notification-count-bubble {
|
||||
background: $session-color-danger;
|
||||
line-height: 30px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
font-size: 14px;
|
||||
margin-left: auto;
|
||||
text-align: center;
|
||||
border-radius: 50%;
|
||||
color: $session-color-white;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.left-pane-contact {
|
||||
&-section,
|
||||
&-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.module-conversation-list-item {
|
||||
&--has-friend-request {
|
||||
border: 1px solid $session-shade-8 !important;
|
||||
|
||||
.module-conversation__user__profile-number,
|
||||
.module-conversation__user__profile-name {
|
||||
font-size: 13px !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
display: flex;
|
||||
|
||||
.session-button {
|
||||
font-size: 11px;
|
||||
padding: 6px;
|
||||
height: auto;
|
||||
margin: 0px;
|
||||
line-height: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-bottom-buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.session-button.square-outline.square.green,
|
||||
.session-button.square-outline.square.white {
|
||||
flex-grow: 1;
|
||||
border: 1px solid $session-shade-8;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.panel-text-divider {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid $session-color-dark-grey;
|
||||
line-height: 0.1em;
|
||||
margin: 50px 0 50px;
|
||||
|
||||
span {
|
||||
padding: 5px 10px;
|
||||
border-radius: 50px;
|
||||
background-color: $session-shade-18;
|
||||
color: $session-color-light-grey;
|
||||
border: 1px solid $session-color-dark-grey;
|
||||
}
|
||||
}
|
|
@ -725,7 +725,7 @@ body.dark-theme {
|
|||
.module-message__link-preview__location {
|
||||
color: $color-gray-25;
|
||||
}
|
||||
|
||||
.module-conversation__user,
|
||||
.module-message__author {
|
||||
color: $color-white;
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
@import 'session';
|
||||
@import 'session_signin';
|
||||
@import 'session_theme_dark';
|
||||
@import 'session_theme_dark_left_pane';
|
||||
|
||||
// Installer
|
||||
@import 'options';
|
||||
|
|
|
@ -19,24 +19,15 @@
|
|||
|
||||
<script type='text/x-tmpl-mustache' id='app-loading-screen'>
|
||||
<div class='content'>
|
||||
<img src='/images/loki/loki_icon_128.png' height='150'>
|
||||
<div class='container'>
|
||||
<span class='dot'></span>
|
||||
<span class='dot'></span>
|
||||
<span class='dot'></span>
|
||||
</div>
|
||||
<div class='message'>{{ message }}</div>
|
||||
<img src='images/session/brand.svg' class='session-filter-color-green session-logo-128' />
|
||||
<p class='session-logo-text'>SESSION</p>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type='text/x-tmpl-mustache' id='conversation-loading-screen'>
|
||||
<div class='content'>
|
||||
<img src='/images/loki/loki_icon_128.png'>
|
||||
<div class='container'>
|
||||
<span class='dot'></span>
|
||||
<span class='dot'></span>
|
||||
<span class='dot'></span>
|
||||
</div>
|
||||
<img src='images/session/brand.svg' class='session-filter-color-green session-logo-128' />
|
||||
<p class='session-logo-text'>SESSION</p>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
|
@ -50,9 +41,8 @@
|
|||
<div class='conversation-header'></div>
|
||||
<div class='container'>
|
||||
<div class='content'>
|
||||
<img src='/images/loki/loki_icon_128.png' />
|
||||
<h3>{{ welcomeToSignal }}</h3>
|
||||
<p>{{ selectAContact }}</p>
|
||||
<img src='images/session/brand.svg' class='session-filter-color-green session-logo-128' />
|
||||
<p class='session-logo-text'>SESSION</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -305,16 +295,7 @@
|
|||
</div>
|
||||
{{/action }}
|
||||
</script>
|
||||
<script type='text/x-tmpl-mustache' id='main-header-placeholder'>
|
||||
<div class='main-header-title-wrapper'>
|
||||
<div class='main-header-content-toggle'/>
|
||||
</div>
|
||||
<div class='main-header-content-wrapper'>
|
||||
{{ #items }}
|
||||
<div role='button' id='{{ id }}'>{{ text }}</div>
|
||||
{{ /items }}
|
||||
</div>
|
||||
</script>
|
||||
|
||||
|
||||
<script type='text/x-tmpl-mustache' id='import-flow-template'>
|
||||
{{#isStep2}}
|
||||
|
@ -550,7 +531,6 @@
|
|||
<script type='text/javascript' src='../js/views/file_input_view.js' data-cover></script>
|
||||
<script type='text/javascript' src='../js/views/list_view.js' data-cover></script>
|
||||
<script type='text/javascript' src='../js/views/contact_list_view.js' data-cover></script>
|
||||
<script type='text/javascript' src='../js/views/main_header_view.js'></script>
|
||||
<script type='text/javascript' src='../js/views/timestamp_view.js' data-cover></script>
|
||||
<script type='text/javascript' src='../js/views/message_view.js' data-cover></script>
|
||||
<script type='text/javascript' src='../js/views/key_verification_view.js' data-cover></script>
|
||||
|
@ -561,7 +541,6 @@
|
|||
<script type='text/javascript' src='../js/views/recorder_view.js' data-cover></script>
|
||||
<script type='text/javascript' src='../js/views/conversation_view.js' data-cover></script>
|
||||
<script type='text/javascript' src='../js/views/hint_view.js' data-cover></script>
|
||||
<script type='text/javascript' src='../js/views/main_header_view.js' data-cover></script>
|
||||
<script type='text/javascript' src='../js/views/inbox_view.js' data-cover></script>
|
||||
<script type='text/javascript' src='../js/views/network_status_view.js'></script>
|
||||
<script type='text/javascript' src='../js/views/confirmation_dialog_view.js' data-cover></script>
|
||||
|
|
|
@ -11,6 +11,7 @@ import { ContactName } from './conversation/ContactName';
|
|||
import { TypingAnimation } from './conversation/TypingAnimation';
|
||||
|
||||
import { Colors, LocalizerType } from '../types/Util';
|
||||
import { SessionButton, SessionButtonColor } from './session/SessionButton';
|
||||
|
||||
export type PropsData = {
|
||||
id: string;
|
||||
|
@ -36,7 +37,9 @@ export type PropsData = {
|
|||
isRss: boolean;
|
||||
};
|
||||
|
||||
showFriendRequestIndicator?: boolean;
|
||||
isPendingFriendRequest?: boolean;
|
||||
hasReceivedFriendRequest?: boolean;
|
||||
hasSentFriendRequest?: boolean;
|
||||
isBlocked?: boolean;
|
||||
isOnline?: boolean;
|
||||
hasNickname?: boolean;
|
||||
|
@ -56,6 +59,8 @@ type PropsHousekeeping = {
|
|||
onClearNickname?: () => void;
|
||||
onCopyPublicKey?: () => void;
|
||||
onUnblockContact?: () => void;
|
||||
acceptFriendRequest?: () => void;
|
||||
declineFriendRequest?: () => void;
|
||||
};
|
||||
|
||||
type Props = PropsData & PropsHousekeeping;
|
||||
|
@ -72,9 +77,14 @@ export class ConversationListItem extends React.PureComponent<Props> {
|
|||
phoneNumber,
|
||||
profileName,
|
||||
isOnline,
|
||||
isPendingFriendRequest,
|
||||
} = this.props;
|
||||
|
||||
const borderColor = isOnline ? Colors.ONLINE : Colors.OFFLINE;
|
||||
let borderColor;
|
||||
if (!isPendingFriendRequest) {
|
||||
borderColor = isOnline ? Colors.ONLINE : Colors.OFFLINE;
|
||||
}
|
||||
const iconSize = isPendingFriendRequest ? 28 : 48;
|
||||
|
||||
return (
|
||||
<div className="module-conversation-list-item__avatar-container">
|
||||
|
@ -87,10 +97,9 @@ export class ConversationListItem extends React.PureComponent<Props> {
|
|||
name={name}
|
||||
phoneNumber={phoneNumber}
|
||||
profileName={profileName}
|
||||
size={48}
|
||||
size={iconSize}
|
||||
borderColor={borderColor}
|
||||
/>
|
||||
{this.renderUnread()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -120,10 +129,8 @@ export class ConversationListItem extends React.PureComponent<Props> {
|
|||
i18n,
|
||||
isMe,
|
||||
lastUpdated,
|
||||
name,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
isFriendItem,
|
||||
hasReceivedFriendRequest,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
|
@ -136,17 +143,9 @@ export class ConversationListItem extends React.PureComponent<Props> {
|
|||
: null
|
||||
)}
|
||||
>
|
||||
{isMe ? (
|
||||
i18n('noteToSelf')
|
||||
) : (
|
||||
<ContactName
|
||||
phoneNumber={phoneNumber}
|
||||
name={name}
|
||||
profileName={profileName}
|
||||
i18n={i18n}
|
||||
/>
|
||||
)}
|
||||
{isMe ? i18n('noteToSelf') : this.renderUser()}
|
||||
</div>
|
||||
{hasReceivedFriendRequest || this.renderUnread()}
|
||||
{!isFriendItem && (
|
||||
<div
|
||||
className={classNames(
|
||||
|
@ -156,12 +155,14 @@ export class ConversationListItem extends React.PureComponent<Props> {
|
|||
: null
|
||||
)}
|
||||
>
|
||||
<Timestamp
|
||||
timestamp={lastUpdated}
|
||||
extended={false}
|
||||
module="module-conversation-list-item__header__timestamp"
|
||||
i18n={i18n}
|
||||
/>
|
||||
{!hasReceivedFriendRequest && (
|
||||
<Timestamp
|
||||
timestamp={lastUpdated}
|
||||
extended={false}
|
||||
module="module-conversation-list-item__header__timestamp"
|
||||
i18n={i18n}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -227,6 +228,7 @@ export class ConversationListItem extends React.PureComponent<Props> {
|
|||
unreadCount,
|
||||
i18n,
|
||||
isFriendItem,
|
||||
isPendingFriendRequest,
|
||||
} = this.props;
|
||||
|
||||
if (isFriendItem) {
|
||||
|
@ -244,6 +246,10 @@ export class ConversationListItem extends React.PureComponent<Props> {
|
|||
text = text.replace(/<[^>]*>?/gm, '');
|
||||
}
|
||||
|
||||
if (isPendingFriendRequest) {
|
||||
text = text.replace('Friend Request: ', '');
|
||||
}
|
||||
|
||||
if (isEmpty(text)) {
|
||||
return null;
|
||||
}
|
||||
|
@ -283,6 +289,24 @@ export class ConversationListItem extends React.PureComponent<Props> {
|
|||
);
|
||||
}
|
||||
|
||||
public renderFriendRequestButtons() {
|
||||
const { acceptFriendRequest, declineFriendRequest } = this.props;
|
||||
|
||||
return (
|
||||
<div className="module-conversation-list-item__buttons">
|
||||
<SessionButton
|
||||
text={window.i18n('decline')}
|
||||
buttonColor={SessionButtonColor.None}
|
||||
onClick={declineFriendRequest}
|
||||
/>
|
||||
<SessionButton
|
||||
text={window.i18n('accept')}
|
||||
onClick={acceptFriendRequest}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {
|
||||
phoneNumber,
|
||||
|
@ -290,7 +314,7 @@ export class ConversationListItem extends React.PureComponent<Props> {
|
|||
onClick,
|
||||
id,
|
||||
isSelected,
|
||||
showFriendRequestIndicator,
|
||||
hasReceivedFriendRequest,
|
||||
isBlocked,
|
||||
style,
|
||||
mentionedUs,
|
||||
|
@ -318,7 +342,7 @@ export class ConversationListItem extends React.PureComponent<Props> {
|
|||
? 'module-conversation-list-item--mentioned-us'
|
||||
: null,
|
||||
isSelected ? 'module-conversation-list-item--is-selected' : null,
|
||||
showFriendRequestIndicator
|
||||
hasReceivedFriendRequest
|
||||
? 'module-conversation-list-item--has-friend-request'
|
||||
: null,
|
||||
isBlocked ? 'module-conversation-list-item--is-blocked' : null
|
||||
|
@ -329,10 +353,32 @@ export class ConversationListItem extends React.PureComponent<Props> {
|
|||
{this.renderHeader()}
|
||||
{this.renderMessage()}
|
||||
</div>
|
||||
{hasReceivedFriendRequest && this.renderFriendRequestButtons()}
|
||||
</div>
|
||||
</ContextMenuTrigger>
|
||||
<Portal>{this.renderContextMenu(triggerId)}</Portal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderUser() {
|
||||
const { name, phoneNumber, profileName } = this.props;
|
||||
|
||||
const shortenedPubkey = window.shortenPubkey(phoneNumber);
|
||||
|
||||
const displayedPubkey = profileName ? shortenedPubkey : phoneNumber;
|
||||
|
||||
return (
|
||||
<div className="module-conversation__user">
|
||||
<ContactName
|
||||
phoneNumber={displayedPubkey}
|
||||
name={name}
|
||||
profileName={profileName}
|
||||
module="module-conversation__user"
|
||||
i18n={window.i18n}
|
||||
boldProfileName={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,42 +1,29 @@
|
|||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { AutoSizer, List } from 'react-virtualized';
|
||||
|
||||
import { ActionsPanel, SectionType } from './session/ActionsPanel';
|
||||
import { LeftPaneMessageSection } from './session/LeftPaneMessageSection';
|
||||
|
||||
import { PropsData as ConversationListItemPropsType } from './ConversationListItem';
|
||||
import { PropsData as SearchResultsProps } from './SearchResults';
|
||||
import { SearchOptions } from '../types/Search';
|
||||
import { LeftPaneSectionHeader } from './session/LeftPaneSectionHeader';
|
||||
import {
|
||||
ConversationListItem,
|
||||
PropsData as ConversationListItemPropsType,
|
||||
} from './ConversationListItem';
|
||||
SessionIconButton,
|
||||
SessionIconSize,
|
||||
SessionIconType,
|
||||
} from './session/icon';
|
||||
import { SessionIdEditable } from './session/SessionIdEditable';
|
||||
import { UserSearchDropdown } from './session/UserSearchDropdown';
|
||||
import {
|
||||
PropsData as SearchResultsProps,
|
||||
SearchResults,
|
||||
} from './SearchResults';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
|
||||
export interface Props {
|
||||
conversations?: Array<ConversationListItemPropsType>;
|
||||
friends?: Array<ConversationListItemPropsType>;
|
||||
archivedConversations?: Array<ConversationListItemPropsType>;
|
||||
searchResults?: SearchResultsProps;
|
||||
showArchived?: boolean;
|
||||
|
||||
i18n: LocalizerType;
|
||||
|
||||
// Action Creators
|
||||
startNewConversation: (
|
||||
query: string,
|
||||
options: { regionCode: string }
|
||||
) => void;
|
||||
|
||||
openConversationInternal: (id: string, messageId?: string) => void;
|
||||
showArchivedConversations: () => void;
|
||||
showInbox: () => void;
|
||||
|
||||
// Render Props
|
||||
renderMainHeader: () => JSX.Element;
|
||||
}
|
||||
SessionButton,
|
||||
SessionButtonColor,
|
||||
SessionButtonType,
|
||||
} from './session/SessionButton';
|
||||
import { ConversationType } from '../state/ducks/conversations';
|
||||
import { LeftPaneContactSection } from './session/LeftPaneContactSection';
|
||||
|
||||
// from https://github.com/bvaughn/react-virtualized/blob/fb3484ed5dcc41bffae8eab029126c0fb8f7abc0/source/List/types.js#L5
|
||||
type RowRendererParamsType = {
|
||||
export type RowRendererParamsType = {
|
||||
index: number;
|
||||
isScrolling: boolean;
|
||||
isVisible: boolean;
|
||||
|
@ -45,235 +32,207 @@ type RowRendererParamsType = {
|
|||
style: Object;
|
||||
};
|
||||
|
||||
export class LeftPane extends React.Component<Props, any> {
|
||||
interface State {
|
||||
selectedSection: SectionType;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
conversations: Array<ConversationListItemPropsType>;
|
||||
friends: Array<ConversationType>;
|
||||
searchResults?: SearchResultsProps;
|
||||
searchTerm: string;
|
||||
isSecondaryDevice: boolean;
|
||||
|
||||
openConversationInternal: (id: string, messageId?: string) => void;
|
||||
updateSearchTerm: (searchTerm: string) => void;
|
||||
search: (query: string, options: SearchOptions) => void;
|
||||
clearSearch: () => void;
|
||||
}
|
||||
|
||||
export class LeftPane extends React.Component<Props, State> {
|
||||
public state = {
|
||||
currentTab: 'conversations',
|
||||
selectedSection: SectionType.Contact,
|
||||
};
|
||||
|
||||
public getCurrentConversations():
|
||||
| Array<ConversationListItemPropsType>
|
||||
| undefined {
|
||||
const { conversations, friends } = this.props;
|
||||
const { currentTab } = this.state;
|
||||
|
||||
let conversationList =
|
||||
currentTab === 'conversations' ? conversations : friends;
|
||||
if (conversationList !== undefined) {
|
||||
conversationList = conversationList.filter(
|
||||
conversation => !conversation.isSecondary
|
||||
);
|
||||
}
|
||||
|
||||
return conversationList;
|
||||
public constructor(props: any) {
|
||||
super(props);
|
||||
this.handleSectionSelected = this.handleSectionSelected.bind(this);
|
||||
}
|
||||
|
||||
public renderTabs(): JSX.Element {
|
||||
const { i18n } = this.props;
|
||||
const { currentTab } = this.state;
|
||||
const tabs = [
|
||||
{
|
||||
id: 'conversations',
|
||||
name: i18n('conversationsTab'),
|
||||
},
|
||||
{
|
||||
id: 'friends',
|
||||
name: i18n('friendsTab'),
|
||||
},
|
||||
];
|
||||
|
||||
// this static function is set here to be used by all subsections (message, contacts,...) to render their headers
|
||||
public static RENDER_HEADER(
|
||||
labels: Array<string>,
|
||||
onTabSelected?: any,
|
||||
buttonLabel?: string,
|
||||
buttonClicked?: any,
|
||||
notificationCount?: number
|
||||
): JSX.Element {
|
||||
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,
|
||||
style,
|
||||
}: RowRendererParamsType): JSX.Element => {
|
||||
const {
|
||||
archivedConversations,
|
||||
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'
|
||||
);
|
||||
}
|
||||
|
||||
if (!showArchived && index === conversations.length) {
|
||||
return this.renderArchivedButton({ key, style });
|
||||
}
|
||||
|
||||
const conversation = showArchived
|
||||
? archivedConversations[index]
|
||||
: conversations[index];
|
||||
|
||||
return (
|
||||
<ConversationListItem
|
||||
key={key}
|
||||
style={style}
|
||||
{...conversation}
|
||||
onClick={openConversationInternal}
|
||||
i18n={i18n}
|
||||
isFriendItem={currentTab !== 'conversations'}
|
||||
<LeftPaneSectionHeader
|
||||
onTabSelected={onTabSelected}
|
||||
selectedTab={0}
|
||||
labels={labels}
|
||||
buttonLabel={buttonLabel}
|
||||
buttonClicked={buttonClicked}
|
||||
notificationCount={notificationCount}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
public renderArchivedButton({
|
||||
key,
|
||||
style,
|
||||
}: {
|
||||
key: string;
|
||||
style: Object;
|
||||
}): JSX.Element {
|
||||
const {
|
||||
archivedConversations,
|
||||
i18n,
|
||||
showArchivedConversations,
|
||||
} = this.props;
|
||||
|
||||
if (!archivedConversations || !archivedConversations.length) {
|
||||
throw new Error(
|
||||
'renderArchivedButton: Tried to render without archivedConversations'
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className="module-left-pane__archived-button"
|
||||
style={style}
|
||||
role="button"
|
||||
onClick={showArchivedConversations}
|
||||
>
|
||||
{i18n('archivedConversations')}{' '}
|
||||
<span className="module-left-pane__archived-button__archived-count">
|
||||
{archivedConversations.length}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public renderList(): JSX.Element | Array<JSX.Element | null> {
|
||||
const {
|
||||
archivedConversations,
|
||||
i18n,
|
||||
openConversationInternal,
|
||||
startNewConversation,
|
||||
searchResults,
|
||||
showArchived,
|
||||
} = this.props;
|
||||
|
||||
if (searchResults) {
|
||||
return (
|
||||
<SearchResults
|
||||
{...searchResults}
|
||||
openConversation={openConversationInternal}
|
||||
startNewConversation={startNewConversation}
|
||||
i18n={i18n}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const conversations = this.getCurrentConversations();
|
||||
|
||||
if (!conversations || !archivedConversations) {
|
||||
throw new Error(
|
||||
'render: must provided conversations and archivedConverstions if no search results are provided'
|
||||
);
|
||||
}
|
||||
|
||||
// That extra 1 element added to the list is the 'archived converastions' button
|
||||
const length = showArchived
|
||||
? archivedConversations.length
|
||||
: conversations.length + (archivedConversations.length ? 1 : 0);
|
||||
|
||||
const archived = showArchived ? (
|
||||
<div className="module-left-pane__archive-helper-text" key={0}>
|
||||
{i18n('archiveHelperText')}
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
// We ensure that the listKey differs between inbox and archive views, which ensures
|
||||
// that AutoSizer properly detects the new size of its slot in the flexbox. The
|
||||
// archive explainer text at the top of the archive view causes problems otherwise.
|
||||
// It also ensures that we scroll to the top when switching views.
|
||||
const listKey = showArchived ? 1 : 0;
|
||||
|
||||
// Note: conversations is not a known prop for List, but it is required to ensure that
|
||||
// it re-renders when our conversation data changes. Otherwise it would just render
|
||||
// on startup and scroll.
|
||||
const list = (
|
||||
<div className="module-left-pane__list" key={listKey}>
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<List
|
||||
className="module-left-pane__virtual-list"
|
||||
conversations={conversations}
|
||||
height={height}
|
||||
rowCount={length}
|
||||
rowHeight={64}
|
||||
rowRenderer={this.renderRow}
|
||||
width={width}
|
||||
autoHeight={true}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
);
|
||||
|
||||
return [this.renderTabs(), archived, list];
|
||||
}
|
||||
|
||||
public renderArchivedHeader(): JSX.Element {
|
||||
const { i18n, showInbox } = this.props;
|
||||
public static RENDER_CLOSABLE_OVERLAY(
|
||||
isAddContactView: boolean,
|
||||
onChangeSessionID: any,
|
||||
onCloseClick: any,
|
||||
onButtonClick: any,
|
||||
searchTerm: string,
|
||||
searchResults?: any,
|
||||
updateSearch?: any
|
||||
): JSX.Element {
|
||||
const title = isAddContactView
|
||||
? window.i18n('addContact')
|
||||
: window.i18n('enterRecipient');
|
||||
const buttonText = isAddContactView
|
||||
? window.i18n('addContact')
|
||||
: window.i18n('message');
|
||||
const ourSessionID = window.textsecure.storage.user.getNumber();
|
||||
|
||||
return (
|
||||
<div className="module-left-pane__archive-header">
|
||||
<div
|
||||
role="button"
|
||||
onClick={showInbox}
|
||||
className="module-left-pane__to-inbox-button"
|
||||
/>
|
||||
<div className="module-left-pane__archive-header-text">
|
||||
{i18n('archivedConversations')}
|
||||
<div className="module-left-pane-compose">
|
||||
<div className="exit">
|
||||
<SessionIconButton
|
||||
iconSize={SessionIconSize.Small}
|
||||
iconType={SessionIconType.Exit}
|
||||
onClick={onCloseClick}
|
||||
/>
|
||||
</div>
|
||||
<h2>{title}</h2>
|
||||
<h3>{window.i18n('enterSessionID')}</h3>
|
||||
<div className="module-left-pane-compose-border-container">
|
||||
<hr className="white" />
|
||||
<hr className="green" />
|
||||
</div>
|
||||
<SessionIdEditable
|
||||
editable={true}
|
||||
placeholder={window.i18n('pasteSessionIDRecipient')}
|
||||
onChange={onChangeSessionID}
|
||||
/>
|
||||
|
||||
<div className="session-description-long">
|
||||
{window.i18n('usersCanShareTheir...')}
|
||||
</div>
|
||||
{isAddContactView || <h4>{window.i18n('or')}</h4>}
|
||||
|
||||
{isAddContactView || (
|
||||
<UserSearchDropdown
|
||||
searchTerm={searchTerm}
|
||||
updateSearch={updateSearch}
|
||||
placeholder={window.i18n('searchByIDOrDisplayName')}
|
||||
searchResults={searchResults}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isAddContactView && (
|
||||
<div className="panel-text-divider">
|
||||
<span>{window.i18n('yourPublicKey')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isAddContactView && (
|
||||
<SessionIdEditable
|
||||
editable={false}
|
||||
placeholder=""
|
||||
text={ourSessionID}
|
||||
/>
|
||||
)}
|
||||
<SessionButton
|
||||
buttonColor={SessionButtonColor.Green}
|
||||
buttonType={SessionButtonType.BrandOutline}
|
||||
text={buttonText}
|
||||
onClick={onButtonClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public handleSectionSelected(section: SectionType) {
|
||||
this.setState({ selectedSection: section });
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
const { renderMainHeader, showArchived } = this.props;
|
||||
|
||||
return (
|
||||
<div className="module-left-pane">
|
||||
<div className="module-left-pane__header">
|
||||
{showArchived ? this.renderArchivedHeader() : renderMainHeader()}
|
||||
</div>
|
||||
{this.renderList()}
|
||||
<div className="module-left-pane-session">
|
||||
<ActionsPanel
|
||||
selectedSection={this.state.selectedSection}
|
||||
onSectionSelected={this.handleSectionSelected}
|
||||
conversations={this.props.conversations}
|
||||
/>
|
||||
<div className="module-left-pane">{this.renderSection()}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderSection(): JSX.Element | undefined {
|
||||
switch (this.state.selectedSection) {
|
||||
case SectionType.Message:
|
||||
return this.renderMessageSection();
|
||||
case SectionType.Contact:
|
||||
return this.renderContactSection();
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private renderMessageSection() {
|
||||
const {
|
||||
openConversationInternal,
|
||||
conversations,
|
||||
searchResults,
|
||||
searchTerm,
|
||||
isSecondaryDevice,
|
||||
updateSearchTerm,
|
||||
search,
|
||||
clearSearch,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<LeftPaneMessageSection
|
||||
openConversationInternal={openConversationInternal}
|
||||
conversations={conversations}
|
||||
searchResults={searchResults}
|
||||
searchTerm={searchTerm}
|
||||
isSecondaryDevice={isSecondaryDevice}
|
||||
updateSearchTerm={updateSearchTerm}
|
||||
search={search}
|
||||
clearSearch={clearSearch}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
private renderContactSection() {
|
||||
const {
|
||||
openConversationInternal,
|
||||
friends,
|
||||
conversations,
|
||||
searchResults,
|
||||
searchTerm,
|
||||
isSecondaryDevice,
|
||||
updateSearchTerm,
|
||||
search,
|
||||
clearSearch,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<LeftPaneContactSection
|
||||
openConversationInternal={openConversationInternal}
|
||||
conversations={conversations}
|
||||
friends={friends}
|
||||
searchResults={searchResults}
|
||||
searchTerm={searchTerm}
|
||||
isSecondaryDevice={isSecondaryDevice}
|
||||
updateSearchTerm={updateSearchTerm}
|
||||
search={search}
|
||||
clearSearch={clearSearch}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,12 +7,9 @@ import {
|
|||
MessageSearchResult,
|
||||
PropsData as MessageSearchResultPropsType,
|
||||
} from './MessageSearchResult';
|
||||
import { StartNewConversation } from './StartNewConversation';
|
||||
|
||||
import { LocalizerType } from '../types/Util';
|
||||
|
||||
declare var mixpanel: any;
|
||||
|
||||
export type PropsData = {
|
||||
contacts: Array<ConversationListItemPropsType>;
|
||||
friends: Array<ConversationListItemPropsType>;
|
||||
|
@ -27,21 +24,11 @@ export type PropsData = {
|
|||
type PropsHousekeeping = {
|
||||
i18n: LocalizerType;
|
||||
openConversation: (id: string, messageId?: string) => void;
|
||||
startNewConversation: (
|
||||
query: string,
|
||||
options: { regionCode: string }
|
||||
) => void;
|
||||
};
|
||||
|
||||
type Props = PropsData & PropsHousekeeping;
|
||||
|
||||
export class SearchResults extends React.Component<Props> {
|
||||
public handleStartNewConversation = () => {
|
||||
const { regionCode, searchTerm, startNewConversation } = this.props;
|
||||
mixpanel.track('New Conversation Started');
|
||||
startNewConversation(searchTerm, { regionCode });
|
||||
};
|
||||
|
||||
public render() {
|
||||
const {
|
||||
conversations,
|
||||
|
@ -73,13 +60,6 @@ export class SearchResults extends React.Component<Props> {
|
|||
{i18n('noSearchResults', [searchTerm])}
|
||||
</div>
|
||||
) : null}
|
||||
{showStartNewConversation ? (
|
||||
<StartNewConversation
|
||||
phoneNumber={searchTerm}
|
||||
i18n={i18n}
|
||||
onClick={this.handleStartNewConversation}
|
||||
/>
|
||||
) : null}
|
||||
{haveConversations ? (
|
||||
<div className="module-search-results__conversations">
|
||||
<div className="module-search-results__conversations-header">
|
||||
|
@ -95,12 +75,6 @@ export class SearchResults extends React.Component<Props> {
|
|||
))}
|
||||
</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 : (
|
||||
|
@ -121,27 +95,4 @@ 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
import React from 'react';
|
||||
import { PropsData as ConversationListItemPropsType } from './ConversationListItem';
|
||||
|
||||
import { LocalizerType } from '../types/Util';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export type PropsData = {
|
||||
contacts: Array<ConversationListItemPropsType>;
|
||||
regionCode: string;
|
||||
searchTerm: string;
|
||||
selectedContact: number;
|
||||
onContactSelected: any;
|
||||
};
|
||||
|
||||
type PropsHousekeeping = {
|
||||
i18n: LocalizerType;
|
||||
};
|
||||
|
||||
type Props = PropsData & PropsHousekeeping;
|
||||
|
||||
export class UserSearchResults extends React.Component<Props> {
|
||||
public constructor(props: Props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { contacts, i18n, searchTerm } = this.props;
|
||||
|
||||
const haveContacts = contacts && contacts.length;
|
||||
const noResults = !haveContacts;
|
||||
|
||||
return (
|
||||
<div className="module-search-results">
|
||||
{noResults ? (
|
||||
<div className="module-search-results__no-results">
|
||||
{i18n('noSearchResults', [searchTerm])}
|
||||
</div>
|
||||
) : null}
|
||||
{haveContacts ? this.renderContacts(contacts) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderContacts(items: Array<ConversationListItemPropsType>) {
|
||||
return (
|
||||
<div className="contacts-dropdown">
|
||||
{items.map((contact, index) => this.renderContact(contact, index))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderContact(contact: ConversationListItemPropsType, index: Number) {
|
||||
const { profileName, phoneNumber } = contact;
|
||||
const { selectedContact } = this.props;
|
||||
|
||||
const shortenedPubkey = window.shortenPubkey(phoneNumber);
|
||||
const rowContent = `${profileName} ${shortenedPubkey}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'contacts-dropdown-row',
|
||||
selectedContact === index && 'contacts-dropdown-row-selected'
|
||||
)}
|
||||
key={contact.phoneNumber}
|
||||
onClick={() => this.props.onContactSelected(contact.phoneNumber)}
|
||||
role="button"
|
||||
>
|
||||
{rowContent}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,235 @@
|
|||
import React from 'react';
|
||||
import { SessionIconButton, SessionIconSize, SessionIconType } from './icon';
|
||||
import { Avatar } from '../Avatar';
|
||||
import { PropsData as ConversationListItemPropsType } from '../ConversationListItem';
|
||||
|
||||
export enum SectionType {
|
||||
Profile,
|
||||
Message,
|
||||
Contact,
|
||||
Globe,
|
||||
Settings,
|
||||
Moon,
|
||||
}
|
||||
|
||||
interface State {
|
||||
avatarPath: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
onSectionSelected: any;
|
||||
selectedSection: SectionType;
|
||||
conversations: Array<ConversationListItemPropsType> | undefined;
|
||||
}
|
||||
|
||||
const Section = ({
|
||||
isSelected,
|
||||
onSelect,
|
||||
type,
|
||||
avatarPath,
|
||||
notificationCount,
|
||||
}: {
|
||||
isSelected: boolean;
|
||||
onSelect?: (event: SectionType) => void;
|
||||
type: SectionType;
|
||||
avatarPath?: string;
|
||||
avatarColor?: string;
|
||||
notificationCount?: number;
|
||||
}) => {
|
||||
const handleClick = onSelect
|
||||
? () => {
|
||||
onSelect(type);
|
||||
}
|
||||
: undefined;
|
||||
|
||||
if (type === SectionType.Profile) {
|
||||
if (!isSelected) {
|
||||
return (
|
||||
<Avatar
|
||||
avatarPath={avatarPath}
|
||||
conversationType="direct"
|
||||
i18n={window.i18n}
|
||||
// tslint:disable-next-line: no-backbone-get-set-outside-model
|
||||
phoneNumber={window.storage.get('primaryDevicePubKey')}
|
||||
size={28}
|
||||
onAvatarClick={handleClick}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Avatar
|
||||
avatarPath={avatarPath}
|
||||
conversationType="direct"
|
||||
i18n={window.i18n}
|
||||
// tslint:disable-next-line: no-backbone-get-set-outside-model
|
||||
phoneNumber={window.storage.get('primaryDevicePubKey')}
|
||||
size={28}
|
||||
onAvatarClick={handleClick}
|
||||
borderColor={'#fff'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let iconType: SessionIconType;
|
||||
switch (type) {
|
||||
case SectionType.Message:
|
||||
iconType = SessionIconType.ChatBubble;
|
||||
break;
|
||||
case SectionType.Contact:
|
||||
iconType = SessionIconType.Users;
|
||||
break;
|
||||
case SectionType.Globe:
|
||||
iconType = SessionIconType.Globe;
|
||||
break;
|
||||
case SectionType.Settings:
|
||||
iconType = SessionIconType.Gear;
|
||||
break;
|
||||
case SectionType.Moon:
|
||||
iconType = SessionIconType.Moon;
|
||||
break;
|
||||
|
||||
default:
|
||||
iconType = SessionIconType.Moon;
|
||||
}
|
||||
if (!isSelected) {
|
||||
return (
|
||||
<SessionIconButton
|
||||
iconSize={SessionIconSize.Medium}
|
||||
iconType={iconType}
|
||||
notificationCount={notificationCount}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<SessionIconButton
|
||||
iconSize={SessionIconSize.Medium}
|
||||
iconType={iconType}
|
||||
notificationCount={notificationCount}
|
||||
onClick={handleClick}
|
||||
isSelected={isSelected}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export class ActionsPanel extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
avatarPath: '',
|
||||
};
|
||||
}
|
||||
|
||||
public static GET_FRIEND_REQUESTS_COUNT(
|
||||
conversations: Array<ConversationListItemPropsType> | undefined
|
||||
): number {
|
||||
let friendRequestCount = 0;
|
||||
if (conversations !== undefined) {
|
||||
// We assume a friend request already read is still a friend valid request
|
||||
conversations.some(conversation => {
|
||||
// Ignore friend request with lastmessage status as sent as this is a friend request we made ourself
|
||||
friendRequestCount += conversation.hasReceivedFriendRequest ? 1 : 0;
|
||||
if (friendRequestCount > 9) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
return friendRequestCount;
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
// tslint:disable-next-line: no-backbone-get-set-outside-model
|
||||
const ourNumber = window.storage.get('primaryDevicePubKey');
|
||||
|
||||
window.ConversationController.getOrCreateAndWait(ourNumber, 'private').then(
|
||||
(conversation: any) => {
|
||||
this.setState({
|
||||
avatarPath: conversation.getAvatarPath(),
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
const { selectedSection, conversations } = this.props;
|
||||
|
||||
const friendRequestCount = ActionsPanel.GET_FRIEND_REQUESTS_COUNT(
|
||||
conversations
|
||||
);
|
||||
const unreadMessageCount = this.getUnreadMessageCount();
|
||||
|
||||
const isProfilePageSelected = selectedSection === SectionType.Profile;
|
||||
const isMessagePageSelected = selectedSection === SectionType.Message;
|
||||
const isContactPageSelected = selectedSection === SectionType.Contact;
|
||||
const isGlobePageSelected = selectedSection === SectionType.Globe;
|
||||
const isSettingsPageSelected = selectedSection === SectionType.Settings;
|
||||
const isMoonPageSelected = selectedSection === SectionType.Moon;
|
||||
|
||||
return (
|
||||
<div className="module-left-pane__sections-container">
|
||||
<Section
|
||||
type={SectionType.Profile}
|
||||
avatarPath={this.state.avatarPath}
|
||||
isSelected={isProfilePageSelected}
|
||||
onSelect={this.handleSectionSelect}
|
||||
/>
|
||||
<Section
|
||||
type={SectionType.Message}
|
||||
isSelected={isMessagePageSelected}
|
||||
onSelect={this.handleSectionSelect}
|
||||
notificationCount={unreadMessageCount}
|
||||
/>
|
||||
<Section
|
||||
type={SectionType.Contact}
|
||||
isSelected={isContactPageSelected}
|
||||
onSelect={this.handleSectionSelect}
|
||||
notificationCount={friendRequestCount}
|
||||
/>
|
||||
<Section
|
||||
type={SectionType.Globe}
|
||||
isSelected={isGlobePageSelected}
|
||||
onSelect={this.handleSectionSelect}
|
||||
/>
|
||||
<Section
|
||||
type={SectionType.Settings}
|
||||
isSelected={isSettingsPageSelected}
|
||||
onSelect={this.handleSectionSelect}
|
||||
/>
|
||||
<Section
|
||||
type={SectionType.Moon}
|
||||
isSelected={isMoonPageSelected}
|
||||
onSelect={this.handleSectionSelect}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private getUnreadMessageCount(): number {
|
||||
const { conversations } = this.props;
|
||||
let unreadCount = 0;
|
||||
if (conversations !== undefined) {
|
||||
conversations.some(conversation => {
|
||||
if (conversation.isPendingFriendRequest) {
|
||||
return false;
|
||||
}
|
||||
unreadCount += conversation.unreadCount;
|
||||
if (unreadCount > 9) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
return unreadCount;
|
||||
}
|
||||
|
||||
private readonly handleSectionSelect = (section: SectionType): void => {
|
||||
this.props.onSectionSelected(section);
|
||||
};
|
||||
}
|
|
@ -0,0 +1,316 @@
|
|||
import React from 'react';
|
||||
|
||||
import {
|
||||
ConversationListItem,
|
||||
PropsData as ConversationListItemPropsType,
|
||||
} from '../ConversationListItem';
|
||||
import { PropsData as SearchResultsProps } from '../SearchResults';
|
||||
import { debounce } from 'lodash';
|
||||
import { cleanSearchTerm } from '../../util/cleanSearchTerm';
|
||||
import { SearchOptions } from '../../types/Search';
|
||||
import { LeftPane, RowRendererParamsType } from '../LeftPane';
|
||||
import {
|
||||
SessionButton,
|
||||
SessionButtonColor,
|
||||
SessionButtonType,
|
||||
} from './SessionButton';
|
||||
import { AutoSizer, List } from 'react-virtualized';
|
||||
import { validateNumber } from '../../types/PhoneNumber';
|
||||
import { ActionsPanel } from './ActionsPanel';
|
||||
import { ConversationType } from '../../state/ducks/conversations';
|
||||
|
||||
export interface Props {
|
||||
searchTerm: string;
|
||||
isSecondaryDevice: boolean;
|
||||
|
||||
conversations: Array<ConversationListItemPropsType>;
|
||||
friends: Array<ConversationType>;
|
||||
|
||||
searchResults?: SearchResultsProps;
|
||||
|
||||
updateSearchTerm: (searchTerm: string) => void;
|
||||
search: (query: string, options: SearchOptions) => void;
|
||||
openConversationInternal: (id: string, messageId?: string) => void;
|
||||
clearSearch: () => void;
|
||||
}
|
||||
|
||||
export class LeftPaneContactSection extends React.Component<Props, any> {
|
||||
private readonly debouncedSearch: (searchTerm: string) => void;
|
||||
|
||||
public constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
showAddContactView: false,
|
||||
selectedTab: 0,
|
||||
addContactRecipientID: '',
|
||||
};
|
||||
|
||||
this.debouncedSearch = debounce(this.search.bind(this), 20);
|
||||
this.handleTabSelected = this.handleTabSelected.bind(this);
|
||||
this.handleToggleOverlay = this.handleToggleOverlay.bind(this);
|
||||
this.handleOnAddContact = this.handleOnAddContact.bind(this);
|
||||
this.handleRecipientSessionIDChanged = this.handleRecipientSessionIDChanged.bind(
|
||||
this
|
||||
);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.updateSearch('');
|
||||
this.setState({ addContactRecipientID: '' });
|
||||
}
|
||||
|
||||
public handleTabSelected(tabType: number) {
|
||||
this.setState({ selectedTab: tabType, showAddContactView: false });
|
||||
}
|
||||
|
||||
public renderHeader(): JSX.Element | undefined {
|
||||
const labels = [window.i18n('contactsHeader'), window.i18n('lists')];
|
||||
const friendRequestCount = ActionsPanel.GET_FRIEND_REQUESTS_COUNT(
|
||||
this.props.conversations
|
||||
);
|
||||
|
||||
return LeftPane.RENDER_HEADER(
|
||||
labels,
|
||||
this.handleTabSelected,
|
||||
undefined,
|
||||
undefined,
|
||||
friendRequestCount
|
||||
);
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<div className="left-pane-contact-section">
|
||||
{this.renderHeader()}
|
||||
{this.state.showAddContactView
|
||||
? LeftPane.RENDER_CLOSABLE_OVERLAY(
|
||||
true,
|
||||
this.handleRecipientSessionIDChanged,
|
||||
this.handleToggleOverlay,
|
||||
this.handleOnAddContact,
|
||||
''
|
||||
)
|
||||
: this.renderContacts()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public getCurrentFriends(): Array<ConversationType> {
|
||||
const { friends } = this.props;
|
||||
|
||||
let friendList = friends;
|
||||
if (friendList !== undefined) {
|
||||
friendList = friendList.filter(
|
||||
friend => friend.type === 'direct' && !friend.isMe
|
||||
);
|
||||
}
|
||||
|
||||
return friendList;
|
||||
}
|
||||
|
||||
public renderRow = ({
|
||||
index,
|
||||
key,
|
||||
style,
|
||||
}: RowRendererParamsType): JSX.Element | undefined => {
|
||||
const receivedFriendsRequest = this.getFriendRequests(true);
|
||||
const sentFriendsRequest = this.getFriendRequests(false);
|
||||
const friends = this.getCurrentFriends();
|
||||
|
||||
const combined = [
|
||||
...receivedFriendsRequest,
|
||||
...sentFriendsRequest,
|
||||
...friends,
|
||||
];
|
||||
|
||||
const item = combined[index];
|
||||
let onClick;
|
||||
if (index >= receivedFriendsRequest.length) {
|
||||
onClick = this.props.openConversationInternal;
|
||||
}
|
||||
|
||||
return (
|
||||
<ConversationListItem
|
||||
key={key}
|
||||
style={style}
|
||||
{...item}
|
||||
i18n={window.i18n}
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
public updateSearch(searchTerm: string) {
|
||||
const { updateSearchTerm, clearSearch } = this.props;
|
||||
|
||||
if (!searchTerm) {
|
||||
clearSearch();
|
||||
|
||||
return;
|
||||
}
|
||||
// reset our pubKeyPasted, we can either have a pasted sessionID or a sessionID got from a search
|
||||
this.setState({ pubKeyPasted: '' }, () => {
|
||||
window.Session.emptyContentEditableDivs();
|
||||
});
|
||||
|
||||
if (updateSearchTerm) {
|
||||
updateSearchTerm(searchTerm);
|
||||
}
|
||||
|
||||
if (searchTerm.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cleanedTerm = cleanSearchTerm(searchTerm);
|
||||
if (!cleanedTerm) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.debouncedSearch(cleanedTerm);
|
||||
}
|
||||
|
||||
public clearSearch() {
|
||||
this.props.clearSearch();
|
||||
//this.setFocus();
|
||||
}
|
||||
|
||||
public search() {
|
||||
const { search } = this.props;
|
||||
const { searchTerm, isSecondaryDevice } = this.props;
|
||||
|
||||
if (search) {
|
||||
search(searchTerm, {
|
||||
noteToSelf: window.i18n('noteToSelf').toLowerCase(),
|
||||
ourNumber: window.textsecure.storage.user.getNumber(),
|
||||
regionCode: '',
|
||||
isSecondaryDevice,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private handleToggleOverlay() {
|
||||
this.setState((prevState: { showAddContactView: any }) => ({
|
||||
showAddContactView: !prevState.showAddContactView,
|
||||
}));
|
||||
}
|
||||
|
||||
private handleOnAddContact() {
|
||||
const sessionID = this.state.addContactRecipientID;
|
||||
const error = validateNumber(sessionID, window.i18n);
|
||||
|
||||
if (error) {
|
||||
window.pushToast({
|
||||
title: error,
|
||||
type: 'error',
|
||||
id: 'addContact',
|
||||
});
|
||||
} else {
|
||||
window.Whisper.events.trigger('showConversation', sessionID);
|
||||
}
|
||||
}
|
||||
|
||||
private handleRecipientSessionIDChanged(event: any) {
|
||||
if (event.target.innerHTML) {
|
||||
// remove br elements or div elements
|
||||
const cleanText = event.target.innerHTML.replace(/<\/?[^>]+(>|$)/g, '');
|
||||
this.setState({ addContactRecipientID: cleanText });
|
||||
}
|
||||
}
|
||||
|
||||
private renderContacts() {
|
||||
return (
|
||||
<div className="left-pane-contact-content">
|
||||
{this.renderList()}
|
||||
{this.renderBottomButtons()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderBottomButtons(): JSX.Element {
|
||||
const { selectedTab } = this.state;
|
||||
const edit = window.i18n('edit');
|
||||
const addContact = window.i18n('addContact');
|
||||
const createGroup = window.i18n('createGroup');
|
||||
|
||||
return (
|
||||
<div className="left-pane-contact-bottom-buttons">
|
||||
<SessionButton
|
||||
text={edit}
|
||||
buttonType={SessionButtonType.SquareOutline}
|
||||
buttonColor={SessionButtonColor.White}
|
||||
/>
|
||||
{selectedTab === 0 ? (
|
||||
<SessionButton
|
||||
text={addContact}
|
||||
buttonType={SessionButtonType.SquareOutline}
|
||||
buttonColor={SessionButtonColor.Green}
|
||||
onClick={this.handleToggleOverlay}
|
||||
/>
|
||||
) : (
|
||||
<SessionButton
|
||||
text={createGroup}
|
||||
buttonType={SessionButtonType.SquareOutline}
|
||||
buttonColor={SessionButtonColor.Green}
|
||||
onClick={this.handleToggleOverlay}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// true: received only, false: sent only
|
||||
private getFriendRequests(
|
||||
received: boolean
|
||||
): Array<ConversationListItemPropsType> {
|
||||
const { conversations } = this.props;
|
||||
|
||||
let conversationsList = conversations;
|
||||
if (conversationsList !== undefined) {
|
||||
if (received) {
|
||||
conversationsList = conversationsList.filter(
|
||||
conversation => conversation.hasReceivedFriendRequest
|
||||
);
|
||||
} else {
|
||||
conversationsList = conversationsList.filter(
|
||||
conversation => conversation.hasSentFriendRequest
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return conversationsList;
|
||||
}
|
||||
|
||||
private renderList() {
|
||||
const receivedFriendsRequest = this.getFriendRequests(true);
|
||||
const sentFriendsRequest = this.getFriendRequests(false);
|
||||
const friends = this.getCurrentFriends();
|
||||
|
||||
const combined = [
|
||||
...receivedFriendsRequest,
|
||||
...sentFriendsRequest,
|
||||
...friends,
|
||||
];
|
||||
|
||||
const length = combined.length;
|
||||
|
||||
const list = (
|
||||
<div className="module-left-pane__list" key={0}>
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<List
|
||||
className="module-left-pane__virtual-list"
|
||||
height={height}
|
||||
rowCount={length}
|
||||
rowHeight={64}
|
||||
rowRenderer={this.renderRow}
|
||||
width={width}
|
||||
autoHeight={true}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
);
|
||||
|
||||
return [list];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,271 @@
|
|||
import React from 'react';
|
||||
import { AutoSizer, List } from 'react-virtualized';
|
||||
|
||||
import {
|
||||
ConversationListItem,
|
||||
PropsData as ConversationListItemPropsType,
|
||||
} from '../ConversationListItem';
|
||||
import {
|
||||
PropsData as SearchResultsProps,
|
||||
SearchResults,
|
||||
} from '../SearchResults';
|
||||
import { SessionSearchInput } from './SessionSearchInput';
|
||||
import { debounce } from 'lodash';
|
||||
import { cleanSearchTerm } from '../../util/cleanSearchTerm';
|
||||
import { SearchOptions } from '../../types/Search';
|
||||
import { validateNumber } from '../../types/PhoneNumber';
|
||||
import { LeftPane, RowRendererParamsType } from '../LeftPane';
|
||||
|
||||
export interface Props {
|
||||
searchTerm: string;
|
||||
isSecondaryDevice: boolean;
|
||||
|
||||
conversations?: Array<ConversationListItemPropsType>;
|
||||
|
||||
searchResults?: SearchResultsProps;
|
||||
|
||||
updateSearchTerm: (searchTerm: string) => void;
|
||||
search: (query: string, options: SearchOptions) => void;
|
||||
openConversationInternal: (id: string, messageId?: string) => void;
|
||||
clearSearch: () => void;
|
||||
}
|
||||
|
||||
export class LeftPaneMessageSection extends React.Component<Props, any> {
|
||||
private readonly updateSearchBound: (searchedString: string) => void;
|
||||
private readonly debouncedSearch: (searchTerm: string) => void;
|
||||
|
||||
public constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
showComposeView: false,
|
||||
pubKeyPasted: '',
|
||||
};
|
||||
|
||||
this.updateSearchBound = this.updateSearch.bind(this);
|
||||
this.handleComposeClick = this.handleComposeClick.bind(this);
|
||||
this.handleOnPasteSessionID = this.handleOnPasteSessionID.bind(this);
|
||||
this.handleMessageButtonClick = this.handleMessageButtonClick.bind(this);
|
||||
this.debouncedSearch = debounce(this.search.bind(this), 20);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.updateSearch('');
|
||||
}
|
||||
|
||||
public getCurrentConversations():
|
||||
| Array<ConversationListItemPropsType>
|
||||
| undefined {
|
||||
const { conversations } = this.props;
|
||||
|
||||
let conversationList = conversations;
|
||||
if (conversationList !== undefined) {
|
||||
conversationList = conversationList.filter(
|
||||
conversation =>
|
||||
!conversation.isSecondary && !conversation.isPendingFriendRequest
|
||||
);
|
||||
}
|
||||
|
||||
return conversationList;
|
||||
}
|
||||
|
||||
public renderRow = ({
|
||||
index,
|
||||
key,
|
||||
style,
|
||||
}: RowRendererParamsType): JSX.Element => {
|
||||
const { openConversationInternal } = this.props;
|
||||
|
||||
const conversations = this.getCurrentConversations();
|
||||
|
||||
if (!conversations) {
|
||||
throw new Error('renderRow: Tried to render without conversations');
|
||||
}
|
||||
|
||||
const conversation = conversations[index];
|
||||
|
||||
return (
|
||||
<ConversationListItem
|
||||
key={key}
|
||||
style={style}
|
||||
{...conversation}
|
||||
onClick={openConversationInternal}
|
||||
i18n={window.i18n}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
public renderList(): JSX.Element | Array<JSX.Element | null> {
|
||||
const { openConversationInternal, searchResults } = this.props;
|
||||
|
||||
if (searchResults) {
|
||||
return (
|
||||
<SearchResults
|
||||
{...searchResults}
|
||||
openConversation={openConversationInternal}
|
||||
i18n={window.i18n}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const conversations = this.getCurrentConversations();
|
||||
|
||||
if (!conversations) {
|
||||
throw new Error(
|
||||
'render: must provided conversations if no search results are provided'
|
||||
);
|
||||
}
|
||||
|
||||
const length = conversations.length;
|
||||
const listKey = 0;
|
||||
|
||||
// Note: conversations is not a known prop for List, but it is required to ensure that
|
||||
// it re-renders when our conversation data changes. Otherwise it would just render
|
||||
// on startup and scroll.
|
||||
const list = (
|
||||
<div className="module-left-pane__list" key={listKey}>
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<List
|
||||
className="module-left-pane__virtual-list"
|
||||
conversations={conversations}
|
||||
height={height}
|
||||
rowCount={length}
|
||||
rowHeight={64}
|
||||
rowRenderer={this.renderRow}
|
||||
width={width}
|
||||
autoHeight={true}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
);
|
||||
|
||||
return [list];
|
||||
}
|
||||
|
||||
public renderHeader(): JSX.Element {
|
||||
const labels = [window.i18n('messagesHeader')];
|
||||
|
||||
return LeftPane.RENDER_HEADER(
|
||||
labels,
|
||||
null,
|
||||
window.i18n('compose'),
|
||||
this.handleComposeClick
|
||||
);
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
{this.renderHeader()}
|
||||
{this.state.showComposeView
|
||||
? LeftPane.RENDER_CLOSABLE_OVERLAY(
|
||||
false,
|
||||
this.handleOnPasteSessionID,
|
||||
this.handleComposeClick,
|
||||
this.handleMessageButtonClick,
|
||||
this.props.searchTerm,
|
||||
this.props.searchResults,
|
||||
this.updateSearchBound
|
||||
)
|
||||
: this.renderConversations()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public renderConversations() {
|
||||
return (
|
||||
<div>
|
||||
<SessionSearchInput
|
||||
searchString={this.props.searchTerm}
|
||||
onChange={this.updateSearchBound}
|
||||
placeholder={window.i18n('searchForAKeyPhrase')}
|
||||
/>
|
||||
{this.renderList()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public updateSearch(searchTerm: string) {
|
||||
const { updateSearchTerm, clearSearch } = this.props;
|
||||
|
||||
if (!searchTerm) {
|
||||
clearSearch();
|
||||
|
||||
return;
|
||||
}
|
||||
// reset our pubKeyPasted, we can either have a pasted sessionID or a sessionID got from a search
|
||||
this.setState({ pubKeyPasted: '' }, () => {
|
||||
window.Session.emptyContentEditableDivs();
|
||||
});
|
||||
|
||||
if (updateSearchTerm) {
|
||||
updateSearchTerm(searchTerm);
|
||||
}
|
||||
|
||||
if (searchTerm.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cleanedTerm = cleanSearchTerm(searchTerm);
|
||||
if (!cleanedTerm) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.debouncedSearch(cleanedTerm);
|
||||
}
|
||||
|
||||
public clearSearch() {
|
||||
this.props.clearSearch();
|
||||
//this.setFocus();
|
||||
}
|
||||
|
||||
public search() {
|
||||
const { search } = this.props;
|
||||
const { searchTerm, isSecondaryDevice } = this.props;
|
||||
|
||||
if (search) {
|
||||
search(searchTerm, {
|
||||
noteToSelf: window.i18n('noteToSelf').toLowerCase(),
|
||||
ourNumber: window.textsecure.storage.user.getNumber(),
|
||||
regionCode: '',
|
||||
isSecondaryDevice,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private handleComposeClick() {
|
||||
this.setState((state: any) => {
|
||||
return { showComposeView: !state.showComposeView };
|
||||
});
|
||||
// empty our generalized searchedString (one for the whole app)
|
||||
this.updateSearch('');
|
||||
}
|
||||
|
||||
private handleOnPasteSessionID(e: any) {
|
||||
// reset our search, we can either have a pasted sessionID or a sessionID got from a search
|
||||
this.updateSearch('');
|
||||
this.setState({ pubKeyPasted: e.target.innerHTML });
|
||||
}
|
||||
|
||||
private handleMessageButtonClick() {
|
||||
const { openConversationInternal } = this.props;
|
||||
|
||||
if (!this.state.pubKeyPasted && !this.props.searchTerm) {
|
||||
return;
|
||||
}
|
||||
let pubkey: string;
|
||||
pubkey = this.state.pubKeyPasted || this.props.searchTerm;
|
||||
|
||||
const error = validateNumber(pubkey);
|
||||
if (!error) {
|
||||
openConversationInternal(pubkey);
|
||||
} else {
|
||||
window.pushToast({
|
||||
title: error,
|
||||
type: 'error',
|
||||
id: 'invalidPubKey',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { SessionButton } from './SessionButton';
|
||||
|
||||
const Tab = ({
|
||||
isSelected,
|
||||
label,
|
||||
onSelect,
|
||||
type,
|
||||
}: {
|
||||
isSelected: boolean;
|
||||
label: string;
|
||||
onSelect?: (event: number) => void;
|
||||
type: number;
|
||||
}) => {
|
||||
const handleClick = onSelect
|
||||
? () => {
|
||||
onSelect(type);
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<h1
|
||||
className={classNames(
|
||||
'module-left-pane__title',
|
||||
isSelected ? 'active' : null
|
||||
)}
|
||||
onClick={handleClick}
|
||||
role="button"
|
||||
>
|
||||
{label}
|
||||
</h1>
|
||||
);
|
||||
};
|
||||
|
||||
interface Props {
|
||||
onTabSelected: any;
|
||||
selectedTab: number;
|
||||
labels: Array<string>;
|
||||
notificationCount?: number;
|
||||
buttonLabel?: string;
|
||||
buttonClicked?: any;
|
||||
}
|
||||
|
||||
interface State {
|
||||
selectedTab: number;
|
||||
}
|
||||
|
||||
export class LeftPaneSectionHeader extends React.Component<Props, State> {
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
this.state = { selectedTab: 0 };
|
||||
}
|
||||
|
||||
public render() {
|
||||
return this.renderTabs();
|
||||
}
|
||||
|
||||
private renderTabs() {
|
||||
const { selectedTab } = this.state;
|
||||
const {
|
||||
labels,
|
||||
buttonLabel,
|
||||
buttonClicked,
|
||||
notificationCount,
|
||||
} = this.props;
|
||||
|
||||
const children = [];
|
||||
//loop to create children
|
||||
for (let i = 0; i < labels.length; i++) {
|
||||
children.push(
|
||||
<Tab
|
||||
label={labels[i]}
|
||||
type={i}
|
||||
isSelected={selectedTab === i}
|
||||
onSelect={this.handleTabSelect}
|
||||
key={i}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (buttonLabel) {
|
||||
children.push(
|
||||
<SessionButton
|
||||
text={window.i18n('compose')}
|
||||
onClick={buttonClicked}
|
||||
key="compose"
|
||||
/>
|
||||
);
|
||||
} else if (notificationCount && notificationCount > 0) {
|
||||
const shortenedNotificationCount =
|
||||
notificationCount > 9 ? 9 : notificationCount;
|
||||
children.push(
|
||||
<div className="contact-notification-count-bubble">
|
||||
{shortenedNotificationCount}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
//Create the parent and add the children
|
||||
return <div className="module-left-pane__header">{children}</div>;
|
||||
}
|
||||
|
||||
private readonly handleTabSelect = (tabType: number): void => {
|
||||
this.setState({
|
||||
selectedTab: tabType,
|
||||
});
|
||||
this.props.onTabSelected(tabType);
|
||||
};
|
||||
}
|
|
@ -9,6 +9,7 @@ import {
|
|||
} from './SessionButton';
|
||||
import { trigger } from '../../shims/events';
|
||||
import { SessionHtmlRenderer } from './SessionHTMLRenderer';
|
||||
import { SessionIdEditable } from './SessionIdEditable';
|
||||
|
||||
enum SignInMode {
|
||||
Default,
|
||||
|
@ -285,7 +286,9 @@ export class RegistrationTabs extends React.Component<{}, State> {
|
|||
private renderSignUpHeader() {
|
||||
const allUsersAreRandomly = window.i18n('allUsersAreRandomly...');
|
||||
|
||||
return <div className="session-signup-header">{allUsersAreRandomly}</div>;
|
||||
return (
|
||||
<div className="session-description-long">{allUsersAreRandomly}</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderSignUpButton() {
|
||||
|
@ -387,7 +390,7 @@ export class RegistrationTabs extends React.Component<{}, State> {
|
|||
if (signUpMode === SignUpMode.EnterDetails) {
|
||||
return (
|
||||
<div className={classNames('session-registration__entry-fields')}>
|
||||
{this.renderNamePasswordAndVerifyPasswordFields()};
|
||||
{this.renderNamePasswordAndVerifyPasswordFields()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -442,14 +445,11 @@ export class RegistrationTabs extends React.Component<{}, State> {
|
|||
const enterSessionIDHere = window.i18n('enterSessionIDHere');
|
||||
|
||||
return (
|
||||
<div
|
||||
className="session-signin-enter-session-id"
|
||||
<SessionIdEditable
|
||||
editable={contentEditable}
|
||||
placeholder={enterSessionIDHere}
|
||||
contentEditable={contentEditable}
|
||||
onInput={(e: any) => {
|
||||
if (contentEditable) {
|
||||
this.onSecondDeviceSessionIDChanged(e);
|
||||
}
|
||||
onChange={(e: any) => {
|
||||
this.onSecondDeviceSessionIDChanged(e);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
@ -475,7 +475,7 @@ export class RegistrationTabs extends React.Component<{}, State> {
|
|||
SessionButtonType.BrandOutline,
|
||||
SessionButtonColor.Green
|
||||
)}
|
||||
<div className="session-registration__or">{or}</div>
|
||||
<h4>{or}</h4>
|
||||
{this.renderLinkDeviceToExistingAccountButton()}
|
||||
</div>
|
||||
);
|
||||
|
@ -485,7 +485,7 @@ export class RegistrationTabs extends React.Component<{}, State> {
|
|||
return (
|
||||
<div>
|
||||
{this.renderContinueYourSessionButton()}
|
||||
<div className="session-registration__or">{or}</div>
|
||||
<h4>{or}</h4>
|
||||
{this.renderRestoreUsingSeedButton(
|
||||
SessionButtonType.BrandOutline,
|
||||
SessionButtonColor.Green
|
||||
|
@ -497,18 +497,21 @@ export class RegistrationTabs extends React.Component<{}, State> {
|
|||
return (
|
||||
<div>
|
||||
{this.renderContinueYourSessionButton()}
|
||||
<div className="session-registration__or">{or}</div>
|
||||
<h4>{or}</h4>
|
||||
{this.renderLinkDeviceToExistingAccountButton()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderTermsConditionAgreement() {
|
||||
// FIXME link to our Terms and Conditions and privacy statement
|
||||
// FIXME
|
||||
console.log(
|
||||
'FIXME: add link to our Terms and Conditions and privacy statement'
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="session-terms-conditions-agreement">
|
||||
<SessionHtmlRenderer html={window.i18n('ByUsingThiService...')} />
|
||||
<SessionHtmlRenderer html={window.i18n('ByUsingThisService...')} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -549,8 +552,6 @@ export class RegistrationTabs extends React.Component<{}, State> {
|
|||
displayName: '',
|
||||
signUpMode: SignUpMode.Default,
|
||||
});
|
||||
//FIXME ugly hack to empty the content editable div used on enter session ID
|
||||
window.Session.emptyContentEditableDivs();
|
||||
}}
|
||||
buttonType={buttonType}
|
||||
buttonColor={buttonColor}
|
||||
|
|
|
@ -19,6 +19,7 @@ export enum SessionButtonColor {
|
|||
Success = 'success',
|
||||
Danger = 'danger',
|
||||
Warning = 'warning',
|
||||
None = '',
|
||||
}
|
||||
|
||||
interface Props {
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
placeholder: string;
|
||||
text?: string;
|
||||
editable?: boolean;
|
||||
onChange?: any;
|
||||
}
|
||||
|
||||
export class SessionIdEditable extends React.PureComponent<Props> {
|
||||
public componentWillUnmount() {
|
||||
//FIXME ugly hack to empty the content editable div used on enter session ID
|
||||
window.Session.emptyContentEditableDivs();
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { placeholder, editable, onChange, text } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="session-id-editable"
|
||||
placeholder={placeholder}
|
||||
contentEditable={editable}
|
||||
onInput={(e: any) => {
|
||||
if (editable) {
|
||||
onChange(e);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
import React from 'react';
|
||||
import { SessionIconButton, SessionIconSize, SessionIconType } from './icon';
|
||||
|
||||
interface Props {
|
||||
searchString: string;
|
||||
onChange: any;
|
||||
handleNavigation?: any;
|
||||
placeholder: string;
|
||||
}
|
||||
|
||||
export class SessionSearchInput extends React.Component<Props> {
|
||||
public constructor(props: Props) {
|
||||
super(props);
|
||||
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { searchString } = this.props;
|
||||
|
||||
return (
|
||||
<div className="session-search-input">
|
||||
<SessionIconButton
|
||||
iconSize={SessionIconSize.Medium}
|
||||
iconType={SessionIconType.Search}
|
||||
/>
|
||||
<input
|
||||
value={searchString}
|
||||
onChange={e => this.props.onChange(e.target.value)}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
placeholder={this.props.placeholder}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public handleKeyDown(e: any) {
|
||||
if (e.keyCode === 38 || e.keyCode === 40 || e.key === 'Enter') {
|
||||
// Up or Bottom arrow pressed
|
||||
if (this.props.handleNavigation) {
|
||||
e.stopPropagation();
|
||||
this.props.handleNavigation(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
import React from 'react';
|
||||
|
||||
import { UserSearchResults } from '../UserSearchResults';
|
||||
import { SessionSearchInput } from './SessionSearchInput';
|
||||
|
||||
import { PropsData as SearchResultsProps } from '../SearchResults';
|
||||
|
||||
export interface Props {
|
||||
searchTerm: string;
|
||||
placeholder: string;
|
||||
searchResults?: SearchResultsProps;
|
||||
updateSearch: (searchTerm: string) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
selectedContact: number;
|
||||
}
|
||||
|
||||
export class UserSearchDropdown extends React.Component<Props, State> {
|
||||
private readonly updateSearchBound: (searchedString: string) => void;
|
||||
|
||||
public constructor(props: Props) {
|
||||
super(props);
|
||||
this.updateSearchBound = this.updateSearch.bind(this);
|
||||
this.handleNavigation = this.handleNavigation.bind(this);
|
||||
this.handleContactSelected = this.handleContactSelected.bind(this);
|
||||
this.state = {
|
||||
selectedContact: -1,
|
||||
};
|
||||
}
|
||||
|
||||
public handleNavigation(e: any) {
|
||||
const { selectedContact } = this.state;
|
||||
const { searchResults } = this.props;
|
||||
// arrow up/down button should select next/previous list element
|
||||
if (
|
||||
e.keyCode === 38 &&
|
||||
selectedContact > 0 &&
|
||||
searchResults &&
|
||||
searchResults.contacts.length > 0
|
||||
) {
|
||||
this.setState(prevState => ({
|
||||
selectedContact: +prevState.selectedContact - 1,
|
||||
}));
|
||||
} else if (
|
||||
e.keyCode === 40 &&
|
||||
searchResults &&
|
||||
selectedContact < searchResults.contacts.length - 1
|
||||
) {
|
||||
this.setState(prevState => ({
|
||||
selectedContact: +prevState.selectedContact + 1,
|
||||
}));
|
||||
} else if (
|
||||
e.key === 'Enter' &&
|
||||
searchResults &&
|
||||
searchResults.contacts.length > 0
|
||||
) {
|
||||
this.handleContactSelected(
|
||||
searchResults.contacts[selectedContact].phoneNumber
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { searchResults, placeholder } = this.props;
|
||||
const { selectedContact } = this.state;
|
||||
|
||||
return (
|
||||
<div className="user-search-dropdown">
|
||||
<SessionSearchInput
|
||||
searchString={this.props.searchTerm}
|
||||
onChange={this.updateSearchBound}
|
||||
placeholder={placeholder}
|
||||
handleNavigation={this.handleNavigation}
|
||||
/>
|
||||
{searchResults && (
|
||||
<UserSearchResults
|
||||
{...searchResults}
|
||||
i18n={window.i18n}
|
||||
selectedContact={selectedContact}
|
||||
onContactSelected={this.handleContactSelected}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public updateSearch(data: string) {
|
||||
this.setState({ selectedContact: -1 });
|
||||
this.props.updateSearch(data);
|
||||
}
|
||||
|
||||
public handleContactSelected(key: string) {
|
||||
this.updateSearch(key);
|
||||
}
|
||||
}
|
|
@ -5,11 +5,15 @@ import { Props, SessionIcon } from '../icon';
|
|||
|
||||
interface SProps extends Props {
|
||||
onClick: any;
|
||||
notificationCount: number | undefined;
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
||||
export class SessionIconButton extends React.PureComponent<SProps> {
|
||||
public static readonly extendedDefaults = {
|
||||
onClick: () => null,
|
||||
notificationCount: undefined,
|
||||
isSelected: false,
|
||||
};
|
||||
public static readonly defaultProps = {
|
||||
...SessionIcon.defaultProps,
|
||||
|
@ -28,14 +32,24 @@ export class SessionIconButton extends React.PureComponent<SProps> {
|
|||
iconColor,
|
||||
iconRotation,
|
||||
iconPadded,
|
||||
isSelected,
|
||||
} = this.props;
|
||||
|
||||
let { notificationCount } = this.props;
|
||||
|
||||
if (notificationCount === 0) {
|
||||
notificationCount = undefined;
|
||||
} else if (notificationCount !== undefined && notificationCount > 9) {
|
||||
notificationCount = 9;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'session-icon-button',
|
||||
iconSize,
|
||||
iconPadded ? 'padded' : ''
|
||||
iconPadded ? 'padded' : '',
|
||||
isSelected ? 'no-opacity' : ''
|
||||
)}
|
||||
role="button"
|
||||
onClick={e => {
|
||||
|
@ -48,6 +62,9 @@ export class SessionIconButton extends React.PureComponent<SProps> {
|
|||
iconColor={iconColor}
|
||||
iconRotation={iconRotation}
|
||||
/>
|
||||
{notificationCount !== undefined && (
|
||||
<span className="notification-count">{notificationCount}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ interface Window {
|
|||
i18n: any;
|
||||
friends: any;
|
||||
generateID: any;
|
||||
storage: any;
|
||||
pushToast: any;
|
||||
}
|
||||
|
||||
|
|
|
@ -204,10 +204,9 @@ async function queryConversationsAndContacts(
|
|||
} else {
|
||||
conversations.push(primaryDevice);
|
||||
}
|
||||
} else if (
|
||||
conversation.type === 'direct' &&
|
||||
!Boolean(conversation.lastMessage)
|
||||
) {
|
||||
} else if (conversation.type === 'direct') {
|
||||
contacts.push(conversation.id);
|
||||
} else if (conversation.type !== 'group') {
|
||||
contacts.push(conversation.id);
|
||||
} else {
|
||||
conversations.push(conversation.id);
|
||||
|
|
|
@ -67,6 +67,11 @@ export const getSearchResults = createSelector(
|
|||
state.conversations.map(id => {
|
||||
const value = lookup[id];
|
||||
|
||||
// Don't return anything when activeAt is undefined (i.e. no current conversations with this user)
|
||||
if (value.activeAt === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (value && id === selectedConversation) {
|
||||
return {
|
||||
...value,
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { mapDispatchToProps } from '../actions';
|
||||
import { LeftPane } from '../../components/LeftPane';
|
||||
import { StateType } from '../reducer';
|
||||
|
||||
import { getSearchResults, isSearching } from '../selectors/search';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { getQuery, getSearchResults, isSearching } from '../selectors/search';
|
||||
import {
|
||||
getIntl,
|
||||
getIsSecondaryDevice,
|
||||
getRegionCode,
|
||||
getUserNumber,
|
||||
} from '../selectors/user';
|
||||
import { getLeftPaneLists, getShowArchived } from '../selectors/conversations';
|
||||
|
||||
import { SmartMainHeader } from './MainHeader';
|
||||
|
||||
// Workaround: A react component's required properties are filtering up through connect()
|
||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
|
||||
const FilteredSmartMainHeader = SmartMainHeader as any;
|
||||
|
||||
const mapStateToProps = (state: StateType) => {
|
||||
const showSearch = isSearching(state);
|
||||
|
@ -22,10 +23,13 @@ const mapStateToProps = (state: StateType) => {
|
|||
|
||||
return {
|
||||
...lists,
|
||||
searchTerm: getQuery(state),
|
||||
regionCode: getRegionCode(state),
|
||||
ourNumber: getUserNumber(state),
|
||||
isSecondaryDevice: getIsSecondaryDevice(state),
|
||||
searchResults,
|
||||
showArchived: getShowArchived(state),
|
||||
i18n: getIntl(state),
|
||||
renderMainHeader: () => <FilteredSmartMainHeader />,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -55,7 +55,10 @@ function isValidNumber(number: string) {
|
|||
return !error;
|
||||
}
|
||||
|
||||
export function validateNumber(number: string, i18n: LocalizerType) {
|
||||
export function validateNumber(
|
||||
number: string,
|
||||
i18n: LocalizerType = window.i18n
|
||||
) {
|
||||
const error = validate(number);
|
||||
|
||||
return error && i18n(error);
|
||||
|
|
|
@ -24,5 +24,5 @@ export type ColorType =
|
|||
export enum Colors {
|
||||
OFFLINE = '#3d3e44',
|
||||
OFFLINE_LIGHT = '#cccece',
|
||||
ONLINE = '#1c8260',
|
||||
ONLINE = '#00f782',
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue