mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-03-14 09:45:42 +00:00
android, desktop: bulk actions with group members (#5708)
* android, desktop: bulk actions with group members * fix layout * fix update * fix responsivenes when closing selecting bar * events * unused * role
This commit is contained in:
parent
8c7df76c24
commit
9dac472191
12 changed files with 531 additions and 140 deletions
|
@ -1958,8 +1958,8 @@ data class GroupMember (
|
|||
|
||||
fun canBlockForAll(groupInfo: GroupInfo): Boolean {
|
||||
val userRole = groupInfo.membership.memberRole
|
||||
return memberStatus != GroupMemberStatus.MemRemoved && memberStatus != GroupMemberStatus.MemLeft && memberRole < GroupMemberRole.Admin
|
||||
&& userRole >= GroupMemberRole.Admin && userRole >= memberRole && groupInfo.membership.memberActive
|
||||
return memberStatus != GroupMemberStatus.MemRemoved && memberStatus != GroupMemberStatus.MemLeft && memberRole < GroupMemberRole.Moderator
|
||||
&& userRole >= GroupMemberRole.Moderator && userRole >= memberRole && groupInfo.membership.memberActive
|
||||
}
|
||||
|
||||
val memberIncognito = memberProfile.profileId != memberContactProfileId
|
||||
|
@ -2439,14 +2439,14 @@ data class ChatItem (
|
|||
fun memberToModerate(chatInfo: ChatInfo): Pair<GroupInfo, GroupMember?>? {
|
||||
return if (chatInfo is ChatInfo.Group && chatDir is CIDirection.GroupRcv) {
|
||||
val m = chatInfo.groupInfo.membership
|
||||
if (m.memberRole >= GroupMemberRole.Admin && m.memberRole >= chatDir.groupMember.memberRole && meta.itemDeleted == null) {
|
||||
if (m.memberRole >= GroupMemberRole.Moderator && m.memberRole >= chatDir.groupMember.memberRole && meta.itemDeleted == null) {
|
||||
chatInfo.groupInfo to chatDir.groupMember
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else if (chatInfo is ChatInfo.Group && chatDir is CIDirection.GroupSnd) {
|
||||
val m = chatInfo.groupInfo.membership
|
||||
if (m.memberRole >= GroupMemberRole.Admin) {
|
||||
if (m.memberRole >= GroupMemberRole.Moderator) {
|
||||
chatInfo.groupInfo to null
|
||||
} else {
|
||||
null
|
||||
|
@ -3259,6 +3259,7 @@ sealed class CIContent: ItemContent {
|
|||
when (role) {
|
||||
GroupMemberRole.Owner -> generalGetString(MR.strings.feature_roles_owners)
|
||||
GroupMemberRole.Admin -> generalGetString(MR.strings.feature_roles_admins)
|
||||
GroupMemberRole.Moderator -> generalGetString(MR.strings.feature_roles_moderators)
|
||||
else -> generalGetString(MR.strings.feature_roles_all_members)
|
||||
}
|
||||
|
||||
|
|
|
@ -175,7 +175,7 @@ fun ChatView(
|
|||
)
|
||||
}
|
||||
} else {
|
||||
SelectedItemsBottomToolbar(
|
||||
SelectedItemsButtonsToolbar(
|
||||
contentTag = contentTag,
|
||||
selectedChatItems = selectedChatItems,
|
||||
chatInfo = chatInfo,
|
||||
|
@ -274,34 +274,46 @@ fun ChatView(
|
|||
}
|
||||
if (!isActive) return@launch
|
||||
|
||||
ModalManager.end.showModalCloseable(true) { close ->
|
||||
val chatInfo = remember { activeChatInfo }.value
|
||||
if (chatInfo is ChatInfo.Direct) {
|
||||
var contactInfo: Pair<ConnectionStats?, Profile?>? by remember { mutableStateOf(preloadedContactInfo) }
|
||||
var code: String? by remember { mutableStateOf(preloadedCode) }
|
||||
KeyChangeEffect(chatInfo.id, ChatModel.networkStatuses.toMap()) {
|
||||
contactInfo = chatModel.controller.apiContactInfo(chatRh, chatInfo.apiId)
|
||||
preloadedContactInfo = contactInfo
|
||||
code = chatModel.controller.apiGetContactCode(chatRh, chatInfo.apiId)?.second
|
||||
preloadedCode = code
|
||||
val selectedItems: MutableState<Set<Long>?> = mutableStateOf(null)
|
||||
ModalManager.end.showCustomModal { close ->
|
||||
val appBar = remember { mutableStateOf(null as @Composable (BoxScope.() -> Unit)?) }
|
||||
ModalView(close, appBar = appBar.value) {
|
||||
val chatInfo = remember { activeChatInfo }.value
|
||||
if (chatInfo is ChatInfo.Direct) {
|
||||
var contactInfo: Pair<ConnectionStats?, Profile?>? by remember { mutableStateOf(preloadedContactInfo) }
|
||||
var code: String? by remember { mutableStateOf(preloadedCode) }
|
||||
KeyChangeEffect(chatInfo.id, ChatModel.networkStatuses.toMap()) {
|
||||
contactInfo = chatModel.controller.apiContactInfo(chatRh, chatInfo.apiId)
|
||||
preloadedContactInfo = contactInfo
|
||||
code = chatModel.controller.apiGetContactCode(chatRh, chatInfo.apiId)?.second
|
||||
preloadedCode = code
|
||||
}
|
||||
ChatInfoView(chatModel, chatInfo.contact, contactInfo?.first, contactInfo?.second, chatInfo.localAlias, code, close) {
|
||||
showSearch.value = true
|
||||
}
|
||||
} else if (chatInfo is ChatInfo.Group) {
|
||||
var link: Pair<String, GroupMemberRole>? by remember(chatInfo.id) { mutableStateOf(preloadedLink) }
|
||||
KeyChangeEffect(chatInfo.id) {
|
||||
setGroupMembers(chatRh, chatInfo.groupInfo, chatModel)
|
||||
link = chatModel.controller.apiGetGroupLink(chatRh, chatInfo.groupInfo.groupId)
|
||||
preloadedLink = link
|
||||
}
|
||||
GroupChatInfoView(chatRh, chatInfo.id, link?.first, link?.second, selectedItems, appBar, scrollToItemId, {
|
||||
link = it
|
||||
preloadedLink = it
|
||||
}, close, { showSearch.value = true })
|
||||
} else {
|
||||
LaunchedEffect(Unit) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
ChatInfoView(chatModel, chatInfo.contact, contactInfo?.first, contactInfo?.second, chatInfo.localAlias, code, close) {
|
||||
showSearch.value = true
|
||||
}
|
||||
} else if (chatInfo is ChatInfo.Group) {
|
||||
var link: Pair<String, GroupMemberRole>? by remember(chatInfo.id) { mutableStateOf(preloadedLink) }
|
||||
KeyChangeEffect(chatInfo.id) {
|
||||
setGroupMembers(chatRh, chatInfo.groupInfo, chatModel)
|
||||
link = chatModel.controller.apiGetGroupLink(chatRh, chatInfo.groupInfo.groupId)
|
||||
preloadedLink = link
|
||||
}
|
||||
GroupChatInfoView(chatModel, chatRh, chatInfo.id, link?.first, link?.second, scrollToItemId, {
|
||||
link = it
|
||||
preloadedLink = it
|
||||
}, close, { showSearch.value = true })
|
||||
} else {
|
||||
LaunchedEffect(Unit) {
|
||||
close()
|
||||
snapshotFlow { activeChatInfo.value?.id }
|
||||
.drop(1)
|
||||
.collect {
|
||||
appBar.value = null
|
||||
selectedItems.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -788,7 +800,7 @@ fun ChatLayout(
|
|||
) {
|
||||
AnimatedVisibility(selectedChatItems.value != null) {
|
||||
if (chatInfo != null) {
|
||||
SelectedItemsBottomToolbar(
|
||||
SelectedItemsButtonsToolbar(
|
||||
contentTag = contentTag,
|
||||
selectedChatItems = selectedChatItems,
|
||||
chatInfo = chatInfo,
|
||||
|
@ -846,7 +858,7 @@ fun ChatLayout(
|
|||
if (selectedChatItems.value == null) {
|
||||
GroupReportsAppBar(contentTag, { ModalManager.end.closeModal() }, onSearchValueChanged)
|
||||
} else {
|
||||
SelectedItemsTopToolbar(selectedChatItems, !oneHandUI.value)
|
||||
SelectedItemsCounterToolbar(selectedChatItems, !oneHandUI.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -858,7 +870,7 @@ fun ChatLayout(
|
|||
ChatInfoToolbar(chatInfo, contentTag, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch)
|
||||
}
|
||||
} else {
|
||||
SelectedItemsTopToolbar(selectedChatItems, !oneHandUI.value || !chatBottomBar.value)
|
||||
SelectedItemsCounterToolbar(selectedChatItems, !oneHandUI.value || !chatBottomBar.value)
|
||||
}
|
||||
}
|
||||
if (contentTag == null && reportsCount > 0 && (!oneHandUI.value || !chatBottomBar.value)) {
|
||||
|
@ -1432,7 +1444,7 @@ fun BoxScope.ChatItemsList(
|
|||
fun Item() {
|
||||
ChatItemBox(Modifier.layoutId(CHAT_BUBBLE_LAYOUT_ID)) {
|
||||
androidx.compose.animation.AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) {
|
||||
SelectedChatItem(Modifier, cItem.id, selectedChatItems)
|
||||
SelectedListItem(Modifier, cItem.id, selectedChatItems)
|
||||
}
|
||||
Row(Modifier.graphicsLayer { translationX = selectionOffset.toPx() }) {
|
||||
val member = cItem.chatDir.groupMember
|
||||
|
@ -1457,7 +1469,7 @@ fun BoxScope.ChatItemsList(
|
|||
} else {
|
||||
ChatItemBox {
|
||||
AnimatedVisibility (selectionVisible, enter = fadeIn(), exit = fadeOut()) {
|
||||
SelectedChatItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems)
|
||||
SelectedListItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems)
|
||||
}
|
||||
Row(
|
||||
Modifier
|
||||
|
@ -1472,7 +1484,7 @@ fun BoxScope.ChatItemsList(
|
|||
} else {
|
||||
ChatItemBox {
|
||||
AnimatedVisibility (selectionVisible, enter = fadeIn(), exit = fadeOut()) {
|
||||
SelectedChatItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems)
|
||||
SelectedListItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems)
|
||||
}
|
||||
Box(
|
||||
Modifier
|
||||
|
@ -1487,7 +1499,7 @@ fun BoxScope.ChatItemsList(
|
|||
} else { // direct message
|
||||
ChatItemBox {
|
||||
AnimatedVisibility (selectionVisible, enter = fadeIn(), exit = fadeOut()) {
|
||||
SelectedChatItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems)
|
||||
SelectedListItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems)
|
||||
}
|
||||
|
||||
Box(
|
||||
|
@ -2296,12 +2308,12 @@ private fun BoxScope.BottomEndFloatingButton(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun SelectedChatItem(
|
||||
fun SelectedListItem(
|
||||
modifier: Modifier,
|
||||
ciId: Long,
|
||||
selectedChatItems: State<Set<Long>?>,
|
||||
id: Long,
|
||||
selectedItems: State<Set<Long>?>,
|
||||
) {
|
||||
val checked = remember { derivedStateOf { selectedChatItems.value?.contains(ciId) == true } }
|
||||
val checked = remember { derivedStateOf { selectedItems.value?.contains(id) == true } }
|
||||
Icon(
|
||||
painterResource(if (checked.value) MR.images.ic_check_circle_filled else MR.images.ic_radio_button_unchecked),
|
||||
null,
|
||||
|
|
|
@ -9,12 +9,10 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.platform.BackHandler
|
||||
import chat.simplex.common.platform.chatModel
|
||||
import chat.simplex.common.views.helpers.*
|
||||
|
@ -23,32 +21,44 @@ import chat.simplex.res.MR
|
|||
import dev.icerock.moko.resources.compose.painterResource
|
||||
|
||||
@Composable
|
||||
fun BoxScope.SelectedItemsTopToolbar(selectedChatItems: MutableState<Set<Long>?>, onTop: Boolean) {
|
||||
val onBackClicked = { selectedChatItems.value = null }
|
||||
fun BoxScope.SelectedItemsCounterToolbar(selectedItems: MutableState<Set<Long>?>, onTop: Boolean, selectAll: (() -> Unit)? = null) {
|
||||
val onBackClicked = { selectedItems.value = null }
|
||||
BackHandler(onBack = onBackClicked)
|
||||
val count = selectedChatItems.value?.size ?: 0
|
||||
DefaultAppBar(
|
||||
navigationButton = { NavigationButtonClose(onButtonClicked = onBackClicked) },
|
||||
title = {
|
||||
Text(
|
||||
if (count == 0) {
|
||||
stringResource(MR.strings.selected_chat_items_nothing_selected)
|
||||
} else {
|
||||
stringResource(MR.strings.selected_chat_items_selected_n).format(count)
|
||||
},
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
},
|
||||
onTitleClick = null,
|
||||
onTop = onTop,
|
||||
onSearchValueChanged = {},
|
||||
)
|
||||
val count = selectedItems.value?.size ?: 0
|
||||
Box(if (onTop) Modifier else Modifier.imePadding()) {
|
||||
DefaultAppBar(
|
||||
navigationButton = { NavigationButtonClose(onButtonClicked = onBackClicked) },
|
||||
title = {
|
||||
Text(
|
||||
if (count == 0) {
|
||||
stringResource(MR.strings.selected_chat_items_nothing_selected)
|
||||
} else {
|
||||
stringResource(MR.strings.selected_chat_items_selected_n).format(count)
|
||||
},
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
},
|
||||
onTitleClick = null,
|
||||
onTop = onTop,
|
||||
onSearchValueChanged = {},
|
||||
buttons = if (selectAll != null) { { SelectAllButton(selectAll) } } else {{}}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SelectedItemsBottomToolbar(
|
||||
private fun SelectAllButton(onClick: () -> Unit) {
|
||||
IconButton(onClick) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_checklist), stringResource(MR.strings.back), Modifier.height(24.dp), tint = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SelectedItemsButtonsToolbar(
|
||||
chatInfo: ChatInfo,
|
||||
contentTag: MsgContentTag?,
|
||||
selectedChatItems: MutableState<Set<Long>?>,
|
||||
|
@ -162,4 +172,4 @@ private fun recheckItems(chatInfo: ChatInfo,
|
|||
}
|
||||
|
||||
private fun possibleToModerate(chatInfo: ChatInfo): Boolean =
|
||||
chatInfo is ChatInfo.Group && chatInfo.groupInfo.membership.memberRole >= GroupMemberRole.Admin
|
||||
chatInfo is ChatInfo.Group && chatInfo.groupInfo.membership.memberRole >= GroupMemberRole.Moderator
|
||||
|
|
|
@ -8,6 +8,8 @@ import SectionItemViewLongClickable
|
|||
import SectionSpacer
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
|
@ -17,6 +19,7 @@ import androidx.compose.runtime.*
|
|||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
|
@ -37,7 +40,7 @@ import chat.simplex.common.views.usersettings.*
|
|||
import chat.simplex.common.model.GroupInfo
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.chat.*
|
||||
import chat.simplex.common.views.chat.item.ItemAction
|
||||
import chat.simplex.common.views.chat.item.*
|
||||
import chat.simplex.common.views.chatlist.*
|
||||
import chat.simplex.common.views.database.TtlOptions
|
||||
import chat.simplex.res.MR
|
||||
|
@ -49,7 +52,18 @@ val MEMBER_ROW_AVATAR_SIZE = 42.dp
|
|||
val MEMBER_ROW_VERTICAL_PADDING = 8.dp
|
||||
|
||||
@Composable
|
||||
fun ModalData.GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: String, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, scrollToItemId: MutableState<Long?>, onGroupLinkUpdated: (Pair<String, GroupMemberRole>?) -> Unit, close: () -> Unit, onSearchClicked: () -> Unit) {
|
||||
fun ModalData.GroupChatInfoView(
|
||||
rhId: Long?,
|
||||
chatId: String,
|
||||
groupLink: String?,
|
||||
groupLinkMemberRole: GroupMemberRole?,
|
||||
selectedItems: MutableState<Set<Long>?>,
|
||||
appBar: MutableState<@Composable (BoxScope.() -> Unit)?>,
|
||||
scrollToItemId: MutableState<Long?>,
|
||||
onGroupLinkUpdated: (Pair<String, GroupMemberRole>?) -> Unit,
|
||||
close: () -> Unit,
|
||||
onSearchClicked: () -> Unit
|
||||
) {
|
||||
BackHandler(onBack = close)
|
||||
// TODO derivedStateOf?
|
||||
val chat = chatModel.chats.value.firstOrNull { ch -> ch.id == chatId && ch.remoteHostId == rhId }
|
||||
|
@ -82,12 +96,14 @@ fun ModalData.GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: Strin
|
|||
|
||||
setChatTTLAlert(chat.remoteHostId, chat.chatInfo, chatItemTTL, previousChatTTL, deletingItems)
|
||||
},
|
||||
members = remember { chatModel.groupMembers }.value
|
||||
activeSortedMembers = remember { chatModel.groupMembers }.value
|
||||
.filter { it.memberStatus != GroupMemberStatus.MemLeft && it.memberStatus != GroupMemberStatus.MemRemoved }
|
||||
.sortedByDescending { it.memberRole },
|
||||
developerTools,
|
||||
onLocalAliasChanged = { setGroupAlias(chat, it, chatModel) },
|
||||
groupLink,
|
||||
selectedItems,
|
||||
appBar,
|
||||
scrollToItemId,
|
||||
addMembers = {
|
||||
scope.launch(Dispatchers.Default) {
|
||||
|
@ -212,21 +228,23 @@ private fun removeMemberAlert(rhId: Long?, groupInfo: GroupInfo, mem: GroupMembe
|
|||
text = generalGetString(messageId),
|
||||
confirmText = generalGetString(MR.strings.remove_member_confirmation),
|
||||
onConfirm = {
|
||||
withBGApi {
|
||||
val updatedMembers = chatModel.controller.apiRemoveMembers(rhId, groupInfo.groupId, listOf(mem.groupMemberId))
|
||||
if (updatedMembers != null) {
|
||||
withChats {
|
||||
updatedMembers.forEach { updatedMember ->
|
||||
upsertGroupMember(rhId, groupInfo, updatedMember)
|
||||
}
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
updatedMembers.forEach { updatedMember ->
|
||||
upsertGroupMember(rhId, groupInfo, updatedMember)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
removeMembers(rhId, groupInfo, listOf(mem.groupMemberId))
|
||||
},
|
||||
destructive = true,
|
||||
)
|
||||
}
|
||||
|
||||
private fun removeMembersAlert(rhId: Long?, groupInfo: GroupInfo, memberIds: List<Long>, onSuccess: () -> Unit = {}) {
|
||||
val messageId = if (groupInfo.businessChat == null)
|
||||
MR.strings.members_will_be_removed_from_group_cannot_be_undone
|
||||
else
|
||||
MR.strings.members_will_be_removed_from_chat_cannot_be_undone
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.button_remove_members_question),
|
||||
text = generalGetString(messageId),
|
||||
confirmText = generalGetString(MR.strings.remove_member_confirmation),
|
||||
onConfirm = {
|
||||
removeMembers(rhId, groupInfo, memberIds, onSuccess)
|
||||
},
|
||||
destructive = true,
|
||||
)
|
||||
|
@ -309,10 +327,12 @@ fun ModalData.GroupChatInfoLayout(
|
|||
setSendReceipts: (SendReceipts) -> Unit,
|
||||
chatItemTTL: MutableState<ChatItemTTL?>,
|
||||
setChatItemTTL: (ChatItemTTL?) -> Unit,
|
||||
members: List<GroupMember>,
|
||||
activeSortedMembers: List<GroupMember>,
|
||||
developerTools: Boolean,
|
||||
onLocalAliasChanged: (String) -> Unit,
|
||||
groupLink: String?,
|
||||
selectedItems: MutableState<Set<Long>?>,
|
||||
appBar: MutableState<@Composable (BoxScope.() -> Unit)?>,
|
||||
scrollToItemId: MutableState<Long?>,
|
||||
addMembers: () -> Unit,
|
||||
showMemberInfo: (GroupMember) -> Unit,
|
||||
|
@ -333,20 +353,37 @@ fun ModalData.GroupChatInfoLayout(
|
|||
scope.launch { listState.scrollToItem(0) }
|
||||
}
|
||||
val searchText = remember { stateGetOrPut("searchText") { TextFieldValue() } }
|
||||
val filteredMembers = remember(members) {
|
||||
val filteredMembers = remember(activeSortedMembers) {
|
||||
derivedStateOf {
|
||||
val s = searchText.value.text.trim().lowercase()
|
||||
if (s.isEmpty()) members else members.filter { m -> m.anyNameContains(s) }
|
||||
if (s.isEmpty()) activeSortedMembers else activeSortedMembers.filter { m -> m.anyNameContains(s) }
|
||||
}
|
||||
}
|
||||
Box {
|
||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||
val selectedItemsBarHeight = if (selectedItems.value != null) AppBarHeight * fontSizeSqrtMultiplier else 0.dp
|
||||
val navBarPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
||||
val imePadding = WindowInsets.ime.asPaddingValues().calculateBottomPadding()
|
||||
LazyColumnWithScrollBar(
|
||||
state = listState,
|
||||
contentPadding = if (oneHandUI.value) {
|
||||
PaddingValues(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + DEFAULT_PADDING + 5.dp, bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding())
|
||||
PaddingValues(
|
||||
top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + DEFAULT_PADDING + 5.dp,
|
||||
bottom = navBarPadding +
|
||||
imePadding +
|
||||
selectedItemsBarHeight +
|
||||
// TODO: that's workaround but works. Actually, something in the codebase doesn't consume padding for AppBar and it produce
|
||||
// different padding when the user has NavigationBar and doesn't have it with ime shown (developer options helps to test it nav bars)
|
||||
(if (navBarPadding > 0.dp && imePadding > 0.dp) 0.dp else AppBarHeight * fontSizeSqrtMultiplier)
|
||||
)
|
||||
} else {
|
||||
PaddingValues(top = topPaddingToContent(false))
|
||||
PaddingValues(
|
||||
top = topPaddingToContent(false),
|
||||
bottom = navBarPadding +
|
||||
imePadding +
|
||||
selectedItemsBarHeight +
|
||||
(if (navBarPadding > 0.dp && imePadding > 0.dp) -AppBarHeight * fontSizeSqrtMultiplier else 0.dp)
|
||||
)
|
||||
}
|
||||
) {
|
||||
item {
|
||||
|
@ -401,7 +438,7 @@ fun ModalData.GroupChatInfoLayout(
|
|||
}
|
||||
}
|
||||
}
|
||||
if (members.filter { it.memberCurrent }.size <= SMALL_GROUPS_RCPS_MEM_LIMIT) {
|
||||
if (activeSortedMembers.filter { it.memberCurrent }.size <= SMALL_GROUPS_RCPS_MEM_LIMIT) {
|
||||
SendReceiptsOption(currentUser, sendReceipts, setSendReceipts)
|
||||
} else {
|
||||
SendReceiptsOptionDisabled()
|
||||
|
@ -424,7 +461,7 @@ fun ModalData.GroupChatInfoLayout(
|
|||
ChatTTLSection(chatItemTTL, setChatItemTTL, deletingItems)
|
||||
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = true)
|
||||
|
||||
SectionView(title = String.format(generalGetString(MR.strings.group_info_section_title_num_members), members.count() + 1)) {
|
||||
SectionView(title = String.format(generalGetString(MR.strings.group_info_section_title_num_members), activeSortedMembers.count() + 1)) {
|
||||
if (groupInfo.canAddMembers) {
|
||||
if (groupInfo.businessChat == null) {
|
||||
if (groupLink == null) {
|
||||
|
@ -442,7 +479,7 @@ fun ModalData.GroupChatInfoLayout(
|
|||
}
|
||||
AddMembersButton(addMembersTitleId, tint, onAddMembersClick)
|
||||
}
|
||||
if (members.size > 8) {
|
||||
if (activeSortedMembers.size > 8) {
|
||||
SectionItemView(padding = PaddingValues(start = 14.dp, end = DEFAULT_PADDING_HALF)) {
|
||||
SearchRowView(searchText)
|
||||
}
|
||||
|
@ -452,12 +489,34 @@ fun ModalData.GroupChatInfoLayout(
|
|||
}
|
||||
}
|
||||
}
|
||||
items(filteredMembers.value) { member ->
|
||||
items(filteredMembers.value, key = { it.groupMemberId }) { member ->
|
||||
Divider()
|
||||
val showMenu = remember { mutableStateOf(false) }
|
||||
SectionItemViewLongClickable({ showMemberInfo(member) }, { showMenu.value = true }, minHeight = 54.dp, padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
DropDownMenuForMember(chat.remoteHostId, member, groupInfo, showMenu)
|
||||
MemberRow(member)
|
||||
val canBeSelected = groupInfo.membership.memberRole >= member.memberRole && member.memberRole < GroupMemberRole.Moderator
|
||||
SectionItemViewLongClickable(
|
||||
click = {
|
||||
if (selectedItems.value != null) {
|
||||
if (canBeSelected) {
|
||||
toggleItemSelection(member.groupMemberId, selectedItems)
|
||||
}
|
||||
} else {
|
||||
showMemberInfo(member)
|
||||
}
|
||||
},
|
||||
longClick = { showMenu.value = true },
|
||||
minHeight = 54.dp,
|
||||
padding = PaddingValues(horizontal = DEFAULT_PADDING)
|
||||
) {
|
||||
Box(contentAlignment = Alignment.CenterStart) {
|
||||
androidx.compose.animation.AnimatedVisibility(selectedItems.value != null, enter = fadeIn(), exit = fadeOut()) {
|
||||
SelectedListItem(Modifier.alpha(if (canBeSelected) 1f else 0f).padding(start = 2.dp), member.groupMemberId, selectedItems)
|
||||
}
|
||||
val selectionOffset by animateDpAsState(if (selectedItems.value != null) 20.dp + 22.dp * fontSizeMultiplier else 0.dp)
|
||||
DropDownMenuForMember(chat.remoteHostId, member, groupInfo, selectedItems, showMenu)
|
||||
Box(Modifier.padding(start = selectionOffset)) {
|
||||
MemberRow(member)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
|
@ -482,12 +541,92 @@ fun ModalData.GroupChatInfoLayout(
|
|||
}
|
||||
}
|
||||
SectionBottomSpacer()
|
||||
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars))
|
||||
}
|
||||
}
|
||||
if (!oneHandUI.value) {
|
||||
NavigationBarBackground(oneHandUI.value, oneHandUI.value)
|
||||
}
|
||||
SelectedItemsButtonsToolbar(chat, groupInfo, selectedItems, rememberUpdatedState(activeSortedMembers))
|
||||
SelectedItemsCounterToolbarSetter(groupInfo, selectedItems, filteredMembers, appBar)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxScope.SelectedItemsButtonsToolbar(chat: Chat, groupInfo: GroupInfo, selectedItems: MutableState<Set<Long>?>, activeMembers: State<List<GroupMember>>) {
|
||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||
Column(Modifier.align(Alignment.BottomCenter)) {
|
||||
AnimatedVisibility(selectedItems.value != null) {
|
||||
SelectedItemsMembersToolbar(
|
||||
selectedItems = selectedItems,
|
||||
activeMembers = activeMembers,
|
||||
groupInfo = groupInfo,
|
||||
delete = {
|
||||
removeMembersAlert(chat.remoteHostId, groupInfo, selectedItems.value!!.sorted()) {
|
||||
selectedItems.value = null
|
||||
}
|
||||
},
|
||||
blockForAll = { block ->
|
||||
if (block) {
|
||||
blockForAllAlert(chat.remoteHostId, groupInfo, selectedItems.value!!.sorted()) {
|
||||
selectedItems.value = null
|
||||
}
|
||||
} else {
|
||||
unblockForAllAlert(chat.remoteHostId, groupInfo, selectedItems.value!!.sorted()) {
|
||||
selectedItems.value = null
|
||||
}
|
||||
}
|
||||
},
|
||||
changeRole = { toRole ->
|
||||
updateMembersRoleDialog(toRole, groupInfo) {
|
||||
updateMembersRole(toRole, chat.remoteHostId, groupInfo, selectedItems.value!!.sorted()) {
|
||||
selectedItems.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
if (oneHandUI.value) {
|
||||
// That's placeholder to take some space for bottom app bar in oneHandUI
|
||||
Box(Modifier.height(AppBarHeight * fontSizeSqrtMultiplier))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SelectedItemsCounterToolbarSetter(
|
||||
groupInfo: GroupInfo,
|
||||
selectedItems: MutableState<Set<Long>?>,
|
||||
filteredMembers: State<List<GroupMember>>,
|
||||
appBar: MutableState<@Composable (BoxScope.() -> Unit)?>
|
||||
) {
|
||||
LaunchedEffect(
|
||||
groupInfo,
|
||||
/* variable, not value - intentionally - to reduce work but handle variable change because it changes in remember(members) { derivedState {} } */
|
||||
filteredMembers
|
||||
) {
|
||||
snapshotFlow { selectedItems.value == null }
|
||||
.collect { nullItems ->
|
||||
if (!nullItems) {
|
||||
appBar.value = {
|
||||
SelectedItemsCounterToolbar(selectedItems, !remember { appPrefs.oneHandUI.state }.value) {
|
||||
if (!groupInfo.membership.memberActive) return@SelectedItemsCounterToolbar
|
||||
val ids: MutableSet<Long> = mutableSetOf()
|
||||
for (mem in filteredMembers.value) {
|
||||
if (groupInfo.membership.memberActive && groupInfo.membership.memberRole >= mem.memberRole && mem.memberRole < GroupMemberRole.Moderator) {
|
||||
ids.add(mem.groupMemberId)
|
||||
}
|
||||
}
|
||||
if (ids.isNotEmpty() && (selectedItems.value ?: setOf()).containsAll(ids)) {
|
||||
selectedItems.value = (selectedItems.value ?: setOf()).minus(ids)
|
||||
} else {
|
||||
selectedItems.value = (selectedItems.value ?: setOf()).union(ids)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
appBar.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -612,7 +751,7 @@ fun MemberRow(member: GroupMember, user: Boolean = false, infoPage: Boolean = tr
|
|||
Text(stringResource(MR.strings.member_info_member_blocked), color = MaterialTheme.colors.secondary)
|
||||
} else {
|
||||
val role = member.memberRole
|
||||
if (role in listOf(GroupMemberRole.Owner, GroupMemberRole.Admin, GroupMemberRole.Observer)) {
|
||||
if (role in listOf(GroupMemberRole.Owner, GroupMemberRole.Admin, GroupMemberRole.Moderator, GroupMemberRole.Observer)) {
|
||||
Text(role.text, color = MaterialTheme.colors.secondary)
|
||||
}
|
||||
}
|
||||
|
@ -686,8 +825,8 @@ private fun MemberVerifiedShield() {
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun DropDownMenuForMember(rhId: Long?, member: GroupMember, groupInfo: GroupInfo, showMenu: MutableState<Boolean>) {
|
||||
if (groupInfo.membership.memberRole >= GroupMemberRole.Admin) {
|
||||
private fun DropDownMenuForMember(rhId: Long?, member: GroupMember, groupInfo: GroupInfo, selectedItems: MutableState<Set<Long>?>, showMenu: MutableState<Boolean>) {
|
||||
if (groupInfo.membership.memberRole >= GroupMemberRole.Moderator) {
|
||||
val canBlockForAll = member.canBlockForAll(groupInfo)
|
||||
val canRemove = member.canBeRemoved(groupInfo)
|
||||
if (canBlockForAll || canRemove) {
|
||||
|
@ -711,6 +850,10 @@ private fun DropDownMenuForMember(rhId: Long?, member: GroupMember, groupInfo: G
|
|||
showMenu.value = false
|
||||
})
|
||||
}
|
||||
if (selectedItems.value == null && member.memberRole < GroupMemberRole.Moderator) {
|
||||
Divider()
|
||||
SelectItemAction(showMenu) { toggleItemSelection(member.groupMemberId, selectedItems) }
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!member.blockedByAdmin) {
|
||||
|
@ -819,6 +962,37 @@ private fun setGroupAlias(chat: Chat, localAlias: String, chatModel: ChatModel)
|
|||
}
|
||||
}
|
||||
|
||||
fun removeMembers(rhId: Long?, groupInfo: GroupInfo, memberIds: List<Long>, onSuccess: () -> Unit = {}) {
|
||||
withBGApi {
|
||||
val updatedMembers = chatModel.controller.apiRemoveMembers(rhId, groupInfo.groupId, memberIds)
|
||||
if (updatedMembers != null) {
|
||||
withChats {
|
||||
updatedMembers.forEach { updatedMember ->
|
||||
upsertGroupMember(rhId, groupInfo, updatedMember)
|
||||
}
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
updatedMembers.forEach { updatedMember ->
|
||||
upsertGroupMember(rhId, groupInfo, updatedMember)
|
||||
}
|
||||
}
|
||||
onSuccess()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> toggleItemSelection(itemId: T, selectedItems: MutableState<Set<T>?>) {
|
||||
val select = selectedItems.value?.contains(itemId) != true
|
||||
if (select) {
|
||||
val sel = selectedItems.value ?: setOf()
|
||||
selectedItems.value = sel + itemId
|
||||
} else {
|
||||
val sel = (selectedItems.value ?: setOf()).toMutableSet()
|
||||
sel.remove(itemId)
|
||||
selectedItems.value = sel
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewGroupChatInfoLayout() {
|
||||
|
@ -835,10 +1009,12 @@ fun PreviewGroupChatInfoLayout() {
|
|||
setSendReceipts = {},
|
||||
chatItemTTL = remember { mutableStateOf(ChatItemTTL.fromSeconds(0)) },
|
||||
setChatItemTTL = {},
|
||||
members = listOf(GroupMember.sampleData, GroupMember.sampleData, GroupMember.sampleData),
|
||||
activeSortedMembers = listOf(GroupMember.sampleData, GroupMember.sampleData, GroupMember.sampleData),
|
||||
developerTools = false,
|
||||
onLocalAliasChanged = {},
|
||||
groupLink = null,
|
||||
selectedItems = remember { mutableStateOf(null) },
|
||||
appBar = remember { mutableStateOf(null) },
|
||||
scrollToItemId = remember { mutableStateOf(null) },
|
||||
addMembers = {}, showMemberInfo = {}, editGroupProfile = {}, addOrEditWelcomeMessage = {}, openPreferences = {}, deleteGroup = {}, clearChat = {}, leaveGroup = {}, manageGroupLink = {}, onSearchClicked = {}, deletingItems = remember { mutableStateOf(true) }
|
||||
)
|
||||
|
|
|
@ -137,26 +137,10 @@ fun GroupMemberInfoView(
|
|||
if (it == newRole.value) return@GroupMemberInfoLayout
|
||||
val prevValue = newRole.value
|
||||
newRole.value = it
|
||||
updateMemberRoleDialog(it, groupInfo, member, onDismiss = {
|
||||
updateMemberRoleDialog(it, groupInfo, member.memberCurrent, onDismiss = {
|
||||
newRole.value = prevValue
|
||||
}) {
|
||||
withBGApi {
|
||||
kotlin.runCatching {
|
||||
val members = chatModel.controller.apiMembersRole(rhId, groupInfo.groupId, listOf(member.groupMemberId), it)
|
||||
withChats {
|
||||
members.forEach { member ->
|
||||
upsertGroupMember(rhId, groupInfo, member)
|
||||
}
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
members.forEach { member ->
|
||||
upsertGroupMember(rhId, groupInfo, member)
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
newRole.value = prevValue
|
||||
}
|
||||
}
|
||||
updateMembersRole(newRole.value, rhId, groupInfo, listOf(member.groupMemberId), onFailure = { newRole.value = prevValue })
|
||||
}
|
||||
},
|
||||
switchMemberAddress = {
|
||||
|
@ -317,7 +301,7 @@ fun GroupMemberInfoLayout(
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun AdminDestructiveSection() {
|
||||
fun ModeratorDestructiveSection() {
|
||||
val canBlockForAll = member.canBlockForAll(groupInfo)
|
||||
val canRemove = member.canBeRemoved(groupInfo)
|
||||
if (canBlockForAll || canRemove) {
|
||||
|
@ -494,8 +478,8 @@ fun GroupMemberInfoLayout(
|
|||
}
|
||||
}
|
||||
|
||||
if (groupInfo.membership.memberRole >= GroupMemberRole.Admin) {
|
||||
AdminDestructiveSection()
|
||||
if (groupInfo.membership.memberRole >= GroupMemberRole.Moderator) {
|
||||
ModeratorDestructiveSection()
|
||||
} else {
|
||||
NonAdminBlockSection()
|
||||
}
|
||||
|
@ -709,16 +693,37 @@ fun MemberProfileImage(
|
|||
)
|
||||
}
|
||||
|
||||
private fun updateMemberRoleDialog(
|
||||
fun updateMembersRole(newRole: GroupMemberRole, rhId: Long?, groupInfo: GroupInfo, memberIds: List<Long>, onFailure: () -> Unit = {}, onSuccess: () -> Unit = {}) {
|
||||
withBGApi {
|
||||
kotlin.runCatching {
|
||||
val members = chatModel.controller.apiMembersRole(rhId, groupInfo.groupId, memberIds, newRole)
|
||||
withChats {
|
||||
members.forEach { member ->
|
||||
upsertGroupMember(rhId, groupInfo, member)
|
||||
}
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
members.forEach { member ->
|
||||
upsertGroupMember(rhId, groupInfo, member)
|
||||
}
|
||||
}
|
||||
onSuccess()
|
||||
}.onFailure {
|
||||
onFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateMemberRoleDialog(
|
||||
newRole: GroupMemberRole,
|
||||
groupInfo: GroupInfo,
|
||||
member: GroupMember,
|
||||
memberCurrent: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: () -> Unit
|
||||
) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.change_member_role_question),
|
||||
text = if (member.memberCurrent) {
|
||||
text = if (memberCurrent) {
|
||||
if (groupInfo.businessChat == null)
|
||||
String.format(generalGetString(MR.strings.member_role_will_be_changed_with_notification), newRole.text)
|
||||
else
|
||||
|
@ -732,6 +737,22 @@ private fun updateMemberRoleDialog(
|
|||
)
|
||||
}
|
||||
|
||||
fun updateMembersRoleDialog(
|
||||
newRole: GroupMemberRole,
|
||||
groupInfo: GroupInfo,
|
||||
onConfirm: () -> Unit
|
||||
) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.change_member_role_question),
|
||||
text = if (groupInfo.businessChat == null)
|
||||
String.format(generalGetString(MR.strings.member_role_will_be_changed_with_notification), newRole.text)
|
||||
else
|
||||
String.format(generalGetString(MR.strings.member_role_will_be_changed_with_notification_chat), newRole.text),
|
||||
confirmText = generalGetString(MR.strings.change_verb),
|
||||
onConfirm = onConfirm,
|
||||
)
|
||||
}
|
||||
|
||||
fun connectViaMemberAddressAlert(rhId: Long?, connReqUri: String) {
|
||||
try {
|
||||
withBGApi {
|
||||
|
@ -793,7 +814,19 @@ fun blockForAllAlert(rhId: Long?, gInfo: GroupInfo, mem: GroupMember) {
|
|||
text = generalGetString(MR.strings.block_member_desc).format(mem.chatViewName),
|
||||
confirmText = generalGetString(MR.strings.block_for_all),
|
||||
onConfirm = {
|
||||
blockMemberForAll(rhId, gInfo, mem, true)
|
||||
blockMemberForAll(rhId, gInfo, listOf(mem.groupMemberId), true)
|
||||
},
|
||||
destructive = true,
|
||||
)
|
||||
}
|
||||
|
||||
fun blockForAllAlert(rhId: Long?, gInfo: GroupInfo, memberIds: List<Long>, onSuccess: () -> Unit = {}) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.block_members_for_all_question),
|
||||
text = generalGetString(MR.strings.block_members_desc),
|
||||
confirmText = generalGetString(MR.strings.block_for_all),
|
||||
onConfirm = {
|
||||
blockMemberForAll(rhId, gInfo, memberIds, true, onSuccess)
|
||||
},
|
||||
destructive = true,
|
||||
)
|
||||
|
@ -805,14 +838,25 @@ fun unblockForAllAlert(rhId: Long?, gInfo: GroupInfo, mem: GroupMember) {
|
|||
text = generalGetString(MR.strings.unblock_member_desc).format(mem.chatViewName),
|
||||
confirmText = generalGetString(MR.strings.unblock_for_all),
|
||||
onConfirm = {
|
||||
blockMemberForAll(rhId, gInfo, mem, false)
|
||||
blockMemberForAll(rhId, gInfo, listOf(mem.groupMemberId), false)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun blockMemberForAll(rhId: Long?, gInfo: GroupInfo, member: GroupMember, blocked: Boolean) {
|
||||
fun unblockForAllAlert(rhId: Long?, gInfo: GroupInfo, memberIds: List<Long>, onSuccess: () -> Unit = {}) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.unblock_members_for_all_question),
|
||||
text = generalGetString(MR.strings.unblock_members_desc),
|
||||
confirmText = generalGetString(MR.strings.unblock_for_all),
|
||||
onConfirm = {
|
||||
blockMemberForAll(rhId, gInfo, memberIds, false, onSuccess)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun blockMemberForAll(rhId: Long?, gInfo: GroupInfo, memberIds: List<Long>, blocked: Boolean, onSuccess: () -> Unit = {}) {
|
||||
withBGApi {
|
||||
val updatedMembers = ChatController.apiBlockMembersForAll(rhId, gInfo.groupId, listOf(member.groupMemberId), blocked)
|
||||
val updatedMembers = ChatController.apiBlockMembersForAll(rhId, gInfo.groupId, memberIds, blocked)
|
||||
withChats {
|
||||
updatedMembers.forEach { updatedMember ->
|
||||
upsertGroupMember(rhId, gInfo, updatedMember)
|
||||
|
@ -823,6 +867,7 @@ fun blockMemberForAll(rhId: Long?, gInfo: GroupInfo, member: GroupMember, blocke
|
|||
upsertGroupMember(rhId, gInfo, updatedMember)
|
||||
}
|
||||
}
|
||||
onSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
package chat.simplex.common.views.chat.group
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.max
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.chatModel
|
||||
import chat.simplex.common.ui.theme.WarningOrange
|
||||
import chat.simplex.common.views.chat.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
|
||||
@Composable
|
||||
fun SelectedItemsMembersToolbar(
|
||||
selectedItems: MutableState<Set<Long>?>,
|
||||
activeMembers: State<List<GroupMember>>,
|
||||
groupInfo: GroupInfo,
|
||||
delete: () -> Unit,
|
||||
blockForAll: (Boolean) -> Unit, // Boolean - block or unlock
|
||||
changeRole: (GroupMemberRole) -> Unit,
|
||||
) {
|
||||
val deleteEnabled = remember { mutableStateOf(false) }
|
||||
val blockForAllEnabled = remember { mutableStateOf(false) }
|
||||
val unblockForAllEnabled = remember { mutableStateOf(false) }
|
||||
val blockForAllButtonEnabled = remember { derivedStateOf { (blockForAllEnabled.value && !unblockForAllEnabled.value) || (!blockForAllEnabled.value && unblockForAllEnabled.value) } }
|
||||
|
||||
val roleToMemberEnabled = remember { mutableStateOf(false) }
|
||||
val roleToObserverEnabled = remember { mutableStateOf(false) }
|
||||
val roleButtonEnabled = remember { derivedStateOf { (roleToMemberEnabled.value && !roleToObserverEnabled.value) || (!roleToMemberEnabled.value && roleToObserverEnabled.value) } }
|
||||
Box(
|
||||
Modifier
|
||||
.background(MaterialTheme.colors.background)
|
||||
.navigationBarsPadding()
|
||||
.imePadding()
|
||||
) {
|
||||
// It's hard to measure exact height of ComposeView with different fontSizes. Better to depend on actual ComposeView, even empty
|
||||
Box(Modifier.alpha(0f)) {
|
||||
ComposeView(chatModel = chatModel, Chat.sampleData, remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }, remember { mutableStateOf(null) }, {}, remember { FocusRequester() })
|
||||
}
|
||||
Row(
|
||||
Modifier
|
||||
.matchParentSize()
|
||||
.padding(horizontal = 2.dp)
|
||||
.height(AppBarHeight * fontSizeSqrtMultiplier)
|
||||
.pointerInput(Unit) {
|
||||
detectGesture {
|
||||
true
|
||||
}
|
||||
},
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
IconButton(delete, enabled = deleteEnabled.value) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_delete),
|
||||
null,
|
||||
Modifier.size(22.dp),
|
||||
tint = if (!deleteEnabled.value) MaterialTheme.colors.secondary else MaterialTheme.colors.error
|
||||
)
|
||||
}
|
||||
|
||||
IconButton({ blockForAll(blockForAllEnabled.value) }, enabled = blockForAllButtonEnabled.value) {
|
||||
Icon(
|
||||
painterResource(if (unblockForAllEnabled.value && blockForAllButtonEnabled.value) MR.images.ic_do_not_touch else MR.images.ic_back_hand),
|
||||
null,
|
||||
Modifier.size(22.dp),
|
||||
tint = if (!blockForAllButtonEnabled.value) MaterialTheme.colors.secondary else if (blockForAllEnabled.value) MaterialTheme.colors.error else WarningOrange
|
||||
)
|
||||
}
|
||||
|
||||
IconButton({ changeRole(if (roleToMemberEnabled.value) GroupMemberRole.Member else GroupMemberRole.Observer) }, enabled = roleButtonEnabled.value) {
|
||||
Icon(
|
||||
painterResource(if (roleToObserverEnabled.value || !roleButtonEnabled.value) MR.images.ic_person else MR.images.ic_person_edit),
|
||||
null,
|
||||
Modifier.size(22.dp),
|
||||
tint = if (!roleButtonEnabled.value) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
Divider(Modifier.align(Alignment.TopStart))
|
||||
}
|
||||
LaunchedEffect(groupInfo, activeMembers.value.toList(), selectedItems.value) {
|
||||
recheckItems(groupInfo, selectedItems, activeMembers.value, deleteEnabled, blockForAllEnabled, unblockForAllEnabled, roleToMemberEnabled, roleToObserverEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
private fun recheckItems(
|
||||
groupInfo: GroupInfo,
|
||||
selectedItems: MutableState<Set<Long>?>,
|
||||
activeMembers: List<GroupMember>,
|
||||
deleteEnabled: MutableState<Boolean>,
|
||||
blockForAllEnabled: MutableState<Boolean>,
|
||||
unblockForAllEnabled: MutableState<Boolean>,
|
||||
roleToMemberEnabled: MutableState<Boolean>,
|
||||
roleToObserverEnabled: MutableState<Boolean>,
|
||||
) {
|
||||
val selected = selectedItems.value ?: return
|
||||
var rDeleteEnabled = true
|
||||
var rBlockForAllEnabled = true
|
||||
var rUnblockForAllEnabled = true
|
||||
var rRoleToMemberEnabled = true
|
||||
var rRoleToObserverEnabled = true
|
||||
val rSelectedItems = mutableSetOf<Long>()
|
||||
for (mem in activeMembers) {
|
||||
if (selected.contains(mem.groupMemberId) && groupInfo.membership.memberRole >= mem.memberRole && mem.memberRole < GroupMemberRole.Moderator && groupInfo.membership.memberActive) {
|
||||
rDeleteEnabled = rDeleteEnabled && mem.memberStatus != GroupMemberStatus.MemRemoved && mem.memberStatus != GroupMemberStatus.MemLeft
|
||||
rBlockForAllEnabled = rBlockForAllEnabled && !mem.blockedByAdmin
|
||||
rUnblockForAllEnabled = rUnblockForAllEnabled && mem.blockedByAdmin
|
||||
rRoleToMemberEnabled = rRoleToMemberEnabled && mem.memberRole != GroupMemberRole.Member
|
||||
rRoleToObserverEnabled = rRoleToObserverEnabled && mem.memberRole != GroupMemberRole.Observer
|
||||
rSelectedItems.add(mem.groupMemberId) // we are collecting new selected items here to account for any changes in members list
|
||||
}
|
||||
}
|
||||
deleteEnabled.value = rDeleteEnabled
|
||||
blockForAllEnabled.value = rBlockForAllEnabled
|
||||
unblockForAllEnabled.value = rUnblockForAllEnabled
|
||||
roleToMemberEnabled.value = rRoleToMemberEnabled
|
||||
roleToObserverEnabled.value = rRoleToObserverEnabled
|
||||
selectedItems.value = rSelectedItems
|
||||
}
|
|
@ -21,6 +21,7 @@ import chat.simplex.res.MR
|
|||
|
||||
private val featureRoles: List<Pair<GroupMemberRole?, String>> = listOf(
|
||||
null to generalGetString(MR.strings.feature_roles_all_members),
|
||||
GroupMemberRole.Moderator to generalGetString(MR.strings.feature_roles_moderators),
|
||||
GroupMemberRole.Admin to generalGetString(MR.strings.feature_roles_admins),
|
||||
GroupMemberRole.Owner to generalGetString(MR.strings.feature_roles_owners)
|
||||
)
|
||||
|
|
|
@ -865,14 +865,14 @@ fun ModerateItemAction(
|
|||
@Composable
|
||||
fun SelectItemAction(
|
||||
showMenu: MutableState<Boolean>,
|
||||
selectChatItem: () -> Unit,
|
||||
selectItem: () -> Unit,
|
||||
) {
|
||||
ItemAction(
|
||||
stringResource(MR.strings.select_verb),
|
||||
painterResource(MR.images.ic_check_circle),
|
||||
onClick = {
|
||||
showMenu.value = false
|
||||
selectChatItem()
|
||||
selectItem()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ fun ModalView(
|
|||
searchAlwaysVisible: Boolean = false,
|
||||
onSearchValueChanged: (String) -> Unit = {},
|
||||
endButtons: @Composable RowScope.() -> Unit = {},
|
||||
appBar: @Composable (BoxScope.() -> Unit)? = null,
|
||||
content: @Composable BoxScope.() -> Unit,
|
||||
) {
|
||||
if (showClose && showAppBar) {
|
||||
|
@ -48,14 +49,20 @@ fun ModalView(
|
|||
StatusBarBackground()
|
||||
}
|
||||
Box(Modifier.align(if (oneHandUI.value) Alignment.BottomStart else Alignment.TopStart)) {
|
||||
DefaultAppBar(
|
||||
navigationButton = if (showClose) {{ NavigationButtonBack(onButtonClicked = if (enableClose) close else null) }} else null,
|
||||
onTop = !oneHandUI.value,
|
||||
showSearch = showSearch,
|
||||
searchAlwaysVisible = searchAlwaysVisible,
|
||||
onSearchValueChanged = onSearchValueChanged,
|
||||
buttons = endButtons
|
||||
)
|
||||
if (appBar != null) {
|
||||
appBar()
|
||||
} else {
|
||||
DefaultAppBar(
|
||||
navigationButton = if (showClose) {
|
||||
{ NavigationButtonBack(onButtonClicked = if (enableClose) close else null) }
|
||||
} else null,
|
||||
onTop = !oneHandUI.value,
|
||||
showSearch = showSearch,
|
||||
searchAlwaysVisible = searchAlwaysVisible,
|
||||
onSearchValueChanged = onSearchValueChanged,
|
||||
buttons = endButtons
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1738,25 +1738,32 @@
|
|||
|
||||
<!-- GroupMemberInfoView.kt -->
|
||||
<string name="button_remove_member_question">Remove member?</string>
|
||||
<string name="button_remove_members_question">Remove members?</string>
|
||||
<string name="button_remove_member">Remove member</string>
|
||||
|
||||
<string name="button_send_direct_message">Send direct message</string>
|
||||
<string name="member_will_be_removed_from_group_cannot_be_undone">Member will be removed from group - this cannot be undone!</string>
|
||||
<string name="members_will_be_removed_from_group_cannot_be_undone">Members will be removed from group - this cannot be undone!</string>
|
||||
<string name="member_will_be_removed_from_chat_cannot_be_undone">Member will be removed from chat - this cannot be undone!</string>
|
||||
<string name="members_will_be_removed_from_chat_cannot_be_undone">Members will be removed from chat - this cannot be undone!</string>
|
||||
<string name="remove_member_confirmation">Remove</string>
|
||||
<string name="remove_member_button">Remove member</string>
|
||||
<string name="block_member_question">Block member?</string>
|
||||
<string name="block_member_button">Block member</string>
|
||||
<string name="block_member_confirmation">Block</string>
|
||||
<string name="block_for_all_question">Block member for all?</string>
|
||||
<string name="block_members_for_all_question">Block members for all?</string>
|
||||
<string name="block_for_all">Block for all</string>
|
||||
<string name="block_member_desc">All new messages from %s will be hidden!</string>
|
||||
<string name="block_members_desc">All new messages from these members will be hidden!</string>
|
||||
<string name="unblock_member_question">Unblock member?</string>
|
||||
<string name="unblock_member_button">Unblock member</string>
|
||||
<string name="unblock_member_confirmation">Unblock</string>
|
||||
<string name="unblock_for_all_question">Unblock member for all?</string>
|
||||
<string name="unblock_members_for_all_question">Unblock members for all?</string>
|
||||
<string name="unblock_for_all">Unblock for all</string>
|
||||
<string name="unblock_member_desc">Messages from %s will be shown!</string>
|
||||
<string name="unblock_members_desc">Messages from these members will be shown!</string>
|
||||
<string name="member_blocked_by_admin">Blocked by admin</string>
|
||||
<string name="member_info_member_blocked">blocked</string>
|
||||
<string name="member_info_member_disabled">disabled</string>
|
||||
|
@ -2122,6 +2129,7 @@
|
|||
<string name="feature_offered_item_with_param">offered %s: %2s</string>
|
||||
<string name="feature_cancelled_item">cancelled %s</string>
|
||||
<string name="feature_roles_all_members">all members</string>
|
||||
<string name="feature_roles_moderators">moderators</string>
|
||||
<string name="feature_roles_admins">admins</string>
|
||||
<string name="feature_roles_owners">owners</string>
|
||||
<string name="feature_enabled_for">Enabled for</string>
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="m225.5-216-140-140 40-40.5 100 99 179-179L445-435 225.5-216Zm0-320-140-140 40-40.5 100 99 179-179L445-755 225.5-536ZM521-291.5V-349h354v57.5H521Zm0-320V-669h354v57.5H521Z"/></svg>
|
After Width: | Height: | Size: 295 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M500-224Zm-335.5 57.5v-91q0-37.75 18.75-64.12Q202-348 231.68-361.67 298-391 358.5-406t121.28-15q37.97 0 74.55 6t74.17 17l-45.43 45q-26.57-5.5-51.47-8-24.89-2.5-51.36-2.5-56.74 0-109.74 11.5-53 11.5-116 42-14 7-23.25 21.73T222-257.26V-224h278v57.5H164.5Zm393 41.5v-121.5L778-466q9-8.5 19.75-12.5 10.76-4 21.51-4 11.73 0 22.49 4.25Q852.5-474 861.5-465l37 37q8.76 8.85 12.63 19.68Q915-397.5 915-386.75t-4.38 22.03q-4.38 11.28-13.05 19.74L679-125H557.5Zm299-262-37-37 37 37Zm-240 203h37.76L776.5-307l-17.89-19-18.88-18L616.5-222v38Zm142-142-19-18 37 37-18-19ZM480-480.5q-62 0-104.75-42.75T332.5-628q0-62 42.75-104.75T480-775.5q62 0 104.75 42.75T627.5-628q0 62-42.75 104.75T480-480.5Zm0-57.5q38 0 64-26t26-64q0-38-26-64t-64-26q-38 0-64 26t-26 64q0 38 26 64t64 26Zm0-90Z"/></svg>
|
After Width: | Height: | Size: 889 B |
Loading…
Add table
Reference in a new issue