mirror of
https://github.com/oxen-io/session-android.git
synced 2023-12-14 02:53:01 +01:00
feat: add info name and description updates
This commit is contained in:
parent
4b6a7c145e
commit
ae6307c2e2
5 changed files with 281 additions and 102 deletions
|
@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.database
|
|||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.google.protobuf.ByteString
|
||||
import kotlinx.coroutines.runBlocking
|
||||
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_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.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
|
||||
|
@ -72,6 +70,7 @@ import org.session.libsession.messaging.utilities.UpdateMessageData
|
|||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.session.libsession.snode.RawResponse
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
import org.session.libsession.snode.SnodeAPI.signingKeyCallback
|
||||
import org.session.libsession.snode.SnodeMessage
|
||||
import org.session.libsession.utilities.Address
|
||||
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.SignalServiceGroup
|
||||
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.GroupUpdateMemberChangeMessage
|
||||
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateMessage
|
||||
|
@ -1323,8 +1323,6 @@ open class Storage(
|
|||
|
||||
keysConfig.rekey(infoConfig, membersConfig)
|
||||
|
||||
val sentTimestamp = SnodeAPI.nowWithOffset
|
||||
|
||||
// build unrevocation, in case of re-adding members
|
||||
val unrevocation = SnodeAPI.buildAuthenticatedUnrevokeSubKeyBatchRequest(
|
||||
groupSessionId,
|
||||
|
@ -1333,52 +1331,35 @@ open class Storage(
|
|||
) ?: return Log.e("ClosedGroup", "Failed to build revocation update")
|
||||
|
||||
// Build and store the key update in group swarm
|
||||
val message = SnodeMessage(
|
||||
groupSessionId,
|
||||
Base64.encodeBytes(keysConfig.pendingConfig()!!), // should not be null from checking has pending
|
||||
SnodeMessage.CONFIG_TTL,
|
||||
sentTimestamp
|
||||
)
|
||||
val authenticatedBatch = SnodeAPI.buildAuthenticatedStoreBatchInfo(
|
||||
keysConfig.namespace(),
|
||||
message,
|
||||
adminKey
|
||||
val toDelete = mutableListOf<String>()
|
||||
|
||||
val signCallback = signingKeyCallback(adminKey)
|
||||
|
||||
val keyMessage = keysConfig.messageInformation(groupSessionId, adminKey)
|
||||
val infoMessage = infoConfig.messageInformation(toDelete, groupSessionId, adminKey)
|
||||
val membersMessage = membersConfig.messageInformation(toDelete, groupSessionId, adminKey)
|
||||
|
||||
val delete = SnodeAPI.buildAuthenticatedDeleteBatchInfo(
|
||||
groupSessionId,
|
||||
toDelete,
|
||||
signCallback
|
||||
)
|
||||
|
||||
val stores = listOf(keyMessage, infoMessage, membersMessage).map(ConfigurationSyncJob.ConfigMessageInformation::batch)
|
||||
|
||||
val response = SnodeAPI.getSingleTargetSnode(groupSessionId).bind { snode ->
|
||||
SnodeAPI.getRawBatchResponse(
|
||||
snode,
|
||||
groupSessionId,
|
||||
listOf(unrevocation, authenticatedBatch),
|
||||
stores + unrevocation + delete,
|
||||
sequence = true
|
||||
)
|
||||
}
|
||||
|
||||
val destination = Destination.ClosedGroup(groupSessionId)
|
||||
|
||||
try {
|
||||
response.get()
|
||||
// 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)
|
||||
|
||||
val job = InviteContactsJob(groupSessionId, filteredMembers.toTypedArray())
|
||||
|
@ -1388,7 +1369,7 @@ open class Storage(
|
|||
val messageToSign = "MEMBER_CHANGE${GroupUpdateMemberChangeMessage.Type.ADDED.name}$timestamp"
|
||||
val signature = SodiumUtilities.sign(messageToSign.toByteArray(), adminKey)
|
||||
val updatedMessage = GroupUpdated(
|
||||
DataMessage.GroupUpdateMessage.newBuilder()
|
||||
GroupUpdateMessage.newBuilder()
|
||||
.setMemberChangeMessage(
|
||||
GroupUpdateMemberChangeMessage.newBuilder()
|
||||
.addAllMemberSessionIds(filteredMembers)
|
||||
|
@ -1512,24 +1493,29 @@ open class Storage(
|
|||
removedMembers.map { keys.getSubAccountToken(SessionId.from(it)) }.toTypedArray()
|
||||
) ?: return Log.e("ClosedGroup", "Failed to build revocation update")
|
||||
|
||||
// Build and store the key update in group swarm
|
||||
val storeKeyMessage = SnodeMessage(
|
||||
keys.rekey(info, members)
|
||||
|
||||
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,
|
||||
Base64.encodeBytes(keys.pendingConfig()!!), // should not be null from checking has pending
|
||||
SnodeMessage.CONFIG_TTL,
|
||||
SnodeAPI.nowWithOffset
|
||||
)
|
||||
val authenticatedBatch = SnodeAPI.buildAuthenticatedStoreBatchInfo(
|
||||
keys.namespace(),
|
||||
storeKeyMessage,
|
||||
adminKey
|
||||
toDelete,
|
||||
signCallback
|
||||
)
|
||||
|
||||
val stores = listOf(keyMessage, infoMessage, membersMessage).map(ConfigurationSyncJob.ConfigMessageInformation::batch)
|
||||
|
||||
val response = SnodeAPI.getSingleTargetSnode(groupSessionId).bind { snode ->
|
||||
SnodeAPI.getRawBatchResponse(
|
||||
snode,
|
||||
groupSessionId,
|
||||
listOf(revocation, authenticatedBatch),
|
||||
stores + revocation + delete,
|
||||
sequence = true
|
||||
)
|
||||
}
|
||||
|
@ -1542,31 +1528,11 @@ open class Storage(
|
|||
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)
|
||||
info.free()
|
||||
members.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 messageToSign = "MEMBER_CHANGE${GroupUpdateMemberChangeMessage.Type.REMOVED.name}$timestamp"
|
||||
val signature = SodiumUtilities.sign(messageToSign.toByteArray(), adminKey)
|
||||
|
@ -1624,7 +1590,7 @@ open class Storage(
|
|||
if (closedGroup.hasAdminKey()) {
|
||||
// re-key and do a new config removing the previous member
|
||||
val adminKey = closedGroup.adminKey
|
||||
val signCallback = SnodeAPI.signingKeyCallback(adminKey)
|
||||
val signCallback = signingKeyCallback(adminKey)
|
||||
val info = configFactory.getGroupInfoConfig(closedGroupId) ?: return
|
||||
val members = configFactory.getGroupMemberConfig(closedGroupId) ?: 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 signature = SodiumUtilities.sign(messageToSign.toByteArray(), adminKey)
|
||||
val updatedMessage = GroupUpdated(
|
||||
DataMessage.GroupUpdateMessage.newBuilder()
|
||||
GroupUpdateMessage.newBuilder()
|
||||
.setMemberChangeMessage(
|
||||
GroupUpdateMemberChangeMessage.newBuilder()
|
||||
.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>) {
|
||||
return DatabaseComponent.get(context).lokiAPIDatabase().setServerCapabilities(server, capabilities)
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@ import android.content.Context
|
|||
import android.widget.Toast
|
||||
import androidx.compose.foundation.background
|
||||
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.Row
|
||||
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.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextButton
|
||||
|
@ -24,11 +27,15 @@ import androidx.compose.material.rememberScaffoldState
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
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.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
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.style.TextAlign
|
||||
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.sp
|
||||
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.ResultBackNavigator
|
||||
import com.ramcosta.composedestinations.result.ResultRecipient
|
||||
import com.ramcosta.composedestinations.spec.DestinationStyle
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
@ -60,9 +69,12 @@ import org.session.libsession.messaging.jobs.InviteContactsJob
|
|||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
import org.thoughtcrime.securesms.groups.ContactList
|
||||
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.LocalExtraColors
|
||||
import org.thoughtcrime.securesms.ui.NavigationBar
|
||||
import org.thoughtcrime.securesms.ui.PreviewTheme
|
||||
import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider
|
||||
|
||||
@EditGroupNavGraph(start = true)
|
||||
@Composable
|
||||
|
@ -70,6 +82,7 @@ import org.thoughtcrime.securesms.ui.PreviewTheme
|
|||
fun EditClosedGroupScreen(
|
||||
navigator: DestinationsNavigator,
|
||||
resultSelectContact: ResultRecipient<EditClosedGroupInviteScreenDestination, ContactList>,
|
||||
resultEditName: ResultRecipient<EditClosedGroupNameScreenDestination, String>,
|
||||
viewModel: EditGroupViewModel,
|
||||
onFinish: () -> Unit
|
||||
) {
|
||||
|
@ -84,6 +97,12 @@ fun EditClosedGroupScreen(
|
|||
}
|
||||
}
|
||||
|
||||
resultEditName.onNavResult { navResult ->
|
||||
if (navResult is NavResult.Value) {
|
||||
eventSink(EditGroupEvent.ChangeName(navResult.value))
|
||||
}
|
||||
}
|
||||
|
||||
EditGroupView(
|
||||
onBack = {
|
||||
onFinish()
|
||||
|
@ -100,10 +119,125 @@ fun EditClosedGroupScreen(
|
|||
onRemove = { contact ->
|
||||
eventSink(EditGroupEvent.RemoveContact(contact))
|
||||
},
|
||||
onEditName = {
|
||||
navigator.navigate(EditClosedGroupNameScreenDestination)
|
||||
},
|
||||
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
|
||||
@Composable
|
||||
@Destination
|
||||
|
@ -198,6 +332,9 @@ class EditGroupViewModel @AssistedInject constructor(
|
|||
is EditGroupEvent.RemoveContact -> {
|
||||
storage.removeMember(groupSessionId, arrayOf(event.contactSessionId))
|
||||
}
|
||||
is EditGroupEvent.ChangeName -> {
|
||||
storage.setName(groupSessionId, event.newName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -254,6 +391,7 @@ fun EditGroupView(
|
|||
onReinvite: (String)->Unit,
|
||||
onPromote: (String)->Unit,
|
||||
onRemove: (String)->Unit,
|
||||
onEditName: ()->Unit,
|
||||
viewState: EditGroupViewState,
|
||||
) {
|
||||
val scaffoldState = rememberScaffoldState()
|
||||
|
@ -270,15 +408,31 @@ fun EditGroupView(
|
|||
) { paddingValues ->
|
||||
Column(modifier = Modifier.padding(paddingValues)) {
|
||||
// Group name title
|
||||
Text(
|
||||
text = viewState.groupName,
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
fontSize = 26.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
horizontalArrangement = Arrangement.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
|
||||
|
||||
// Invite
|
||||
|
@ -454,6 +608,7 @@ sealed class EditGroupEvent {
|
|||
data class ReInviteContact(val contactSessionId: String): EditGroupEvent()
|
||||
data class PromoteContact(val contactSessionId: String): EditGroupEvent()
|
||||
data class RemoveContact(val contactSessionId: String): EditGroupEvent()
|
||||
data class ChangeName(val newName: String): EditGroupEvent()
|
||||
}
|
||||
|
||||
data class EditGroupInviteViewState(
|
||||
|
@ -461,43 +616,57 @@ data class EditGroupInviteViewState(
|
|||
val allContacts: Set<Contact>
|
||||
)
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewDialogChange(@PreviewParameter(ThemeResPreviewParameterProvider::class) styleRes: Int) {
|
||||
|
||||
PreviewTheme(themeResId = styleRes) {
|
||||
EditClosedGroupView {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
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) {
|
||||
|
||||
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(
|
||||
onBack = {},
|
||||
onInvite = {},
|
||||
onReinvite = {},
|
||||
onPromote = {},
|
||||
onRemove = {},
|
||||
onEditName = {},
|
||||
viewState = viewState
|
||||
)
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ val LocalPreviewMode = staticCompositionLocalOf { false }
|
|||
|
||||
data class ExtraColors(
|
||||
val settingsBackground: Color,
|
||||
val destructive: Color
|
||||
)
|
||||
|
||||
/**
|
||||
|
@ -35,6 +36,7 @@ fun AppTheme(
|
|||
val extraColors = LocalContext.current.run {
|
||||
ExtraColors(
|
||||
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_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="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>
|
||||
|
|
|
@ -174,6 +174,7 @@ interface StorageProtocol {
|
|||
fun handlePromoted(keyPair: KeyPair)
|
||||
fun handleMemberLeft(message: GroupUpdated, closedGroupId: SessionId)
|
||||
fun leaveGroup(groupSessionId: String)
|
||||
fun setName(groupSessionId: String, newName: String)
|
||||
|
||||
// Groups
|
||||
fun getAllGroups(includeInactive: Boolean): List<GroupRecord>
|
||||
|
|
Loading…
Reference in a new issue