Merge branch 'clearnet' of https://github.com/loki-project/session-desktop into clearnet

pull/1002/head
Vincent 5 years ago
commit 8152e98636

@ -63,7 +63,7 @@ module.exports = {
// high value as a buffer to let Prettier control the line length:
code: 999,
// We still want to limit comments as before:
comments: 90,
comments: 150,
ignoreUrls: true,
ignoreRegExpLiterals: true,
},

@ -3,11 +3,12 @@
## Summary
Session integrates directly with [Loki Service Nodes](https://lokidocs.com/ServiceNodes/SNOverview/), which are a set of distributed, decentralized and Sybil resistant nodes. Service Nodes act as servers which store messages offline, and a set of nodes which allow for onion routing functionality obfuscating users IP Addresses. For a full understanding of how Session works, read the [Session Whitepaper](https://getsession.org/whitepaper).
<br/><br/>
![DesktopSession](https://i.imgur.com/ZnHvYjo.jpg)
## Want to Contribute? Found a Bug or Have a feature request?
Please search for any [existing issues](https://github.com/loki-project/session-desktop/issues) that describe your bugs in order to avoid duplicate submissions. Submissions can be made by making a pull request to our development branch. If you don't know where to start contributing , try reading the Github issues page for ideas.
Please search for any [existing issues](https://github.com/loki-project/session-desktop/issues) that describe your bugs in order to avoid duplicate submissions. <br><br>Submissions can be made by making a pull request to our development branch. If you don't know where to start contributing, try reading the Github issues page for ideas.
## Build instruction
@ -15,10 +16,7 @@ Build instructions can be found in [BUILDING.md](BUILDING.md).
## License
Copyright 2011 Whisper Systems
Copyright 2013-2017 Open Whisper Systems
Copyright 2019-2020 The Loki Project
Licensed under the GPLv3: http://www.gnu.org/licenses/gpl-3.0.html
Copyright 2011 Whisper Systems<br/>
Copyright 2013-2017 Open Whisper Systems<br/>
Copyright 2019-2020 The Loki Project<br/>
Licensed under the GPLv3: http://www.gnu.org/licenses/gpl-3.0.html<br/>

@ -2204,12 +2204,16 @@
"message": "Edit Profile",
"description": "Button action that the user can click to edit their profile"
},
"editGroupName": {
"message": "Edit group name",
"description": "Button action that the user can click to edit a group name"
},
"createGroupDialogTitle": {
"message": "Creating a Private Group Chat",
"message": "Creating a Closed Group",
"description": "Title for the dialog box used to create a new private group"
},
"updateGroupDialogTitle": {
"message": "Updating a Private Group Chat",
"message": "Updating a Closed Group",
"description":
"Title for the dialog box used to update an existing private group"
},
@ -2529,6 +2533,9 @@
"noFriendsToAdd": {
"message": "No friends to add"
},
"noMembersInThisGroup": {
"message": "No other members in this group"
},
"noModeratorsToRemove": {
"message": "no moderators to remove"
},

@ -1135,9 +1135,14 @@
}
});
Whisper.events.on('updateGroup', async groupConvo => {
Whisper.events.on('updateGroupName', async groupConvo => {
if (appView) {
appView.showUpdateGroupDialog(groupConvo);
appView.showUpdateGroupNameDialog(groupConvo);
}
});
Whisper.events.on('updateGroupMembers', async groupConvo => {
if (appView) {
appView.showUpdateGroupMembersDialog(groupConvo);
}
});

@ -475,7 +475,6 @@ SecretSessionCipher.prototype = {
// private byte[] decrypt(UnidentifiedSenderMessageContent message)
_decryptWithUnidentifiedSenderMessage(message) {
const { SessionCipher } = this;
const signalProtocolStore = this.storage;
const sender = new libsignal.SignalProtocolAddress(
@ -485,12 +484,12 @@ SecretSessionCipher.prototype = {
switch (message.type) {
case CiphertextMessage.WHISPER_TYPE:
return new SessionCipher(
return new libloki.crypto.LokiSessionCipher(
signalProtocolStore,
sender
).decryptWhisperMessage(message.content);
case CiphertextMessage.PREKEY_TYPE:
return new SessionCipher(
return new libloki.crypto.LokiSessionCipher(
signalProtocolStore,
sender
).decryptPreKeyWhisperMessage(message.content);

@ -34,8 +34,8 @@ const {
ConversationHeader,
} = require('../../ts/components/conversation/ConversationHeader');
const {
SessionChannelSettings,
} = require('../../ts/components/session/SessionChannelSettings');
SessionGroupSettings,
} = require('../../ts/components/session/SessionGroupSettings');
const {
EmbeddedContact,
} = require('../../ts/components/conversation/EmbeddedContact');
@ -93,8 +93,11 @@ const {
} = require('../../ts/components/session/SessionRegistrationView');
const {
UpdateGroupDialog,
} = require('../../ts/components/conversation/UpdateGroupDialog');
UpdateGroupNameDialog,
} = require('../../ts/components/conversation/UpdateGroupNameDialog');
const {
UpdateGroupMembersDialog,
} = require('../../ts/components/conversation/UpdateGroupMembersDialog');
const {
InviteFriendsDialog,
} = require('../../ts/components/conversation/InviteFriendsDialog');
@ -278,7 +281,7 @@ exports.setup = (options = {}) => {
ContactListItem,
ContactName,
ConversationHeader,
SessionChannelSettings,
SessionGroupSettings,
SettingsView,
EmbeddedContact,
Emojify,
@ -293,7 +296,8 @@ exports.setup = (options = {}) => {
DevicePairingDialog,
SessionRegistrationView,
ConfirmDialog,
UpdateGroupDialog,
UpdateGroupNameDialog,
UpdateGroupMembersDialog,
InviteFriendsDialog,
AddModeratorsDialog,
RemoveModeratorsDialog,

@ -228,10 +228,15 @@
const dialog = new Whisper.CreateGroupDialogView();
this.el.append(dialog.el);
},
showUpdateGroupDialog(groupConvo) {
const dialog = new Whisper.UpdateGroupDialogView(groupConvo);
this.el.prepend(dialog.el);
showUpdateGroupNameDialog(groupConvo) {
const dialog = new Whisper.UpdateGroupNameDialogView(groupConvo);
this.el.append(dialog.el);
},
showUpdateGroupMembersDialog(groupConvo) {
const dialog = new Whisper.UpdateGroupMembersDialogView(groupConvo);
this.el.append(dialog.el);
},
showSessionRestoreConfirmation(options) {
const dialog = new Whisper.ConfirmSessionResetView(options);
this.el.append(dialog.el);

@ -244,11 +244,6 @@
onMoveToInbox: () => {
this.model.setArchived(false);
},
onUpdateGroup: () => {
window.Whisper.events.trigger('updateGroup', this.model);
},
onLeaveGroup: () => {
window.Whisper.events.trigger('leaveGroup', this.model);
},
@ -276,7 +271,8 @@
},
};
};
const getGroupSettingsProp = () => {
const getGroupSettingsProps = () => {
const ourPK = window.textsecure.storage.user.getNumber();
const members = this.model.get('members') || [];
return {
@ -288,6 +284,7 @@
avatarPath: this.model.getAvatarPath(),
isGroup: !this.model.isPrivate(),
isPublic: this.model.isPublic(),
isAdmin: this.model.get('groupAdmins').includes(ourPK),
isRss: this.model.isRss(),
memberCount: members.length,
@ -303,8 +300,11 @@
this.hideConversationRight();
},
onUpdateGroup: () => {
window.Whisper.events.trigger('updateGroup', this.model);
onUpdateGroupName: () => {
window.Whisper.events.trigger('updateGroupName', this.model);
},
onUpdateGroupMembers: () => {
window.Whisper.events.trigger('updateGroupMembers', this.model);
},
onLeaveGroup: () => {
@ -355,12 +355,15 @@
if (!this.groupSettings) {
this.groupSettings = new Whisper.ReactWrapperView({
className: 'group-settings',
Component: window.Signal.Components.SessionChannelSettings,
props: getGroupSettingsProp(this.model),
Component: window.Signal.Components.SessionGroupSettings,
props: getGroupSettingsProps(this.model),
});
this.$('.conversation-content-right').append(this.groupSettings.el);
this.updateGroupSettingsPanel = () =>
this.groupSettings.update(getGroupSettingsProps(this.model));
this.listenTo(this.model, 'change', this.updateGroupSettingsPanel);
} else {
this.groupSettings.update(getGroupSettingsProp(this.model));
this.groupSettings.update(getGroupSettingsProps(this.model));
}
this.showConversationRight();

@ -47,13 +47,13 @@
},
});
Whisper.UpdateGroupDialogView = Whisper.View.extend({
Whisper.UpdateGroupNameDialogView = Whisper.View.extend({
className: 'loki-dialog modal',
initialize(groupConvo) {
this.groupName = groupConvo.get('name');
this.conversation = groupConvo;
this.titleText = `${i18n('updateGroupDialogTitle')}: ${this.groupName}`;
this.titleText = i18n('updateGroupDialogTitle');
this.okText = i18n('ok');
this.cancelText = i18n('cancel');
this.close = this.close.bind(this);
@ -106,7 +106,90 @@
render() {
this.dialogView = new Whisper.ReactWrapperView({
className: 'create-group-dialog',
Component: window.Signal.Components.UpdateGroupDialog,
Component: window.Signal.Components.UpdateGroupNameDialog,
props: {
titleText: this.titleText,
groupName: this.groupName,
okText: this.okText,
isPublic: this.isPublic,
cancelText: this.cancelText,
existingMembers: this.existingMembers,
isAdmin: this.isAdmin,
onClose: this.close,
onSubmit: this.onSubmit,
},
});
this.$el.append(this.dialogView.el);
return this;
},
onSubmit(newGroupName, members) {
const groupId = this.conversation.get('id');
window.doUpdateGroup(groupId, newGroupName, members);
},
close() {
this.remove();
},
});
Whisper.UpdateGroupMembersDialogView = Whisper.View.extend({
className: 'loki-dialog modal',
initialize(groupConvo) {
this.groupName = groupConvo.get('name');
this.conversation = groupConvo;
this.titleText = i18n('updateGroupDialogTitle');
this.okText = i18n('ok');
this.cancelText = i18n('cancel');
this.close = this.close.bind(this);
this.onSubmit = this.onSubmit.bind(this);
this.isPublic = groupConvo.isPublic();
const ourPK = textsecure.storage.user.getNumber();
this.isAdmin = groupConvo.get('groupAdmins').includes(ourPK);
const convos = window.getConversations().models.filter(d => !!d);
let existingMembers = groupConvo.get('members') || [];
// Show a contact if they are our friend or if they are a member
const friendsAndMembers = convos.filter(
d => existingMembers.includes(d.id) && d.isPrivate() && !d.isMe()
);
this.friendsAndMembers = _.uniq(friendsAndMembers, true, d => d.id);
// at least make sure it's an array
if (!Array.isArray(existingMembers)) {
existingMembers = [];
}
this.existingMembers = existingMembers;
// public chat settings overrides
if (this.isPublic) {
// fix the title
this.titleText = `${i18n('updatePublicGroupDialogTitle')}: ${
this.groupName
}`;
// I'd much prefer to integrate mods with groupAdmins
// but lets discuss first...
this.isAdmin = groupConvo.isModerator(
window.storage.get('primaryDevicePubKey')
);
// zero out friendList for now
this.friendsAndMembers = [];
this.existingMembers = [];
}
this.$el.focus();
this.render();
},
render() {
this.dialogView = new Whisper.ReactWrapperView({
className: 'create-group-dialog',
Component: window.Signal.Components.UpdateGroupMembersDialog,
props: {
titleText: this.titleText,
groupName: this.groupName,
@ -124,12 +207,12 @@
this.$el.append(this.dialogView.el);
return this;
},
onSubmit(newGroupName, newMembers) {
onSubmit(groupName, newMembers) {
const ourPK = textsecure.storage.user.getNumber();
const allMembers = window.Lodash.concat(newMembers, [ourPK]);
const groupId = this.conversation.get('id');
window.doUpdateGroup(groupId, newGroupName, allMembers);
window.doUpdateGroup(groupId, groupName, allMembers);
},
close() {
this.remove();

@ -204,10 +204,10 @@
});
},
showFileSizeError() {
showFileSizeError(limit, units) {
window.pushToast({
title: i18n('fileSizeWarning'),
description: `Max size: ${this.model.limit} ${this.model.units}`,
description: `Max size: ${limit} ${units}`,
type: 'error',
id: 'fileSizeWarning',
});
@ -339,7 +339,7 @@
contentType,
file,
});
let limitKb = 1000000;
let limitKb = 10000;
const blobType =
file.type === 'image/gif' ? 'gif' : contentType.split('/')[0];
@ -348,19 +348,19 @@
limitKb = 6000;
break;
case 'gif':
limitKb = 25000;
limitKb = 10000;
break;
case 'audio':
limitKb = 100000;
limitKb = 10000;
break;
case 'video':
limitKb = 100000;
limitKb = 10000;
break;
default:
limitKb = 100000;
limitKb = 10000;
break;
}
if ((blob.size / 1024).toFixed(4) >= limitKb) {
if ((blob.file.size / 1024).toFixed(4) >= limitKb) {
const units = ['kB', 'MB', 'GB'];
let u = -1;
let limit = limitKb * 1000;
@ -368,7 +368,7 @@
limit /= 1000;
u += 1;
} while (limit >= 1000 && u < units.length - 1);
this.showFileSizeError();
this.showFileSizeError(limit, units[u]);
return;
}
} catch (error) {

@ -1,4 +1,4 @@
/* global Whisper */
/* global Whisper, _ */
// eslint-disable-next-line func-names
(function() {
@ -22,6 +22,8 @@
this.chatName = convo.get('name');
this.chatServer = convo.get('server');
this.channelId = convo.get('channelId');
this.isPublic = !!convo.cachedProps.isPublic;
this.convo = convo;
this.$el.focus();
this.render();
@ -45,14 +47,56 @@
this.remove();
},
submit(pubkeys) {
window.sendGroupInvitations(
{
address: this.chatServer,
name: this.chatName,
channelId: this.channelId,
},
pubkeys
);
// public group chats
if (this.isPublic) {
window.sendGroupInvitations(
{
address: this.chatServer,
name: this.chatName,
channelId: this.channelId,
},
pubkeys
);
} else {
// private group chats
const ourPK = window.textsecure.storage.user.getNumber();
let existingMembers = this.convo.get('members') || [];
// at least make sure it's an array
if (!Array.isArray(existingMembers)) {
existingMembers = [];
}
existingMembers = existingMembers.filter(d => !!d);
const newMembers = pubkeys.filter(d => !existingMembers.includes(d));
if (newMembers.length > 0) {
// Do not trigger an update if there is too many members
if (
newMembers.length + existingMembers.length >
window.SMALL_GROUP_SIZE_LIMIT
) {
const msg = `${window.i18n('maxGroupMembersError')} ${
window.SMALL_GROUP_SIZE_LIMIT
}`;
window.pushToast({
title: msg,
type: 'error',
id: 'tooManyMembers',
});
return;
}
const allMembers = window.Lodash.concat(existingMembers, newMembers, [
ourPK,
]);
const uniqMembers = _.uniq(allMembers, true, d => d);
const groupId = this.convo.get('id');
const groupName = this.convo.get('name');
window.doUpdateGroup(groupId, groupName, uniqMembers);
}
}
},
});
})();

@ -324,6 +324,154 @@
GRANT: 2,
});
/**
* A wrapper around Signal's SessionCipher.
* This handles specific session reset logic that we need.
*/
class LokiSessionCipher {
constructor(storage, protocolAddress) {
this.storage = storage;
this.protocolAddress = protocolAddress;
this.sessionCipher = new libsignal.SessionCipher(
storage,
protocolAddress
);
this.TYPE = Object.freeze({
MESSAGE: 1,
PREKEY: 2,
});
}
decryptWhisperMessage(buffer, encoding) {
return this._decryptMessage(this.TYPE.MESSAGE, buffer, encoding);
}
decryptPreKeyWhisperMessage(buffer, encoding) {
return this._decryptMessage(this.TYPE.PREKEY, buffer, encoding);
}
async _decryptMessage(type, buffer, encoding) {
// Capture active session
const activeSessionBaseKey = await this._getCurrentSessionBaseKey();
if (type === this.TYPE.PREKEY && !activeSessionBaseKey) {
const wrapped = dcodeIO.ByteBuffer.wrap(buffer);
await window.libloki.storage.verifyFriendRequestAcceptPreKey(
this.protocolAddress.getName(),
wrapped
);
}
const decryptFunction =
type === this.TYPE.PREKEY
? this.sessionCipher.decryptPreKeyWhisperMessage
: this.sessionCipher.decryptWhisperMessage;
const result = await decryptFunction(buffer, encoding);
// Handle session reset
// This needs to be done synchronously so that the next time we decrypt a message,
// we have the correct session
try {
await this._handleSessionResetIfNeeded(activeSessionBaseKey);
} catch (e) {
window.log.info('Failed to handle session reset: ', e);
}
return result;
}
async _handleSessionResetIfNeeded(previousSessionBaseKey) {
if (!previousSessionBaseKey) {
return;
}
let conversation;
try {
conversation = await window.ConversationController.getOrCreateAndWait(
this.protocolAddress.getName(),
'private'
);
} catch (e) {
window.log.info(
'Error getting conversation: ',
this.protocolAddress.getName()
);
return;
}
if (conversation.isSessionResetOngoing()) {
const currentSessionBaseKey = await this._getCurrentSessionBaseKey();
if (currentSessionBaseKey !== previousSessionBaseKey) {
if (conversation.isSessionResetReceived()) {
// The other user used an old session to contact us; wait for them to switch to a new one.
await this._restoreSession(previousSessionBaseKey);
} else {
// Our session reset was successful; we initiated one and got a new session back from the other user.
await this._deleteAllSessionExcept(currentSessionBaseKey);
await conversation.onNewSessionAdopted();
}
} else if (conversation.isSessionResetReceived()) {
// Our session reset was successful; we received a message with the same session from the other user.
await this._deleteAllSessionExcept(previousSessionBaseKey);
await conversation.onNewSessionAdopted();
}
}
}
async _getCurrentSessionBaseKey() {
const record = await this.sessionCipher.getRecord(
this.protocolAddress.toString()
);
if (!record) {
return null;
}
const openSession = record.getOpenSession();
if (!openSession) {
return null;
}
const { baseKey } = openSession.indexInfo;
return baseKey;
}
async _restoreSession(sessionBaseKey) {
const record = await this.sessionCipher.getRecord(
this.protocolAddress.toString()
);
if (!record) {
return;
}
record.archiveCurrentState();
const sessionToRestore = record.sessions[sessionBaseKey];
if (!sessionToRestore) {
throw new Error(`Cannot find session with base key ${sessionBaseKey}`);
}
record.promoteState(sessionToRestore);
record.updateSessionState(sessionToRestore);
await this.storage.storeSession(
this.protocolAddress.toString(),
record.serialize()
);
}
async _deleteAllSessionExcept(sessionBaseKey) {
const record = await this.sessionCipher.getRecord(
this.protocolAddress.toString()
);
if (!record) {
return;
}
const sessionToKeep = record.sessions[sessionBaseKey];
record.sessions = {};
record.updateSessionState(sessionToKeep);
await this.storage.storeSession(
this.protocolAddress.toString(),
record.serialize()
);
}
}
window.libloki.crypto = {
DHEncrypt,
DHDecrypt,
@ -336,6 +484,7 @@
verifyAuthorisation,
validateAuthorisation,
PairingType,
LokiSessionCipher,
// for testing
_LokiSnodeChannel: LokiSnodeChannel,
_decodeSnodeAddressToPubKey: decodeSnodeAddressToPubKey,

@ -667,58 +667,29 @@ MessageReceiver.prototype.extend({
async decrypt(envelope, ciphertext) {
let promise;
// We don't have source at this point yet (with sealed sender)
// This needs a massive cleanup!
const address = new libsignal.SignalProtocolAddress(
envelope.source,
envelope.sourceDevice
);
const ourNumber = textsecure.storage.user.getNumber();
const number = address.toString().split('.')[0];
const options = {};
// No limit on message keys if we're communicating with our other devices
if (ourNumber === number) {
options.messageKeysLimit = false;
}
// Will become obsolete
const sessionCipher = new libsignal.SessionCipher(
textsecure.storage.protocol,
address,
options
);
const me = {
number: ourNumber,
deviceId: parseInt(textsecure.storage.user.getDeviceId(), 10),
};
// Will become obsolete
const getCurrentSessionBaseKey = async () => {
const record = await sessionCipher.getRecord(address.toString());
if (!record) {
return null;
}
const openSession = record.getOpenSession();
if (!openSession) {
return null;
}
const { baseKey } = openSession.indexInfo;
return baseKey;
};
// Envelope.source will be null on UNIDENTIFIED_SENDER
// Don't use it there!
const address = new libsignal.SignalProtocolAddress(
envelope.source,
envelope.sourceDevice
);
// Will become obsolete
const captureActiveSession = async () => {
this.activeSessionBaseKey = await getCurrentSessionBaseKey(sessionCipher);
};
const lokiSessionCipher = new libloki.crypto.LokiSessionCipher(
textsecure.storage.protocol,
address
);
switch (envelope.type) {
case textsecure.protobuf.Envelope.Type.CIPHERTEXT:
window.log.info('message from', this.getEnvelopeId(envelope));
promise = captureActiveSession()
.then(() => sessionCipher.decryptWhisperMessage(ciphertext))
promise = lokiSessionCipher
.decryptWhisperMessage(ciphertext)
.then(this.unpad);
break;
case textsecure.protobuf.Envelope.Type.FRIEND_REQUEST: {
@ -735,25 +706,11 @@ MessageReceiver.prototype.extend({
}
case textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE:
window.log.info('prekey message from', this.getEnvelopeId(envelope));
promise = captureActiveSession(sessionCipher).then(async () => {
if (!this.activeSessionBaseKey) {
try {
const buffer = dcodeIO.ByteBuffer.wrap(ciphertext);
await window.libloki.storage.verifyFriendRequestAcceptPreKey(
envelope.source,
buffer
);
} catch (e) {
await this.removeFromCache(envelope);
throw e;
}
}
return this.decryptPreKeyWhisperMessage(
ciphertext,
sessionCipher,
address
);
});
promise = this.decryptPreKeyWhisperMessage(
ciphertext,
lokiSessionCipher,
address
);
break;
case textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER: {
window.log.info('received unidentified sender message');
@ -856,72 +813,6 @@ MessageReceiver.prototype.extend({
window.log.info('Error getting conversation: ', envelope.source);
}
// lint hates anything after // (so /// is no good)
// *** BEGIN: session reset ***
// we have address in scope from parent scope
// seems to be the same input parameters
// going to comment out due to lint complaints
/*
const address = new libsignal.SignalProtocolAddress(
envelope.source,
envelope.sourceDevice
);
*/
const restoreActiveSession = async () => {
const record = await sessionCipher.getRecord(address.toString());
if (!record) {
return;
}
record.archiveCurrentState();
// NOTE: activeSessionBaseKey will be undefined here...
const sessionToRestore = record.sessions[this.activeSessionBaseKey];
record.promoteState(sessionToRestore);
record.updateSessionState(sessionToRestore);
await textsecure.storage.protocol.storeSession(
address.toString(),
record.serialize()
);
};
const deleteAllSessionExcept = async sessionBaseKey => {
const record = await sessionCipher.getRecord(address.toString());
if (!record) {
return;
}
const sessionToKeep = record.sessions[sessionBaseKey];
record.sessions = {};
record.updateSessionState(sessionToKeep);
await textsecure.storage.protocol.storeSession(
address.toString(),
record.serialize()
);
};
if (conversation.isSessionResetOngoing()) {
const currentSessionBaseKey = await getCurrentSessionBaseKey(
sessionCipher
);
if (
this.activeSessionBaseKey &&
currentSessionBaseKey !== this.activeSessionBaseKey
) {
if (conversation.isSessionResetReceived()) {
await restoreActiveSession();
} else {
await deleteAllSessionExcept(currentSessionBaseKey);
await conversation.onNewSessionAdopted();
}
} else if (conversation.isSessionResetReceived()) {
await deleteAllSessionExcept(this.activeSessionBaseKey);
await conversation.onNewSessionAdopted();
}
}
// lint hates anything after // (so /// is no good)
// *** END ***
// Type here can actually be UNIDENTIFIED_SENDER even if
// the underlying message is FRIEND_REQUEST
if (
@ -1470,6 +1361,7 @@ MessageReceiver.prototype.extend({
content.preKeyBundleMessage
);
}
if (content.lokiAddressMessage) {
return this.handleLokiAddressMessage(
envelope,
@ -1833,7 +1725,7 @@ MessageReceiver.prototype.extend({
textsecure.storage.protocol,
address
);
builder.processPreKey(device);
await builder.processPreKey(device);
})
);
await conversation.onSessionResetReceived();

@ -391,6 +391,8 @@ OutgoingMessage.prototype = {
: null;
const isEndSession =
flags === textsecure.protobuf.DataMessage.Flags.END_SESSION;
const isSessionRequest =
flags === textsecure.protobuf.DataMessage.Flags.SESSION_REQUEST;
const signalCipher = new libsignal.SessionCipher(
textsecure.storage.protocol,
address
@ -485,6 +487,7 @@ OutgoingMessage.prototype = {
content,
pubKey: devicePubKey,
isFriendRequest: enableFallBackEncryption,
isSessionRequest,
};
})
)
@ -494,7 +497,12 @@ OutgoingMessage.prototype = {
if (!outgoingObject) {
return;
}
const destination = outgoingObject.pubKey;
const {
pubKey: destination,
ttl,
isFriendRequest,
isSessionRequest,
} = outgoingObject;
try {
const socketMessage = await this.wrapInWebsocketMessage(
outgoingObject
@ -503,9 +511,9 @@ OutgoingMessage.prototype = {
destination,
socketMessage,
this.timestamp,
outgoingObject.ttl
ttl
);
if (outgoingObject.isFriendRequest) {
if (!this.isGroup && isFriendRequest && !isSessionRequest) {
const conversation = ConversationController.get(destination);
if (conversation) {
// Redundant for primary device but marks secondary devices as pending

@ -1041,64 +1041,6 @@ MessageSender.prototype = {
silent,
options
).catch(logError('resetSession/sendToContact error:'));
/*
const deleteAllSessions = targetNumber =>
textsecure.storage.protocol.getDeviceIds(targetNumber).then(deviceIds =>
Promise.all(
deviceIds.map(deviceId => {
const address = new libsignal.SignalProtocolAddress(
targetNumber,
deviceId
);
window.log.info('deleting sessions for', address.toString());
const sessionCipher = new libsignal.SessionCipher(
textsecure.storage.protocol,
address
);
return sessionCipher.deleteAllSessionsForDevice();
})
)
);
const sendToContactPromise = deleteAllSessions(number)
.catch(logError('resetSession/deleteAllSessions1 error:'))
.then(() => {
window.log.info(
'finished closing local sessions, now sending to contact'
);
return this.sendIndividualProto(
number,
proto,
timestamp,
silent,
options
).catch(logError('resetSession/sendToContact error:'));
})
.then(() =>
deleteAllSessions(number).catch(
logError('resetSession/deleteAllSessions2 error:')
)
);
const myNumber = textsecure.storage.user.getNumber();
// We already sent the reset session to our other devices in the code above!
if (number === myNumber) {
return sendToContactPromise;
}
const buffer = proto.toArrayBuffer();
const sendSyncPromise = this.sendSyncMessage(
buffer,
timestamp,
number,
null,
[],
[],
options
).catch(logError('resetSession/sendSync error:'));
return Promise.all([sendToContact, sendSyncPromise]);
*/
},
async sendMessageToGroup(

@ -202,7 +202,7 @@
"webpack": "4.4.1"
},
"engines": {
"node": "10.13.0"
"node": "^10.13.0"
},
"build": {
"appId": "com.loki-project.messenger-desktop",

@ -802,6 +802,15 @@ label {
}
}
.create-group-dialog .session-modal__body {
display: flex;
flex-direction: column;
.friend-selection-list {
width: unset;
}
}
.session-confirm {
&-wrapper {
.session-modal__body .session-modal__centered {

@ -32,7 +32,6 @@ interface Props {
phoneNumber: string;
profileName?: string;
color: string;
avatarPath?: string;
isVerified: boolean;
@ -88,7 +87,6 @@ interface Props {
onCopyPublicKey: () => void;
onUpdateGroup: () => void;
onLeaveGroup: () => void;
onAddModerators: () => void;
onRemoveModerators: () => void;
@ -206,7 +204,6 @@ export class ConversationHeader extends React.Component<Props> {
public renderAvatar() {
const {
avatarPath,
color,
i18n,
isGroup,
isMe,
@ -223,7 +220,6 @@ export class ConversationHeader extends React.Component<Props> {
<span className="module-conversation-header__avatar">
<Avatar
avatarPath={avatarPath}
color={color}
conversationType={conversationType}
i18n={i18n}
noteToSelf={isMe}
@ -305,7 +301,6 @@ export class ConversationHeader extends React.Component<Props> {
onDeleteMessages,
onDeleteContact,
onCopyPublicKey,
onUpdateGroup,
onLeaveGroup,
onAddModerators,
onRemoveModerators,
@ -323,9 +318,6 @@ export class ConversationHeader extends React.Component<Props> {
<MenuItem onClick={onCopyPublicKey}>{copyIdLabel}</MenuItem>
) : null}
<MenuItem onClick={onDeleteMessages}>{i18n('deleteMessages')}</MenuItem>
{isPrivateGroup || amMod ? (
<MenuItem onClick={onUpdateGroup}>{i18n('updateGroup')}</MenuItem>
) : null}
{amMod ? (
<MenuItem onClick={onAddModerators}>{i18n('addModerators')}</MenuItem>
) : null}

@ -2,7 +2,8 @@ import React from 'react';
import classNames from 'classnames';
import { Contact, MemberList } from './MemberList';
import { SessionModal } from './../session/SessionModal';
import { SessionModal } from '../session/SessionModal';
import { SessionButton } from '../session/SessionButton';
interface Props {
titleText: string;
@ -21,12 +22,11 @@ interface Props {
interface State {
friendList: Array<Contact>;
groupName: string;
errorDisplayed: boolean;
errorMessage: string;
}
export class UpdateGroupDialog extends React.Component<Props, State> {
export class UpdateGroupMembersDialog extends React.Component<Props, State> {
constructor(props: any) {
super(props);
@ -57,7 +57,6 @@ export class UpdateGroupDialog extends React.Component<Props, State> {
this.state = {
friendList: friends,
groupName: this.props.groupName,
errorDisplayed: false,
errorMessage: 'placeholder',
};
@ -70,13 +69,7 @@ export class UpdateGroupDialog extends React.Component<Props, State> {
d => d.id
);
if (!this.state.groupName.trim()) {
this.onShowError(this.props.i18n('emptyGroupNameError'));
return;
}
this.props.onSubmit(this.state.groupName, members);
this.props.onSubmit(this.props.groupName, members);
this.closeDialog();
}
@ -111,25 +104,16 @@ export class UpdateGroupDialog extends React.Component<Props, State> {
);
return (
<SessionModal title={titleText} onClose={() => null} onOk={() => null}>
<SessionModal
title={titleText}
// tslint:disable-next-line: no-void-expression
onClose={() => this.closeDialog()}
onOk={() => null}
>
<div className="spacer-md" />
<p className={errorMessageClasses}>{errorMsg}</p>
<div className="spacer-md" />
<input
type="text"
id="group-name"
className="group-name"
placeholder={this.props.i18n('groupNamePlaceholder')}
value={this.state.groupName}
disabled={!this.props.isAdmin}
onChange={this.onGroupNameChanged}
tabIndex={0}
required={true}
aria-required={true}
autoFocus={true}
/>
<div className="friend-selection-list">
<MemberList
members={this.state.friendList}
@ -139,15 +123,13 @@ export class UpdateGroupDialog extends React.Component<Props, State> {
/>
</div>
<p className={noFriendsClasses}>{`(${this.props.i18n(
'noFriendsToAdd'
'noMembersInThisGroup'
)})`}</p>
<div className="buttons">
<button className="cancel" tabIndex={0} onClick={this.closeDialog}>
{cancelText}
</button>
<button className="ok" tabIndex={0} onClick={this.onClickOK}>
{okText}
</button>
<div className="session-modal__button-group">
<SessionButton text={okText} onClick={this.onClickOK} />
<SessionButton text={cancelText} onClick={this.closeDialog} />
</div>
</SessionModal>
);

@ -0,0 +1,148 @@
import React from 'react';
import classNames from 'classnames';
import { SessionModal } from '../session/SessionModal';
import { SessionButton } from '../session/SessionButton';
interface Props {
titleText: string;
groupName: string;
okText: string;
cancelText: string;
isAdmin: boolean;
i18n: any;
onSubmit: any;
onClose: any;
existingMembers: Array<String>;
}
interface State {
groupName: string;
errorDisplayed: boolean;
errorMessage: string;
}
export class UpdateGroupNameDialog extends React.Component<Props, State> {
constructor(props: any) {
super(props);
this.onClickOK = this.onClickOK.bind(this);
this.onKeyUp = this.onKeyUp.bind(this);
this.closeDialog = this.closeDialog.bind(this);
this.onGroupNameChanged = this.onGroupNameChanged.bind(this);
this.state = {
groupName: this.props.groupName,
errorDisplayed: false,
errorMessage: 'placeholder',
};
window.addEventListener('keyup', this.onKeyUp);
}
public onClickOK() {
if (!this.state.groupName.trim()) {
this.onShowError(this.props.i18n('emptyGroupNameError'));
return;
}
this.props.onSubmit(this.state.groupName, this.props.existingMembers);
this.closeDialog();
}
public render() {
const okText = this.props.okText;
const cancelText = this.props.cancelText;
let titleText;
titleText = `${this.props.titleText}`;
const errorMsg = this.state.errorMessage;
const errorMessageClasses = classNames(
'error-message',
this.state.errorDisplayed ? 'error-shown' : 'error-faded'
);
return (
<SessionModal
title={titleText}
// tslint:disable-next-line: no-void-expression
onClose={() => this.closeDialog()}
onOk={() => null}
>
<div className="spacer-md" />
<p className={errorMessageClasses}>{errorMsg}</p>
<div className="spacer-md" />
<input
type="text"
className="profile-name-input"
value={this.state.groupName}
placeholder={this.props.i18n('groupNamePlaceholder')}
onChange={this.onGroupNameChanged}
tabIndex={0}
required={true}
aria-required={true}
autoFocus={true}
disabled={!this.props.isAdmin}
/>
<div className="session-modal__button-group">
<SessionButton text={okText} onClick={this.onClickOK} />
<SessionButton text={cancelText} onClick={this.closeDialog} />
</div>
</SessionModal>
);
}
private onShowError(msg: string) {
if (this.state.errorDisplayed) {
return;
}
this.setState({
errorDisplayed: true,
errorMessage: msg,
});
setTimeout(() => {
this.setState({
errorDisplayed: false,
});
}, 3000);
}
private onKeyUp(event: any) {
switch (event.key) {
case 'Enter':
this.onClickOK();
break;
case 'Esc':
case 'Escape':
this.closeDialog();
break;
default:
}
}
private closeDialog() {
window.removeEventListener('keyup', this.onKeyUp);
this.props.onClose();
}
private onGroupNameChanged(event: any) {
event.persist();
this.setState(state => {
return {
...state,
groupName: event.target.value,
};
});
}
}

@ -19,15 +19,18 @@ interface Props {
avatarPath: string;
timerOptions: Array<TimerOption>;
isPublic: boolean;
isAdmin: boolean;
onGoBack: () => void;
onInviteFriends: () => void;
onLeaveGroup: () => void;
onUpdateGroupName: () => void;
onUpdateGroupMembers: () => void;
onShowLightBox: (options: any) => void;
onSetDisappearingMessages: (seconds: number) => void;
}
export class SessionChannelSettings extends React.Component<Props, any> {
export class SessionGroupSettings extends React.Component<Props, any> {
public constructor(props: Props) {
super(props);
@ -207,6 +210,7 @@ export class SessionChannelSettings extends React.Component<Props, any> {
timerOptions,
onLeaveGroup,
isPublic,
isAdmin,
} = this.props;
const { documents, media, onItemClick } = this.state;
const showMemberCount = !!(memberCount && memberCount > 0);
@ -231,7 +235,7 @@ export class SessionChannelSettings extends React.Component<Props, any> {
{showMemberCount && (
<>
<div className="spacer-lg" />
<div className="text-subtle">
<div role="button" className="text-subtle">
{window.i18n('members', memberCount)}
</div>
<div className="spacer-lg" />
@ -241,7 +245,26 @@ export class SessionChannelSettings extends React.Component<Props, any> {
className="description"
placeholder={window.i18n('description')}
/>
{!isPublic && (
<>
{isAdmin && (
<div
className="group-settings-item"
role="button"
onClick={this.props.onUpdateGroupName}
>
{window.i18n('editGroupName')}
</div>
)}
<div
className="group-settings-item"
role="button"
onClick={this.props.onUpdateGroupMembers}
>
{window.i18n('showMembers')}
</div>
</>
)}
<div className="group-settings-item">
{window.i18n('notifications')}
</div>
@ -269,8 +292,16 @@ export class SessionChannelSettings extends React.Component<Props, any> {
}
private renderHeader() {
const { id, onGoBack, onInviteFriends, avatarPath } = this.props;
const shouldShowInviteFriends = !this.props.isPublic;
const {
id,
onGoBack,
onInviteFriends,
avatarPath,
isAdmin,
isPublic,
} = this.props;
const showInviteFriends = isPublic || isAdmin;
return (
<div className="group-settings-header">
@ -286,9 +317,8 @@ export class SessionChannelSettings extends React.Component<Props, any> {
conversationType="group"
size={80}
/>
<div className="invite-friends-container">
{shouldShowInviteFriends && (
{showInviteFriends && (
<SessionIconButton
iconType={SessionIconType.AddUser}
iconSize={SessionIconSize.Medium}
Loading…
Cancel
Save