feat: all the control messages and basic sending of leave group
This commit is contained in:
parent
56e9a42086
commit
d02230cee3
|
@ -1,9 +1,13 @@
|
|||
package org.thoughtcrime.securesms.conversation.settings
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Typeface
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.style.StyleSpan
|
||||
import android.view.View
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.text.set
|
||||
import androidx.core.view.isVisible
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import network.loki.messenger.R
|
||||
|
@ -68,6 +72,7 @@ class ConversationSettingsActivity: PassphraseRequiredActionBarActivity(), View.
|
|||
binding.notificationSettings.setOnClickListener(this)
|
||||
binding.editGroup.setOnClickListener(this)
|
||||
binding.addAdmins.setOnClickListener(this)
|
||||
binding.leaveGroup.setOnClickListener(this)
|
||||
binding.back.setOnClickListener(this)
|
||||
binding.autoDownloadMediaSwitch.setOnCheckedChangeListener { _, isChecked ->
|
||||
viewModel.setAutoDownloadAttachments(isChecked)
|
||||
|
@ -86,7 +91,7 @@ class ConversationSettingsActivity: PassphraseRequiredActionBarActivity(), View.
|
|||
}
|
||||
// Setup group description (if group)
|
||||
binding.conversationSubtitle.isVisible = recipient.isClosedGroupRecipient.apply {
|
||||
binding.conversationSubtitle.text = "TODO: This is a test for group descriptions"
|
||||
binding.conversationSubtitle.text = viewModel.closedGroupInfo(recipient.address.serialize())?.description
|
||||
}
|
||||
|
||||
// Toggle group-specific settings
|
||||
|
@ -99,10 +104,12 @@ class ConversationSettingsActivity: PassphraseRequiredActionBarActivity(), View.
|
|||
val isUserGroupAdmin = areGroupOptionsVisible && viewModel.isUserGroupAdmin()
|
||||
with (binding) {
|
||||
groupMembersDivider.root.isVisible = areGroupOptionsVisible && !isUserGroupAdmin
|
||||
groupMembers.isVisible = areGroupOptionsVisible && !isUserGroupAdmin
|
||||
groupMembers.isVisible = !isUserGroupAdmin
|
||||
adminControlsGroup.isVisible = isUserGroupAdmin
|
||||
deleteGroup.isVisible = isUserGroupAdmin
|
||||
clearMessages.isVisible = isUserGroupAdmin
|
||||
clearMessagesDivider.root.isVisible = isUserGroupAdmin
|
||||
leaveGroupDivider.root.isVisible = isUserGroupAdmin
|
||||
}
|
||||
|
||||
// Set pinned state
|
||||
|
@ -161,6 +168,35 @@ class ConversationSettingsActivity: PassphraseRequiredActionBarActivity(), View.
|
|||
cancelButton()
|
||||
}
|
||||
}
|
||||
v === binding.leaveGroup -> {
|
||||
showSessionDialog {
|
||||
|
||||
title(R.string.conversation_settings_leave_group)
|
||||
|
||||
val name = viewModel.recipient!!.name!!
|
||||
val text = getString(R.string.conversation_settings_leave_group_name)
|
||||
val textWithArgs = getString(R.string.conversation_settings_leave_group_name, name)
|
||||
|
||||
// Searches for the start index of %1$s
|
||||
val startIndex = """%1${"\\$"}s""".toRegex().find(text)?.range?.start
|
||||
val endIndex = startIndex?.plus(name.length)
|
||||
|
||||
val styledText = if (startIndex == null || endIndex == null) {
|
||||
textWithArgs
|
||||
} else {
|
||||
val boldName = SpannableStringBuilder(textWithArgs)
|
||||
boldName[startIndex .. endIndex] = StyleSpan(Typeface.BOLD)
|
||||
boldName
|
||||
}
|
||||
text(styledText)
|
||||
destructiveButton(
|
||||
R.string.conversation_settings_leave_group,
|
||||
R.string.conversation_settings_leave_group
|
||||
) {
|
||||
viewModel.leaveGroup()
|
||||
}
|
||||
}
|
||||
}
|
||||
v === binding.editGroup -> {
|
||||
val recipient = viewModel.recipient ?: return
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope
|
|||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.launch
|
||||
import network.loki.messenger.libsession_util.util.GroupDisplayInfo
|
||||
import org.session.libsession.database.StorageProtocol
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
|
@ -61,6 +62,14 @@ class ConversationSettingsViewModel(
|
|||
}
|
||||
}
|
||||
|
||||
fun closedGroupInfo(address: String): GroupDisplayInfo? = storage.getClosedGroupDisplayInfo(address)
|
||||
|
||||
fun leaveGroup() {
|
||||
viewModelScope.launch {
|
||||
storage.leaveGroup(recipient!!.address.serialize())
|
||||
}
|
||||
}
|
||||
|
||||
// DI-related
|
||||
@dagger.assisted.AssistedFactory
|
||||
interface AssistedFactory {
|
||||
|
|
|
@ -6,6 +6,7 @@ import androidx.core.database.getBlobOrNull
|
|||
import androidx.core.database.getLongOrNull
|
||||
import androidx.sqlite.db.transaction
|
||||
import org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage
|
||||
import org.session.libsignal.utilities.SessionId
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||
|
||||
class ConfigDatabase(context: Context, helper: SQLCipherOpenHelper): Database(context, helper) {
|
||||
|
@ -22,6 +23,7 @@ class ConfigDatabase(context: Context, helper: SQLCipherOpenHelper): Database(co
|
|||
"CREATE TABLE $TABLE_NAME ($VARIANT TEXT NOT NULL, $PUBKEY TEXT NOT NULL, $DATA BLOB, $TIMESTAMP INTEGER NOT NULL DEFAULT 0, PRIMARY KEY($VARIANT, $PUBKEY));"
|
||||
|
||||
private const val VARIANT_AND_PUBKEY_WHERE = "$VARIANT = ? AND $PUBKEY = ?"
|
||||
private const val VARIANT_IN_AND_PUBKEY_WHERE = "$VARIANT in (?) AND $PUBKEY = ?"
|
||||
|
||||
val KEYS_VARIANT = SharedConfigMessage.Kind.ENCRYPTION_KEYS.name
|
||||
val INFO_VARIANT = SharedConfigMessage.Kind.CLOSED_GROUP_INFO.name
|
||||
|
@ -39,6 +41,16 @@ class ConfigDatabase(context: Context, helper: SQLCipherOpenHelper): Database(co
|
|||
db.insertOrUpdate(TABLE_NAME, contentValues, VARIANT_AND_PUBKEY_WHERE, arrayOf(variant, publicKey))
|
||||
}
|
||||
|
||||
fun deleteGroupConfigs(closedGroupId: SessionId) {
|
||||
val db = writableDatabase
|
||||
db.transaction {
|
||||
val variants = arrayOf(KEYS_VARIANT, INFO_VARIANT, MEMBER_VARIANT)
|
||||
db.delete(TABLE_NAME, VARIANT_IN_AND_PUBKEY_WHERE,
|
||||
arrayOf(variants, closedGroupId.hexString())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun storeGroupConfigs(publicKey: String, keysConfig: ByteArray, infoConfig: ByteArray, memberConfig: ByteArray, timestamp: Long) {
|
||||
val db = writableDatabase
|
||||
db.transaction {
|
||||
|
|
|
@ -34,6 +34,7 @@ import org.session.libsession.messaging.jobs.ConfigurationSyncJob
|
|||
import org.session.libsession.messaging.jobs.GroupAvatarDownloadJob
|
||||
import org.session.libsession.messaging.jobs.InviteContactsJob
|
||||
import org.session.libsession.messaging.jobs.Job
|
||||
import org.session.libsession.messaging.jobs.JobDelegate
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
import org.session.libsession.messaging.jobs.MessageReceiveJob
|
||||
import org.session.libsession.messaging.jobs.MessageSendJob
|
||||
|
@ -87,6 +88,7 @@ import org.session.libsignal.messages.SignalServiceGroup
|
|||
import org.session.libsignal.protos.SignalServiceProtos.DataMessage
|
||||
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateInviteResponseMessage
|
||||
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateMemberChangeMessage
|
||||
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateMessage
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.utilities.IdPrefix
|
||||
|
@ -185,7 +187,7 @@ open class Storage(
|
|||
// these should be removed in the group leave / handling new configs
|
||||
Log.w("Loki", "Thread delete called for open group address, expecting to be handled elsewhere")
|
||||
} else if (address.isClosedGroup) {
|
||||
TODO("add the thread deleted checks for new closed groups")
|
||||
Log.w("Loki", "Thread delete called for closed group address, expecting to be handled elsewhere")
|
||||
}
|
||||
} else {
|
||||
// non-standard contact prefixes: 15, 00 etc shouldn't be stored in config
|
||||
|
@ -1345,10 +1347,24 @@ open class Storage(
|
|||
response.get()
|
||||
|
||||
val newConfigSync = ConfigurationSyncJob(destination)
|
||||
var exception: Exception? = null
|
||||
val delegate = object: JobDelegate {
|
||||
override fun handleJobSucceeded(job: Job, dispatcherName: String) {}
|
||||
override fun handleJobFailed(job: Job, dispatcherName: String, error: Exception) { exception = error }
|
||||
override fun handleJobFailedPermanently(
|
||||
job: Job,
|
||||
dispatcherName: String,
|
||||
error: Exception
|
||||
) { exception = error }
|
||||
}
|
||||
newConfigSync.delegate = delegate
|
||||
runBlocking {
|
||||
newConfigSync.execute("updating-members")
|
||||
}
|
||||
|
||||
// rethrow failure
|
||||
exception?.let { throw it }
|
||||
|
||||
configFactory.saveGroupConfigs(keysConfig, infoConfig, membersConfig)
|
||||
|
||||
val job = InviteContactsJob(groupSessionId, filteredMembers.toTypedArray())
|
||||
|
@ -1366,8 +1382,9 @@ open class Storage(
|
|||
.setAdminSignature(ByteString.copyFrom(signature))
|
||||
)
|
||||
.build()
|
||||
)
|
||||
).apply { this.sentTimestamp = timestamp }
|
||||
MessageSender.send(updatedMessage, fromSerialized(groupSessionId))
|
||||
insertGroupInfoChange(updatedMessage, sessionId)
|
||||
infoConfig.free()
|
||||
membersConfig.free()
|
||||
keysConfig.free()
|
||||
|
@ -1383,12 +1400,12 @@ open class Storage(
|
|||
}
|
||||
|
||||
override fun insertGroupInfoChange(message: GroupUpdated, closedGroup: SessionId) {
|
||||
val sentTimestamp = message.sentTimestamp ?: return
|
||||
val senderPublicKey = message.sender ?: return
|
||||
val sentTimestamp = message.sentTimestamp ?: SnodeAPI.nowWithOffset
|
||||
val senderPublicKey = message.sender
|
||||
val userPublicKey = getUserPublicKey()!!
|
||||
val updateData = UpdateMessageData.buildGroupUpdate(message)?.toJSON() ?: return
|
||||
|
||||
if (senderPublicKey == userPublicKey) {
|
||||
if (senderPublicKey == null || senderPublicKey == userPublicKey) {
|
||||
val recipient = Recipient.from(context, fromSerialized(closedGroup.hexString()), false)
|
||||
val infoMessage = OutgoingGroupMediaMessage(recipient, updateData, closedGroup.hexString(), null, sentTimestamp, 0, true, null, listOf(), listOf())
|
||||
val mmsDB = DatabaseComponent.get(context).mmsDatabase()
|
||||
|
@ -1451,8 +1468,11 @@ open class Storage(
|
|||
.setAdminSignature(ByteString.copyFrom(signature))
|
||||
)
|
||||
.build()
|
||||
).apply { sentTimestamp = timestamp }
|
||||
).apply {
|
||||
sentTimestamp = timestamp
|
||||
}
|
||||
MessageSender.send(message, fromSerialized(groupSessionId))
|
||||
insertGroupInfoChange(message, closedGroupId)
|
||||
}
|
||||
|
||||
override fun handlePromoted(keyPair: KeyPair) {
|
||||
|
@ -1480,6 +1500,27 @@ open class Storage(
|
|||
keys.free()
|
||||
}
|
||||
|
||||
override fun leaveGroup(groupSessionId: String) {
|
||||
val closedGroupId = SessionId.from(groupSessionId)
|
||||
val message = GroupUpdated(
|
||||
GroupUpdateMessage.newBuilder()
|
||||
.setMemberLeftMessage(DataMessage.GroupUpdateMemberLeftMessage.getDefaultInstance())
|
||||
.build()
|
||||
)
|
||||
try {
|
||||
MessageSender.sendNonDurably(message, fromSerialized(groupSessionId), false).get()
|
||||
pollerFactory.pollerFor(closedGroupId)?.stop()
|
||||
// TODO: unsub from pushes
|
||||
getThreadId(fromSerialized(groupSessionId))?.let { threadId ->
|
||||
deleteConversation(threadId)
|
||||
}
|
||||
configFactory.removeGroup(closedGroupId)
|
||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
|
||||
} catch (e: Exception) {
|
||||
Log.e("ClosedGroup", "Failed to send leave group message")
|
||||
}
|
||||
}
|
||||
|
||||
override fun setServerCapabilities(server: String, capabilities: List<String>) {
|
||||
return DatabaseComponent.get(context).lokiAPIDatabase().setServerCapabilities(server, capabilities)
|
||||
}
|
||||
|
|
|
@ -402,6 +402,12 @@ class ConfigFactory(
|
|||
configDatabase.storeGroupConfigs(pubKey, groupKeys.dump(), groupInfo.dump(), groupMembers.dump(), timestamp)
|
||||
}
|
||||
|
||||
override fun removeGroup(closedGroupId: SessionId) {
|
||||
val groups = userGroups ?: return
|
||||
groups.eraseClosedGroup(closedGroupId.hexString())
|
||||
configDatabase.deleteGroupConfigs(closedGroupId)
|
||||
}
|
||||
|
||||
override fun scheduleUpdate(destination: Destination) {
|
||||
// there's probably a better way to do this
|
||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(destination)
|
||||
|
|
|
@ -1079,6 +1079,5 @@
|
|||
<string name="media_overview_activity__clear_media">Clear All</string>
|
||||
<string name="activity_create_closed_group_select_contacts">Select Contacts</string>
|
||||
<string name="activity_create_closed_group_add_account_or_ons">Add Account ID or ONS</string>
|
||||
<string name="closed_group_update_control__single_joined"></string>
|
||||
<string name="closed_group_update_control__two_joined"></string>
|
||||
<string name="conversation_settings_leave_group_name">Are you sure you want to leave %1$s?</string>
|
||||
</resources>
|
||||
|
|
|
@ -171,6 +171,7 @@ interface StorageProtocol {
|
|||
fun insertGroupInfoChange(message: GroupUpdated, closedGroup: SessionId)
|
||||
fun promoteMember(groupSessionId: String, promotions: Array<String>)
|
||||
fun handlePromoted(keyPair: KeyPair)
|
||||
fun leaveGroup(groupSessionId: String)
|
||||
|
||||
// Groups
|
||||
fun getAllGroups(includeInactive: Boolean): List<GroupRecord>
|
||||
|
|
|
@ -10,6 +10,8 @@ class GroupUpdated(val inner: GroupUpdateMessage): ControlMessage() {
|
|||
return true // TODO: add the validation here
|
||||
}
|
||||
|
||||
override val isSelfSendValid: Boolean = true
|
||||
|
||||
companion object {
|
||||
fun fromProto(message: Content): GroupUpdated? =
|
||||
if (message.hasDataMessage() && message.dataMessage.hasGroupUpdateMessage())
|
||||
|
|
|
@ -77,52 +77,54 @@ object UpdateMessageBuilder {
|
|||
val number = updateData.sessionIds.size
|
||||
if (number == 1) context.getString(
|
||||
R.string.ConversationItem_group_member_added_single,
|
||||
getSenderName(updateData.sessionIds.first())
|
||||
context.youOrSender(updateData.sessionIds.first())
|
||||
)
|
||||
else if (number == 2) context.getString(
|
||||
R.string.ConversationItem_group_member_added_two,
|
||||
getSenderName(updateData.sessionIds.first()),
|
||||
getSenderName(updateData.sessionIds.last())
|
||||
context.youOrSender(updateData.sessionIds.first()),
|
||||
context.youOrSender(updateData.sessionIds.last())
|
||||
)
|
||||
else context.getString(
|
||||
R.string.ConversationItem_group_member_added_multiple,
|
||||
getSenderName(updateData.sessionIds.first()),
|
||||
context.youOrSender(updateData.sessionIds.first()),
|
||||
updateData.sessionIds.size - 1
|
||||
)
|
||||
}
|
||||
UpdateMessageData.MemberUpdateType.PROMOTED -> {
|
||||
val number = updateData.sessionIds.size
|
||||
if (number == 1) context.getString(
|
||||
R.string.ConversationItem_group_member_promoted_single,
|
||||
getSenderName(updateData.sessionIds.first())
|
||||
)
|
||||
else if (number == 2) context.getString(
|
||||
R.string.ConversationItem_group_member_promoted_two,
|
||||
getSenderName(updateData.sessionIds.first()),
|
||||
getSenderName(updateData.sessionIds.last())
|
||||
)
|
||||
else context.getString(
|
||||
R.string.ConversationItem_group_member_promoted_multiple,
|
||||
getSenderName(updateData.sessionIds.first()),
|
||||
updateData.sessionIds.size - 1
|
||||
)
|
||||
when (updateData.sessionIds.size) {
|
||||
1 -> context.getString(
|
||||
R.string.ConversationItem_group_member_promoted_single,
|
||||
context.youOrSender(updateData.sessionIds.first())
|
||||
)
|
||||
2 -> context.getString(
|
||||
R.string.ConversationItem_group_member_promoted_two,
|
||||
context.youOrSender(updateData.sessionIds.first()),
|
||||
context.youOrSender(updateData.sessionIds.last())
|
||||
)
|
||||
else -> context.getString(
|
||||
R.string.ConversationItem_group_member_promoted_multiple,
|
||||
context.youOrSender(updateData.sessionIds.first()),
|
||||
updateData.sessionIds.size - 1
|
||||
)
|
||||
}
|
||||
}
|
||||
UpdateMessageData.MemberUpdateType.REMOVED -> {
|
||||
val number = updateData.sessionIds.size
|
||||
if (number == 1) context.getString(
|
||||
R.string.ConversationItem_group_member_removed_single,
|
||||
getSenderName(updateData.sessionIds.first())
|
||||
)
|
||||
else if (number == 2) context.getString(
|
||||
R.string.ConversationItem_group_member_removed_two,
|
||||
getSenderName(updateData.sessionIds.first()),
|
||||
getSenderName(updateData.sessionIds.last())
|
||||
)
|
||||
else context.getString(
|
||||
R.string.ConversationItem_group_member_removed_multiple,
|
||||
getSenderName(updateData.sessionIds.first()),
|
||||
updateData.sessionIds.size - 1
|
||||
)
|
||||
when (updateData.sessionIds.size) {
|
||||
1 -> context.getString(
|
||||
R.string.ConversationItem_group_member_removed_single,
|
||||
context.youOrSender(updateData.sessionIds.first())
|
||||
)
|
||||
2 -> context.getString(
|
||||
R.string.ConversationItem_group_member_removed_two,
|
||||
context.youOrSender(updateData.sessionIds.first()),
|
||||
context.youOrSender(updateData.sessionIds.last())
|
||||
)
|
||||
else -> context.getString(
|
||||
R.string.ConversationItem_group_member_removed_multiple,
|
||||
context.youOrSender(updateData.sessionIds.first()),
|
||||
updateData.sessionIds.size - 1
|
||||
)
|
||||
}
|
||||
}
|
||||
null -> ""
|
||||
}
|
||||
|
@ -131,6 +133,8 @@ object UpdateMessageBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
fun Context.youOrSender(sessionId: String) = if (storage.getUserPublicKey() == sessionId) getString(R.string.MessageRecord_you) else getSenderName(sessionId)
|
||||
|
||||
fun buildExpirationTimerMessage(context: Context, duration: Long, senderId: String? = null, isOutgoing: Boolean = false): String {
|
||||
if (!isOutgoing && senderId == null) return ""
|
||||
val senderName: String = if (!isOutgoing) {
|
||||
|
|
|
@ -35,6 +35,7 @@ interface ConfigFactoryProtocol {
|
|||
groupInfo: GroupInfoConfig,
|
||||
groupMembers: GroupMembersConfig
|
||||
)
|
||||
fun removeGroup(closedGroupId: SessionId)
|
||||
|
||||
fun scheduleUpdate(destination: Destination)
|
||||
fun constructGroupKeysConfig(
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package org.session.libsession.utilities
|
||||
|
||||
fun truncateIdForDisplay(id: String): String =
|
||||
id.takeIf { it.length > 8 }?.apply{ "${take(4)}…${takeLast(4)}" } ?: id
|
||||
id.takeIf { it.length > 8 }?.run{ "${take(4)}…${takeLast(4)}" } ?: id
|
||||
|
|
Loading…
Reference in New Issue