mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-28 20:45:17 +00:00
UI adjustment
This commit is contained in:
parent
74f7bbb6d5
commit
6e1fa1b257
@ -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<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
|
||||
// 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<String> = groupInfo
|
||||
.map { it?.first?.name.orEmpty() }
|
||||
val groupName: StateFlow<String> = 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<AccountId>,
|
||||
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<AccountId>,
|
||||
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) =
|
||||
|
@ -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),
|
||||
|
@ -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<ContactItem>,
|
||||
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),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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<GroupMemberState?>(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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
||||
|
@ -42,7 +42,7 @@ interface GroupManagerV2 {
|
||||
members: List<AccountId>
|
||||
)
|
||||
|
||||
suspend fun handleMemberLeft(message: GroupUpdated, group: AccountId)
|
||||
suspend fun handleMemberLeftMessage(memberId: AccountId, group: AccountId)
|
||||
|
||||
suspend fun leaveGroup(groupId:
|
||||
AccountId, deleteOnLeave: Boolean)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 -> ""
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user