Merge pull request #790 from vincentbavitz/clearnet

Closed groups UI & styling fixes
pull/792/head
Vince 5 years ago committed by GitHub
commit badfb8a203
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1504,7 +1504,7 @@
"description": "Shown in the conversation history when someone updates the group"
},
"titleIsNow": {
"message": "Title is now '$name$'",
"message": "Group name has been set to '$name$'",
"description": "Shown in the conversation history when someone changes the title of the group",
"placeholders": {
"name": {

@ -1504,7 +1504,7 @@
"description": "Shown in the conversation history when someone updates the group"
},
"titleIsNow": {
"message": "Title is now '$name$'",
"message": "Group name has been set to '$name$'",
"description": "Shown in the conversation history when someone changes the title of the group",
"placeholders": {
"name": {

@ -1107,7 +1107,7 @@
"Placeholder text in the message entry field when it is disabled while we are waiting for a friend request approval"
},
"sendMessageFriendRequest": {
"message": "Hi there! This is ...!",
"message": "Send your first message",
"description":
"Placeholder text in the message entry field when it is the first message sent to that contact"
},
@ -1440,7 +1440,8 @@
"description": "Header for notification settings"
},
"readReceiptSettingDescription": {
"message": "Enable the sending and receiving of read receipts",
"message":
"See and share when messages have been read (enables read receipts in all sessions).",
"description": "Description of the read receipts setting"
},
"readReceiptSettingTitle": {
@ -1993,7 +1994,7 @@
"Shown in the conversation history when someone updates the group"
},
"titleIsNow": {
"message": "Title is now '$name$'",
"message": "Group name has been set to '$name$'.",
"description":
"Shown in the conversation history when someone changes the title of the group",
"placeholders": {
@ -2164,6 +2165,9 @@
"description":
"Title shown to the user to confirm they want to leave the group"
},
"noContactsForGroup": {
"message": "You don't have any contacts to start a group with."
},
"copiedPublicKey": {
"message": "Session ID copied",
"description": "A toast message telling the user that the key was copied"
@ -2610,6 +2614,9 @@
"message": "Complete Sign Up"
},
"compose": {
"message": "Compose"
},
"newSession": {
"message": "New Session"
},
"searchForAKeyPhrase": {
@ -2655,11 +2662,11 @@
"decline": {
"message": "Decline"
},
"generalSettingsTitle": {
"message": "General"
"appearanceSettingsTitle": {
"message": "Appearance"
},
"generalSettingsDescription": {
"message": "General settings and configuration"
"appearanceSettingsDescription": {
"message": "Appearance and interface options"
},
"accountSettingsTitle": {
"message": "Account"
@ -2683,13 +2690,13 @@
"message": "Notifications"
},
"notificationSettingsDescription": {
"message": "Choose what you're notified about"
"message": "Configure notification options"
},
"devicesSettingsTitle": {
"message": "Devices"
},
"devicesSettingsDescription": {
"message": "Manage linked devices"
"message": "Manage your linked devices"
},
"mnemonicEmpty": {
"message": "Seed is mandatory"
@ -2718,11 +2725,33 @@
"addChannel": {
"message": "Join Open Group"
},
"joinOpenGroup": {
"message": "Join Open Group"
},
"newClosedGroup": {
"message": "New Closed Group"
},
"createClosedGroup": {
"message": "Create Closed Group"
},
"createClosedGroupDescription": {
"message":
"Closed groups are end-to-end encrypted group chats for up to 10 members. They provide the same privacy protections as one-on-one sessions."
},
"createClosedGroupNamePrompt": {
"message": "Group Name"
},
"createClosedGroupPlaceholder": {
"message": "Enter a group name"
},
"closedGroupCreatedToastTitle": {
"message": "Group created successfully"
},
"enterChannelURL": {
"message": "Enter Open Group URL"
},
"channelUrlPlaceholder": {
"message": "https://chat.lokinet.org"
"message": "chat.getsession.org"
},
"addChannelDescription": {
"message": "Enter an open group URL."

@ -1368,7 +1368,7 @@
"description": "Shown in the conversation history when someone updates the group"
},
"titleIsNow": {
"message": "Title is now '$name$'",
"message": "Group name has been set to '$name$'",
"description": "Shown in the conversation history when someone changes the title of the group",
"placeholders": {
"name": {

@ -1504,7 +1504,7 @@
"description": "Shown in the conversation history when someone updates the group"
},
"titleIsNow": {
"message": "Title is now '$name$'",
"message": "Group name has been set to '$name$'",
"description": "Shown in the conversation history when someone changes the title of the group",
"placeholders": {
"name": {

@ -1504,7 +1504,7 @@
"description": "Shown in the conversation history when someone updates the group"
},
"titleIsNow": {
"message": "Title is now '$name$'",
"message": "Group name has been set to '$name$'",
"description": "Shown in the conversation history when someone changes the title of the group",
"placeholders": {
"name": {

@ -1504,7 +1504,7 @@
"description": "Shown in the conversation history when someone updates the group"
},
"titleIsNow": {
"message": "Title is now '$name$'",
"message": "Group name has been set to '$name$'",
"description": "Shown in the conversation history when someone changes the title of the group",
"placeholders": {
"name": {

@ -1504,7 +1504,7 @@
"description": "Shown in the conversation history when someone updates the group"
},
"titleIsNow": {
"message": "Title is now '$name$'",
"message": "Group name has been set to '$name$'",
"description": "Shown in the conversation history when someone changes the title of the group",
"placeholders": {
"name": {

@ -1504,7 +1504,7 @@
"description": "Shown in the conversation history when someone updates the group"
},
"titleIsNow": {
"message": "Title is now '$name$'",
"message": "Group name has been set to '$name$'",
"description": "Shown in the conversation history when someone changes the title of the group",
"placeholders": {
"name": {

@ -1504,7 +1504,7 @@
"description": "Shown in the conversation history when someone updates the group"
},
"titleIsNow": {
"message": "Title is now '$name$'",
"message": "Group name has been set to '$name$'",
"description": "Shown in the conversation history when someone changes the title of the group",
"placeholders": {
"name": {

@ -1504,7 +1504,7 @@
"description": "Shown in the conversation history when someone updates the group"
},
"titleIsNow": {
"message": "Title is now '$name$'",
"message": "Group name has been set to '$name$'",
"description": "Shown in the conversation history when someone changes the title of the group",
"placeholders": {
"name": {

@ -1504,7 +1504,7 @@
"description": "Shown in the conversation history when someone updates the group"
},
"titleIsNow": {
"message": "Title is now '$name$'",
"message": "Group name has been set to '$name$'",
"description": "Shown in the conversation history when someone changes the title of the group",
"placeholders": {
"name": {

@ -902,7 +902,7 @@ async function updateToLokiSchemaVersion1(currentVersion, instance) {
rssFeed: 'https://loki.network/feed/',
closable: true,
name: 'Loki.network News',
profileAvatar: 'images/loki/session_icon.png',
profileAvatar: 'images/session/session_chat_icon.png',
};
const updatesRssFeedData = {
@ -911,7 +911,7 @@ async function updateToLokiSchemaVersion1(currentVersion, instance) {
rssFeed: 'https://loki.network/category/messenger-updates/feed/',
closable: false,
name: 'Messenger updates',
profileAvatar: 'images/loki/session_icon.png',
profileAvatar: 'images/session/session_chat_icon.png',
};
const autoJoinLokiChats = false;

@ -35,6 +35,6 @@
"-----BEGIN CERTIFICATE-----\nMIID7zCCAtegAwIBAgIJAIm6LatK5PNiMA0GCSqGSIb3DQEBBQUAMIGNMQswCQYD\nVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5j\naXNjbzEdMBsGA1UECgwUT3BlbiBXaGlzcGVyIFN5c3RlbXMxHTAbBgNVBAsMFE9w\nZW4gV2hpc3BlciBTeXN0ZW1zMRMwEQYDVQQDDApUZXh0U2VjdXJlMB4XDTEzMDMy\nNTIyMTgzNVoXDTIzMDMyMzIyMTgzNVowgY0xCzAJBgNVBAYTAlVTMRMwEQYDVQQI\nDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMR0wGwYDVQQKDBRP\ncGVuIFdoaXNwZXIgU3lzdGVtczEdMBsGA1UECwwUT3BlbiBXaGlzcGVyIFN5c3Rl\nbXMxEzARBgNVBAMMClRleHRTZWN1cmUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw\nggEKAoIBAQDBSWBpOCBDF0i4q2d4jAXkSXUGpbeWugVPQCjaL6qD9QDOxeW1afvf\nPo863i6Crq1KDxHpB36EwzVcjwLkFTIMeo7t9s1FQolAt3mErV2U0vie6Ves+yj6\ngrSfxwIDAcdsKmI0a1SQCZlr3Q1tcHAkAKFRxYNawADyps5B+Zmqcgf653TXS5/0\nIPPQLocLn8GWLwOYNnYfBvILKDMItmZTtEbucdigxEA9mfIvvHADEbteLtVgwBm9\nR5vVvtwrD6CCxI3pgH7EH7kMP0Od93wLisvn1yhHY7FuYlrkYqdkMvWUrKoASVw4\njb69vaeJCUdU+HCoXOSP1PQcL6WenNCHAgMBAAGjUDBOMB0GA1UdDgQWBBQBixjx\nP/s5GURuhYa+lGUypzI8kDAfBgNVHSMEGDAWgBQBixjxP/s5GURuhYa+lGUypzI8\nkDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQB+Hr4hC56m0LvJAu1R\nK6NuPDbTMEN7/jMojFHxH4P3XPFfupjR+bkDq0pPOU6JjIxnrD1XD/EVmTTaTVY5\niOheyv7UzJOefb2pLOc9qsuvI4fnaESh9bhzln+LXxtCrRPGhkxA1IMIo3J/s2WF\n/KVYZyciu6b4ubJ91XPAuBNZwImug7/srWvbpk0hq6A6z140WTVSKtJG7EP41kJe\n/oF4usY5J7LPkxK3LWzMJnb5EIJDmRvyH8pyRwWg6Qm6qiGFaI4nL8QU4La1x2en\n4DGXRaLMPRwjELNgQPodR38zoCMuA8gHZfZYYoZ7D7Q1wNUiVHcxuFrEeBaYJbLE\nrwLV\n-----END CERTIFICATE-----\n",
"import": false,
"serverTrustRoot": "BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx",
"defaultPublicChatServer": "https://chat.lokinet.org/",
"defaultFileServer": "https://file.lokinet.org"
"defaultPublicChatServer": "https://chat.getsession.org",
"defaultFileServer": "https://file.getsession.org"
}

@ -121,7 +121,6 @@
'x.svg',
'x_white.svg',
'icon-paste.svg',
'loki/loki_icon_text.png',
'loki/session_icon_128.png',
]);
@ -988,6 +987,18 @@
return toastID;
};
window.getFriendsFromContacts = contacts => {
// To call from TypeScript, input / output are both
// of type Array<ConversationType>
let friendList = contacts;
if (friendList !== undefined) {
friendList = friendList.filter(
friend => friend.type === 'direct' && !friend.isMe
);
}
return friendList;
};
// Get memberlist. This function is not accurate >>
// window.getMemberList = window.lokiPublicChatAPI.getListOfMembers();

@ -203,6 +203,13 @@
}
return conversation.getDisplayName();
},
getLokiNameForNumber(number) {
const conversation = ConversationController.get(number);
if (!conversation) {
return number;
}
return conversation.getLokiProfile().displayName;
},
getDescription() {
if (this.isGroupUpdate()) {
const groupUpdate = this.get('group_update');
@ -224,10 +231,10 @@
messages.push(i18n('titleIsNow', groupUpdate.name));
}
if (groupUpdate.joined && groupUpdate.joined.length) {
const names = _.map(
groupUpdate.joined,
this.getNameForNumber.bind(this)
const names = groupUpdate.joined.map(name =>
this.getLokiNameForNumber(name)
);
if (names.length > 1) {
messages.push(i18n('multipleJoinedTheGroup', names.join(', ')));
} else {

@ -310,7 +310,7 @@ class LokiAppDotNetServerAPI {
if (
window.lokiFeatureFlags.useSnodeProxy &&
(this.baseServerUrl === 'https://file-dev.lokinet.org' ||
this.baseServerUrl === 'https://file.lokinet.org')
this.baseServerUrl === 'https://file.getsession.org')
) {
const finalOptions = { ...fetchOptions };
if (!fetchOptions.method) {
@ -483,7 +483,7 @@ class LokiAppDotNetServerAPI {
if (
window.lokiFeatureFlags.useSnodeProxy &&
(this.baseServerUrl === 'https://file-dev.lokinet.org' ||
this.baseServerUrl === 'https://file.lokinet.org')
this.baseServerUrl === 'https://file.getsession.org')
) {
mode = '_sendToProxy';

@ -1282,8 +1282,9 @@
},
deleteSelectedMessages() {
const ourPubkey = textsecure.storage.user.getNumber();
const selected = Array.from(this.model.selectedMessages);
const isModerator = this.model.isModerator(this.model.OUR_NUMBER);
const isModerator = this.model.isModerator(ourPubkey);
const isAllOurs = selected.every(
message => message.attributes.source === message.OUR_NUMBER
);

@ -19,7 +19,7 @@
const messages = ['Updated the group.'];
if (this.model.name) {
messages.push(`Title is now '${this.model.name}'.`);
messages.push(`Group name has been set to '${this.model.name}'.`);
}
if (this.model.joined) {
messages.push(`${this.model.joined.join(', ')} joined the group`);

@ -64,6 +64,7 @@ window.CONSTANTS = {
MAX_LOGIN_TRIES: 3,
MAX_PASSWORD_LENGTH: 32,
MAX_USERNAME_LENGTH: 20,
MAX_GROUP_NAME_LENGTH: 64,
DEFAULT_PUBLIC_CHAT_URL: appConfig.get('defaultPublicChatServer'),
MAX_CONNECTION_DURATION: 5000,
};

@ -210,7 +210,7 @@
align-self: flex-start;
box-shadow: 2px 2px lightgrey;
box-shadow: none;
.title {
margin: 6px;
@ -246,17 +246,18 @@
}
.join-btn {
background-color: #e0e0e0;
background-color: #00f782;
color: white;
padding: 6px 10px;
margin-left: 6px;
border-radius: 6px;
box-shadow: 2px 2px 1px #c0c0c0;
color: #404040;
border-radius: 2px;
box-shadow: none;
user-select: none;
cursor: pointer;
transition: 0.25s;
&:hover {
background-color: #c7c7c7;
background-color: #00d672;
}
}
}
@ -266,7 +267,7 @@
.group-invitation {
background-color: #242424;
border-color: #303030;
box-shadow: 2px 2px #4f4f4f;
box-shadow: none;
.title {
color: lightgrey;
@ -670,7 +671,7 @@
.module-last-seen-indicator__bar {
background-color: $color-light-60;
width: 100%;
height: 4px;
height: 2px;
}
.module-last-seen-indicator__text {

@ -1235,11 +1235,17 @@
}
.module-group-notification__change {
margin-top: 10px;
background-color: #212121;
width: 90%;
max-width: 700px;
margin: 10px auto;
padding: 5px 20px;
border-radius: 4px;
}
.module-group-notification__contact {
font-weight: 300;
font-family: 'SF Pro Text';
font-weight: bold;
}
// Module: Reset Session Notification

@ -150,6 +150,11 @@ div.spacer-lg {
width: 100%;
}
input,
textarea {
caret-color: $session-color-green !important;
}
@mixin text-highlight($color) {
background-color: $color;
padding: $session-margin-xs $session-margin-sm;
@ -269,7 +274,7 @@ $session_message-container-border-radius: 5px;
&.brand {
color: $session-color-white;
&:hover {
&:not(.disabled):hover {
filter: brightness(90%);
}
@ -352,12 +357,12 @@ $session_message-container-border-radius: 5px;
min-width: 165px;
height: 45px;
line-height: 40px;
padding: 0;
padding: 0px $session-margin-lg;
font-size: $session-font-md;
font-family: $session-font-family;
border-radius: 500px;
&:hover {
&:not(.disabled):hover {
color: $session-color-white;
border-color: $session-color-white;
}
@ -559,6 +564,11 @@ label {
position: relative;
}
.module-left-pane-overlay {
h3 {
margin-bottom: 6px;
}
}
.message-selection-overlay {
display: none;
position: absolute;
@ -1387,6 +1397,7 @@ input {
background-color: $session-shade-4;
border: $session-separator-element-border;
display: flex;
align-items: center;
transition: $session-transition-duration;
.module-avatar,
@ -1676,3 +1687,77 @@ input {
}
}
}
.group-member-list {
&__container {
padding: 2px 0px;
width: 100%;
max-height: 400px;
overflow-y: auto;
box-shadow: inset 0px 14px 7px -15px $session-color-dark-grey,
inset 0px -14px 7px -15px $session-color-dark-grey;
}
&__selection {
height: 100%;
display: flex;
flex-direction: column;
}
&__no-contacts {
font-family: 'SpaceMono';
text-align: center;
padding: 20px;
}
}
.create-group-name-input {
.session-id-editable {
height: 60px !important;
textarea {
padding-bottom: 0px !important;
}
&-disabled {
border: 1px solid $session-color-dark-grey !important;
}
}
}
.session-member-item {
cursor: pointer;
font-family: 'SF Pro Text';
padding: 0px $session-margin-sm;
height: 50px;
display: flex;
justify-content: space-between;
transition: $session-transition-duration;
&.selected {
background-color: $session-shade-4;
}
&__checkmark {
opacity: 0;
transition: $session-transition-duration;
&.selected {
opacity: 1;
}
}
&__info,
&__checkmark {
display: flex;
align-items: center;
}
&__name {
font-weight: bold;
margin-left: $session-margin-md;
}
&__pubkey {
margin-left: 5px;
opacity: 0.8;
}
}

@ -178,8 +178,8 @@ $session-compose-margin: 20px;
&__header {
display: flex;
flex-direction: row;
margin: 15px 7px 14px 0px;
height: 33px;
padding: 15px 7px 14px 0px;
height: 63px;
@at-root .light-theme #{&} {
background-color: $session-color-white;
@ -271,8 +271,8 @@ $session-compose-margin: 20px;
position: relative;
height: 1px;
opacity: 0.3;
margin-top: -10px;
margin-bottom: 50px;
margin-top: 2px;
margin-bottom: 40px;
}
.exit {
@ -291,7 +291,8 @@ $session-compose-margin: 20px;
}
.session-description-long {
font-size: 13px;
font-size: $session-font-sm;
line-height: $session-font-h3;
margin: 0px 20px;
font-family: 'SF Pro Display';
}

@ -116,7 +116,7 @@ body.dark-theme {
}
.module-last-seen-indicator__bar {
background-color: $color-dark-30;
background-color: #353535;
}
.module-last-seen-indicator__text {

@ -101,7 +101,7 @@ describe('MessageCollection', () => {
message = messages.add({ group_update: { name: 'blerg' } });
assert.equal(
message.getDescription(),
"Title is now 'blerg'",
"Group name has been set to 'blerg'",
'Returns a single notice if only group_updates.name changes.'
);
@ -126,7 +126,7 @@ describe('MessageCollection', () => {
});
assert.equal(
message.getDescription(),
"Title is now 'blerg', Bob joined the group",
"Group name has been set to 'blerg', Bob joined the group",
'Notes when there are multiple changes to group_updates properties.'
);

@ -177,6 +177,7 @@ export class LeftPane extends React.Component<Props, State> {
private renderChannelSection() {
const {
friends,
openConversationInternal,
conversations,
searchResults,
@ -189,6 +190,7 @@ export class LeftPane extends React.Component<Props, State> {
return (
<LeftPaneChannelSection
friends={friends}
openConversationInternal={openConversationInternal}
conversations={conversations}
searchResults={searchResults}

@ -395,7 +395,7 @@ export class ConversationHeader extends React.Component<Props> {
{this.renderOptions(triggerId)}
{this.renderTitle()}
{/* This might be redundant as we show the title in the title: */}
{isPrivateGroup ? this.renderMemberCount() : null}
{/*isPrivateGroup ? this.renderMemberCount() : null*/}
</div>
</div>
{this.renderExpirationLength()}

@ -39,6 +39,7 @@ export class CreateGroupDialog extends React.Component<Props, State> {
this.onGroupNameChanged = this.onGroupNameChanged.bind(this);
let friends = this.props.friendList;
friends = friends.map(d => {
const lokiProfile = d.getLokiProfile();
const name = lokiProfile ? lokiProfile.displayName : 'Anonymous';
@ -95,6 +96,8 @@ export class CreateGroupDialog extends React.Component<Props, State> {
return (
<SessionModal title={titleText} onClose={() => null} onOk={() => null}>
<div className="spacer-lg" />
<p className={errorMessageClasses}>{this.state.errorMessage}</p>
<input
type="text"
@ -116,6 +119,9 @@ export class CreateGroupDialog extends React.Component<Props, State> {
onMemberClicked={this.onMemberClicked}
/>
</div>
<div className="spacer-lg" />
<div className="buttons">
<button className="cancel" tabIndex={0} onClick={this.closeDialog}>
{cancelText}

@ -23,7 +23,7 @@ export class GroupInvitation extends React.Component<Props> {
<div className="contents">
<img
alt="group-avatar"
src="images/loki/session_icon.png"
src="images/session/session_chat_icon.png"
className="invite-group-avatar"
/>
<span className="group-details">

@ -2,7 +2,6 @@ import React from 'react';
// import classNames from 'classnames';
import { compact, flatten } from 'lodash';
import { ContactName } from './ContactName';
import { Intl } from '../Intl';
import { LocalizerType } from '../../types/Util';
@ -39,12 +38,7 @@ export class GroupNotification extends React.Component<Props> {
key={`external-${contact.phoneNumber}`}
className="module-group-notification__contact"
>
<ContactName
i18n={i18n}
phoneNumber={contact.phoneNumber}
profileName={contact.profileName}
name={contact.name}
/>
{contact.profileName}
</span>
);

@ -31,6 +31,7 @@ export class InviteFriendsDialog extends React.Component<Props, State> {
this.onKeyUp = this.onKeyUp.bind(this);
let friends = this.props.friendList;
friends = friends.map(d => {
const lokiProfile = d.getLokiProfile();
const name = lokiProfile ? lokiProfile.displayName : 'Anonymous';
@ -70,6 +71,8 @@ export class InviteFriendsDialog extends React.Component<Props, State> {
onOk={() => null}
onClose={this.closeDialog}
>
<div className="spacer-lg" />
<div className="friend-selection-list">
<MemberList
members={this.state.friendList}
@ -86,6 +89,8 @@ export class InviteFriendsDialog extends React.Component<Props, State> {
</>
)}
<div className="spacer-lg" />
<div className="session-modal__button-group">
<SessionButton text={cancelText} onClick={this.closeDialog} />
<SessionButton

@ -33,6 +33,7 @@ class MemberItem extends React.Component<MemberItemProps> {
const pubkey = this.props.member.authorPhoneNumber;
const selected = this.props.selected;
const existingMember = this.props.existingMember;
const shortPubkey = window.shortenPubkey(pubkey);
let markType: 'none' | 'kicked' | 'added' | 'existing' = 'none';
@ -79,7 +80,7 @@ class MemberItem extends React.Component<MemberItemProps> {
>
{this.renderAvatar()}
<span className="name-part">{name}</span>
<span className="pubkey-part">{pubkey}</span>
<span className="pubkey-part">{shortPubkey}</span>
<span className={classNames(markClasses)}>{mark}</span>
</div>
);

@ -22,6 +22,7 @@ import { cleanSearchTerm } from '../../util/cleanSearchTerm';
import { SessionSearchInput } from './SessionSearchInput';
import { SessionClosableOverlay } from './SessionClosableOverlay';
import { MainViewController } from '../MainViewController';
import { ContactType } from './SessionMemberListItem';
export interface Props {
searchTerm: string;
@ -37,11 +38,17 @@ export interface Props {
clearSearch: () => void;
}
export enum SessionGroupType {
Open = 'open-group',
Closed = 'closed-group',
}
interface State {
showAddChannelView: boolean;
channelUrlPasted: string;
loading: boolean;
connectSuccess: boolean;
// The type of group that is being added. Undefined in default view.
groupAddType: SessionGroupType | undefined;
}
export class LeftPaneChannelSection extends React.Component<Props, State> {
@ -51,10 +58,10 @@ export class LeftPaneChannelSection extends React.Component<Props, State> {
public constructor(props: Props) {
super(props);
this.state = {
showAddChannelView: false,
channelUrlPasted: '',
loading: false,
connectSuccess: false,
groupAddType: undefined,
};
this.handleOnPasteUrl = this.handleOnPasteUrl.bind(this);
@ -181,8 +188,8 @@ export class LeftPaneChannelSection extends React.Component<Props, State> {
return (
<div className="session-left-pane-section-content">
{this.renderHeader()}
{this.state.showAddChannelView
? this.renderClosableOverlay()
{this.state.groupAddType
? this.renderClosableOverlay(this.state.groupAddType)
: this.renderGroups()}
</div>
);
@ -247,32 +254,73 @@ export class LeftPaneChannelSection extends React.Component<Props, State> {
}
}
private handleToggleOverlay() {
this.setState(prevState => ({
showAddChannelView: !prevState.showAddChannelView,
}));
private handleToggleOverlay(groupType?: SessionGroupType) {
// If no groupType, return to default view.
// Close the overlay with handleToggleOverlay(undefined)
switch (groupType) {
case SessionGroupType.Open:
this.setState({
groupAddType: SessionGroupType.Open,
});
break;
case SessionGroupType.Closed:
this.setState({
groupAddType: SessionGroupType.Closed,
});
break;
default:
// Exit overlay
this.setState({
groupAddType: undefined,
});
}
}
private renderClosableOverlay() {
private renderClosableOverlay(groupType: SessionGroupType) {
const { searchTerm } = this.props;
const { loading } = this.state;
return (
const openGroupElement = (
<SessionClosableOverlay
overlayMode="channel"
overlayMode={SessionGroupType.Open}
onChangeSessionID={this.handleOnPasteUrl}
onCloseClick={this.handleToggleOverlay}
onCloseClick={() => {
this.handleToggleOverlay(undefined);
}}
onButtonClick={this.handleJoinChannelButtonClick}
searchTerm={searchTerm}
updateSearch={this.updateSearchBound}
showSpinner={loading}
/>
);
const closedGroupElement = (
<SessionClosableOverlay
overlayMode={SessionGroupType.Closed}
onChangeSessionID={this.handleOnPasteUrl}
onCloseClick={() => {
this.handleToggleOverlay(undefined);
}}
onButtonClick={async (
groupName: string,
groupMembers: Array<ContactType>
) => this.onCreateClosedGroup(groupName, groupMembers)}
searchTerm={searchTerm}
updateSearch={this.updateSearchBound}
showSpinner={loading}
/>
);
return groupType === SessionGroupType.Open
? openGroupElement
: closedGroupElement;
}
private renderBottomButtons(): JSX.Element {
const edit = window.i18n('edit');
const addChannel = window.i18n('addChannel');
const joinOpenGroup = window.i18n('joinOpenGroup');
const createClosedGroup = window.i18n('createClosedGroup');
const showEditButton = false;
return (
@ -284,11 +332,22 @@ export class LeftPaneChannelSection extends React.Component<Props, State> {
buttonColor={SessionButtonColor.White}
/>
)}
<SessionButton
text={addChannel}
text={joinOpenGroup}
buttonType={SessionButtonType.SquareOutline}
buttonColor={SessionButtonColor.Green}
onClick={this.handleToggleOverlay}
onClick={() => {
this.handleToggleOverlay(SessionGroupType.Open);
}}
/>
<SessionButton
text={createClosedGroup}
buttonType={SessionButtonType.SquareOutline}
buttonColor={SessionButtonColor.White}
onClick={() => {
this.handleToggleOverlay(SessionGroupType.Closed);
}}
/>
</div>
);
@ -327,7 +386,33 @@ export class LeftPaneChannelSection extends React.Component<Props, State> {
return false;
}
joinChannelStateManager(this, channelUrlPasted, this.handleToggleOverlay);
joinChannelStateManager(this, channelUrlPasted, () => {
this.handleToggleOverlay(SessionGroupType.Open);
});
return true;
}
private async onCreateClosedGroup(
groupName: string,
groupMembers: Array<ContactType>
) {
// Validate groupName and groupMembers length
if (
groupMembers.length === 0 ||
groupName.length === 0 ||
groupName.length > window.CONSTANTS.MAX_GROUP_NAME_LENGTH
) {
return;
}
await window.doCreateGroup(groupName, groupMembers);
this.handleToggleOverlay(undefined);
window.pushToast({
title: window.i18n('closedGroupCreatedToastTitle'),
type: 'success',
});
return true;
}

@ -140,7 +140,7 @@ export class LeftPaneContactSection extends React.Component<Props, State> {
style,
}: RowRendererParamsType): JSX.Element | undefined => {
const { sentFriendsRequest } = this.props;
const friends = this.getCurrentFriends();
const friends = window.getFriendsFromContacts(this.props.friends);
const combined = [...sentFriendsRequest, ...friends];
const item = combined[index];
@ -211,19 +211,6 @@ export class LeftPaneContactSection extends React.Component<Props, State> {
);
}
private getCurrentFriends(): Array<ConversationType> {
const { friends } = this.props;
let friendList = friends;
if (friendList !== undefined) {
friendList = friendList.filter(
friend => friend.type === 'direct' && !friend.isMe
);
}
return friendList;
}
private handleToggleOverlay() {
this.setState((prevState: { showAddContactView: boolean }) => ({
showAddContactView: !prevState.showAddContactView,
@ -334,8 +321,8 @@ export class LeftPaneContactSection extends React.Component<Props, State> {
private renderList() {
const { sentFriendsRequest } = this.props;
const friends = this.getCurrentFriends();
const length = sentFriendsRequest.length + friends.length;
const friends = window.getFriendsFromContacts(this.props.friends);
const length = Number(sentFriendsRequest.length) + Number(friends.length);
const combined = [...sentFriendsRequest, ...friends];
const list = (

@ -191,7 +191,7 @@ export class LeftPaneMessageSection extends React.Component<Props, any> {
return LeftPane.RENDER_HEADER(
labels,
null,
window.i18n('compose'),
window.i18n('newSession'),
this.handleToggleOverlay
);
}

@ -82,7 +82,7 @@ export class LeftPaneSectionHeader extends React.Component<Props, State> {
if (buttonLabel) {
children.push(
<SessionButton
text={window.i18n('compose')}
text={window.i18n('newSession')}
onClick={buttonClicked}
key="compose"
disabled={false}

@ -25,7 +25,7 @@ export class LeftPaneSettingSection extends React.Component<any, State> {
super(props);
this.state = {
settingCategory: SessionSettingCategory.General,
settingCategory: SessionSettingCategory.Appearance,
searchQuery: '',
};
@ -179,9 +179,9 @@ export class LeftPaneSettingSection extends React.Component<any, State> {
public getCategories() {
return [
{
id: SessionSettingCategory.General,
title: window.i18n('generalSettingsTitle'),
description: window.i18n('generalSettingsDescription'),
id: SessionSettingCategory.Appearance,
title: window.i18n('appearanceSettingsTitle'),
description: window.i18n('appearanceSettingsDescription'),
hidden: false,
},
{

@ -208,9 +208,13 @@ export class SessionChannelSettings extends React.Component<Props, any> {
{this.renderHeader()}
<h2>{name}</h2>
{showMemberCount && (
<div className="text-subtle">
{window.i18n('members', memberCount)}
</div>
<>
<div className="spacer-lg" />
<div className="text-subtle">
{window.i18n('members', memberCount)}
</div>
<div className="spacer-lg" />
</>
)}
<input
className="description"

@ -3,31 +3,46 @@ import React from 'react';
import { SessionIconButton, SessionIconSize, SessionIconType } from './icon';
import { SessionIdEditable } from './SessionIdEditable';
import { UserSearchDropdown } from './UserSearchDropdown';
import { ContactType, SessionMemberListItem } from './SessionMemberListItem';
import { ConversationType } from '../../state/ducks/conversations';
import {
SessionButton,
SessionButtonColor,
SessionButtonType,
} from './SessionButton';
import { SessionSpinner } from './SessionSpinner';
import { SessionGroupType } from './LeftPaneChannelSection';
interface Props {
overlayMode: 'message' | 'contact' | 'channel';
overlayMode: 'message' | 'contact' | SessionGroupType;
onChangeSessionID: any;
onCloseClick: any;
onButtonClick: any;
contacts?: Array<ConversationType>;
searchTerm?: string;
searchResults?: any;
updateSearch?: any;
showSpinner?: boolean;
}
export class SessionClosableOverlay extends React.Component<Props> {
interface State {
groupName: string;
selectedMembers: Array<ContactType>;
}
export class SessionClosableOverlay extends React.Component<Props, State> {
private readonly inputRef: React.RefObject<SessionIdEditable>;
public constructor(props: Props) {
super(props);
this.state = {
groupName: '',
selectedMembers: [],
};
this.inputRef = React.createRef();
this.onGroupNameChanged = this.onGroupNameChanged.bind(this);
}
public componentDidMount() {
@ -36,6 +51,39 @@ export class SessionClosableOverlay extends React.Component<Props> {
}
}
public getContacts() {
const conversations = window.getConversations();
let conversationList = conversations;
if (conversationList !== undefined) {
conversationList = conversationList.filter((conv: any) => {
return !conv.isRss() && !conv.isPublic() && conv.attributes.lastMessage;
});
}
const friends = conversationList.map((d: any) => {
const lokiProfile = d.getLokiProfile();
const name = lokiProfile ? lokiProfile.displayName : 'Anonymous';
// TODO: should take existing members into account
const existingMember = false;
return {
id: d.id,
authorPhoneNumber: d.id,
authorProfileName: name,
selected: false,
authorName: name,
authorColor: d.getColor(),
checkmarked: false,
existingMember,
};
});
return friends;
}
// tslint:disable-next-line max-func-body-length */
public render(): JSX.Element {
const {
overlayMode,
@ -50,7 +98,9 @@ export class SessionClosableOverlay extends React.Component<Props> {
const isAddContactView = overlayMode === 'contact';
const isMessageView = overlayMode === 'message';
// const isChannelView = overlayMode === 'channel';
const isOpenGroupView = overlayMode === SessionGroupType.Open;
const isClosedGroupView = overlayMode === SessionGroupType.Closed;
let title;
let buttonText;
@ -59,7 +109,7 @@ export class SessionClosableOverlay extends React.Component<Props> {
let placeholder;
switch (overlayMode) {
case 'message':
title = window.i18n('enterRecipient');
title = window.i18n('newSession');
buttonText = window.i18n('next');
descriptionLong = window.i18n('usersCanShareTheir...');
subtitle = window.i18n('enterSessionID');
@ -72,17 +122,31 @@ export class SessionClosableOverlay extends React.Component<Props> {
subtitle = window.i18n('enterSessionID');
placeholder = window.i18n('pasteSessionIDRecipient');
break;
case 'channel':
default:
case 'open-group':
title = window.i18n('addChannel');
buttonText = window.i18n('joinChannel');
descriptionLong = window.i18n('addChannelDescription');
subtitle = window.i18n('enterChannelURL');
placeholder = window.i18n('channelUrlPlaceholder');
break;
case 'closed-group':
title = window.i18n('newClosedGroup');
buttonText = window.i18n('createClosedGroup');
descriptionLong = window.i18n('createClosedGroupDescription');
subtitle = window.i18n('createClosedGroupNamePrompt');
placeholder = window.i18n('createClosedGroupPlaceholder');
break;
default:
break;
}
const { groupName, selectedMembers } = this.state;
const ourSessionID = window.textsecure.storage.user.getNumber();
const contacts = this.getContacts();
const noContactsForClosedGroup =
overlayMode === SessionGroupType.Closed && contacts.length === 0;
return (
<div className="module-left-pane-overlay">
<div className="exit">
@ -92,19 +156,61 @@ export class SessionClosableOverlay extends React.Component<Props> {
onClick={onCloseClick}
/>
</div>
<div className="spacer-md" />
<h2>{title}</h2>
<h3>
{subtitle}
<hr className="green-border" />
</h3>
<hr className="white-border" />
<SessionIdEditable
ref={this.inputRef}
editable={true}
placeholder={placeholder}
onChange={onChangeSessionID}
/>
{isOpenGroupView || isClosedGroupView ? (
<div className="create-group-name-input">
<SessionIdEditable
ref={this.inputRef}
editable={!noContactsForClosedGroup}
placeholder={placeholder}
value={this.state.groupName}
maxLength={window.CONSTANTS.MAX_GROUPNAME_LENGTH}
onChange={this.onGroupNameChanged}
/>
{/* */}
</div>
) : (
<SessionIdEditable
ref={this.inputRef}
editable={true}
placeholder={placeholder}
onChange={onChangeSessionID}
/>
)}
{showSpinner && <SessionSpinner />}
{isClosedGroupView && (
<>
<div className="spacer-lg" />
<div className="group-member-list__container">
{noContactsForClosedGroup ? (
<div className="group-member-list__no-contacts">
{window.i18n('noContactsForGroup')}
</div>
) : (
<div className="group-member-list__selection">
{this.renderMemberList()}
</div>
)}
</div>
<div className="spacer-lg" />
</>
)}
<div className="session-description-long">{descriptionLong}</div>
{isMessageView && <h4>{window.i18n('or')}</h4>}
@ -130,13 +236,58 @@ export class SessionClosableOverlay extends React.Component<Props> {
text={ourSessionID}
/>
)}
<SessionButton
buttonColor={SessionButtonColor.Green}
buttonType={SessionButtonType.BrandOutline}
text={buttonText}
onClick={onButtonClick}
disabled={noContactsForClosedGroup}
onClick={() => onButtonClick(groupName, selectedMembers)}
/>
</div>
);
}
private renderMemberList() {
const members = this.getContacts();
const memberList = members.map((member: ContactType) => (
<SessionMemberListItem
member={member}
isSelected={false}
onSelect={(selectedMember: ContactType) => {
this.handleSelectMember(selectedMember);
}}
onUnselect={(selectedMember: ContactType) => {
this.handleUnselectMember(selectedMember);
}}
/>
));
return memberList;
}
private handleSelectMember(member: ContactType) {
if (this.state.selectedMembers.includes(member)) {
return;
}
this.setState({
selectedMembers: [...this.state.selectedMembers, member],
});
}
private handleUnselectMember(member: ContactType) {
this.setState({
selectedMembers: this.state.selectedMembers.filter(selectedMember => {
return selectedMember !== member;
}),
});
}
private onGroupNameChanged(event: any) {
this.setState({
groupName: event,
});
}
}

@ -3,10 +3,12 @@ import classNames from 'classnames';
interface Props {
placeholder?: string;
value?: string;
text?: string;
editable?: boolean;
onChange?: any;
onPressEnter?: any;
maxLength?: number;
}
export class SessionIdEditable extends React.PureComponent<Props> {
@ -26,7 +28,7 @@ export class SessionIdEditable extends React.PureComponent<Props> {
}
public render() {
const { placeholder, editable, text } = this.props;
const { placeholder, editable, text, value, maxLength } = this.props;
return (
<div
@ -43,7 +45,8 @@ export class SessionIdEditable extends React.PureComponent<Props> {
spellCheck={false}
onKeyDown={this.handleKeyDown}
onChange={this.handleChange}
value={text}
value={value || text}
maxLength={maxLength}
/>
</div>
);

@ -0,0 +1,128 @@
import React from 'react';
import classNames from 'classnames';
import { Avatar } from '../Avatar';
import { SessionIcon, SessionIconSize, SessionIconType } from './icon';
export interface ContactType {
id: string;
selected: boolean;
authorProfileName: string;
authorPhoneNumber: string;
authorName: string;
authorColor: any;
authorAvatarPath: string;
checkmarked: boolean;
existingMember: boolean;
}
interface Props {
member: ContactType;
isSelected: boolean;
onSelect?: any;
onUnselect?: any;
}
interface State {
isSelected: boolean;
}
export class SessionMemberListItem extends React.Component<Props, State> {
public static defaultProps = {
isSelected: false,
};
constructor(props: any) {
super(props);
this.state = {
isSelected: this.props.isSelected,
};
this.handleSelectionAction = this.handleSelectionAction.bind(this);
this.selectMember = this.selectMember.bind(this);
this.unselectMember = this.unselectMember.bind(this);
this.renderAvatar = this.renderAvatar.bind(this);
}
public render() {
const { isSelected } = this.state;
const name = this.props.member.authorProfileName;
const pubkey = this.props.member.authorPhoneNumber;
const shortPubkey = window.shortenPubkey(pubkey);
return (
<div
className={classNames('session-member-item', isSelected && 'selected')}
onClick={this.handleSelectionAction}
role="button"
>
<div className="session-member-item__info">
<span className="session-member-item__avatar">
{this.renderAvatar()}
</span>
<span className="session-member-item__name">{name}</span>
<span className="session-member-item__pubkey">{shortPubkey}</span>
</div>
<span
className={classNames(
'session-member-item__checkmark',
isSelected && 'selected'
)}
>
<SessionIcon
iconType={SessionIconType.Check}
iconSize={SessionIconSize.Medium}
iconColor={'#00f782'}
/>
</span>
</div>
);
}
private renderAvatar() {
return (
<Avatar
avatarPath={this.props.member.authorAvatarPath}
color={this.props.member.authorColor}
conversationType="direct"
i18n={window.i18n}
name={this.props.member.authorName}
phoneNumber={this.props.member.authorPhoneNumber}
profileName={this.props.member.authorProfileName}
size={28}
/>
);
}
private handleSelectionAction() {
if (this.state.isSelected) {
this.unselectMember();
return;
}
this.selectMember();
}
private selectMember() {
this.setState({
isSelected: true,
});
if (this.props.onSelect) {
this.props.onSelect(this.props.member);
}
}
private unselectMember() {
this.setState({
isSelected: false,
});
if (this.props.onUnselect) {
this.props.onUnselect(this.props.member);
}
}
}

@ -9,7 +9,7 @@ import {
} from '../SessionButton';
export enum SessionSettingCategory {
General = 'general',
Appearance = 'appearance',
Account = 'account',
Privacy = 'privacy',
Permissions = 'permissions',
@ -326,7 +326,7 @@ export class SettingsView extends React.Component<SettingsViewProps, State> {
hidden: true,
comparisonValue: 'light',
type: SessionSettingType.Toggle,
category: SessionSettingCategory.General,
category: SessionSettingCategory.Appearance,
setFn: window.toggleTheme,
content: undefined,
onClick: undefined,
@ -338,7 +338,7 @@ export class SettingsView extends React.Component<SettingsViewProps, State> {
description: window.i18n('hideMenuBarDescription'),
hidden: !Settings.isHideMenuBarSupported(),
type: SessionSettingType.Toggle,
category: SessionSettingCategory.General,
category: SessionSettingCategory.Appearance,
setFn: window.toggleMenuBar,
content: { defaultValue: true },
comparisonValue: undefined,
@ -351,7 +351,7 @@ export class SettingsView extends React.Component<SettingsViewProps, State> {
description: window.i18n('spellCheckDescription'),
hidden: false,
type: SessionSettingType.Toggle,
category: SessionSettingCategory.General,
category: SessionSettingCategory.Appearance,
setFn: window.toggleSpellCheck,
content: undefined,
comparisonValue: undefined,
@ -364,7 +364,7 @@ export class SettingsView extends React.Component<SettingsViewProps, State> {
description: window.i18n('linkPreviewDescription'),
hidden: false,
type: SessionSettingType.Toggle,
category: SessionSettingCategory.General,
category: SessionSettingCategory.Appearance,
setFn: window.toggleLinkPreview,
content: undefined,
comparisonValue: undefined,

3
ts/global.d.ts vendored

@ -6,12 +6,15 @@ interface Window {
deleteAllData: any;
clearLocalData: any;
getAccountManager: any;
getConversations: any;
getFriendsFromContacts: any;
mnemonic: any;
clipboard: any;
attemptConnection: any;
passwordUtil: any;
userConfig: any;
shortenPubkey: any;
dcodeIO: any;
libsignal: any;

Loading…
Cancel
Save