UI adjustment

This commit is contained in:
SessionHero01 2024-10-24 11:02:34 +11:00
parent 74f7bbb6d5
commit 6e1fa1b257
No known key found for this signature in database
10 changed files with 245 additions and 238 deletions

View File

@ -30,6 +30,7 @@ import org.session.libsession.database.StorageProtocol
import org.session.libsession.messaging.groups.GroupManagerV2 import org.session.libsession.messaging.groups.GroupManagerV2
import org.session.libsession.utilities.ConfigUpdateNotification import org.session.libsession.utilities.ConfigUpdateNotification
import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.AccountId
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.dependencies.ConfigFactory
const val MAX_GROUP_NAME_LENGTH = 100 const val MAX_GROUP_NAME_LENGTH = 100
@ -45,6 +46,11 @@ class EditGroupViewModel @AssistedInject constructor(
// Input/Output state // Input/Output state
private val mutableEditingName = MutableStateFlow<String?>(null) private val mutableEditingName = MutableStateFlow<String?>(null)
// Input/Output: the name that has been written and submitted for change to push to the server,
// but not yet confirmed by the server. When this state is present, it takes precedence over
// the group name in the group info.
private val mutablePendingEditedName = MutableStateFlow<String?>(null)
// Input: invite/promote member's intermediate states. This is needed because we don't have // Input: invite/promote member's intermediate states. This is needed because we don't have
// a state that we can map into in the config system. The config system only provides "sent", "failed", etc. // a state that we can map into in the config system. The config system only provides "sent", "failed", etc.
// The intermediate states are needed to show the user that the operation is in progress, and the // The intermediate states are needed to show the user that the operation is in progress, and the
@ -94,8 +100,8 @@ class EditGroupViewModel @AssistedInject constructor(
.stateIn(viewModelScope, SharingStarted.Eagerly, false) .stateIn(viewModelScope, SharingStarted.Eagerly, false)
// Output: The name of the group. This is the current name of the group, not the name being edited. // Output: The name of the group. This is the current name of the group, not the name being edited.
val groupName: StateFlow<String> = groupInfo val groupName: StateFlow<String> = combine(groupInfo
.map { it?.first?.name.orEmpty() } .map { it?.first?.name.orEmpty() }, mutablePendingEditedName) { name, pendingName -> pendingName ?: name }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), "") .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), "")
// Output: the list of the members and their state in the group. // Output: the list of the members and their state in the group.
@ -260,11 +266,22 @@ class EditGroupViewModel @AssistedInject constructor(
fun onEditNameConfirmClicked() { fun onEditNameConfirmClicked() {
val newName = mutableEditingName.value val newName = mutableEditingName.value
if (newName.isNullOrBlank()) {
return
}
// Move the edited name into the pending state
mutableEditingName.value = null
mutablePendingEditedName.value = newName
performGroupOperation { performGroupOperation {
if (!newName.isNullOrBlank()) { try {
groupManager.setName(groupId, newName) groupManager.setName(groupId, newName)
mutableEditingName.value = null } finally {
// As soon as the operation is done, clear the pending state,
// no matter if it's successful or not. So that we update the UI to reflect the
// real state.
mutablePendingEditedName.value = null
} }
} }
} }

View File

@ -11,7 +11,6 @@ import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_VISIBLE import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_VISIBLE
import network.loki.messenger.libsession_util.util.Conversation import network.loki.messenger.libsession_util.util.Conversation
@ -323,12 +322,39 @@ class GroupManagerV2Impl @Inject constructor(
removedMembers: List<AccountId>, removedMembers: List<AccountId>,
removeMessages: Boolean removeMessages: Boolean
) { ) {
val adminKey = requireAdminAccess(groupAccountId)
// Update the config to mark this member as "removed"
flagMembersForRemoval( flagMembersForRemoval(
group = groupAccountId, group = groupAccountId,
groupAdminKey = adminKey,
members = removedMembers, members = removedMembers,
alsoRemoveMembersMessage = removeMessages, alsoRemoveMembersMessage = removeMessages,
sendMemberChangeMessage = true
) )
val timestamp = clock.currentTimeMills()
val signature = SodiumUtilities.sign(
buildMemberChangeSignature(
GroupUpdateMemberChangeMessage.Type.REMOVED,
timestamp
),
adminKey
)
val updateMessage = GroupUpdateMessage.newBuilder()
.setMemberChangeMessage(
GroupUpdateMemberChangeMessage.newBuilder()
.addAllMemberSessionIds(removedMembers.map { it.hexString })
.setType(GroupUpdateMemberChangeMessage.Type.REMOVED)
.setAdminSignature(ByteString.copyFrom(signature))
)
.build()
val message = GroupUpdated(
updateMessage
).apply { sentTimestamp = timestamp }
MessageSender.send(message, Destination.ClosedGroup(groupAccountId.hexString), false).await()
storage.insertGroupInfoChange(message, groupAccountId)
} }
override suspend fun removeMemberMessages( override suspend fun removeMemberMessages(
@ -362,32 +388,17 @@ class GroupManagerV2Impl @Inject constructor(
SnodeAPI.deleteMessage(groupAccountId.hexString, groupAdminAuth, messagesToDelete) SnodeAPI.deleteMessage(groupAccountId.hexString, groupAdminAuth, messagesToDelete)
} }
override suspend fun handleMemberLeft(message: GroupUpdated, group: AccountId) { override suspend fun handleMemberLeftMessage(memberId: AccountId, group: AccountId) {
val closedGroup = configFactory.getClosedGroup(group) ?: return val closedGroup = configFactory.getClosedGroup(group) ?: return
val groupAdminKey = closedGroup.adminKey
if (closedGroup.hasAdminKey()) { if (groupAdminKey != null) {
flagMembersForRemoval( flagMembersForRemoval(
group = group, group = group,
members = listOf(AccountId(message.sender!!)), groupAdminKey = groupAdminKey,
members = listOf(memberId),
alsoRemoveMembersMessage = false, alsoRemoveMembersMessage = false,
sendMemberChangeMessage = false
) )
} else {
val hasAnyAdminRemaining = configFactory.withGroupConfigs(group) { configs ->
configs.groupMembers.all()
.asSequence()
.filterNot { it.sessionId == message.sender }
.any { it.admin && !it.removed }
}
// if the leaving member is last admin, disable the group and remove it
// This is just to emulate the "existing" group behaviour, this will probably be removed in future
if (!hasAnyAdminRemaining) {
pollerFactory.pollerFor(group)?.stop()
storage.getThreadId(Address.fromSerialized(group.hexString))
?.let(storage::deleteConversation)
configFactory.removeGroup(group)
}
} }
} }
@ -530,15 +541,17 @@ class GroupManagerV2Impl @Inject constructor(
storage.insertGroupInfoChange(message, group) storage.insertGroupInfoChange(message, group)
} }
private suspend fun flagMembersForRemoval( /**
* Mark this member as "removed" in the group config.
*
* [RemoveGroupMemberHandler] should be able to pick up the config changes and remove the member from the group.
*/
private fun flagMembersForRemoval(
group: AccountId, group: AccountId,
groupAdminKey: ByteArray, // Not used ATM required here for verification purpose
members: List<AccountId>, members: List<AccountId>,
alsoRemoveMembersMessage: Boolean, alsoRemoveMembersMessage: Boolean,
sendMemberChangeMessage: Boolean
) { ) {
val adminKey = requireAdminAccess(group)
// 1. Mark the members as removed in the group configs
configFactory.withMutableGroupConfigs(group) { configs -> configFactory.withMutableGroupConfigs(group) { configs ->
for (member in members) { for (member in members) {
val memberConfig = configs.groupMembers.get(member.hexString) val memberConfig = configs.groupMembers.get(member.hexString)
@ -547,33 +560,6 @@ class GroupManagerV2Impl @Inject constructor(
} }
} }
} }
// 2. Send a member change message
if (sendMemberChangeMessage) {
val timestamp = clock.currentTimeMills()
val signature = SodiumUtilities.sign(
buildMemberChangeSignature(
GroupUpdateMemberChangeMessage.Type.REMOVED,
timestamp
),
adminKey
)
val updateMessage = GroupUpdateMessage.newBuilder()
.setMemberChangeMessage(
GroupUpdateMemberChangeMessage.newBuilder()
.addAllMemberSessionIds(members.map { it.hexString })
.setType(GroupUpdateMemberChangeMessage.Type.REMOVED)
.setAdminSignature(ByteString.copyFrom(signature))
)
.build()
val message = GroupUpdated(
updateMessage
).apply { sentTimestamp = timestamp }
MessageSender.send(message, Destination.ClosedGroup(group.hexString), false).await()
storage.insertGroupInfoChange(message, group)
}
} }
override suspend fun respondToInvitation(groupId: AccountId, approved: Boolean) = override suspend fun respondToInvitation(groupId: AccountId, approved: Boolean) =

