mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-06-28 20:29:53 +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 {
|
fun canBlockForAll(groupInfo: GroupInfo): Boolean {
|
||||||
val userRole = groupInfo.membership.memberRole
|
val userRole = groupInfo.membership.memberRole
|
||||||
return memberStatus != GroupMemberStatus.MemRemoved && memberStatus != GroupMemberStatus.MemLeft && memberRole < GroupMemberRole.Admin
|
return memberStatus != GroupMemberStatus.MemRemoved && memberStatus != GroupMemberStatus.MemLeft && memberRole < GroupMemberRole.Moderator
|
||||||
&& userRole >= GroupMemberRole.Admin && userRole >= memberRole && groupInfo.membership.memberActive
|
&& userRole >= GroupMemberRole.Moderator && userRole >= memberRole && groupInfo.membership.memberActive
|
||||||
}
|
}
|
||||||
|
|
||||||
val memberIncognito = memberProfile.profileId != memberContactProfileId
|
val memberIncognito = memberProfile.profileId != memberContactProfileId
|
||||||
|
@ -2439,14 +2439,14 @@ data class ChatItem (
|
||||||
fun memberToModerate(chatInfo: ChatInfo): Pair<GroupInfo, GroupMember?>? {
|
fun memberToModerate(chatInfo: ChatInfo): Pair<GroupInfo, GroupMember?>? {
|
||||||
return if (chatInfo is ChatInfo.Group && chatDir is CIDirection.GroupRcv) {
|
return if (chatInfo is ChatInfo.Group && chatDir is CIDirection.GroupRcv) {
|
||||||
val m = chatInfo.groupInfo.membership
|
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
|
chatInfo.groupInfo to chatDir.groupMember
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
} else if (chatInfo is ChatInfo.Group && chatDir is CIDirection.GroupSnd) {
|
} else if (chatInfo is ChatInfo.Group && chatDir is CIDirection.GroupSnd) {
|
||||||
val m = chatInfo.groupInfo.membership
|
val m = chatInfo.groupInfo.membership
|
||||||
if (m.memberRole >= GroupMemberRole.Admin) {
|
if (m.memberRole >= GroupMemberRole.Moderator) {
|
||||||
chatInfo.groupInfo to null
|
chatInfo.groupInfo to null
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
|
@ -3259,6 +3259,7 @@ sealed class CIContent: ItemContent {
|
||||||
when (role) {
|
when (role) {
|
||||||
GroupMemberRole.Owner -> generalGetString(MR.strings.feature_roles_owners)
|
GroupMemberRole.Owner -> generalGetString(MR.strings.feature_roles_owners)
|
||||||
GroupMemberRole.Admin -> generalGetString(MR.strings.feature_roles_admins)
|
GroupMemberRole.Admin -> generalGetString(MR.strings.feature_roles_admins)
|
||||||
|
GroupMemberRole.Moderator -> generalGetString(MR.strings.feature_roles_moderators)
|
||||||
else -> generalGetString(MR.strings.feature_roles_all_members)
|
else -> generalGetString(MR.strings.feature_roles_all_members)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -175,7 +175,7 @@ fun ChatView(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
SelectedItemsBottomToolbar(
|
SelectedItemsButtonsToolbar(
|
||||||
contentTag = contentTag,
|
contentTag = contentTag,
|
||||||
selectedChatItems = selectedChatItems,
|
selectedChatItems = selectedChatItems,
|
||||||
chatInfo = chatInfo,
|
chatInfo = chatInfo,
|
||||||
|
@ -274,34 +274,46 @@ fun ChatView(
|
||||||
}
|
}
|
||||||
if (!isActive) return@launch
|
if (!isActive) return@launch
|
||||||
|
|
||||||
ModalManager.end.showModalCloseable(true) { close ->
|
val selectedItems: MutableState<Set<Long>?> = mutableStateOf(null)
|
||||||
val chatInfo = remember { activeChatInfo }.value
|
ModalManager.end.showCustomModal { close ->
|
||||||
if (chatInfo is ChatInfo.Direct) {
|
val appBar = remember { mutableStateOf(null as @Composable (BoxScope.() -> Unit)?) }
|
||||||
var contactInfo: Pair<ConnectionStats?, Profile?>? by remember { mutableStateOf(preloadedContactInfo) }
|
ModalView(close, appBar = appBar.value) {
|
||||||
var code: String? by remember { mutableStateOf(preloadedCode) }
|
val chatInfo = remember { activeChatInfo }.value
|
||||||
KeyChangeEffect(chatInfo.id, ChatModel.networkStatuses.toMap()) {
|
if (chatInfo is ChatInfo.Direct) {
|
||||||
contactInfo = chatModel.controller.apiContactInfo(chatRh, chatInfo.apiId)
|
var contactInfo: Pair<ConnectionStats?, Profile?>? by remember { mutableStateOf(preloadedContactInfo) }
|
||||||
preloadedContactInfo = contactInfo
|
var code: String? by remember { mutableStateOf(preloadedCode) }
|
||||||
code = chatModel.controller.apiGetContactCode(chatRh, chatInfo.apiId)?.second
|
KeyChangeEffect(chatInfo.id, ChatModel.networkStatuses.toMap()) {
|
||||||
preloadedCode = code
|
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) {
|
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) {
|
AnimatedVisibility(selectedChatItems.value != null) {
|
||||||
if (chatInfo != null) {
|
if (chatInfo != null) {
|
||||||
SelectedItemsBottomToolbar(
|
SelectedItemsButtonsToolbar(
|
||||||
contentTag = contentTag,
|
contentTag = contentTag,
|
||||||
selectedChatItems = selectedChatItems,
|
selectedChatItems = selectedChatItems,
|
||||||
chatInfo = chatInfo,
|
chatInfo = chatInfo,
|
||||||
|
@ -846,7 +858,7 @@ fun ChatLayout(
|
||||||
if (selectedChatItems.value == null) {
|
if (selectedChatItems.value == null) {
|
||||||
GroupReportsAppBar(contentTag, { ModalManager.end.closeModal() }, onSearchValueChanged)
|
GroupReportsAppBar(contentTag, { ModalManager.end.closeModal() }, onSearchValueChanged)
|
||||||
} else {
|
} 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)
|
ChatInfoToolbar(chatInfo, contentTag, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
SelectedItemsTopToolbar(selectedChatItems, !oneHandUI.value || !chatBottomBar.value)
|
SelectedItemsCounterToolbar(selectedChatItems, !oneHandUI.value || !chatBottomBar.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (contentTag == null && reportsCount > 0 && (!oneHandUI.value || !chatBottomBar.value)) {
|
if (contentTag == null && reportsCount > 0 && (!oneHandUI.value || !chatBottomBar.value)) {
|
||||||
|
@ -1432,7 +1444,7 @@ fun BoxScope.ChatItemsList(
|
||||||
fun Item() {
|
fun Item() {
|
||||||
ChatItemBox(Modifier.layoutId(CHAT_BUBBLE_LAYOUT_ID)) {
|
ChatItemBox(Modifier.layoutId(CHAT_BUBBLE_LAYOUT_ID)) {
|
||||||
androidx.compose.animation.AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) {
|
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() }) {
|
Row(Modifier.graphicsLayer { translationX = selectionOffset.toPx() }) {
|
||||||
val member = cItem.chatDir.groupMember
|
val member = cItem.chatDir.groupMember
|
||||||
|
@ -1457,7 +1469,7 @@ fun BoxScope.ChatItemsList(
|
||||||
} else {
|
} else {
|
||||||
ChatItemBox {
|
ChatItemBox {
|
||||||
AnimatedVisibility (selectionVisible, enter = fadeIn(), exit = fadeOut()) {
|
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(
|
Row(
|
||||||
Modifier
|
Modifier
|
||||||
|
@ -1472,7 +1484,7 @@ fun BoxScope.ChatItemsList(
|
||||||
} else {
|
} else {
|
||||||
ChatItemBox {
|
ChatItemBox {
|
||||||
AnimatedVisibility (selectionVisible, enter = fadeIn(), exit = fadeOut()) {
|
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(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
|
@ -1487,7 +1499,7 @@ fun BoxScope.ChatItemsList(
|
||||||
} else { // direct message
|
} else { // direct message
|
||||||
ChatItemBox {
|
ChatItemBox {
|
||||||
AnimatedVisibility (selectionVisible, enter = fadeIn(), exit = fadeOut()) {
|
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(
|
Box(
|
||||||
|
@ -2296,12 +2308,12 @@ private fun BoxScope.BottomEndFloatingButton(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SelectedChatItem(
|
fun SelectedListItem(
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
ciId: Long,
|
id: Long,
|
||||||
selectedChatItems: State<Set<Long>?>,
|
selectedItems: State<Set<Long>?>,
|
||||||
) {
|
) {
|
||||||
val checked = remember { derivedStateOf { selectedChatItems.value?.contains(ciId) == true } }
|
val checked = remember { derivedStateOf { selectedItems.value?.contains(id) == true } }
|
||||||
Icon(
|
Icon(
|
||||||
painterResource(if (checked.value) MR.images.ic_check_circle_filled else MR.images.ic_radio_button_unchecked),
|
painterResource(if (checked.value) MR.images.ic_check_circle_filled else MR.images.ic_radio_button_unchecked),
|
||||||
null,
|
null,
|
||||||
|
|
|
@ -9,12 +9,10 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
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.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import chat.simplex.common.model.*
|
import chat.simplex.common.model.*
|
||||||
import chat.simplex.common.model.ChatController.appPrefs
|
|
||||||
import chat.simplex.common.platform.BackHandler
|
import chat.simplex.common.platform.BackHandler
|
||||||
import chat.simplex.common.platform.chatModel
|
import chat.simplex.common.platform.chatModel
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
|
@ -23,32 +21,44 @@ import chat.simplex.res.MR
|
||||||
import dev.icerock.moko.resources.compose.painterResource
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BoxScope.SelectedItemsTopToolbar(selectedChatItems: MutableState<Set<Long>?>, onTop: Boolean) {
|
fun BoxScope.SelectedItemsCounterToolbar(selectedItems: MutableState<Set<Long>?>, onTop: Boolean, selectAll: (() -> Unit)? = null) {
|
||||||
val onBackClicked = { selectedChatItems.value = null }
|
val onBackClicked = { selectedItems.value = null }
|
||||||
BackHandler(onBack = onBackClicked)
|
BackHandler(onBack = onBackClicked)
|
||||||
val count = selectedChatItems.value?.size ?: 0
|
val count = selectedItems.value?.size ?: 0
|
||||||
DefaultAppBar(
|
Box(if (onTop) Modifier else Modifier.imePadding()) {
|
||||||
navigationButton = { NavigationButtonClose(onButtonClicked = onBackClicked) },
|
DefaultAppBar(
|
||||||
title = {
|
navigationButton = { NavigationButtonClose(onButtonClicked = onBackClicked) },
|
||||||
Text(
|
title = {
|
||||||
if (count == 0) {
|
Text(
|
||||||
stringResource(MR.strings.selected_chat_items_nothing_selected)
|
if (count == 0) {
|
||||||
} else {
|
stringResource(MR.strings.selected_chat_items_nothing_selected)
|
||||||
stringResource(MR.strings.selected_chat_items_selected_n).format(count)
|
} else {
|
||||||
},
|
stringResource(MR.strings.selected_chat_items_selected_n).format(count)
|
||||||
fontWeight = FontWeight.SemiBold,
|
},
|
||||||
maxLines = 1,
|
fontWeight = FontWeight.SemiBold,
|
||||||
overflow = TextOverflow.Ellipsis
|
maxLines = 1,
|
||||||
)
|
overflow = TextOverflow.Ellipsis
|
||||||
},
|
)
|
||||||
onTitleClick = null,
|
},
|
||||||
onTop = onTop,
|
onTitleClick = null,
|
||||||
onSearchValueChanged = {},
|
onTop = onTop,
|
||||||
)
|
onSearchValueChanged = {},
|
||||||
|
buttons = if (selectAll != null) { { SelectAllButton(selectAll) } } else {{}}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@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,
|
chatInfo: ChatInfo,
|
||||||
contentTag: MsgContentTag?,
|
contentTag: MsgContentTag?,
|
||||||
selectedChatItems: MutableState<Set<Long>?>,
|
selectedChatItems: MutableState<Set<Long>?>,
|
||||||
|
@ -162,4 +172,4 @@ private fun recheckItems(chatInfo: ChatInfo,
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun possibleToModerate(chatInfo: ChatInfo): Boolean =
|
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 SectionSpacer
|
||||||
import SectionTextFooter
|
import SectionTextFooter
|
||||||
import SectionView
|
import SectionView
|
||||||
|
import androidx.compose.animation.*
|
||||||
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
@ -17,6 +19,7 @@ import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
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.model.GroupInfo
|
||||||
import chat.simplex.common.platform.*
|
import chat.simplex.common.platform.*
|
||||||
import chat.simplex.common.views.chat.*
|
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.chatlist.*
|
||||||
import chat.simplex.common.views.database.TtlOptions
|
import chat.simplex.common.views.database.TtlOptions
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
|
@ -49,7 +52,18 @@ val MEMBER_ROW_AVATAR_SIZE = 42.dp
|
||||||
val MEMBER_ROW_VERTICAL_PADDING = 8.dp
|
val MEMBER_ROW_VERTICAL_PADDING = 8.dp
|
||||||
|
|
||||||
@Composable
|
@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)
|
BackHandler(onBack = close)
|
||||||
// TODO derivedStateOf?
|
// TODO derivedStateOf?
|
||||||
val chat = chatModel.chats.value.firstOrNull { ch -> ch.id == chatId && ch.remoteHostId == rhId }
|
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)
|
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 }
|
.filter { it.memberStatus != GroupMemberStatus.MemLeft && it.memberStatus != GroupMemberStatus.MemRemoved }
|
||||||
.sortedByDescending { it.memberRole },
|
.sortedByDescending { it.memberRole },
|
||||||
developerTools,
|
developerTools,
|
||||||
onLocalAliasChanged = { setGroupAlias(chat, it, chatModel) },
|
onLocalAliasChanged = { setGroupAlias(chat, it, chatModel) },
|
||||||
groupLink,
|
groupLink,
|
||||||
|
selectedItems,
|
||||||
|
appBar,
|
||||||
scrollToItemId,
|
scrollToItemId,
|
||||||
addMembers = {
|
addMembers = {
|
||||||
scope.launch(Dispatchers.Default) {
|
scope.launch(Dispatchers.Default) {
|
||||||
|
@ -212,21 +228,23 @@ private fun removeMemberAlert(rhId: Long?, groupInfo: GroupInfo, mem: GroupMembe
|
||||||
text = generalGetString(messageId),
|
text = generalGetString(messageId),
|
||||||
confirmText = generalGetString(MR.strings.remove_member_confirmation),
|
confirmText = generalGetString(MR.strings.remove_member_confirmation),
|
||||||
onConfirm = {
|
onConfirm = {
|
||||||
withBGApi {
|
removeMembers(rhId, groupInfo, listOf(mem.groupMemberId))
|
||||||
val updatedMembers = chatModel.controller.apiRemoveMembers(rhId, groupInfo.groupId, listOf(mem.groupMemberId))
|
},
|
||||||
if (updatedMembers != null) {
|
destructive = true,
|
||||||
withChats {
|
)
|
||||||
updatedMembers.forEach { updatedMember ->
|
}
|
||||||
upsertGroupMember(rhId, groupInfo, updatedMember)
|
|
||||||
}
|
private fun removeMembersAlert(rhId: Long?, groupInfo: GroupInfo, memberIds: List<Long>, onSuccess: () -> Unit = {}) {
|
||||||
}
|
val messageId = if (groupInfo.businessChat == null)
|
||||||
withReportsChatsIfOpen {
|
MR.strings.members_will_be_removed_from_group_cannot_be_undone
|
||||||
updatedMembers.forEach { updatedMember ->
|
else
|
||||||
upsertGroupMember(rhId, groupInfo, updatedMember)
|
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,
|
destructive = true,
|
||||||
)
|
)
|
||||||
|
@ -309,10 +327,12 @@ fun ModalData.GroupChatInfoLayout(
|
||||||
setSendReceipts: (SendReceipts) -> Unit,
|
setSendReceipts: (SendReceipts) -> Unit,
|
||||||
chatItemTTL: MutableState<ChatItemTTL?>,
|
chatItemTTL: MutableState<ChatItemTTL?>,
|
||||||
setChatItemTTL: (ChatItemTTL?) -> Unit,
|
setChatItemTTL: (ChatItemTTL?) -> Unit,
|
||||||
members: List<GroupMember>,
|
activeSortedMembers: List<GroupMember>,
|
||||||
developerTools: Boolean,
|
developerTools: Boolean,
|
||||||
onLocalAliasChanged: (String) -> Unit,
|
onLocalAliasChanged: (String) -> Unit,
|
||||||
groupLink: String?,
|
groupLink: String?,
|
||||||
|
selectedItems: MutableState<Set<Long>?>,
|
||||||
|
appBar: MutableState<@Composable (BoxScope.() -> Unit)?>,
|
||||||
scrollToItemId: MutableState<Long?>,
|
scrollToItemId: MutableState<Long?>,
|
||||||
addMembers: () -> Unit,
|
addMembers: () -> Unit,
|
||||||
showMemberInfo: (GroupMember) -> Unit,
|
showMemberInfo: (GroupMember) -> Unit,
|
||||||
|
@ -333,20 +353,37 @@ fun ModalData.GroupChatInfoLayout(
|
||||||
scope.launch { listState.scrollToItem(0) }
|
scope.launch { listState.scrollToItem(0) }
|
||||||
}
|
}
|
||||||
val searchText = remember { stateGetOrPut("searchText") { TextFieldValue() } }
|
val searchText = remember { stateGetOrPut("searchText") { TextFieldValue() } }
|
||||||
val filteredMembers = remember(members) {
|
val filteredMembers = remember(activeSortedMembers) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
val s = searchText.value.text.trim().lowercase()
|
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 {
|
Box {
|
||||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
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(
|
LazyColumnWithScrollBar(
|
||||||
state = listState,
|
state = listState,
|
||||||
contentPadding = if (oneHandUI.value) {
|
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 {
|
} 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 {
|
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)
|
SendReceiptsOption(currentUser, sendReceipts, setSendReceipts)
|
||||||
} else {
|
} else {
|
||||||
SendReceiptsOptionDisabled()
|
SendReceiptsOptionDisabled()
|
||||||
|
@ -424,7 +461,7 @@ fun ModalData.GroupChatInfoLayout(
|
||||||
ChatTTLSection(chatItemTTL, setChatItemTTL, deletingItems)
|
ChatTTLSection(chatItemTTL, setChatItemTTL, deletingItems)
|
||||||
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = true)
|
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.canAddMembers) {
|
||||||
if (groupInfo.businessChat == null) {
|
if (groupInfo.businessChat == null) {
|
||||||
if (groupLink == null) {
|
if (groupLink == null) {
|
||||||
|
@ -442,7 +479,7 @@ fun ModalData.GroupChatInfoLayout(
|
||||||
}
|
}
|
||||||
AddMembersButton(addMembersTitleId, tint, onAddMembersClick)
|
AddMembersButton(addMembersTitleId, tint, onAddMembersClick)
|
||||||
}
|
}
|
||||||
if (members.size > 8) {
|
if (activeSortedMembers.size > 8) {
|
||||||
SectionItemView(padding = PaddingValues(start = 14.dp, end = DEFAULT_PADDING_HALF)) {
|
SectionItemView(padding = PaddingValues(start = 14.dp, end = DEFAULT_PADDING_HALF)) {
|
||||||
SearchRowView(searchText)
|
SearchRowView(searchText)
|
||||||
}
|
}
|
||||||
|
@ -452,12 +489,34 @@ fun ModalData.GroupChatInfoLayout(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
items(filteredMembers.value) { member ->
|
items(filteredMembers.value, key = { it.groupMemberId }) { member ->
|
||||||
Divider()
|
Divider()
|
||||||
val showMenu = remember { mutableStateOf(false) }
|
val showMenu = remember { mutableStateOf(false) }
|
||||||
SectionItemViewLongClickable({ showMemberInfo(member) }, { showMenu.value = true }, minHeight = 54.dp, padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
val canBeSelected = groupInfo.membership.memberRole >= member.memberRole && member.memberRole < GroupMemberRole.Moderator
|
||||||
DropDownMenuForMember(chat.remoteHostId, member, groupInfo, showMenu)
|
SectionItemViewLongClickable(
|
||||||
MemberRow(member)
|
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 {
|
item {
|
||||||
|
@ -482,12 +541,92 @@ fun ModalData.GroupChatInfoLayout(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SectionBottomSpacer()
|
SectionBottomSpacer()
|
||||||
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!oneHandUI.value) {
|
if (!oneHandUI.value) {
|
||||||
NavigationBarBackground(oneHandUI.value, 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)
|
Text(stringResource(MR.strings.member_info_member_blocked), color = MaterialTheme.colors.secondary)
|
||||||
} else {
|
} else {
|
||||||
val role = member.memberRole
|
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)
|
Text(role.text, color = MaterialTheme.colors.secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -686,8 +825,8 @@ private fun MemberVerifiedShield() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DropDownMenuForMember(rhId: Long?, member: GroupMember, groupInfo: GroupInfo, showMenu: MutableState<Boolean>) {
|
private fun DropDownMenuForMember(rhId: Long?, member: GroupMember, groupInfo: GroupInfo, selectedItems: MutableState<Set<Long>?>, showMenu: MutableState<Boolean>) {
|
||||||
if (groupInfo.membership.memberRole >= GroupMemberRole.Admin) {
|
if (groupInfo.membership.memberRole >= GroupMemberRole.Moderator) {
|
||||||
val canBlockForAll = member.canBlockForAll(groupInfo)
|
val canBlockForAll = member.canBlockForAll(groupInfo)
|
||||||
val canRemove = member.canBeRemoved(groupInfo)
|
val canRemove = member.canBeRemoved(groupInfo)
|
||||||
if (canBlockForAll || canRemove) {
|
if (canBlockForAll || canRemove) {
|
||||||
|
@ -711,6 +850,10 @@ private fun DropDownMenuForMember(rhId: Long?, member: GroupMember, groupInfo: G
|
||||||
showMenu.value = false
|
showMenu.value = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if (selectedItems.value == null && member.memberRole < GroupMemberRole.Moderator) {
|
||||||
|
Divider()
|
||||||
|
SelectItemAction(showMenu) { toggleItemSelection(member.groupMemberId, selectedItems) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (!member.blockedByAdmin) {
|
} 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
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun PreviewGroupChatInfoLayout() {
|
fun PreviewGroupChatInfoLayout() {
|
||||||
|
@ -835,10 +1009,12 @@ fun PreviewGroupChatInfoLayout() {
|
||||||
setSendReceipts = {},
|
setSendReceipts = {},
|
||||||
chatItemTTL = remember { mutableStateOf(ChatItemTTL.fromSeconds(0)) },
|
chatItemTTL = remember { mutableStateOf(ChatItemTTL.fromSeconds(0)) },
|
||||||
setChatItemTTL = {},
|
setChatItemTTL = {},
|
||||||
members = listOf(GroupMember.sampleData, GroupMember.sampleData, GroupMember.sampleData),
|
activeSortedMembers = listOf(GroupMember.sampleData, GroupMember.sampleData, GroupMember.sampleData),
|
||||||
developerTools = false,
|
developerTools = false,
|
||||||
onLocalAliasChanged = {},
|
onLocalAliasChanged = {},
|
||||||
groupLink = null,
|
groupLink = null,
|
||||||
|
selectedItems = remember { mutableStateOf(null) },
|
||||||
|
appBar = remember { mutableStateOf(null) },
|
||||||
scrollToItemId = remember { mutableStateOf(null) },
|
scrollToItemId = remember { mutableStateOf(null) },
|
||||||
addMembers = {}, showMemberInfo = {}, editGroupProfile = {}, addOrEditWelcomeMessage = {}, openPreferences = {}, deleteGroup = {}, clearChat = {}, leaveGroup = {}, manageGroupLink = {}, onSearchClicked = {}, deletingItems = remember { mutableStateOf(true) }
|
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
|
if (it == newRole.value) return@GroupMemberInfoLayout
|
||||||
val prevValue = newRole.value
|
val prevValue = newRole.value
|
||||||
newRole.value = it
|
newRole.value = it
|
||||||
updateMemberRoleDialog(it, groupInfo, member, onDismiss = {
|
updateMemberRoleDialog(it, groupInfo, member.memberCurrent, onDismiss = {
|
||||||
newRole.value = prevValue
|
newRole.value = prevValue
|
||||||
}) {
|
}) {
|
||||||
withBGApi {
|
updateMembersRole(newRole.value, rhId, groupInfo, listOf(member.groupMemberId), onFailure = { newRole.value = prevValue })
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
switchMemberAddress = {
|
switchMemberAddress = {
|
||||||
|
@ -317,7 +301,7 @@ fun GroupMemberInfoLayout(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AdminDestructiveSection() {
|
fun ModeratorDestructiveSection() {
|
||||||
val canBlockForAll = member.canBlockForAll(groupInfo)
|
val canBlockForAll = member.canBlockForAll(groupInfo)
|
||||||
val canRemove = member.canBeRemoved(groupInfo)
|
val canRemove = member.canBeRemoved(groupInfo)
|
||||||
if (canBlockForAll || canRemove) {
|
if (canBlockForAll || canRemove) {
|
||||||
|
@ -494,8 +478,8 @@ fun GroupMemberInfoLayout(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (groupInfo.membership.memberRole >= GroupMemberRole.Admin) {
|
if (groupInfo.membership.memberRole >= GroupMemberRole.Moderator) {
|
||||||
AdminDestructiveSection()
|
ModeratorDestructiveSection()
|
||||||
} else {
|
} else {
|
||||||
NonAdminBlockSection()
|
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,
|
newRole: GroupMemberRole,
|
||||||
groupInfo: GroupInfo,
|
groupInfo: GroupInfo,
|
||||||
member: GroupMember,
|
memberCurrent: Boolean,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onConfirm: () -> Unit
|
onConfirm: () -> Unit
|
||||||
) {
|
) {
|
||||||
AlertManager.shared.showAlertDialog(
|
AlertManager.shared.showAlertDialog(
|
||||||
title = generalGetString(MR.strings.change_member_role_question),
|
title = generalGetString(MR.strings.change_member_role_question),
|
||||||
text = if (member.memberCurrent) {
|
text = if (memberCurrent) {
|
||||||
if (groupInfo.businessChat == null)
|
if (groupInfo.businessChat == null)
|
||||||
String.format(generalGetString(MR.strings.member_role_will_be_changed_with_notification), newRole.text)
|
String.format(generalGetString(MR.strings.member_role_will_be_changed_with_notification), newRole.text)
|
||||||
else
|
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) {
|
fun connectViaMemberAddressAlert(rhId: Long?, connReqUri: String) {
|
||||||
try {
|
try {
|
||||||
withBGApi {
|
withBGApi {
|
||||||
|
@ -793,7 +814,19 @@ fun blockForAllAlert(rhId: Long?, gInfo: GroupInfo, mem: GroupMember) {
|
||||||
text = generalGetString(MR.strings.block_member_desc).format(mem.chatViewName),
|
text = generalGetString(MR.strings.block_member_desc).format(mem.chatViewName),
|
||||||
confirmText = generalGetString(MR.strings.block_for_all),
|
confirmText = generalGetString(MR.strings.block_for_all),
|
||||||
onConfirm = {
|
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,
|
destructive = true,
|
||||||
)
|
)
|
||||||
|
@ -805,14 +838,25 @@ fun unblockForAllAlert(rhId: Long?, gInfo: GroupInfo, mem: GroupMember) {
|
||||||
text = generalGetString(MR.strings.unblock_member_desc).format(mem.chatViewName),
|
text = generalGetString(MR.strings.unblock_member_desc).format(mem.chatViewName),
|
||||||
confirmText = generalGetString(MR.strings.unblock_for_all),
|
confirmText = generalGetString(MR.strings.unblock_for_all),
|
||||||
onConfirm = {
|
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 {
|
withBGApi {
|
||||||
val updatedMembers = ChatController.apiBlockMembersForAll(rhId, gInfo.groupId, listOf(member.groupMemberId), blocked)
|
val updatedMembers = ChatController.apiBlockMembersForAll(rhId, gInfo.groupId, memberIds, blocked)
|
||||||
withChats {
|
withChats {
|
||||||
updatedMembers.forEach { updatedMember ->
|
updatedMembers.forEach { updatedMember ->
|
||||||
upsertGroupMember(rhId, gInfo, updatedMember)
|
upsertGroupMember(rhId, gInfo, updatedMember)
|
||||||
|
@ -823,6 +867,7 @@ fun blockMemberForAll(rhId: Long?, gInfo: GroupInfo, member: GroupMember, blocke
|
||||||
upsertGroupMember(rhId, gInfo, updatedMember)
|
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(
|
private val featureRoles: List<Pair<GroupMemberRole?, String>> = listOf(
|
||||||
null to generalGetString(MR.strings.feature_roles_all_members),
|
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.Admin to generalGetString(MR.strings.feature_roles_admins),
|
||||||
GroupMemberRole.Owner to generalGetString(MR.strings.feature_roles_owners)
|
GroupMemberRole.Owner to generalGetString(MR.strings.feature_roles_owners)
|
||||||
)
|
)
|
||||||
|
|
|
@ -865,14 +865,14 @@ fun ModerateItemAction(
|
||||||
@Composable
|
@Composable
|
||||||
fun SelectItemAction(
|
fun SelectItemAction(
|
||||||
showMenu: MutableState<Boolean>,
|
showMenu: MutableState<Boolean>,
|
||||||
selectChatItem: () -> Unit,
|
selectItem: () -> Unit,
|
||||||
) {
|
) {
|
||||||
ItemAction(
|
ItemAction(
|
||||||
stringResource(MR.strings.select_verb),
|
stringResource(MR.strings.select_verb),
|
||||||
painterResource(MR.images.ic_check_circle),
|
painterResource(MR.images.ic_check_circle),
|
||||||
onClick = {
|
onClick = {
|
||||||
showMenu.value = false
|
showMenu.value = false
|
||||||
selectChatItem()
|
selectItem()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@ fun ModalView(
|
||||||
searchAlwaysVisible: Boolean = false,
|
searchAlwaysVisible: Boolean = false,
|
||||||
onSearchValueChanged: (String) -> Unit = {},
|
onSearchValueChanged: (String) -> Unit = {},
|
||||||
endButtons: @Composable RowScope.() -> Unit = {},
|
endButtons: @Composable RowScope.() -> Unit = {},
|
||||||
|
appBar: @Composable (BoxScope.() -> Unit)? = null,
|
||||||
content: @Composable BoxScope.() -> Unit,
|
content: @Composable BoxScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
if (showClose && showAppBar) {
|
if (showClose && showAppBar) {
|
||||||
|
@ -48,14 +49,20 @@ fun ModalView(
|
||||||
StatusBarBackground()
|
StatusBarBackground()
|
||||||
}
|
}
|
||||||
Box(Modifier.align(if (oneHandUI.value) Alignment.BottomStart else Alignment.TopStart)) {
|
Box(Modifier.align(if (oneHandUI.value) Alignment.BottomStart else Alignment.TopStart)) {
|
||||||
DefaultAppBar(
|
if (appBar != null) {
|
||||||
navigationButton = if (showClose) {{ NavigationButtonBack(onButtonClicked = if (enableClose) close else null) }} else null,
|
appBar()
|
||||||
onTop = !oneHandUI.value,
|
} else {
|
||||||
showSearch = showSearch,
|
DefaultAppBar(
|
||||||
searchAlwaysVisible = searchAlwaysVisible,
|
navigationButton = if (showClose) {
|
||||||
onSearchValueChanged = onSearchValueChanged,
|
{ NavigationButtonBack(onButtonClicked = if (enableClose) close else null) }
|
||||||
buttons = endButtons
|
} else null,
|
||||||
)
|
onTop = !oneHandUI.value,
|
||||||
|
showSearch = showSearch,
|
||||||
|
searchAlwaysVisible = searchAlwaysVisible,
|
||||||
|
onSearchValueChanged = onSearchValueChanged,
|
||||||
|
buttons = endButtons
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1738,25 +1738,32 @@
|
||||||
|
|
||||||
<!-- GroupMemberInfoView.kt -->
|
<!-- GroupMemberInfoView.kt -->
|
||||||
<string name="button_remove_member_question">Remove member?</string>
|
<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_remove_member">Remove member</string>
|
||||||
|
|
||||||
<string name="button_send_direct_message">Send direct message</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="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="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_confirmation">Remove</string>
|
||||||
<string name="remove_member_button">Remove member</string>
|
<string name="remove_member_button">Remove member</string>
|
||||||
<string name="block_member_question">Block member?</string>
|
<string name="block_member_question">Block member?</string>
|
||||||
<string name="block_member_button">Block member</string>
|
<string name="block_member_button">Block member</string>
|
||||||
<string name="block_member_confirmation">Block</string>
|
<string name="block_member_confirmation">Block</string>
|
||||||
<string name="block_for_all_question">Block member for all?</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_for_all">Block for all</string>
|
||||||
<string name="block_member_desc">All new messages from %s will be hidden!</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_question">Unblock member?</string>
|
||||||
<string name="unblock_member_button">Unblock member</string>
|
<string name="unblock_member_button">Unblock member</string>
|
||||||
<string name="unblock_member_confirmation">Unblock</string>
|
<string name="unblock_member_confirmation">Unblock</string>
|
||||||
<string name="unblock_for_all_question">Unblock member for all?</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_for_all">Unblock for all</string>
|
||||||
<string name="unblock_member_desc">Messages from %s will be shown!</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_blocked_by_admin">Blocked by admin</string>
|
||||||
<string name="member_info_member_blocked">blocked</string>
|
<string name="member_info_member_blocked">blocked</string>
|
||||||
<string name="member_info_member_disabled">disabled</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_offered_item_with_param">offered %s: %2s</string>
|
||||||
<string name="feature_cancelled_item">cancelled %s</string>
|
<string name="feature_cancelled_item">cancelled %s</string>
|
||||||
<string name="feature_roles_all_members">all members</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_admins">admins</string>
|
||||||
<string name="feature_roles_owners">owners</string>
|
<string name="feature_roles_owners">owners</string>
|
||||||
<string name="feature_enabled_for">Enabled for</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
Add a link
Reference in a new issue