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" "description": "Shown in the conversation history when someone updates the group"
}, },
"titleIsNow": { "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", "description": "Shown in the conversation history when someone changes the title of the group",
"placeholders": { "placeholders": {
"name": { "name": {

@ -1504,7 +1504,7 @@
"description": "Shown in the conversation history when someone updates the group" "description": "Shown in the conversation history when someone updates the group"
}, },
"titleIsNow": { "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", "description": "Shown in the conversation history when someone changes the title of the group",
"placeholders": { "placeholders": {
"name": { "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" "Placeholder text in the message entry field when it is disabled while we are waiting for a friend request approval"
}, },
"sendMessageFriendRequest": { "sendMessageFriendRequest": {
"message": "Hi there! This is ...!", "message": "Send your first message",
"description": "description":
"Placeholder text in the message entry field when it is the first message sent to that contact" "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" "description": "Header for notification settings"
}, },
"readReceiptSettingDescription": { "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" "description": "Description of the read receipts setting"
}, },
"readReceiptSettingTitle": { "readReceiptSettingTitle": {
@ -1993,7 +1994,7 @@
"Shown in the conversation history when someone updates the group" "Shown in the conversation history when someone updates the group"
}, },
"titleIsNow": { "titleIsNow": {
"message": "Title is now '$name$'", "message": "Group name has been set to '$name$'.",
"description": "description":
"Shown in the conversation history when someone changes the title of the group", "Shown in the conversation history when someone changes the title of the group",
"placeholders": { "placeholders": {
@ -2164,6 +2165,9 @@
"description": "description":
"Title shown to the user to confirm they want to leave the group" "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": { "copiedPublicKey": {
"message": "Session ID copied", "message": "Session ID copied",
"description": "A toast message telling the user that the key was copied" "description": "A toast message telling the user that the key was copied"
@ -2610,6 +2614,9 @@
"message": "Complete Sign Up" "message": "Complete Sign Up"
}, },
"compose": { "compose": {
"message": "Compose"
},
"newSession": {
"message": "New Session" "message": "New Session"
}, },
"searchForAKeyPhrase": { "searchForAKeyPhrase": {
@ -2655,11 +2662,11 @@
"decline": { "decline": {
"message": "Decline" "message": "Decline"
}, },
"generalSettingsTitle": { "appearanceSettingsTitle": {
"message": "General" "message": "Appearance"
}, },
"generalSettingsDescription": { "appearanceSettingsDescription": {
"message": "General settings and configuration" "message": "Appearance and interface options"
}, },
"accountSettingsTitle": { "accountSettingsTitle": {
"message": "Account" "message": "Account"
@ -2683,13 +2690,13 @@
"message": "Notifications" "message": "Notifications"
}, },
"notificationSettingsDescription": { "notificationSettingsDescription": {
"message": "Choose what you're notified about" "message": "Configure notification options"
}, },
"devicesSettingsTitle": { "devicesSettingsTitle": {
"message": "Devices" "message": "Devices"
}, },
"devicesSettingsDescription": { "devicesSettingsDescription": {
"message": "Manage linked devices" "message": "Manage your linked devices"
}, },
"mnemonicEmpty": { "mnemonicEmpty": {
"message": "Seed is mandatory" "message": "Seed is mandatory"
@ -2718,11 +2725,33 @@
"addChannel": { "addChannel": {
"message": "Join Open Group" "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": { "enterChannelURL": {
"message": "Enter Open Group URL" "message": "Enter Open Group URL"
}, },
"channelUrlPlaceholder": { "channelUrlPlaceholder": {
"message": "https://chat.lokinet.org" "message": "chat.getsession.org"
}, },
"addChannelDescription": { "addChannelDescription": {
"message": "Enter an open group URL." "message": "Enter an open group URL."

@ -1368,7 +1368,7 @@
"description": "Shown in the conversation history when someone updates the group" "description": "Shown in the conversation history when someone updates the group"
}, },
"titleIsNow": { "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", "description": "Shown in the conversation history when someone changes the title of the group",
"placeholders": { "placeholders": {
"name": { "name": {

@ -1504,7 +1504,7 @@
"description": "Shown in the conversation history when someone updates the group" "description": "Shown in the conversation history when someone updates the group"
}, },
"titleIsNow": { "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", "description": "Shown in the conversation history when someone changes the title of the group",
"placeholders": { "placeholders": {
"name": { "name": {

@ -1504,7 +1504,7 @@
"description": "Shown in the conversation history when someone updates the group" "description": "Shown in the conversation history when someone updates the group"
}, },
"titleIsNow": { "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", "description": "Shown in the conversation history when someone changes the title of the group",
"placeholders": { "placeholders": {
"name": { "name": {

@ -1504,7 +1504,7 @@
"description": "Shown in the conversation history when someone updates the group" "description": "Shown in the conversation history when someone updates the group"
}, },
"titleIsNow": { "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", "description": "Shown in the conversation history when someone changes the title of the group",
"placeholders": { "placeholders": {
"name": { "name": {

@ -1504,7 +1504,7 @@
"description": "Shown in the conversation history when someone updates the group" "description": "Shown in the conversation history when someone updates the group"
}, },
"titleIsNow": { "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", "description": "Shown in the conversation history when someone changes the title of the group",
"placeholders": { "placeholders": {
"name": { "name": {

@ -1504,7 +1504,7 @@
"description": "Shown in the conversation history when someone updates the group" "description": "Shown in the conversation history when someone updates the group"
}, },
"titleIsNow": { "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", "description": "Shown in the conversation history when someone changes the title of the group",
"placeholders": { "placeholders": {
"name": { "name": {

@ -1504,7 +1504,7 @@
"description": "Shown in the conversation history when someone updates the group" "description": "Shown in the conversation history when someone updates the group"
}, },
"titleIsNow": { "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", "description": "Shown in the conversation history when someone changes the title of the group",
"placeholders": { "placeholders": {
"name": { "name": {

@ -1504,7 +1504,7 @@
"description": "Shown in the conversation history when someone updates the group" "description": "Shown in the conversation history when someone updates the group"
}, },
"titleIsNow": { "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", "description": "Shown in the conversation history when someone changes the title of the group",
"placeholders": { "placeholders": {
"name": { "name": {

@ -1504,7 +1504,7 @@
"description": "Shown in the conversation history when someone updates the group" "description": "Shown in the conversation history when someone updates the group"
}, },
"titleIsNow": { "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", "description": "Shown in the conversation history when someone changes the title of the group",
"placeholders": { "placeholders": {
"name": { "name": {

@ -902,7 +902,7 @@ async function updateToLokiSchemaVersion1(currentVersion, instance) {
rssFeed: 'https://loki.network/feed/', rssFeed: 'https://loki.network/feed/',
closable: true, closable: true,
name: 'Loki.network News', name: 'Loki.network News',
profileAvatar: 'images/loki/session_icon.png', profileAvatar: 'images/session/session_chat_icon.png',
}; };
const updatesRssFeedData = { const updatesRssFeedData = {
@ -911,7 +911,7 @@ async function updateToLokiSchemaVersion1(currentVersion, instance) {
rssFeed: 'https://loki.network/category/messenger-updates/feed/', rssFeed: 'https://loki.network/category/messenger-updates/feed/',
closable: false, closable: false,
name: 'Messenger updates', name: 'Messenger updates',
profileAvatar: 'images/loki/session_icon.png', profileAvatar: 'images/session/session_chat_icon.png',
}; };
const autoJoinLokiChats = false; 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", "-----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, "import": false,
"serverTrustRoot": "BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx", "serverTrustRoot": "BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx",
"defaultPublicChatServer": "https://chat.lokinet.org/", "defaultPublicChatServer": "https://chat.getsession.org",
"defaultFileServer": "https://file.lokinet.org" "defaultFileServer": "https://file.getsession.org"
} }

@ -121,7 +121,6 @@
'x.svg', 'x.svg',
'x_white.svg', 'x_white.svg',
'icon-paste.svg', 'icon-paste.svg',
'loki/loki_icon_text.png',
'loki/session_icon_128.png', 'loki/session_icon_128.png',
]); ]);
@ -988,6 +987,18 @@
return toastID; 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 >> // Get memberlist. This function is not accurate >>
// window.getMemberList = window.lokiPublicChatAPI.getListOfMembers(); // window.getMemberList = window.lokiPublicChatAPI.getListOfMembers();

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

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

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

@ -19,7 +19,7 @@
const messages = ['Updated the group.']; const messages = ['Updated the group.'];
if (this.model.name) { 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) { if (this.model.joined) {
messages.push(`${this.model.joined.join(', ')} joined the group`); messages.push(`${this.model.joined.join(', ')} joined the group`);

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

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

@ -1235,11 +1235,17 @@
} }
.module-group-notification__change { .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 { .module-group-notification__contact {
font-weight: 300; font-family: 'SF Pro Text';
font-weight: bold;
} }
// Module: Reset Session Notification // Module: Reset Session Notification

@ -150,6 +150,11 @@ div.spacer-lg {
width: 100%; width: 100%;
} }
input,
textarea {
caret-color: $session-color-green !important;
}
@mixin text-highlight($color) { @mixin text-highlight($color) {
background-color: $color; background-color: $color;
padding: $session-margin-xs $session-margin-sm; padding: $session-margin-xs $session-margin-sm;
@ -269,7 +274,7 @@ $session_message-container-border-radius: 5px;
&.brand { &.brand {
color: $session-color-white; color: $session-color-white;
&:hover { &:not(.disabled):hover {
filter: brightness(90%); filter: brightness(90%);
} }
@ -352,12 +357,12 @@ $session_message-container-border-radius: 5px;
min-width: 165px; min-width: 165px;
height: 45px; height: 45px;
line-height: 40px; line-height: 40px;
padding: 0; padding: 0px $session-margin-lg;
font-size: $session-font-md; font-size: $session-font-md;
font-family: $session-font-family; font-family: $session-font-family;
border-radius: 500px; border-radius: 500px;
&:hover { &:not(.disabled):hover {
color: $session-color-white; color: $session-color-white;
border-color: $session-color-white; border-color: $session-color-white;
} }
@ -559,6 +564,11 @@ label {
position: relative; position: relative;
} }
.module-left-pane-overlay {
h3 {
margin-bottom: 6px;
}
}
.message-selection-overlay { .message-selection-overlay {
display: none; display: none;
position: absolute; position: absolute;
@ -1387,6 +1397,7 @@ input {
background-color: $session-shade-4; background-color: $session-shade-4;
border: $session-separator-element-border; border: $session-separator-element-border;
display: flex; display: flex;
align-items: center;
transition: $session-transition-duration; transition: $session-transition-duration;
.module-avatar, .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 { &__header {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
margin: 15px 7px 14px 0px; padding: 15px 7px 14px 0px;
height: 33px; height: 63px;
@at-root .light-theme #{&} { @at-root .light-theme #{&} {
background-color: $session-color-white; background-color: $session-color-white;
@ -271,8 +271,8 @@ $session-compose-margin: 20px;
position: relative; position: relative;
height: 1px; height: 1px;
opacity: 0.3; opacity: 0.3;
margin-top: -10px; margin-top: 2px;
margin-bottom: 50px; margin-bottom: 40px;
} }
.exit { .exit {
@ -291,7 +291,8 @@ $session-compose-margin: 20px;
} }
.session-description-long { .session-description-long {
font-size: 13px; font-size: $session-font-sm;
line-height: $session-font-h3;
margin: 0px 20px; margin: 0px 20px;
font-family: 'SF Pro Display'; font-family: 'SF Pro Display';
} }

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

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

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

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

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

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

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

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

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

@ -22,6 +22,7 @@ import { cleanSearchTerm } from '../../util/cleanSearchTerm';
import { SessionSearchInput } from './SessionSearchInput'; import { SessionSearchInput } from './SessionSearchInput';
import { SessionClosableOverlay } from './SessionClosableOverlay'; import { SessionClosableOverlay } from './SessionClosableOverlay';
import { MainViewController } from '../MainViewController'; import { MainViewController } from '../MainViewController';
import { ContactType } from './SessionMemberListItem';
export interface Props { export interface Props {
searchTerm: string; searchTerm: string;
@ -37,11 +38,17 @@ export interface Props {
clearSearch: () => void; clearSearch: () => void;
} }
export enum SessionGroupType {
Open = 'open-group',
Closed = 'closed-group',
}
interface State { interface State {
showAddChannelView: boolean;
channelUrlPasted: string; channelUrlPasted: string;
loading: boolean; loading: boolean;
connectSuccess: 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> { export class LeftPaneChannelSection extends React.Component<Props, State> {
@ -51,10 +58,10 @@ export class LeftPaneChannelSection extends React.Component<Props, State> {
public constructor(props: Props) { public constructor(props: Props) {
super(props); super(props);
this.state = { this.state = {
showAddChannelView: false,
channelUrlPasted: '', channelUrlPasted: '',
loading: false, loading: false,
connectSuccess: false, connectSuccess: false,
groupAddType: undefined,
}; };
this.handleOnPasteUrl = this.handleOnPasteUrl.bind(this); this.handleOnPasteUrl = this.handleOnPasteUrl.bind(this);
@ -181,8 +188,8 @@ export class LeftPaneChannelSection extends React.Component<Props, State> {
return ( return (
<div className="session-left-pane-section-content"> <div className="session-left-pane-section-content">
{this.renderHeader()} {this.renderHeader()}
{this.state.showAddChannelView {this.state.groupAddType
? this.renderClosableOverlay() ? this.renderClosableOverlay(this.state.groupAddType)
: this.renderGroups()} : this.renderGroups()}
</div> </div>
); );
@ -247,32 +254,73 @@ export class LeftPaneChannelSection extends React.Component<Props, State> {
} }
} }
private handleToggleOverlay() { private handleToggleOverlay(groupType?: SessionGroupType) {
this.setState(prevState => ({ // If no groupType, return to default view.
showAddChannelView: !prevState.showAddChannelView, // 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 { searchTerm } = this.props;
const { loading } = this.state; const { loading } = this.state;
return ( const openGroupElement = (
<SessionClosableOverlay <SessionClosableOverlay
overlayMode="channel" overlayMode={SessionGroupType.Open}
onChangeSessionID={this.handleOnPasteUrl} onChangeSessionID={this.handleOnPasteUrl}
onCloseClick={this.handleToggleOverlay} onCloseClick={() => {
this.handleToggleOverlay(undefined);
}}
onButtonClick={this.handleJoinChannelButtonClick} onButtonClick={this.handleJoinChannelButtonClick}
searchTerm={searchTerm} searchTerm={searchTerm}
updateSearch={this.updateSearchBound} updateSearch={this.updateSearchBound}
showSpinner={loading} 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 { private renderBottomButtons(): JSX.Element {
const edit = window.i18n('edit'); const edit = window.i18n('edit');
const addChannel = window.i18n('addChannel'); const joinOpenGroup = window.i18n('joinOpenGroup');
const createClosedGroup = window.i18n('createClosedGroup');
const showEditButton = false; const showEditButton = false;
return ( return (
@ -284,11 +332,22 @@ export class LeftPaneChannelSection extends React.Component<Props, State> {
buttonColor={SessionButtonColor.White} buttonColor={SessionButtonColor.White}
/> />
)} )}
<SessionButton <SessionButton
text={addChannel} text={joinOpenGroup}
buttonType={SessionButtonType.SquareOutline} buttonType={SessionButtonType.SquareOutline}
buttonColor={SessionButtonColor.Green} 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> </div>
); );
@ -327,7 +386,33 @@ export class LeftPaneChannelSection extends React.Component<Props, State> {
return false; 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; return true;
} }

@ -140,7 +140,7 @@ export class LeftPaneContactSection extends React.Component<Props, State> {
style, style,
}: RowRendererParamsType): JSX.Element | undefined => { }: RowRendererParamsType): JSX.Element | undefined => {
const { sentFriendsRequest } = this.props; const { sentFriendsRequest } = this.props;
const friends = this.getCurrentFriends(); const friends = window.getFriendsFromContacts(this.props.friends);
const combined = [...sentFriendsRequest, ...friends]; const combined = [...sentFriendsRequest, ...friends];
const item = combined[index]; 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() { private handleToggleOverlay() {
this.setState((prevState: { showAddContactView: boolean }) => ({ this.setState((prevState: { showAddContactView: boolean }) => ({
showAddContactView: !prevState.showAddContactView, showAddContactView: !prevState.showAddContactView,
@ -334,8 +321,8 @@ export class LeftPaneContactSection extends React.Component<Props, State> {
private renderList() { private renderList() {
const { sentFriendsRequest } = this.props; const { sentFriendsRequest } = this.props;
const friends = this.getCurrentFriends(); const friends = window.getFriendsFromContacts(this.props.friends);
const length = sentFriendsRequest.length + friends.length; const length = Number(sentFriendsRequest.length) + Number(friends.length);
const combined = [...sentFriendsRequest, ...friends]; const combined = [...sentFriendsRequest, ...friends];
const list = ( const list = (

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

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

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

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

@ -3,31 +3,46 @@ import React from 'react';
import { SessionIconButton, SessionIconSize, SessionIconType } from './icon'; import { SessionIconButton, SessionIconSize, SessionIconType } from './icon';
import { SessionIdEditable } from './SessionIdEditable'; import { SessionIdEditable } from './SessionIdEditable';
import { UserSearchDropdown } from './UserSearchDropdown'; import { UserSearchDropdown } from './UserSearchDropdown';
import { ContactType, SessionMemberListItem } from './SessionMemberListItem';
import { ConversationType } from '../../state/ducks/conversations';
import { import {
SessionButton, SessionButton,
SessionButtonColor, SessionButtonColor,
SessionButtonType, SessionButtonType,
} from './SessionButton'; } from './SessionButton';
import { SessionSpinner } from './SessionSpinner'; import { SessionSpinner } from './SessionSpinner';
import { SessionGroupType } from './LeftPaneChannelSection';
interface Props { interface Props {
overlayMode: 'message' | 'contact' | 'channel'; overlayMode: 'message' | 'contact' | SessionGroupType;
onChangeSessionID: any; onChangeSessionID: any;
onCloseClick: any; onCloseClick: any;
onButtonClick: any; onButtonClick: any;
contacts?: Array<ConversationType>;
searchTerm?: string; searchTerm?: string;
searchResults?: any; searchResults?: any;
updateSearch?: any; updateSearch?: any;
showSpinner?: boolean; 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>; private readonly inputRef: React.RefObject<SessionIdEditable>;
public constructor(props: Props) { public constructor(props: Props) {
super(props); super(props);
this.state = {
groupName: '',
selectedMembers: [],
};
this.inputRef = React.createRef(); this.inputRef = React.createRef();
this.onGroupNameChanged = this.onGroupNameChanged.bind(this);
} }
public componentDidMount() { 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 { public render(): JSX.Element {
const { const {
overlayMode, overlayMode,
@ -50,7 +98,9 @@ export class SessionClosableOverlay extends React.Component<Props> {
const isAddContactView = overlayMode === 'contact'; const isAddContactView = overlayMode === 'contact';
const isMessageView = overlayMode === 'message'; const isMessageView = overlayMode === 'message';
// const isChannelView = overlayMode === 'channel';
const isOpenGroupView = overlayMode === SessionGroupType.Open;
const isClosedGroupView = overlayMode === SessionGroupType.Closed;
let title; let title;
let buttonText; let buttonText;
@ -59,7 +109,7 @@ export class SessionClosableOverlay extends React.Component<Props> {
let placeholder; let placeholder;
switch (overlayMode) { switch (overlayMode) {
case 'message': case 'message':
title = window.i18n('enterRecipient'); title = window.i18n('newSession');
buttonText = window.i18n('next'); buttonText = window.i18n('next');
descriptionLong = window.i18n('usersCanShareTheir...'); descriptionLong = window.i18n('usersCanShareTheir...');
subtitle = window.i18n('enterSessionID'); subtitle = window.i18n('enterSessionID');
@ -72,17 +122,31 @@ export class SessionClosableOverlay extends React.Component<Props> {
subtitle = window.i18n('enterSessionID'); subtitle = window.i18n('enterSessionID');
placeholder = window.i18n('pasteSessionIDRecipient'); placeholder = window.i18n('pasteSessionIDRecipient');
break; break;
case 'channel': case 'open-group':
default:
title = window.i18n('addChannel'); title = window.i18n('addChannel');
buttonText = window.i18n('joinChannel'); buttonText = window.i18n('joinChannel');
descriptionLong = window.i18n('addChannelDescription'); descriptionLong = window.i18n('addChannelDescription');
subtitle = window.i18n('enterChannelURL'); subtitle = window.i18n('enterChannelURL');
placeholder = window.i18n('channelUrlPlaceholder'); 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 ourSessionID = window.textsecure.storage.user.getNumber();
const contacts = this.getContacts();
const noContactsForClosedGroup =
overlayMode === SessionGroupType.Closed && contacts.length === 0;
return ( return (
<div className="module-left-pane-overlay"> <div className="module-left-pane-overlay">
<div className="exit"> <div className="exit">
@ -92,19 +156,61 @@ export class SessionClosableOverlay extends React.Component<Props> {
onClick={onCloseClick} onClick={onCloseClick}
/> />
</div> </div>
<div className="spacer-md" />
<h2>{title}</h2> <h2>{title}</h2>
<h3> <h3>
{subtitle} {subtitle}
<hr className="green-border" /> <hr className="green-border" />
</h3> </h3>
<hr className="white-border" /> <hr className="white-border" />
<SessionIdEditable
ref={this.inputRef} {isOpenGroupView || isClosedGroupView ? (
editable={true} <div className="create-group-name-input">
placeholder={placeholder} <SessionIdEditable
onChange={onChangeSessionID} 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 />} {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> <div className="session-description-long">{descriptionLong}</div>
{isMessageView && <h4>{window.i18n('or')}</h4>} {isMessageView && <h4>{window.i18n('or')}</h4>}
@ -130,13 +236,58 @@ export class SessionClosableOverlay extends React.Component<Props> {
text={ourSessionID} text={ourSessionID}
/> />
)} )}
<SessionButton <SessionButton
buttonColor={SessionButtonColor.Green} buttonColor={SessionButtonColor.Green}
buttonType={SessionButtonType.BrandOutline} buttonType={SessionButtonType.BrandOutline}
text={buttonText} text={buttonText}
onClick={onButtonClick} disabled={noContactsForClosedGroup}
onClick={() => onButtonClick(groupName, selectedMembers)}
/> />
</div> </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 { interface Props {
placeholder?: string; placeholder?: string;
value?: string;
text?: string; text?: string;
editable?: boolean; editable?: boolean;
onChange?: any; onChange?: any;
onPressEnter?: any; onPressEnter?: any;
maxLength?: number;
} }
export class SessionIdEditable extends React.PureComponent<Props> { export class SessionIdEditable extends React.PureComponent<Props> {
@ -26,7 +28,7 @@ export class SessionIdEditable extends React.PureComponent<Props> {
} }
public render() { public render() {
const { placeholder, editable, text } = this.props; const { placeholder, editable, text, value, maxLength } = this.props;
return ( return (
<div <div
@ -43,7 +45,8 @@ export class SessionIdEditable extends React.PureComponent<Props> {
spellCheck={false} spellCheck={false}
onKeyDown={this.handleKeyDown} onKeyDown={this.handleKeyDown}
onChange={this.handleChange} onChange={this.handleChange}
value={text} value={value || text}
maxLength={maxLength}
/> />
</div> </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'; } from '../SessionButton';
export enum SessionSettingCategory { export enum SessionSettingCategory {
General = 'general', Appearance = 'appearance',
Account = 'account', Account = 'account',
Privacy = 'privacy', Privacy = 'privacy',
Permissions = 'permissions', Permissions = 'permissions',
@ -326,7 +326,7 @@ export class SettingsView extends React.Component<SettingsViewProps, State> {
hidden: true, hidden: true,
comparisonValue: 'light', comparisonValue: 'light',
type: SessionSettingType.Toggle, type: SessionSettingType.Toggle,
category: SessionSettingCategory.General, category: SessionSettingCategory.Appearance,
setFn: window.toggleTheme, setFn: window.toggleTheme,
content: undefined, content: undefined,
onClick: undefined, onClick: undefined,
@ -338,7 +338,7 @@ export class SettingsView extends React.Component<SettingsViewProps, State> {
description: window.i18n('hideMenuBarDescription'), description: window.i18n('hideMenuBarDescription'),
hidden: !Settings.isHideMenuBarSupported(), hidden: !Settings.isHideMenuBarSupported(),
type: SessionSettingType.Toggle, type: SessionSettingType.Toggle,
category: SessionSettingCategory.General, category: SessionSettingCategory.Appearance,
setFn: window.toggleMenuBar, setFn: window.toggleMenuBar,
content: { defaultValue: true }, content: { defaultValue: true },
comparisonValue: undefined, comparisonValue: undefined,
@ -351,7 +351,7 @@ export class SettingsView extends React.Component<SettingsViewProps, State> {
description: window.i18n('spellCheckDescription'), description: window.i18n('spellCheckDescription'),
hidden: false, hidden: false,
type: SessionSettingType.Toggle, type: SessionSettingType.Toggle,
category: SessionSettingCategory.General, category: SessionSettingCategory.Appearance,
setFn: window.toggleSpellCheck, setFn: window.toggleSpellCheck,
content: undefined, content: undefined,
comparisonValue: undefined, comparisonValue: undefined,
@ -364,7 +364,7 @@ export class SettingsView extends React.Component<SettingsViewProps, State> {
description: window.i18n('linkPreviewDescription'), description: window.i18n('linkPreviewDescription'),
hidden: false, hidden: false,
type: SessionSettingType.Toggle, type: SessionSettingType.Toggle,
category: SessionSettingCategory.General, category: SessionSettingCategory.Appearance,
setFn: window.toggleLinkPreview, setFn: window.toggleLinkPreview,
content: undefined, content: undefined,
comparisonValue: undefined, comparisonValue: undefined,

3
ts/global.d.ts vendored

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

Loading…
Cancel
Save