feat: add info name and description updates
This commit is contained in:
parent
4b6a7c145e
commit
ae6307c2e2
|
@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.database
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.google.protobuf.ByteString
|
import com.google.protobuf.ByteString
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import network.loki.messenger.libsession_util.Config
|
import network.loki.messenger.libsession_util.Config
|
||||||
import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_HIDDEN
|
import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_HIDDEN
|
||||||
import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_PINNED
|
import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_PINNED
|
||||||
|
@ -35,7 +34,6 @@ import org.session.libsession.messaging.jobs.ConfigurationSyncJob.Companion.mess
|
||||||
import org.session.libsession.messaging.jobs.GroupAvatarDownloadJob
|
import org.session.libsession.messaging.jobs.GroupAvatarDownloadJob
|
||||||
import org.session.libsession.messaging.jobs.InviteContactsJob
|
import org.session.libsession.messaging.jobs.InviteContactsJob
|
||||||
import org.session.libsession.messaging.jobs.Job
|
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.JobQueue
|
||||||
import org.session.libsession.messaging.jobs.MessageReceiveJob
|
import org.session.libsession.messaging.jobs.MessageReceiveJob
|
||||||
import org.session.libsession.messaging.jobs.MessageSendJob
|
import org.session.libsession.messaging.jobs.MessageSendJob
|
||||||
|
@ -72,6 +70,7 @@ import org.session.libsession.messaging.utilities.UpdateMessageData
|
||||||
import org.session.libsession.snode.OnionRequestAPI
|
import org.session.libsession.snode.OnionRequestAPI
|
||||||
import org.session.libsession.snode.RawResponse
|
import org.session.libsession.snode.RawResponse
|
||||||
import org.session.libsession.snode.SnodeAPI
|
import org.session.libsession.snode.SnodeAPI
|
||||||
|
import org.session.libsession.snode.SnodeAPI.signingKeyCallback
|
||||||
import org.session.libsession.snode.SnodeMessage
|
import org.session.libsession.snode.SnodeMessage
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
import org.session.libsession.utilities.Address.Companion.fromSerialized
|
import org.session.libsession.utilities.Address.Companion.fromSerialized
|
||||||
|
@ -87,6 +86,7 @@ import org.session.libsignal.crypto.ecc.ECKeyPair
|
||||||
import org.session.libsignal.messages.SignalServiceAttachmentPointer
|
import org.session.libsignal.messages.SignalServiceAttachmentPointer
|
||||||
import org.session.libsignal.messages.SignalServiceGroup
|
import org.session.libsignal.messages.SignalServiceGroup
|
||||||
import org.session.libsignal.protos.SignalServiceProtos.DataMessage
|
import org.session.libsignal.protos.SignalServiceProtos.DataMessage
|
||||||
|
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateInfoChangeMessage
|
||||||
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateInviteResponseMessage
|
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateInviteResponseMessage
|
||||||
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateMemberChangeMessage
|
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateMemberChangeMessage
|
||||||
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateMessage
|
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateMessage
|
||||||
|
@ -1323,8 +1323,6 @@ open class Storage(
|
||||||
|
|
||||||
keysConfig.rekey(infoConfig, membersConfig)
|
keysConfig.rekey(infoConfig, membersConfig)
|
||||||
|
|
||||||
val sentTimestamp = SnodeAPI.nowWithOffset
|
|
||||||
|
|
||||||
// build unrevocation, in case of re-adding members
|
// build unrevocation, in case of re-adding members
|
||||||
val unrevocation = SnodeAPI.buildAuthenticatedUnrevokeSubKeyBatchRequest(
|
val unrevocation = SnodeAPI.buildAuthenticatedUnrevokeSubKeyBatchRequest(
|
||||||
groupSessionId,
|
groupSessionId,
|
||||||
|
@ -1333,52 +1331,35 @@ open class Storage(
|
||||||
) ?: return Log.e("ClosedGroup", "Failed to build revocation update")
|
) ?: return Log.e("ClosedGroup", "Failed to build revocation update")
|
||||||
|
|
||||||
// Build and store the key update in group swarm
|
// Build and store the key update in group swarm
|
||||||
val message = SnodeMessage(
|
val toDelete = mutableListOf<String>()
|
||||||
groupSessionId,
|
|
||||||
Base64.encodeBytes(keysConfig.pendingConfig()!!), // should not be null from checking has pending
|
val signCallback = signingKeyCallback(adminKey)
|
||||||
SnodeMessage.CONFIG_TTL,
|
|
||||||
sentTimestamp
|
val keyMessage = keysConfig.messageInformation(groupSessionId, adminKey)
|
||||||
)
|
val infoMessage = infoConfig.messageInformation(toDelete, groupSessionId, adminKey)
|
||||||
val authenticatedBatch = SnodeAPI.buildAuthenticatedStoreBatchInfo(
|
val membersMessage = membersConfig.messageInformation(toDelete, groupSessionId, adminKey)
|
||||||
keysConfig.namespace(),
|
|
||||||
message,
|
val delete = SnodeAPI.buildAuthenticatedDeleteBatchInfo(
|
||||||
adminKey
|
groupSessionId,
|
||||||
|
toDelete,
|
||||||
|
signCallback
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val stores = listOf(keyMessage, infoMessage, membersMessage).map(ConfigurationSyncJob.ConfigMessageInformation::batch)
|
||||||
|
|
||||||
val response = SnodeAPI.getSingleTargetSnode(groupSessionId).bind { snode ->
|
val response = SnodeAPI.getSingleTargetSnode(groupSessionId).bind { snode ->
|
||||||
SnodeAPI.getRawBatchResponse(
|
SnodeAPI.getRawBatchResponse(
|
||||||
snode,
|
snode,
|
||||||
groupSessionId,
|
groupSessionId,
|
||||||
listOf(unrevocation, authenticatedBatch),
|
stores + unrevocation + delete,
|
||||||
sequence = true
|
sequence = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val destination = Destination.ClosedGroup(groupSessionId)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
response.get()
|
response.get()
|
||||||
// todo: error handling here
|
// todo: error handling here
|
||||||
|
|
||||||
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)
|
configFactory.saveGroupConfigs(keysConfig, infoConfig, membersConfig)
|
||||||
|
|
||||||
val job = InviteContactsJob(groupSessionId, filteredMembers.toTypedArray())
|
val job = InviteContactsJob(groupSessionId, filteredMembers.toTypedArray())
|
||||||
|
@ -1388,7 +1369,7 @@ open class Storage(
|
||||||
val messageToSign = "MEMBER_CHANGE${GroupUpdateMemberChangeMessage.Type.ADDED.name}$timestamp"
|
val messageToSign = "MEMBER_CHANGE${GroupUpdateMemberChangeMessage.Type.ADDED.name}$timestamp"
|
||||||
val signature = SodiumUtilities.sign(messageToSign.toByteArray(), adminKey)
|
val signature = SodiumUtilities.sign(messageToSign.toByteArray(), adminKey)
|
||||||
val updatedMessage = GroupUpdated(
|
val updatedMessage = GroupUpdated(
|
||||||
DataMessage.GroupUpdateMessage.newBuilder()
|
GroupUpdateMessage.newBuilder()
|
||||||
.setMemberChangeMessage(
|
.setMemberChangeMessage(
|
||||||
GroupUpdateMemberChangeMessage.newBuilder()
|
GroupUpdateMemberChangeMessage.newBuilder()
|
||||||
.addAllMemberSessionIds(filteredMembers)
|
.addAllMemberSessionIds(filteredMembers)
|
||||||
|
@ -1512,24 +1493,29 @@ open class Storage(
|
||||||
removedMembers.map { keys.getSubAccountToken(SessionId.from(it)) }.toTypedArray()
|
removedMembers.map { keys.getSubAccountToken(SessionId.from(it)) }.toTypedArray()
|
||||||
) ?: return Log.e("ClosedGroup", "Failed to build revocation update")
|
) ?: return Log.e("ClosedGroup", "Failed to build revocation update")
|
||||||
|
|
||||||
// Build and store the key update in group swarm
|
keys.rekey(info, members)
|
||||||
val storeKeyMessage = SnodeMessage(
|
|
||||||
|
val toDelete = mutableListOf<String>()
|
||||||
|
|
||||||
|
val keyMessage = keys.messageInformation(groupSessionId, adminKey)
|
||||||
|
val infoMessage = info.messageInformation(toDelete, groupSessionId, adminKey)
|
||||||
|
val membersMessage = members.messageInformation(toDelete, groupSessionId, adminKey)
|
||||||
|
|
||||||
|
val signCallback = signingKeyCallback(adminKey)
|
||||||
|
|
||||||
|
val delete = SnodeAPI.buildAuthenticatedDeleteBatchInfo(
|
||||||
groupSessionId,
|
groupSessionId,
|
||||||
Base64.encodeBytes(keys.pendingConfig()!!), // should not be null from checking has pending
|
toDelete,
|
||||||
SnodeMessage.CONFIG_TTL,
|
signCallback
|
||||||
SnodeAPI.nowWithOffset
|
|
||||||
)
|
|
||||||
val authenticatedBatch = SnodeAPI.buildAuthenticatedStoreBatchInfo(
|
|
||||||
keys.namespace(),
|
|
||||||
storeKeyMessage,
|
|
||||||
adminKey
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val stores = listOf(keyMessage, infoMessage, membersMessage).map(ConfigurationSyncJob.ConfigMessageInformation::batch)
|
||||||
|
|
||||||
val response = SnodeAPI.getSingleTargetSnode(groupSessionId).bind { snode ->
|
val response = SnodeAPI.getSingleTargetSnode(groupSessionId).bind { snode ->
|
||||||
SnodeAPI.getRawBatchResponse(
|
SnodeAPI.getRawBatchResponse(
|
||||||
snode,
|
snode,
|
||||||
groupSessionId,
|
groupSessionId,
|
||||||
listOf(revocation, authenticatedBatch),
|
stores + revocation + delete,
|
||||||
sequence = true
|
sequence = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1542,31 +1528,11 @@ open class Storage(
|
||||||
throw Exception("Response wasn't successful for revoke and key update: ${results["body"] as? String}")
|
throw Exception("Response wasn't successful for revoke and key update: ${results["body"] as? String}")
|
||||||
}
|
}
|
||||||
|
|
||||||
val newConfigSync = ConfigurationSyncJob(Destination.ClosedGroup(groupSessionId))
|
|
||||||
|
|
||||||
configFactory.saveGroupConfigs(keys, info, members)
|
configFactory.saveGroupConfigs(keys, info, members)
|
||||||
info.free()
|
info.free()
|
||||||
members.free()
|
members.free()
|
||||||
keys.free()
|
keys.free()
|
||||||
|
|
||||||
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 }
|
|
||||||
|
|
||||||
val timestamp = SnodeAPI.nowWithOffset
|
val timestamp = SnodeAPI.nowWithOffset
|
||||||
val messageToSign = "MEMBER_CHANGE${GroupUpdateMemberChangeMessage.Type.REMOVED.name}$timestamp"
|
val messageToSign = "MEMBER_CHANGE${GroupUpdateMemberChangeMessage.Type.REMOVED.name}$timestamp"
|
||||||
val signature = SodiumUtilities.sign(messageToSign.toByteArray(), adminKey)
|
val signature = SodiumUtilities.sign(messageToSign.toByteArray(), adminKey)
|
||||||
|
@ -1624,7 +1590,7 @@ open class Storage(
|
||||||
if (closedGroup.hasAdminKey()) {
|
if (closedGroup.hasAdminKey()) {
|
||||||
// re-key and do a new config removing the previous member
|
// re-key and do a new config removing the previous member
|
||||||
val adminKey = closedGroup.adminKey
|
val adminKey = closedGroup.adminKey
|
||||||
val signCallback = SnodeAPI.signingKeyCallback(adminKey)
|
val signCallback = signingKeyCallback(adminKey)
|
||||||
val info = configFactory.getGroupInfoConfig(closedGroupId) ?: return
|
val info = configFactory.getGroupInfoConfig(closedGroupId) ?: return
|
||||||
val members = configFactory.getGroupMemberConfig(closedGroupId) ?: return
|
val members = configFactory.getGroupMemberConfig(closedGroupId) ?: return
|
||||||
val keys = configFactory.getGroupKeysConfig(closedGroupId, info, members, free = false) ?: return
|
val keys = configFactory.getGroupKeysConfig(closedGroupId, info, members, free = false) ?: return
|
||||||
|
@ -1665,7 +1631,7 @@ open class Storage(
|
||||||
val messageToSign = "MEMBER_CHANGE${GroupUpdateMemberChangeMessage.Type.REMOVED.name}$timestamp"
|
val messageToSign = "MEMBER_CHANGE${GroupUpdateMemberChangeMessage.Type.REMOVED.name}$timestamp"
|
||||||
val signature = SodiumUtilities.sign(messageToSign.toByteArray(), adminKey)
|
val signature = SodiumUtilities.sign(messageToSign.toByteArray(), adminKey)
|
||||||
val updatedMessage = GroupUpdated(
|
val updatedMessage = GroupUpdated(
|
||||||
DataMessage.GroupUpdateMessage.newBuilder()
|
GroupUpdateMessage.newBuilder()
|
||||||
.setMemberChangeMessage(
|
.setMemberChangeMessage(
|
||||||
GroupUpdateMemberChangeMessage.newBuilder()
|
GroupUpdateMemberChangeMessage.newBuilder()
|
||||||
.addAllMemberSessionIds(listOf(message.sender!!))
|
.addAllMemberSessionIds(listOf(message.sender!!))
|
||||||
|
@ -1713,6 +1679,43 @@ open class Storage(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun setName(groupSessionId: String, newName: String) {
|
||||||
|
val closedGroupId = SessionId.from(groupSessionId)
|
||||||
|
val adminKey = configFactory.userGroups?.getClosedGroup(groupSessionId)?.adminKey ?: return
|
||||||
|
if (adminKey.isEmpty()) {
|
||||||
|
return Log.e("ClosedGroup", "No admin key for group")
|
||||||
|
}
|
||||||
|
val info = configFactory.getGroupInfoConfig(closedGroupId) ?: return
|
||||||
|
val members = configFactory.getGroupMemberConfig(closedGroupId) ?: return
|
||||||
|
val keys = configFactory.getGroupKeysConfig(closedGroupId, info, members, free = false) ?: return
|
||||||
|
|
||||||
|
info.setName(newName)
|
||||||
|
|
||||||
|
configFactory.saveGroupConfigs(keys, info, members)
|
||||||
|
info.free()
|
||||||
|
members.free()
|
||||||
|
keys.free()
|
||||||
|
val groupDestination = Destination.ClosedGroup(groupSessionId)
|
||||||
|
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(groupDestination)
|
||||||
|
val timestamp = SnodeAPI.nowWithOffset
|
||||||
|
val messageToSign = "INFO_CHANGE${GroupUpdateInfoChangeMessage.Type.NAME.name}$timestamp"
|
||||||
|
val signature = SodiumUtilities.sign(messageToSign.toByteArray(), adminKey)
|
||||||
|
val message = GroupUpdated(
|
||||||
|
GroupUpdateMessage.newBuilder()
|
||||||
|
.setInfoChangeMessage(
|
||||||
|
GroupUpdateInfoChangeMessage.newBuilder()
|
||||||
|
.setUpdatedName(newName)
|
||||||
|
.setType(GroupUpdateInfoChangeMessage.Type.NAME)
|
||||||
|
.setAdminSignature(ByteString.copyFrom(signature))
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
).apply {
|
||||||
|
sentTimestamp = timestamp
|
||||||
|
}
|
||||||
|
MessageSender.send(message, fromSerialized(groupSessionId))
|
||||||
|
insertGroupInfoChange(message, closedGroupId)
|
||||||
|
}
|
||||||
|
|
||||||
override fun setServerCapabilities(server: String, capabilities: List<String>) {
|
override fun setServerCapabilities(server: String, capabilities: List<String>) {
|
||||||
return DatabaseComponent.get(context).lokiAPIDatabase().setServerCapabilities(server, capabilities)
|
return DatabaseComponent.get(context).lokiAPIDatabase().setServerCapabilities(server, capabilities)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,8 @@ import android.content.Context
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
@ -17,6 +19,7 @@ import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material.Icon
|
import androidx.compose.material.Icon
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.OutlinedTextField
|
||||||
import androidx.compose.material.Scaffold
|
import androidx.compose.material.Scaffold
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.material.TextButton
|
import androidx.compose.material.TextButton
|
||||||
|
@ -24,11 +27,15 @@ import androidx.compose.material.rememberScaffoldState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.toArgb
|
import androidx.compose.ui.graphics.toArgb
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
@ -37,6 +44,7 @@ import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
@ -49,6 +57,7 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
import com.ramcosta.composedestinations.result.NavResult
|
import com.ramcosta.composedestinations.result.NavResult
|
||||||
import com.ramcosta.composedestinations.result.ResultBackNavigator
|
import com.ramcosta.composedestinations.result.ResultBackNavigator
|
||||||
import com.ramcosta.composedestinations.result.ResultRecipient
|
import com.ramcosta.composedestinations.result.ResultRecipient
|
||||||
|
import com.ramcosta.composedestinations.spec.DestinationStyle
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
|
@ -60,9 +69,12 @@ import org.session.libsession.messaging.jobs.InviteContactsJob
|
||||||
import org.session.libsession.messaging.jobs.JobQueue
|
import org.session.libsession.messaging.jobs.JobQueue
|
||||||
import org.thoughtcrime.securesms.groups.ContactList
|
import org.thoughtcrime.securesms.groups.ContactList
|
||||||
import org.thoughtcrime.securesms.groups.destinations.EditClosedGroupInviteScreenDestination
|
import org.thoughtcrime.securesms.groups.destinations.EditClosedGroupInviteScreenDestination
|
||||||
|
import org.thoughtcrime.securesms.groups.destinations.EditClosedGroupNameScreenDestination
|
||||||
import org.thoughtcrime.securesms.ui.CellWithPaddingAndMargin
|
import org.thoughtcrime.securesms.ui.CellWithPaddingAndMargin
|
||||||
|
import org.thoughtcrime.securesms.ui.LocalExtraColors
|
||||||
import org.thoughtcrime.securesms.ui.NavigationBar
|
import org.thoughtcrime.securesms.ui.NavigationBar
|
||||||
import org.thoughtcrime.securesms.ui.PreviewTheme
|
import org.thoughtcrime.securesms.ui.PreviewTheme
|
||||||
|
import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider
|
||||||
|
|
||||||
@EditGroupNavGraph(start = true)
|
@EditGroupNavGraph(start = true)
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -70,6 +82,7 @@ import org.thoughtcrime.securesms.ui.PreviewTheme
|
||||||
fun EditClosedGroupScreen(
|
fun EditClosedGroupScreen(
|
||||||
navigator: DestinationsNavigator,
|
navigator: DestinationsNavigator,
|
||||||
resultSelectContact: ResultRecipient<EditClosedGroupInviteScreenDestination, ContactList>,
|
resultSelectContact: ResultRecipient<EditClosedGroupInviteScreenDestination, ContactList>,
|
||||||
|
resultEditName: ResultRecipient<EditClosedGroupNameScreenDestination, String>,
|
||||||
viewModel: EditGroupViewModel,
|
viewModel: EditGroupViewModel,
|
||||||
onFinish: () -> Unit
|
onFinish: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
@ -84,6 +97,12 @@ fun EditClosedGroupScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resultEditName.onNavResult { navResult ->
|
||||||
|
if (navResult is NavResult.Value) {
|
||||||
|
eventSink(EditGroupEvent.ChangeName(navResult.value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
EditGroupView(
|
EditGroupView(
|
||||||
onBack = {
|
onBack = {
|
||||||
onFinish()
|
onFinish()
|
||||||
|
@ -100,10 +119,125 @@ fun EditClosedGroupScreen(
|
||||||
onRemove = { contact ->
|
onRemove = { contact ->
|
||||||
eventSink(EditGroupEvent.RemoveContact(contact))
|
eventSink(EditGroupEvent.RemoveContact(contact))
|
||||||
},
|
},
|
||||||
|
onEditName = {
|
||||||
|
navigator.navigate(EditClosedGroupNameScreenDestination)
|
||||||
|
},
|
||||||
viewState = viewState
|
viewState = viewState
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@EditGroupNavGraph
|
||||||
|
@Composable
|
||||||
|
@Destination(style = DestinationStyle.Dialog::class)
|
||||||
|
fun EditClosedGroupNameScreen(
|
||||||
|
resultNavigator: ResultBackNavigator<String>,
|
||||||
|
viewModel: EditGroupViewModel
|
||||||
|
) {
|
||||||
|
EditClosedGroupView { name ->
|
||||||
|
if (name.isEmpty()) {
|
||||||
|
resultNavigator.navigateBack()
|
||||||
|
} else {
|
||||||
|
resultNavigator.navigateBack(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EditClosedGroupView(navigateBack: (String) -> Unit) {
|
||||||
|
|
||||||
|
var newName by remember {
|
||||||
|
mutableStateOf("")
|
||||||
|
}
|
||||||
|
|
||||||
|
var newDescription by remember {
|
||||||
|
mutableStateOf("")
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.shadow(8.dp)
|
||||||
|
.background(MaterialTheme.colors.surface)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(24.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.dialog_edit_group_information_title),
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp),
|
||||||
|
style = MaterialTheme.typography.subtitle1,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.dialog_edit_group_information_message),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(bottom = 16.dp)
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = newName,
|
||||||
|
onValueChange = { newName = it },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 8.dp),
|
||||||
|
maxLines = 1,
|
||||||
|
singleLine = true,
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.dialog_edit_group_information_enter_group_name)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = newDescription,
|
||||||
|
onValueChange = { newDescription = it },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
minLines = 2,
|
||||||
|
maxLines = 2,
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.dialog_edit_group_information_enter_group_description)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Row(modifier = Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.save),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(16.dp)
|
||||||
|
.weight(1f)
|
||||||
|
.clickable {
|
||||||
|
navigateBack(newName)
|
||||||
|
},
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.cancel),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(16.dp)
|
||||||
|
.weight(1f)
|
||||||
|
.clickable {
|
||||||
|
navigateBack("")
|
||||||
|
},
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = LocalExtraColors.current.destructive
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@EditGroupNavGraph
|
@EditGroupNavGraph
|
||||||
@Composable
|
@Composable
|
||||||
@Destination
|
@Destination
|
||||||
|
@ -198,6 +332,9 @@ class EditGroupViewModel @AssistedInject constructor(
|
||||||
is EditGroupEvent.RemoveContact -> {
|
is EditGroupEvent.RemoveContact -> {
|
||||||
storage.removeMember(groupSessionId, arrayOf(event.contactSessionId))
|
storage.removeMember(groupSessionId, arrayOf(event.contactSessionId))
|
||||||
}
|
}
|
||||||
|
is EditGroupEvent.ChangeName -> {
|
||||||
|
storage.setName(groupSessionId, event.newName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -254,6 +391,7 @@ fun EditGroupView(
|
||||||
onReinvite: (String)->Unit,
|
onReinvite: (String)->Unit,
|
||||||
onPromote: (String)->Unit,
|
onPromote: (String)->Unit,
|
||||||
onRemove: (String)->Unit,
|
onRemove: (String)->Unit,
|
||||||
|
onEditName: ()->Unit,
|
||||||
viewState: EditGroupViewState,
|
viewState: EditGroupViewState,
|
||||||
) {
|
) {
|
||||||
val scaffoldState = rememberScaffoldState()
|
val scaffoldState = rememberScaffoldState()
|
||||||
|
@ -270,15 +408,31 @@ fun EditGroupView(
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
Column(modifier = Modifier.padding(paddingValues)) {
|
Column(modifier = Modifier.padding(paddingValues)) {
|
||||||
// Group name title
|
// Group name title
|
||||||
Text(
|
Row(
|
||||||
text = viewState.groupName,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
fontSize = 26.sp,
|
horizontalArrangement = Arrangement.Center
|
||||||
fontWeight = FontWeight.Bold,
|
) {
|
||||||
textAlign = TextAlign.Center
|
Text(
|
||||||
)
|
text = viewState.groupName,
|
||||||
|
fontSize = 26.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
if (viewState.admin) {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.ic_baseline_edit_24),
|
||||||
|
null,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(8.dp)
|
||||||
|
.size(16.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.align(CenterVertically)
|
||||||
|
.clickable { onEditName() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
// Description
|
// Description
|
||||||
|
|
||||||
// Invite
|
// Invite
|
||||||
|
@ -454,6 +608,7 @@ sealed class EditGroupEvent {
|
||||||
data class ReInviteContact(val contactSessionId: String): EditGroupEvent()
|
data class ReInviteContact(val contactSessionId: String): EditGroupEvent()
|
||||||
data class PromoteContact(val contactSessionId: String): EditGroupEvent()
|
data class PromoteContact(val contactSessionId: String): EditGroupEvent()
|
||||||
data class RemoveContact(val contactSessionId: String): EditGroupEvent()
|
data class RemoveContact(val contactSessionId: String): EditGroupEvent()
|
||||||
|
data class ChangeName(val newName: String): EditGroupEvent()
|
||||||
}
|
}
|
||||||
|
|
||||||
data class EditGroupInviteViewState(
|
data class EditGroupInviteViewState(
|
||||||
|
@ -461,43 +616,57 @@ data class EditGroupInviteViewState(
|
||||||
val allContacts: Set<Contact>
|
val allContacts: Set<Contact>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun PreviewDialogChange(@PreviewParameter(ThemeResPreviewParameterProvider::class) styleRes: Int) {
|
||||||
|
|
||||||
|
PreviewTheme(themeResId = styleRes) {
|
||||||
|
EditClosedGroupView {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun PreviewList() {
|
fun PreviewList() {
|
||||||
|
|
||||||
val oneMember = MemberViewModel(
|
|
||||||
"Test User",
|
|
||||||
"05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234",
|
|
||||||
MemberState.InviteSent,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
val twoMember = MemberViewModel(
|
|
||||||
"Test User 2",
|
|
||||||
"05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1235",
|
|
||||||
MemberState.InviteFailed,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
val threeMember = MemberViewModel(
|
|
||||||
"Test User 3",
|
|
||||||
"05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1236",
|
|
||||||
MemberState.Member,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
|
|
||||||
val viewState = EditGroupViewState(
|
|
||||||
"Preview",
|
|
||||||
"This is a preview description",
|
|
||||||
listOf(oneMember, twoMember, threeMember),
|
|
||||||
true
|
|
||||||
)
|
|
||||||
|
|
||||||
PreviewTheme(themeResId = R.style.Classic_Dark) {
|
PreviewTheme(themeResId = R.style.Classic_Dark) {
|
||||||
|
|
||||||
|
val oneMember = MemberViewModel(
|
||||||
|
"Test User",
|
||||||
|
"05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234",
|
||||||
|
MemberState.InviteSent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
val twoMember = MemberViewModel(
|
||||||
|
"Test User 2",
|
||||||
|
"05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1235",
|
||||||
|
MemberState.InviteFailed,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
val threeMember = MemberViewModel(
|
||||||
|
"Test User 3",
|
||||||
|
"05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1236",
|
||||||
|
MemberState.Member,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
val viewState = EditGroupViewState(
|
||||||
|
"Preview",
|
||||||
|
"This is a preview description",
|
||||||
|
listOf(oneMember, twoMember, threeMember),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
|
||||||
EditGroupView(
|
EditGroupView(
|
||||||
onBack = {},
|
onBack = {},
|
||||||
onInvite = {},
|
onInvite = {},
|
||||||
onReinvite = {},
|
onReinvite = {},
|
||||||
onPromote = {},
|
onPromote = {},
|
||||||
onRemove = {},
|
onRemove = {},
|
||||||
|
onEditName = {},
|
||||||
viewState = viewState
|
viewState = viewState
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ val LocalPreviewMode = staticCompositionLocalOf { false }
|
||||||
|
|
||||||
data class ExtraColors(
|
data class ExtraColors(
|
||||||
val settingsBackground: Color,
|
val settingsBackground: Color,
|
||||||
|
val destructive: Color
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -35,6 +36,7 @@ fun AppTheme(
|
||||||
val extraColors = LocalContext.current.run {
|
val extraColors = LocalContext.current.run {
|
||||||
ExtraColors(
|
ExtraColors(
|
||||||
settingsBackground = getColorFromTheme(R.attr.colorSettingsBackground),
|
settingsBackground = getColorFromTheme(R.attr.colorSettingsBackground),
|
||||||
|
destructive = Color(getColor(R.color.destructive)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1080,4 +1080,8 @@
|
||||||
<string name="activity_create_closed_group_select_contacts">Select Contacts</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="activity_create_closed_group_add_account_or_ons">Add Account ID or ONS</string>
|
||||||
<string name="conversation_settings_leave_group_name">Are you sure you want to leave %1$s?</string>
|
<string name="conversation_settings_leave_group_name">Are you sure you want to leave %1$s?</string>
|
||||||
|
<string name="dialog_edit_group_information_title">Update Group Information</string>
|
||||||
|
<string name="dialog_edit_group_information_message">Group name and description is visible to all group members.</string>
|
||||||
|
<string name="dialog_edit_group_information_enter_group_name">Enter group name</string>
|
||||||
|
<string name="dialog_edit_group_information_enter_group_description">Enter group description</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -174,6 +174,7 @@ interface StorageProtocol {
|
||||||
fun handlePromoted(keyPair: KeyPair)
|
fun handlePromoted(keyPair: KeyPair)
|
||||||
fun handleMemberLeft(message: GroupUpdated, closedGroupId: SessionId)
|
fun handleMemberLeft(message: GroupUpdated, closedGroupId: SessionId)
|
||||||
fun leaveGroup(groupSessionId: String)
|
fun leaveGroup(groupSessionId: String)
|
||||||
|
fun setName(groupSessionId: String, newName: String)
|
||||||
|
|
||||||
// Groups
|
// Groups
|
||||||
fun getAllGroups(includeInactive: Boolean): List<GroupRecord>
|
fun getAllGroups(includeInactive: Boolean): List<GroupRecord>
|
||||||
|
|
Loading…
Reference in New Issue