diff --git a/res/layout/activity_edit_closed_group.xml b/res/layout/activity_edit_closed_group.xml
index 16ecefc339..b5e09eb35d 100644
--- a/res/layout/activity_edit_closed_group.xml
+++ b/res/layout/activity_edit_closed_group.xml
@@ -1,9 +1,9 @@
-
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/org/thoughtcrime/securesms/loki/activities/EditClosedGroupActivity.kt b/src/org/thoughtcrime/securesms/loki/activities/EditClosedGroupActivity.kt
index 1ee9270e6b..3d36afcd67 100644
--- a/src/org/thoughtcrime/securesms/loki/activities/EditClosedGroupActivity.kt
+++ b/src/org/thoughtcrime/securesms/loki/activities/EditClosedGroupActivity.kt
@@ -13,17 +13,23 @@ import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.appcompat.content.res.AppCompatResources
+import kotlinx.android.synthetic.main.activity_create_closed_group.*
import kotlinx.android.synthetic.main.activity_create_closed_group.emptyStateContainer
import kotlinx.android.synthetic.main.activity_create_closed_group.mainContentContainer
import kotlinx.android.synthetic.main.activity_edit_closed_group.*
+import kotlinx.android.synthetic.main.activity_edit_closed_group.loader
import kotlinx.android.synthetic.main.activity_linked_devices.recyclerView
import network.loki.messenger.R
+import nl.komponents.kovenant.ui.failUi
+import nl.komponents.kovenant.ui.successUi
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.groups.GroupManager
import org.thoughtcrime.securesms.loki.dialogs.ClosedGroupEditingOptionsBottomSheet
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol
+import org.thoughtcrime.securesms.loki.utilities.fadeIn
+import org.thoughtcrime.securesms.loki.utilities.fadeOut
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.GroupUtil
@@ -238,10 +244,16 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
}
if (isSSKBasedClosedGroup) {
- ClosedGroupsProtocol.update(this, groupPublicKey!!, members.map { it.address.serialize() }, name)
+ loader.fadeIn()
+ ClosedGroupsProtocol.update(this, groupPublicKey!!, members.map { it.address.serialize() }, name).successUi {
+ loader.fadeOut()
+ finish()
+ }.failUi { exception ->
+ val message = if (exception is ClosedGroupsProtocol.Error) exception.description else "An error occurred"
+ Toast.makeText(this@EditClosedGroupActivity, message, Toast.LENGTH_LONG).show()
+ }
} else {
GroupManager.updateGroup(this, groupID, members, null, name, admins)
}
- finish()
}
}
\ No newline at end of file
diff --git a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt
index 9e4d8a4379..9050e39a80 100644
--- a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt
+++ b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt
@@ -25,6 +25,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceGroup
import org.whispersystems.signalservice.api.messages.SignalServiceGroup.GroupType
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext
+import org.whispersystems.signalservice.loki.api.SnodeAPI
import org.whispersystems.signalservice.loki.protocol.closedgroups.ClosedGroupRatchet
import org.whispersystems.signalservice.loki.protocol.closedgroups.ClosedGroupSenderKey
import org.whispersystems.signalservice.loki.protocol.closedgroups.SharedSenderKeysImplementation
@@ -38,7 +39,13 @@ import kotlin.jvm.Throws
object ClosedGroupsProtocol {
val isSharedSenderKeysEnabled = true
val groupSizeLimit = 20
-
+
+ sealed class Error(val description: String) : Exception() {
+ object NoThread : Error("Couldn't find a thread associated with the given group public key")
+ object NoPrivateKey : Error("Couldn't find a private key associated with the given group public key.")
+ object InvalidUpdate : Error("Invalid group update.")
+ }
+
public fun createClosedGroup(context: Context, name: String, members: Collection): Promise {
val deferred = deferred()
Thread {
@@ -98,123 +105,128 @@ object ClosedGroupsProtocol {
val name = group.title
val oldMembers = group.members.map { it.serialize() }.toSet()
val newMembers = oldMembers.minus(userPublicKey)
- update(context, groupPublicKey, newMembers, name)
+ return update(context, groupPublicKey, newMembers, name).get()
}
- public fun update(context: Context, groupPublicKey: String, members: Collection, name: String) {
- val userPublicKey = TextSecurePreferences.getLocalNumber(context)
- val sskDatabase = DatabaseFactory.getSSKDatabase(context)
- val groupDB = DatabaseFactory.getGroupDatabase(context)
- val groupID = doubleEncodeGroupID(groupPublicKey)
- val group = groupDB.getGroup(groupID).orNull()
- if (group == null) {
- Log.d("Loki", "Can't update nonexistent closed group.")
- return
- }
- val oldMembers = group.members.map { it.serialize() }.toSet()
- val newMembers = members.minus(oldMembers)
- val membersAsData = members.map { Hex.fromStringCondensed(it) }
- val admins = group.admins.map { it.serialize() }
- val adminsAsData = admins.map { Hex.fromStringCondensed(it) }
- val groupPrivateKey = DatabaseFactory.getSSKDatabase(context).getClosedGroupPrivateKey(groupPublicKey)
- if (groupPrivateKey == null) {
- Log.d("Loki", "Couldn't get private key for closed group.")
- return
- }
- val wasAnyUserRemoved = members.toSet().intersect(oldMembers) != oldMembers.toSet()
- val removedMembers = oldMembers.minus(members)
- val isUserLeaving = removedMembers.contains(userPublicKey)
- var newSenderKeys = listOf()
- if (wasAnyUserRemoved) {
- if (isUserLeaving && removedMembers.count() != 1) {
- Log.d("Loki", "Can't remove self and others simultaneously.")
- return
+ public fun update(context: Context, groupPublicKey: String, members: Collection, name: String): Promise {
+ val deferred = deferred()
+ Thread {
+ val userPublicKey = TextSecurePreferences.getLocalNumber(context)
+ val sskDatabase = DatabaseFactory.getSSKDatabase(context)
+ val groupDB = DatabaseFactory.getGroupDatabase(context)
+ val groupID = doubleEncodeGroupID(groupPublicKey)
+ val group = groupDB.getGroup(groupID).orNull()
+ if (group == null) {
+ Log.d("Loki", "Can't update nonexistent closed group.")
+ return@Thread deferred.reject(Error.NoThread)
}
- // Establish sessions if needed
- establishSessionsWithMembersIfNeeded(context, members)
- // Send the update to the existing members using established channels (don't include new ratchets as everyone should regenerate new ratchets individually)
- for (member in oldMembers) {
- @Suppress("NAME_SHADOWING")
- val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey),
- name, setOf(), membersAsData, adminsAsData)
- @Suppress("NAME_SHADOWING")
- val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind)
- job.setContext(context)
- job.onRun() // Run the job immediately
+ val oldMembers = group.members.map { it.serialize() }.toSet()
+ val newMembers = members.minus(oldMembers)
+ val membersAsData = members.map { Hex.fromStringCondensed(it) }
+ val admins = group.admins.map { it.serialize() }
+ val adminsAsData = admins.map { Hex.fromStringCondensed(it) }
+ val groupPrivateKey = DatabaseFactory.getSSKDatabase(context).getClosedGroupPrivateKey(groupPublicKey)
+ if (groupPrivateKey == null) {
+ Log.d("Loki", "Couldn't get private key for closed group.")
+ return@Thread deferred.reject(Error.NoPrivateKey)
}
- // Delete all ratchets (it's important that this happens * after * sending out the update)
- sskDatabase.removeAllClosedGroupRatchets(groupPublicKey)
- // Remove the group from the user's set of public keys to poll for if the user is leaving. Otherwise generate a new ratchet and
- // send it out to all members (minus the removed ones) using established channels.
- if (isUserLeaving) {
- sskDatabase.removeClosedGroupPrivateKey(groupPublicKey)
- groupDB.setActive(groupID, false)
- groupDB.remove(groupID, Address.fromSerialized(userPublicKey))
- // Notify the PN server
- LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey)
- } else {
- // Send closed group update messages to any new members using established channels
+ val wasAnyUserRemoved = members.toSet().intersect(oldMembers) != oldMembers.toSet()
+ val removedMembers = oldMembers.minus(members)
+ val isUserLeaving = removedMembers.contains(userPublicKey)
+ var newSenderKeys = listOf()
+ if (wasAnyUserRemoved) {
+ if (isUserLeaving && removedMembers.count() != 1) {
+ Log.d("Loki", "Can't remove self and others simultaneously.")
+ return@Thread deferred.reject(Error.InvalidUpdate)
+ }
+ // Establish sessions if needed
+ establishSessionsWithMembersIfNeeded(context, members)
+ // Send the update to the existing members using established channels (don't include new ratchets as everyone should regenerate new ratchets individually)
+ for (member in oldMembers) {
+ @Suppress("NAME_SHADOWING")
+ val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey),
+ name, setOf(), membersAsData, adminsAsData)
+ @Suppress("NAME_SHADOWING")
+ val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind)
+ job.setContext(context)
+ job.onRun() // Run the job immediately
+ }
+ // Delete all ratchets (it's important that this happens * after * sending out the update)
+ sskDatabase.removeAllClosedGroupRatchets(groupPublicKey)
+ // Remove the group from the user's set of public keys to poll for if the user is leaving. Otherwise generate a new ratchet and
+ // send it out to all members (minus the removed ones) using established channels.
+ if (isUserLeaving) {
+ sskDatabase.removeClosedGroupPrivateKey(groupPublicKey)
+ groupDB.setActive(groupID, false)
+ groupDB.remove(groupID, Address.fromSerialized(userPublicKey))
+ // Notify the PN server
+ LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey)
+ } else {
+ // Send closed group update messages to any new members using established channels
+ for (member in newMembers) {
+ @Suppress("NAME_SHADOWING")
+ val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.New(Hex.fromStringCondensed(groupPublicKey), name,
+ Hex.fromStringCondensed(groupPrivateKey), listOf(), membersAsData, adminsAsData)
+ @Suppress("NAME_SHADOWING")
+ val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind)
+ ApplicationContext.getInstance(context).jobManager.add(job)
+ }
+ // Send out the user's new ratchet to all members (minus the removed ones) using established channels
+ val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey)
+ val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey))
+ for (member in members) {
+ if (member == userPublicKey) { continue }
+ @Suppress("NAME_SHADOWING")
+ val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey)
+ @Suppress("NAME_SHADOWING")
+ val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind)
+ ApplicationContext.getInstance(context).jobManager.add(job)
+ }
+ }
+ } else if (newMembers.isNotEmpty()) {
+ // Generate ratchets for any new members
+ newSenderKeys = newMembers.map { publicKey ->
+ val ratchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, publicKey)
+ ClosedGroupSenderKey(Hex.fromStringCondensed(ratchet.chainKey), ratchet.keyIndex, Hex.fromStringCondensed(publicKey))
+ }
+ // Send a closed group update message to the existing members with the new members' ratchets (this message is aimed at the group)
+ val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey), name,
+ newSenderKeys, membersAsData, adminsAsData)
+ val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind)
+ ApplicationContext.getInstance(context).jobManager.add(job)
+ // Establish sessions if needed
+ establishSessionsWithMembersIfNeeded(context, newMembers)
+ // Send closed group update messages to the new members using established channels
+ var allSenderKeys = sskDatabase.getAllClosedGroupSenderKeys(groupPublicKey);
+ allSenderKeys = allSenderKeys.union(newSenderKeys)
for (member in newMembers) {
@Suppress("NAME_SHADOWING")
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.New(Hex.fromStringCondensed(groupPublicKey), name,
- Hex.fromStringCondensed(groupPrivateKey), listOf(), membersAsData, adminsAsData)
+ Hex.fromStringCondensed(groupPrivateKey), allSenderKeys, membersAsData, adminsAsData)
@Suppress("NAME_SHADOWING")
val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind)
ApplicationContext.getInstance(context).jobManager.add(job)
}
- // Send out the user's new ratchet to all members (minus the removed ones) using established channels
- val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey)
- val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey))
- for (member in members) {
- if (member == userPublicKey) { continue }
- @Suppress("NAME_SHADOWING")
- val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey)
- @Suppress("NAME_SHADOWING")
- val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind)
- ApplicationContext.getInstance(context).jobManager.add(job)
- }
- }
- } else if (newMembers.isNotEmpty()) {
- // Generate ratchets for any new members
- newSenderKeys = newMembers.map { publicKey ->
- val ratchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, publicKey)
- ClosedGroupSenderKey(Hex.fromStringCondensed(ratchet.chainKey), ratchet.keyIndex, Hex.fromStringCondensed(publicKey))
- }
- // Send a closed group update message to the existing members with the new members' ratchets (this message is aimed at the group)
- val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey), name,
- newSenderKeys, membersAsData, adminsAsData)
- val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind)
- ApplicationContext.getInstance(context).jobManager.add(job)
- // Establish sessions if needed
- establishSessionsWithMembersIfNeeded(context, newMembers)
- // Send closed group update messages to the new members using established channels
- var allSenderKeys = sskDatabase.getAllClosedGroupSenderKeys(groupPublicKey);
- allSenderKeys = allSenderKeys.union(newSenderKeys)
- for (member in newMembers) {
- @Suppress("NAME_SHADOWING")
- val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.New(Hex.fromStringCondensed(groupPublicKey), name,
- Hex.fromStringCondensed(groupPrivateKey), allSenderKeys, membersAsData, adminsAsData)
- @Suppress("NAME_SHADOWING")
- val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind)
+ } else {
+ val allSenderKeys = sskDatabase.getAllClosedGroupSenderKeys(groupPublicKey);
+ val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey), name,
+ allSenderKeys, membersAsData, adminsAsData)
+ val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind)
ApplicationContext.getInstance(context).jobManager.add(job)
}
- } else {
- val allSenderKeys = sskDatabase.getAllClosedGroupSenderKeys(groupPublicKey);
- val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey), name,
- allSenderKeys, membersAsData, adminsAsData)
- val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind)
- ApplicationContext.getInstance(context).jobManager.add(job)
- }
- // Update the group
- groupDB.updateTitle(groupID, name)
- if (!isUserLeaving) {
- // The call below sets isActive to true, so if the user is leaving we have to use groupDB.remove(...) instead
- groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) })
- }
- // Notify the user
- val infoType = if (isUserLeaving) GroupContext.Type.QUIT else GroupContext.Type.UPDATE
- val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
- insertOutgoingInfoMessage(context, groupID, infoType, name, members, admins, threadID)
+ // Update the group
+ groupDB.updateTitle(groupID, name)
+ if (!isUserLeaving) {
+ // The call below sets isActive to true, so if the user is leaving we have to use groupDB.remove(...) instead
+ groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) })
+ }
+ // Notify the user
+ val infoType = if (isUserLeaving) GroupContext.Type.QUIT else GroupContext.Type.UPDATE
+ val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
+ insertOutgoingInfoMessage(context, groupID, infoType, name, members, admins, threadID)
+ deferred.resolve(Unit)
+ }.start()
+ return deferred.promise
}
@JvmStatic