View File

@ -6,8 +6,10 @@ import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
@ -44,7 +46,9 @@ import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.AccountId
import org.thoughtcrime.securesms.groups.ContactItem import org.thoughtcrime.securesms.groups.ContactItem
import org.thoughtcrime.securesms.ui.Avatar import org.thoughtcrime.securesms.ui.Avatar
import org.thoughtcrime.securesms.ui.components.RadioButton
import org.thoughtcrime.securesms.ui.theme.LocalColors import org.thoughtcrime.securesms.ui.theme.LocalColors
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
import org.thoughtcrime.securesms.ui.theme.LocalType import org.thoughtcrime.securesms.ui.theme.LocalType
import org.thoughtcrime.securesms.ui.theme.PreviewTheme import org.thoughtcrime.securesms.ui.theme.PreviewTheme
@ -73,31 +77,26 @@ fun LazyListScope.multiSelectMemberList(
onContactItemClicked: (accountId: AccountId) -> Unit, onContactItemClicked: (accountId: AccountId) -> Unit,
enabled: Boolean = true, enabled: Boolean = true,
) { ) {
items(contacts) { contact -> items(contacts.size) { index ->
Column { val contact = contacts[index]
Row( Column(modifier = modifier) {
modifier = modifier if (index == 0) {
.fillMaxWidth() // Show top divider for the first item only
.toggleable( HorizontalDivider(color = LocalColors.current.borders)
}
RadioButton(
onClick = { onContactItemClicked(contact.accountID) },
selected = contact.selected,
enabled = enabled, enabled = enabled,
value = contact.selected, contentPadding = PaddingValues(
onValueChange = { onContactItemClicked(contact.accountID) }, vertical = LocalDimensions.current.xxsSpacing,
role = Role.Checkbox horizontal = LocalDimensions.current.smallSpacing
) )
.padding(vertical = 8.dp, horizontal = 24.dp),
verticalAlignment = CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
ContactPhoto( ContactPhoto(contact.accountID)
contact.accountID, Spacer(modifier = Modifier.size(LocalDimensions.current.smallSpacing))
)
MemberName(name = contact.name) MemberName(name = contact.name)
Checkbox(
checked = contact.selected,
onCheckedChange = null,
colors = CheckboxDefaults.colors(checkedColor = LocalColors.current.primary),
enabled = enabled,
)
} }
HorizontalDivider(color = LocalColors.current.borders) HorizontalDivider(color = LocalColors.current.borders)
@ -105,15 +104,14 @@ fun LazyListScope.multiSelectMemberList(
} }
} }
val MemberNameStyle = TextStyle(fontWeight = FontWeight.Bold)
@Composable @Composable
fun RowScope.MemberName( fun RowScope.MemberName(
name: String, name: String,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) = Text( ) = Text(
text = name, text = name,
style = MemberNameStyle, style = LocalType.current.h8,
color = LocalColors.current.text,
modifier = modifier modifier = modifier
.weight(1f) .weight(1f)
.align(CenterVertically) .align(CenterVertically)
@ -121,7 +119,7 @@ fun RowScope.MemberName(
@Composable @Composable
fun RowScope.ContactPhoto(sessionId: AccountId) { fun ContactPhoto(sessionId: AccountId) {
return if (LocalInspectionMode.current) { return if (LocalInspectionMode.current) {
Image( Image(
painterResource(id = R.drawable.ic_profile_default), painterResource(id = R.drawable.ic_profile_default),

View File

@ -3,11 +3,14 @@ package org.thoughtcrime.securesms.groups.compose
import android.widget.Toast import android.widget.Toast
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement 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.PaddingValues
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@ -71,7 +74,6 @@ fun CreateGroupScreen(
items = viewModel.selectContactsViewModel.contacts.collectAsState().value, items = viewModel.selectContactsViewModel.contacts.collectAsState().value,
onCreateClicked = viewModel::onCreateClicked, onCreateClicked = viewModel::onCreateClicked,
onBack = onBack, onBack = onBack,
onClose = onClose,
) )
} }
@ -88,29 +90,35 @@ fun CreateGroup(
items: List<ContactItem>, items: List<ContactItem>,
onCreateClicked: () -> Unit, onCreateClicked: () -> Unit,
onBack: () -> Unit, onBack: () -> Unit,
onClose: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
Column( Scaffold(
modifier = modifier.padding(bottom = LocalDimensions.current.mediumSpacing), containerColor = LocalColors.current.backgroundSecondary,
horizontalAlignment = Alignment.CenterHorizontally, topBar = {
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
BackAppBar( BackAppBar(
title = stringResource(id = R.string.groupCreate), title = stringResource(id = R.string.groupCreate),
backgroundColor = LocalColors.current.backgroundSecondary,
onBack = onBack, onBack = onBack,
) )
}
) { paddings ->
Box(modifier = modifier.padding(paddings),) {
Column(
modifier = modifier.padding(vertical = LocalDimensions.current.spacing),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.spacing)
) {
SessionOutlinedTextField( SessionOutlinedTextField(
text = groupName, text = groupName,
onChange = onGroupNameChanged, onChange = onGroupNameChanged,
placeholder = stringResource(R.string.groupNameEnter), placeholder = stringResource(R.string.groupNameEnter),
textStyle = LocalType.current.base, textStyle = LocalType.current.base,
modifier = Modifier.padding(horizontal = 16.dp), modifier = Modifier.padding(horizontal = LocalDimensions.current.spacing),
error = groupNameError.takeIf { it.isNotBlank() }, error = groupNameError.takeIf { it.isNotBlank() },
enabled = !showLoading, enabled = !showLoading,
innerPadding = PaddingValues(LocalDimensions.current.smallSpacing),
onContinue = focusManager::clearFocus onContinue = focusManager::clearFocus
) )
@ -118,7 +126,7 @@ fun CreateGroup(
query = contactSearchQuery, query = contactSearchQuery,
onValueChanged = onContactSearchQueryChanged, onValueChanged = onContactSearchQueryChanged,
placeholder = stringResource(R.string.searchContacts), placeholder = stringResource(R.string.searchContacts),
modifier = Modifier.padding(horizontal = 16.dp), modifier = Modifier.padding(horizontal = LocalDimensions.current.spacing),
enabled = !showLoading enabled = !showLoading
) )
@ -130,7 +138,10 @@ fun CreateGroup(
) )
} }
PrimaryOutlineButton(onClick = onCreateClicked, modifier = Modifier.widthIn(min = 120.dp)) { PrimaryOutlineButton(
onClick = onCreateClicked,
modifier = Modifier.widthIn(min = 120.dp)
) {
LoadingArcOr(loading = showLoading) { LoadingArcOr(loading = showLoading) {
Text(stringResource(R.string.create)) Text(stringResource(R.string.create))
} }
@ -138,6 +149,10 @@ fun CreateGroup(
} }
} }
}
}
@Preview @Preview
@Composable @Composable
private fun CreateGroupPreview( private fun CreateGroupPreview(
@ -150,18 +165,17 @@ private fun CreateGroupPreview(
PreviewTheme { PreviewTheme {
CreateGroup( CreateGroup(
modifier = Modifier.background(LocalColors.current.backgroundSecondary), groupName = "",
groupName = "Group Name",
onGroupNameChanged = {}, onGroupNameChanged = {},
groupNameError = "",
contactSearchQuery = "", contactSearchQuery = "",
onContactSearchQueryChanged = {}, onContactSearchQueryChanged = {},
onContactItemClicked = {}, onContactItemClicked = {},
items = previewMembers,
onBack = {},
onClose = {},
onCreateClicked = {},
showLoading = false, showLoading = false,
groupNameError = "", items = previewMembers,
onCreateClicked = {},
onBack = {},
modifier = Modifier.background(LocalColors.current.backgroundSecondary),
) )
} }

View File

@ -1,13 +1,13 @@
package org.thoughtcrime.securesms.groups.compose package org.thoughtcrime.securesms.groups.compose
import android.widget.Toast
import androidx.compose.animation.animateContentSize import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
@ -15,14 +15,10 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SheetState
import androidx.compose.material3.Snackbar
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -50,14 +46,15 @@ import org.thoughtcrime.securesms.groups.GroupMemberState
import org.thoughtcrime.securesms.ui.AlertDialog import org.thoughtcrime.securesms.ui.AlertDialog
import org.thoughtcrime.securesms.ui.DialogButtonModel import org.thoughtcrime.securesms.ui.DialogButtonModel
import org.thoughtcrime.securesms.ui.GetString import org.thoughtcrime.securesms.ui.GetString
import org.thoughtcrime.securesms.ui.components.ActionAppBar import org.thoughtcrime.securesms.ui.components.BackAppBar
import org.thoughtcrime.securesms.ui.components.AppBarBackIcon import org.thoughtcrime.securesms.ui.components.BottomOptionsDialog
import org.thoughtcrime.securesms.ui.components.BottomOptionsDialogItem
import org.thoughtcrime.securesms.ui.components.PrimaryOutlineButton import org.thoughtcrime.securesms.ui.components.PrimaryOutlineButton
import org.thoughtcrime.securesms.ui.components.SessionOutlinedTextField import org.thoughtcrime.securesms.ui.components.SessionOutlinedTextField
import org.thoughtcrime.securesms.ui.theme.LocalColors import org.thoughtcrime.securesms.ui.theme.LocalColors
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
import org.thoughtcrime.securesms.ui.theme.LocalType import org.thoughtcrime.securesms.ui.theme.LocalType
import org.thoughtcrime.securesms.ui.theme.PreviewTheme import org.thoughtcrime.securesms.ui.theme.PreviewTheme
import org.thoughtcrime.securesms.ui.theme.bold
@Composable @Composable
fun EditGroupScreen( fun EditGroupScreen(
@ -130,9 +127,7 @@ fun EditGroup(
showingError: String?, showingError: String?,
onErrorDismissed: () -> Unit, onErrorDismissed: () -> Unit,
) { ) {
val sheetState = rememberModalBottomSheetState() val (showingOptionsDialogForMember, setShowingBottomModelForMember) = remember {
val (showingBottomModelForMember, setShowingBottomModelForMember) = remember {
mutableStateOf<GroupMemberState?>(null) mutableStateOf<GroupMemberState?>(null)
} }
@ -142,20 +137,9 @@ fun EditGroup(
Scaffold( Scaffold(
topBar = { topBar = {
ActionAppBar( BackAppBar(
title = stringResource(id = R.string.groupEdit), title = stringResource(id = R.string.groupEdit),
navigationIcon = { onBack = onBackClick,
AppBarBackIcon(onBack = onBackClick)
},
actions = {
TextButton(onClick = onBackClick) {
Text(
text = stringResource(id = R.string.done),
color = LocalColors.current.text,
style = LocalType.current.large.bold()
)
}
},
) )
} }
) { paddingValues -> ) { paddingValues ->
@ -168,8 +152,11 @@ fun EditGroup(
modifier = Modifier modifier = Modifier
.animateContentSize() .animateContentSize()
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp), .padding(LocalDimensions.current.smallSpacing),
horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally), horizontalArrangement = Arrangement.spacedBy(
LocalDimensions.current.xxxsSpacing,
Alignment.CenterHorizontally
),
verticalAlignment = CenterVertically, verticalAlignment = CenterVertically,
) { ) {
if (editingName != null) { if (editingName != null) {
@ -185,7 +172,11 @@ fun EditGroup(
modifier = Modifier.width(180.dp), modifier = Modifier.width(180.dp),
text = editingName, text = editingName,
onChange = onEditingNameValueChanged, onChange = onEditingNameValueChanged,
textStyle = LocalType.current.large textStyle = LocalType.current.h8,
innerPadding = PaddingValues(
horizontal = LocalDimensions.current.spacing,
vertical = LocalDimensions.current.smallSpacing
)
) )
IconButton(onClick = onEditNameConfirmed) { IconButton(onClick = onEditNameConfirmed) {
@ -198,7 +189,7 @@ fun EditGroup(
} else { } else {
Text( Text(
text = groupName, text = groupName,
style = LocalType.current.h3, style = LocalType.current.h4,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
) )
@ -242,6 +233,7 @@ fun EditGroup(
MemberItem( MemberItem(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
member = member, member = member,
clickable = member.canEdit,
onClick = { setShowingBottomModelForMember(member) } onClick = { setShowingBottomModelForMember(member) }
) )
} }
@ -249,27 +241,26 @@ fun EditGroup(
} }
} }
if (showingBottomModelForMember != null) { if (showingOptionsDialogForMember != null) {
MemberModalBottomSheetOptions( MemberOptionsDialog(
onDismissRequest = { setShowingBottomModelForMember(null) }, onDismissRequest = { setShowingBottomModelForMember(null) },
sheetState = sheetState,
onRemove = { onRemove = {
setShowingConfirmRemovingMember(showingBottomModelForMember) setShowingConfirmRemovingMember(showingOptionsDialogForMember)
setShowingBottomModelForMember(null) setShowingBottomModelForMember(null)
}, },
onPromote = { onPromote = {
setShowingBottomModelForMember(null) setShowingBottomModelForMember(null)
onPromoteClick(showingBottomModelForMember.accountId) onPromoteClick(showingOptionsDialogForMember.accountId)
}, },
onResendInvite = { onResendInvite = {
setShowingBottomModelForMember(null) setShowingBottomModelForMember(null)
onResendInviteClick(showingBottomModelForMember.accountId) onResendInviteClick(showingOptionsDialogForMember.accountId)
}, },
onResendPromotion = { onResendPromotion = {
setShowingBottomModelForMember(null) setShowingBottomModelForMember(null)
onResendPromotionClick(showingBottomModelForMember.accountId) onResendPromotionClick(showingOptionsDialogForMember.accountId)
}, },
member = showingBottomModelForMember, member = showingOptionsDialogForMember,
) )
} }
@ -284,17 +275,13 @@ fun EditGroup(
) )
} }
if (!showingError.isNullOrEmpty()) { val context = LocalContext.current
Snackbar(
dismissAction = { LaunchedEffect(showingError) {
TextButton(onClick = onErrorDismissed) { if (showingError != null) {
Text(text = stringResource(id = R.string.dismiss)) Toast.makeText(context, showingError, Toast.LENGTH_SHORT).show()
onErrorDismissed()
} }
},
content = {
Text(text = showingError)
}
)
} }
} }
@ -328,80 +315,81 @@ private fun ConfirmRemovingMemberDialog(
) )
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun MemberModalBottomSheetOptions( private fun MemberOptionsDialog(
member: GroupMemberState, member: GroupMemberState,
onRemove: () -> Unit, onRemove: () -> Unit,
onPromote: () -> Unit, onPromote: () -> Unit,
onResendInvite: () -> Unit, onResendInvite: () -> Unit,
onResendPromotion: () -> Unit, onResendPromotion: () -> Unit,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
sheetState: SheetState,
) { ) {
ModalBottomSheet(
onDismissRequest = onDismissRequest,
sheetState = sheetState,
) {
if (member.canRemove) {
val context = LocalContext.current val context = LocalContext.current
MemberModalBottomSheetOptionItem(
onClick = onRemove, val options = remember(member) {
text = context.resources.getQuantityString(R.plurals.groupRemoveUserOnly, 1) buildList {
if (member.canRemove) {
this += BottomOptionsDialogItem(
title = context.resources.getQuantityString(R.plurals.groupRemoveUserOnly, 1),
iconRes = R.drawable.ic_delete,
onClick = onRemove
) )
} }
if (member.canPromote) { if (member.canPromote) {
MemberModalBottomSheetOptionItem( this += BottomOptionsDialogItem(
onClick = onPromote, title = context.getString(R.string.adminPromoteToAdmin),
text = stringResource(R.string.adminPromoteToAdmin) iconRes = R.drawable.ic_profile_default,
onClick = onPromote
) )
} }
if (member.canResendInvite) { if (member.canResendInvite) {
MemberModalBottomSheetOptionItem(onClick = onResendInvite, text = "Resend invite") this += BottomOptionsDialogItem(
title = "Resend invite",
iconRes = R.drawable.ic_arrow_left,
onClick = onResendInvite
)
} }
if (member.canResendPromotion) { if (member.canResendPromotion) {
MemberModalBottomSheetOptionItem(onClick = onResendPromotion, text = "Resend promotion") this += BottomOptionsDialogItem(
title = "Resend promotion",
iconRes = R.drawable.ic_arrow_left,
onClick = onResendPromotion
)
} }
Spacer(modifier = Modifier.height(32.dp))
} }
} }
@Composable BottomOptionsDialog(
private fun MemberModalBottomSheetOptionItem( items = options,
text: String, onDismissRequest = onDismissRequest
onClick: () -> Unit
) {
Text(
modifier = Modifier
.clickable(onClick = onClick)
.padding(16.dp)
.fillMaxWidth(),
style = LocalType.current.base,
text = text,
color = LocalColors.current.text,
) )
} }
@Composable @Composable
private fun MemberItem( private fun MemberItem(
clickable: Boolean,
onClick: (accountId: AccountId) -> Unit, onClick: (accountId: AccountId) -> Unit,
member: GroupMemberState, member: GroupMemberState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Row( Row(
modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp), modifier = modifier
horizontalArrangement = Arrangement.spacedBy(16.dp), .clickable(enabled = clickable, onClick = { onClick(member.accountId) })
.padding(
horizontal = LocalDimensions.current.smallSpacing,
vertical = LocalDimensions.current.xsSpacing
),
horizontalArrangement = Arrangement.spacedBy(LocalDimensions.current.smallSpacing),
verticalAlignment = CenterVertically, verticalAlignment = CenterVertically,
) { ) {
ContactPhoto(member.accountId) ContactPhoto(member.accountId)
Column( Column(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(4.dp) verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.xxxsSpacing)
) { ) {
Text( Text(
@ -424,7 +412,6 @@ private fun MemberItem(
} }
if (member.canEdit) { if (member.canEdit) {
IconButton(onClick = { onClick(member.accountId) }) {
Icon( Icon(
painter = painterResource(R.drawable.ic_circle_dot_dot_dot), painter = painterResource(R.drawable.ic_circle_dot_dot_dot),
contentDescription = stringResource(R.string.AccessibilityId_sessionSettings) contentDescription = stringResource(R.string.AccessibilityId_sessionSettings)
@ -432,7 +419,6 @@ private fun MemberItem(
} }
} }
} }
}
@Preview @Preview

View File

@ -544,7 +544,7 @@ fun SearchBar(
painterResource(id = R.drawable.ic_search_24), painterResource(id = R.drawable.ic_search_24),
contentDescription = null, contentDescription = null,
colorFilter = ColorFilter.tint( colorFilter = ColorFilter.tint(
LocalColors.current.text LocalColors.current.textSecondary
), ),
modifier = Modifier modifier = Modifier
.padding(horizontal = 16.dp, vertical = 8.dp) .padding(horizontal = 16.dp, vertical = 8.dp)
@ -557,7 +557,7 @@ fun SearchBar(
Text( Text(
text = placeholder, text = placeholder,
color = LocalColors.current.textSecondary, color = LocalColors.current.textSecondary,
style = LocalType.current.base style = LocalType.current.xl
) )
} }
} }

View File

@ -8,6 +8,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
@ -91,6 +92,7 @@ fun SessionOutlinedTextField(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onChange: (String) -> Unit = {}, onChange: (String) -> Unit = {},
textStyle: TextStyle = LocalType.current.base, textStyle: TextStyle = LocalType.current.base,
innerPadding: PaddingValues = PaddingValues(LocalDimensions.current.spacing),
placeholder: String = "", placeholder: String = "",
onContinue: () -> Unit = {}, onContinue: () -> Unit = {},
error: String? = null, error: String? = null,
@ -122,7 +124,7 @@ fun SessionOutlinedTextField(
) )
.fillMaxWidth() .fillMaxWidth()
.wrapContentHeight() .wrapContentHeight()
.padding(LocalDimensions.current.spacing) .padding(innerPadding)
) { ) {
innerTextField() innerTextField()

View File

@ -42,7 +42,7 @@ interface GroupManagerV2 {
members: List<AccountId> members: List<AccountId>
) )
suspend fun handleMemberLeft(message: GroupUpdated, group: AccountId) suspend fun handleMemberLeftMessage(memberId: AccountId, group: AccountId)
suspend fun leaveGroup(groupId: suspend fun leaveGroup(groupId:
AccountId, deleteOnLeave: Boolean) AccountId, deleteOnLeave: Boolean)

View File

@ -57,7 +57,6 @@ import org.session.libsignal.crypto.ecc.DjbECPublicKey
import org.session.libsignal.crypto.ecc.ECKeyPair import org.session.libsignal.crypto.ecc.ECKeyPair
import org.session.libsignal.messages.SignalServiceGroup import org.session.libsignal.messages.SignalServiceGroup
import org.session.libsignal.protos.SignalServiceProtos import org.session.libsignal.protos.SignalServiceProtos
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateMemberChangeMessage
import org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage import org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage
import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.AccountId
import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Base64
@ -675,8 +674,12 @@ private fun handleMemberChange(message: GroupUpdated, closedGroup: AccountId) {
private fun handleMemberLeft(message: GroupUpdated, closedGroup: AccountId) { private fun handleMemberLeft(message: GroupUpdated, closedGroup: AccountId) {
GlobalScope.launch(Dispatchers.Default) { GlobalScope.launch(Dispatchers.Default) {
runCatching { try {
MessagingModuleConfiguration.shared.groupManagerV2.handleMemberLeft(message, closedGroup) MessagingModuleConfiguration.shared.groupManagerV2.handleMemberLeftMessage(
AccountId(message.sender!!), closedGroup
)
} catch (e: Exception) {
Log.e("GroupUpdated", "Failed to handle member left message", e)
} }
} }
} }

View File

@ -209,11 +209,12 @@ object UpdateMessageBuilder {
if (historyShared) R.string.groupMemberNewYouHistoryMultiple else R.string.groupInviteYouAndMoreNew) if (historyShared) R.string.groupMemberNewYouHistoryMultiple else R.string.groupInviteYouAndMoreNew)
.put(COUNT_KEY, updateData.sessionIds.size - 1) .put(COUNT_KEY, updateData.sessionIds.size - 1)
.format() .format()
else -> Phrase.from(context, number > 0 -> Phrase.from(context,
if (historyShared) R.string.groupMemberNewHistoryMultiple else R.string.groupMemberNewMultiple) if (historyShared) R.string.groupMemberNewHistoryMultiple else R.string.groupMemberNewMultiple)
.put(NAME_KEY, context.youOrSender(updateData.sessionIds.first())) .put(NAME_KEY, context.youOrSender(updateData.sessionIds.first()))
.put(COUNT_KEY, updateData.sessionIds.size - 1) .put(COUNT_KEY, updateData.sessionIds.size - 1)
.format() .format()
else -> ""
} }
} }