From 6e1fa1b2574ec1ed87d8b1c96c71748fedee260a Mon Sep 17 00:00:00 2001 From: SessionHero01 <180888785+SessionHero01@users.noreply.github.com> Date: Thu, 24 Oct 2024 11:02:34 +1100 Subject: [PATCH] UI adjustment --- .../securesms/groups/EditGroupViewModel.kt | 25 ++- .../securesms/groups/GroupManagerV2Impl.kt | 94 ++++----- .../securesms/groups/compose/Components.kt | 52 +++-- .../groups/compose/CreateGroupScreen.kt | 106 +++++----- .../groups/compose/EditGroupScreen.kt | 184 ++++++++---------- .../thoughtcrime/securesms/ui/Components.kt | 4 +- .../securesms/ui/components/Text.kt | 4 +- .../messaging/groups/GroupManagerV2.kt | 2 +- .../ReceivedMessageHandler.kt | 9 +- .../utilities/UpdateMessageBuilder.kt | 3 +- 10 files changed, 245 insertions(+), 238 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/EditGroupViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/EditGroupViewModel.kt index 18d81bab97..c955984cea 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/EditGroupViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/EditGroupViewModel.kt @@ -30,6 +30,7 @@ import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.groups.GroupManagerV2 import org.session.libsession.utilities.ConfigUpdateNotification import org.session.libsignal.utilities.AccountId +import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.dependencies.ConfigFactory const val MAX_GROUP_NAME_LENGTH = 100 @@ -45,6 +46,11 @@ class EditGroupViewModel @AssistedInject constructor( // Input/Output state private val mutableEditingName = MutableStateFlow(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(null) + // 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. // 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) // Output: The name of the group. This is the current name of the group, not the name being edited. - val groupName: StateFlow = groupInfo - .map { it?.first?.name.orEmpty() } + val groupName: StateFlow = combine(groupInfo + .map { it?.first?.name.orEmpty() }, mutablePendingEditedName) { name, pendingName -> pendingName ?: name } .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), "") // Output: the list of the members and their state in the group. @@ -260,11 +266,22 @@ class EditGroupViewModel @AssistedInject constructor( fun onEditNameConfirmClicked() { val newName = mutableEditingName.value + if (newName.isNullOrBlank()) { + return + } + + // Move the edited name into the pending state + mutableEditingName.value = null + mutablePendingEditedName.value = newName performGroupOperation { - if (!newName.isNullOrBlank()) { + try { 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 } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt index ae5d94f059..909d43ff27 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt @@ -11,7 +11,6 @@ import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first -import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.withContext import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_VISIBLE import network.loki.messenger.libsession_util.util.Conversation @@ -323,12 +322,39 @@ class GroupManagerV2Impl @Inject constructor( removedMembers: List, removeMessages: Boolean ) { + val adminKey = requireAdminAccess(groupAccountId) + + // Update the config to mark this member as "removed" flagMembersForRemoval( group = groupAccountId, + groupAdminKey = adminKey, members = removedMembers, 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( @@ -362,32 +388,17 @@ class GroupManagerV2Impl @Inject constructor( 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 groupAdminKey = closedGroup.adminKey - if (closedGroup.hasAdminKey()) { + if (groupAdminKey != null) { flagMembersForRemoval( group = group, - members = listOf(AccountId(message.sender!!)), + groupAdminKey = groupAdminKey, + members = listOf(memberId), 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) } - 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, + groupAdminKey: ByteArray, // Not used ATM required here for verification purpose members: List, alsoRemoveMembersMessage: Boolean, - sendMemberChangeMessage: Boolean ) { - val adminKey = requireAdminAccess(group) - - // 1. Mark the members as removed in the group configs configFactory.withMutableGroupConfigs(group) { configs -> for (member in members) { 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) = diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/Components.kt index 809656332c..d2df499aa6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/Components.kt @@ -6,8 +6,10 @@ import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding 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.thoughtcrime.securesms.groups.ContactItem 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.LocalDimensions import org.thoughtcrime.securesms.ui.theme.LocalType import org.thoughtcrime.securesms.ui.theme.PreviewTheme @@ -73,31 +77,26 @@ fun LazyListScope.multiSelectMemberList( onContactItemClicked: (accountId: AccountId) -> Unit, enabled: Boolean = true, ) { - items(contacts) { contact -> - Column { - Row( - modifier = modifier - .fillMaxWidth() - .toggleable( - enabled = enabled, - value = contact.selected, - onValueChange = { onContactItemClicked(contact.accountID) }, - role = Role.Checkbox - ) - .padding(vertical = 8.dp, horizontal = 24.dp), - verticalAlignment = CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) + items(contacts.size) { index -> + val contact = contacts[index] + Column(modifier = modifier) { + if (index == 0) { + // Show top divider for the first item only + HorizontalDivider(color = LocalColors.current.borders) + } + + RadioButton( + onClick = { onContactItemClicked(contact.accountID) }, + selected = contact.selected, + enabled = enabled, + contentPadding = PaddingValues( + vertical = LocalDimensions.current.xxsSpacing, + horizontal = LocalDimensions.current.smallSpacing + ) ) { - ContactPhoto( - contact.accountID, - ) + ContactPhoto(contact.accountID) + Spacer(modifier = Modifier.size(LocalDimensions.current.smallSpacing)) MemberName(name = contact.name) - Checkbox( - checked = contact.selected, - onCheckedChange = null, - colors = CheckboxDefaults.colors(checkedColor = LocalColors.current.primary), - enabled = enabled, - ) } HorizontalDivider(color = LocalColors.current.borders) @@ -105,15 +104,14 @@ fun LazyListScope.multiSelectMemberList( } } -val MemberNameStyle = TextStyle(fontWeight = FontWeight.Bold) - @Composable fun RowScope.MemberName( name: String, modifier: Modifier = Modifier ) = Text( text = name, - style = MemberNameStyle, + style = LocalType.current.h8, + color = LocalColors.current.text, modifier = modifier .weight(1f) .align(CenterVertically) @@ -121,7 +119,7 @@ fun RowScope.MemberName( @Composable -fun RowScope.ContactPhoto(sessionId: AccountId) { +fun ContactPhoto(sessionId: AccountId) { return if (LocalInspectionMode.current) { Image( painterResource(id = R.drawable.ic_profile_default), diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/CreateGroupScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/CreateGroupScreen.kt index a5bea99e6d..9b138ca8cd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/CreateGroupScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/CreateGroupScreen.kt @@ -3,11 +3,14 @@ package org.thoughtcrime.securesms.groups.compose import android.widget.Toast import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -71,7 +74,6 @@ fun CreateGroupScreen( items = viewModel.selectContactsViewModel.contacts.collectAsState().value, onCreateClicked = viewModel::onCreateClicked, onBack = onBack, - onClose = onClose, ) } @@ -88,54 +90,67 @@ fun CreateGroup( items: List, onCreateClicked: () -> Unit, onBack: () -> Unit, - onClose: () -> Unit, modifier: Modifier = Modifier ) { val focusManager = LocalFocusManager.current - Column( - modifier = modifier.padding(bottom = LocalDimensions.current.mediumSpacing), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - BackAppBar( - title = stringResource(id = R.string.groupCreate), - onBack = onBack, - ) - - SessionOutlinedTextField( - text = groupName, - onChange = onGroupNameChanged, - placeholder = stringResource(R.string.groupNameEnter), - textStyle = LocalType.current.base, - modifier = Modifier.padding(horizontal = 16.dp), - error = groupNameError.takeIf { it.isNotBlank() }, - enabled = !showLoading, - onContinue = focusManager::clearFocus - ) - - SearchBar( - query = contactSearchQuery, - onValueChanged = onContactSearchQueryChanged, - placeholder = stringResource(R.string.searchContacts), - modifier = Modifier.padding(horizontal = 16.dp), - enabled = !showLoading - ) - - LazyColumn(modifier = Modifier.weight(1f)) { - multiSelectMemberList( - contacts = items, - onContactItemClicked = onContactItemClicked, - enabled = !showLoading + Scaffold( + containerColor = LocalColors.current.backgroundSecondary, + topBar = { + BackAppBar( + title = stringResource(id = R.string.groupCreate), + backgroundColor = LocalColors.current.backgroundSecondary, + 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( + text = groupName, + onChange = onGroupNameChanged, + placeholder = stringResource(R.string.groupNameEnter), + textStyle = LocalType.current.base, + modifier = Modifier.padding(horizontal = LocalDimensions.current.spacing), + error = groupNameError.takeIf { it.isNotBlank() }, + enabled = !showLoading, + innerPadding = PaddingValues(LocalDimensions.current.smallSpacing), + onContinue = focusManager::clearFocus + ) - PrimaryOutlineButton(onClick = onCreateClicked, modifier = Modifier.widthIn(min = 120.dp)) { - LoadingArcOr(loading = showLoading) { - Text(stringResource(R.string.create)) + SearchBar( + query = contactSearchQuery, + onValueChanged = onContactSearchQueryChanged, + placeholder = stringResource(R.string.searchContacts), + modifier = Modifier.padding(horizontal = LocalDimensions.current.spacing), + enabled = !showLoading + ) + + LazyColumn(modifier = Modifier.weight(1f)) { + multiSelectMemberList( + contacts = items, + onContactItemClicked = onContactItemClicked, + enabled = !showLoading + ) + } + + PrimaryOutlineButton( + onClick = onCreateClicked, + modifier = Modifier.widthIn(min = 120.dp) + ) { + LoadingArcOr(loading = showLoading) { + Text(stringResource(R.string.create)) + } + } } } + } + } @Preview @@ -150,18 +165,17 @@ private fun CreateGroupPreview( PreviewTheme { CreateGroup( - modifier = Modifier.background(LocalColors.current.backgroundSecondary), - groupName = "Group Name", + groupName = "", onGroupNameChanged = {}, + groupNameError = "", contactSearchQuery = "", onContactSearchQueryChanged = {}, onContactItemClicked = {}, - items = previewMembers, - onBack = {}, - onClose = {}, - onCreateClicked = {}, showLoading = false, - groupNameError = "", + items = previewMembers, + onCreateClicked = {}, + onBack = {}, + modifier = Modifier.background(LocalColors.current.backgroundSecondary), ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/EditGroupScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/EditGroupScreen.kt index 1362deaea1..94da3a396b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/EditGroupScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/EditGroupScreen.kt @@ -1,13 +1,13 @@ package org.thoughtcrime.securesms.groups.compose +import android.widget.Toast import androidx.compose.animation.animateContentSize import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width 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.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Scaffold -import androidx.compose.material3.SheetState -import androidx.compose.material3.Snackbar import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.mutableStateOf 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.DialogButtonModel import org.thoughtcrime.securesms.ui.GetString -import org.thoughtcrime.securesms.ui.components.ActionAppBar -import org.thoughtcrime.securesms.ui.components.AppBarBackIcon +import org.thoughtcrime.securesms.ui.components.BackAppBar +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.SessionOutlinedTextField 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.PreviewTheme -import org.thoughtcrime.securesms.ui.theme.bold @Composable fun EditGroupScreen( @@ -130,9 +127,7 @@ fun EditGroup( showingError: String?, onErrorDismissed: () -> Unit, ) { - val sheetState = rememberModalBottomSheetState() - - val (showingBottomModelForMember, setShowingBottomModelForMember) = remember { + val (showingOptionsDialogForMember, setShowingBottomModelForMember) = remember { mutableStateOf(null) } @@ -142,20 +137,9 @@ fun EditGroup( Scaffold( topBar = { - ActionAppBar( + BackAppBar( title = stringResource(id = R.string.groupEdit), - navigationIcon = { - AppBarBackIcon(onBack = onBackClick) - }, - actions = { - TextButton(onClick = onBackClick) { - Text( - text = stringResource(id = R.string.done), - color = LocalColors.current.text, - style = LocalType.current.large.bold() - ) - } - }, + onBack = onBackClick, ) } ) { paddingValues -> @@ -168,8 +152,11 @@ fun EditGroup( modifier = Modifier .animateContentSize() .fillMaxWidth() - .padding(16.dp), - horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally), + .padding(LocalDimensions.current.smallSpacing), + horizontalArrangement = Arrangement.spacedBy( + LocalDimensions.current.xxxsSpacing, + Alignment.CenterHorizontally + ), verticalAlignment = CenterVertically, ) { if (editingName != null) { @@ -185,7 +172,11 @@ fun EditGroup( modifier = Modifier.width(180.dp), text = editingName, onChange = onEditingNameValueChanged, - textStyle = LocalType.current.large + textStyle = LocalType.current.h8, + innerPadding = PaddingValues( + horizontal = LocalDimensions.current.spacing, + vertical = LocalDimensions.current.smallSpacing + ) ) IconButton(onClick = onEditNameConfirmed) { @@ -198,7 +189,7 @@ fun EditGroup( } else { Text( text = groupName, - style = LocalType.current.h3, + style = LocalType.current.h4, textAlign = TextAlign.Center, ) @@ -242,6 +233,7 @@ fun EditGroup( MemberItem( modifier = Modifier.fillMaxWidth(), member = member, + clickable = member.canEdit, onClick = { setShowingBottomModelForMember(member) } ) } @@ -249,27 +241,26 @@ fun EditGroup( } } - if (showingBottomModelForMember != null) { - MemberModalBottomSheetOptions( + if (showingOptionsDialogForMember != null) { + MemberOptionsDialog( onDismissRequest = { setShowingBottomModelForMember(null) }, - sheetState = sheetState, onRemove = { - setShowingConfirmRemovingMember(showingBottomModelForMember) + setShowingConfirmRemovingMember(showingOptionsDialogForMember) setShowingBottomModelForMember(null) }, onPromote = { setShowingBottomModelForMember(null) - onPromoteClick(showingBottomModelForMember.accountId) + onPromoteClick(showingOptionsDialogForMember.accountId) }, onResendInvite = { setShowingBottomModelForMember(null) - onResendInviteClick(showingBottomModelForMember.accountId) + onResendInviteClick(showingOptionsDialogForMember.accountId) }, onResendPromotion = { setShowingBottomModelForMember(null) - onResendPromotionClick(showingBottomModelForMember.accountId) + onResendPromotionClick(showingOptionsDialogForMember.accountId) }, - member = showingBottomModelForMember, + member = showingOptionsDialogForMember, ) } @@ -284,17 +275,13 @@ fun EditGroup( ) } - if (!showingError.isNullOrEmpty()) { - Snackbar( - dismissAction = { - TextButton(onClick = onErrorDismissed) { - Text(text = stringResource(id = R.string.dismiss)) - } - }, - content = { - Text(text = showingError) - } - ) + val context = LocalContext.current + + LaunchedEffect(showingError) { + if (showingError != null) { + Toast.makeText(context, showingError, Toast.LENGTH_SHORT).show() + onErrorDismissed() + } } } @@ -328,80 +315,81 @@ private fun ConfirmRemovingMemberDialog( ) } -@OptIn(ExperimentalMaterial3Api::class) @Composable -private fun MemberModalBottomSheetOptions( +private fun MemberOptionsDialog( member: GroupMemberState, onRemove: () -> Unit, onPromote: () -> Unit, onResendInvite: () -> Unit, onResendPromotion: () -> Unit, onDismissRequest: () -> Unit, - sheetState: SheetState, ) { - ModalBottomSheet( - onDismissRequest = onDismissRequest, - sheetState = sheetState, - ) { - if (member.canRemove) { - val context = LocalContext.current - MemberModalBottomSheetOptionItem( - onClick = onRemove, - text = context.resources.getQuantityString(R.plurals.groupRemoveUserOnly, 1) - ) - } + val context = LocalContext.current - if (member.canPromote) { - MemberModalBottomSheetOptionItem( - onClick = onPromote, - text = stringResource(R.string.adminPromoteToAdmin) - ) - } + val options = remember(member) { + buildList { + if (member.canRemove) { + this += BottomOptionsDialogItem( + title = context.resources.getQuantityString(R.plurals.groupRemoveUserOnly, 1), + iconRes = R.drawable.ic_delete, + onClick = onRemove + ) + } - if (member.canResendInvite) { - MemberModalBottomSheetOptionItem(onClick = onResendInvite, text = "Resend invite") - } + if (member.canPromote) { + this += BottomOptionsDialogItem( + title = context.getString(R.string.adminPromoteToAdmin), + iconRes = R.drawable.ic_profile_default, + onClick = onPromote + ) + } - if (member.canResendPromotion) { - MemberModalBottomSheetOptionItem(onClick = onResendPromotion, text = "Resend promotion") - } + if (member.canResendInvite) { + this += BottomOptionsDialogItem( + title = "Resend invite", + iconRes = R.drawable.ic_arrow_left, + onClick = onResendInvite + ) + } - Spacer(modifier = Modifier.height(32.dp)) + if (member.canResendPromotion) { + this += BottomOptionsDialogItem( + title = "Resend promotion", + iconRes = R.drawable.ic_arrow_left, + onClick = onResendPromotion + ) + } + } } -} -@Composable -private fun MemberModalBottomSheetOptionItem( - text: String, - onClick: () -> Unit -) { - Text( - modifier = Modifier - .clickable(onClick = onClick) - .padding(16.dp) - .fillMaxWidth(), - style = LocalType.current.base, - text = text, - color = LocalColors.current.text, + BottomOptionsDialog( + items = options, + onDismissRequest = onDismissRequest ) } @Composable private fun MemberItem( + clickable: Boolean, onClick: (accountId: AccountId) -> Unit, member: GroupMemberState, modifier: Modifier = Modifier, ) { Row( - modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp), - horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = modifier + .clickable(enabled = clickable, onClick = { onClick(member.accountId) }) + .padding( + horizontal = LocalDimensions.current.smallSpacing, + vertical = LocalDimensions.current.xsSpacing + ), + horizontalArrangement = Arrangement.spacedBy(LocalDimensions.current.smallSpacing), verticalAlignment = CenterVertically, ) { ContactPhoto(member.accountId) Column( modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(4.dp) + verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.xxxsSpacing) ) { Text( @@ -424,12 +412,10 @@ private fun MemberItem( } if (member.canEdit) { - IconButton(onClick = { onClick(member.accountId) }) { - Icon( - painter = painterResource(R.drawable.ic_circle_dot_dot_dot), - contentDescription = stringResource(R.string.AccessibilityId_sessionSettings) - ) - } + Icon( + painter = painterResource(R.drawable.ic_circle_dot_dot_dot), + contentDescription = stringResource(R.string.AccessibilityId_sessionSettings) + ) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index 08bb511bc9..a5a6232578 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -544,7 +544,7 @@ fun SearchBar( painterResource(id = R.drawable.ic_search_24), contentDescription = null, colorFilter = ColorFilter.tint( - LocalColors.current.text + LocalColors.current.textSecondary ), modifier = Modifier .padding(horizontal = 16.dp, vertical = 8.dp) @@ -557,7 +557,7 @@ fun SearchBar( Text( text = placeholder, color = LocalColors.current.textSecondary, - style = LocalType.current.base + style = LocalType.current.xl ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/components/Text.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/components/Text.kt index 0152c8b280..9ad43fea06 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/components/Text.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/components/Text.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -91,6 +92,7 @@ fun SessionOutlinedTextField( modifier: Modifier = Modifier, onChange: (String) -> Unit = {}, textStyle: TextStyle = LocalType.current.base, + innerPadding: PaddingValues = PaddingValues(LocalDimensions.current.spacing), placeholder: String = "", onContinue: () -> Unit = {}, error: String? = null, @@ -122,7 +124,7 @@ fun SessionOutlinedTextField( ) .fillMaxWidth() .wrapContentHeight() - .padding(LocalDimensions.current.spacing) + .padding(innerPadding) ) { innerTextField() diff --git a/libsession/src/main/java/org/session/libsession/messaging/groups/GroupManagerV2.kt b/libsession/src/main/java/org/session/libsession/messaging/groups/GroupManagerV2.kt index 72b28f14f6..0657e51802 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/groups/GroupManagerV2.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/groups/GroupManagerV2.kt @@ -42,7 +42,7 @@ interface GroupManagerV2 { members: List ) - suspend fun handleMemberLeft(message: GroupUpdated, group: AccountId) + suspend fun handleMemberLeftMessage(memberId: AccountId, group: AccountId) suspend fun leaveGroup(groupId: AccountId, deleteOnLeave: Boolean) diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt index 41db0dfa3c..94d93b5742 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt @@ -57,7 +57,6 @@ import org.session.libsignal.crypto.ecc.DjbECPublicKey import org.session.libsignal.crypto.ecc.ECKeyPair import org.session.libsignal.messages.SignalServiceGroup 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.utilities.AccountId import org.session.libsignal.utilities.Base64 @@ -675,8 +674,12 @@ private fun handleMemberChange(message: GroupUpdated, closedGroup: AccountId) { private fun handleMemberLeft(message: GroupUpdated, closedGroup: AccountId) { GlobalScope.launch(Dispatchers.Default) { - runCatching { - MessagingModuleConfiguration.shared.groupManagerV2.handleMemberLeft(message, closedGroup) + try { + MessagingModuleConfiguration.shared.groupManagerV2.handleMemberLeftMessage( + AccountId(message.sender!!), closedGroup + ) + } catch (e: Exception) { + Log.e("GroupUpdated", "Failed to handle member left message", e) } } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageBuilder.kt b/libsession/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageBuilder.kt index 1f49246325..13c07b8482 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageBuilder.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageBuilder.kt @@ -209,11 +209,12 @@ object UpdateMessageBuilder { if (historyShared) R.string.groupMemberNewYouHistoryMultiple else R.string.groupInviteYouAndMoreNew) .put(COUNT_KEY, updateData.sessionIds.size - 1) .format() - else -> Phrase.from(context, + number > 0 -> Phrase.from(context, if (historyShared) R.string.groupMemberNewHistoryMultiple else R.string.groupMemberNewMultiple) .put(NAME_KEY, context.youOrSender(updateData.sessionIds.first())) .put(COUNT_KEY, updateData.sessionIds.size - 1) .format() + else -> "" } }