android, desktop: add gallery tabs to group info view (#6559)

* Added GroupSettings button and view and moved related logic

* Added tabs in group settings and done UI skeleton for those

* Fixed merge issues

* Reverted back some classes to original places

* Reverted back some classes to original places

---------

Co-authored-by: hayk-space <hayk.nahapetian@space.ge>
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
hayk888997
2026-01-12 18:36:43 +04:00
committed by GitHub
parent bf1783feb4
commit 7eb24956cb
4 changed files with 733 additions and 209 deletions

View File

@@ -0,0 +1,396 @@
package chat.simplex.common.views.chat.group
import SectionDividerSpaced
import SectionItemView
import SectionItemViewLongClickable
import SectionSpacer
import SectionView
import androidx.compose.animation.*
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.*
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.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.*
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.chatModel
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.SelectedListItem
import chat.simplex.common.views.chat.SendReceipts
import chat.simplex.common.views.chat.item.ItemAction
import chat.simplex.common.views.chat.item.SelectItemAction
import chat.simplex.common.views.chat.group.showGroupReportsView
import chat.simplex.common.views.chatlist.cantInviteIncognitoAlert
import chat.simplex.common.views.chatlist.openChat
import chat.simplex.common.views.chatlist.UnreadBadge
import chat.simplex.common.views.chatlist.unreadCountStr
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.usersettings.SettingsActionItem
import chat.simplex.common.views.usersettings.SettingsActionItemWithContent
import chat.simplex.res.MR
import dev.icerock.moko.resources.StringResource
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.launch
enum class GroupInfoTab {
Members,
Images,
Videos,
Links,
Files,
Voices
}
fun LazyListScope.GroupChatInfoTabs(
groupInfo: GroupInfo,
activeSortedMembers: List<GroupMember>,
filteredMembers: State<List<GroupMember>>,
searchText: MutableState<TextFieldValue>,
selectedItems: MutableState<Set<Long>?>,
scrollToItemId: MutableState<Long?>,
addMembers: () -> Unit,
showMemberInfo: (GroupMember) -> Unit,
selectedTab: MutableState<GroupInfoTab>,
filteredChatItems: State<List<ChatItem>>,
chat: Chat,
groupLink: GroupLink?,
manageGroupLink: () -> Unit,
openMemberSupport: () -> Unit
) {
item {
SectionSpacer()
val scrollState = rememberScrollState()
Column {
Row(
Modifier
.fillMaxWidth()
.horizontalScroll(scrollState),
horizontalArrangement = Arrangement.Start
) {
GroupInfoTab.values().forEach { tab ->
Tab(
selected = selectedTab.value == tab,
onClick = { selectedTab.value = tab },
text = { Text(tabTitle(tab), fontSize = 13.sp) },
selectedContentColor = MaterialTheme.colors.primary,
unselectedContentColor = MaterialTheme.colors.secondary,
)
}
}
// Simple indicator line
Box(
Modifier
.fillMaxWidth()
.height(2.dp)
.background(MaterialTheme.colors.surface)
)
}
Divider()
SectionSpacer()
}
when (selectedTab.value) {
GroupInfoTab.Members -> {
MembersTabContent(
groupInfo = groupInfo,
activeSortedMembers = activeSortedMembers,
filteredMembers = filteredMembers,
searchText = searchText,
selectedItems = selectedItems,
showMemberInfo = showMemberInfo,
addMembers = addMembers,
chat = chat,
groupLink = groupLink,
manageGroupLink = manageGroupLink,
openMemberSupport = openMemberSupport,
scrollToItemId = scrollToItemId
)
}
GroupInfoTab.Images,
GroupInfoTab.Videos,
GroupInfoTab.Links,
GroupInfoTab.Files,
GroupInfoTab.Voices -> {
ContentItemsTab(
filteredChatItems = filteredChatItems,
scrollToItemId = scrollToItemId
)
}
}
}
private fun LazyListScope.MembersTabContent(
groupInfo: GroupInfo,
activeSortedMembers: List<GroupMember>,
filteredMembers: State<List<GroupMember>>,
searchText: MutableState<TextFieldValue>,
selectedItems: MutableState<Set<Long>?>,
showMemberInfo: (GroupMember) -> Unit,
addMembers: () -> Unit,
chat: Chat,
groupLink: GroupLink?,
manageGroupLink: () -> Unit,
openMemberSupport: () -> Unit,
scrollToItemId: MutableState<Long?>
) {
item {
val scope = rememberCoroutineScope()
var anyTopSectionRowShow = false
SectionView {
if (groupInfo.canAddMembers && groupInfo.businessChat == null) {
anyTopSectionRowShow = true
if (groupLink == null) {
CreateGroupLinkButton(manageGroupLink)
} else {
GroupLinkButton(manageGroupLink)
}
}
if (groupInfo.businessChat == null && groupInfo.membership.memberRole >= GroupMemberRole.Moderator) {
anyTopSectionRowShow = true
MemberSupportButton(chat, openMemberSupport)
}
if (groupInfo.canModerate) {
anyTopSectionRowShow = true
GroupReportsButton(chat) {
scope.launch {
showGroupReportsView(chatModel.chatId, scrollToItemId, chat.chatInfo)
}
}
}
if (
groupInfo.membership.memberActive &&
(groupInfo.membership.memberRole < GroupMemberRole.Moderator || groupInfo.membership.supportChat != null)
) {
anyTopSectionRowShow = true
UserSupportChatButton(chat, groupInfo, scrollToItemId)
}
}
if (anyTopSectionRowShow) {
SectionDividerSpaced(maxBottomPadding = false)
}
}
if (!groupInfo.nextConnectPrepared) {
item {
SectionView(title = String.format(generalGetString(MR.strings.group_info_section_title_num_members), activeSortedMembers.count() + 1)) {
if (groupInfo.canAddMembers) {
val onAddMembersClick = if (chat.chatInfo.incognito) ::cantInviteIncognitoAlert else addMembers
val tint = if (chat.chatInfo.incognito) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
val addMembersTitleId = when (groupInfo.businessChat?.chatType) {
BusinessChatType.Customer -> MR.strings.button_add_team_members
BusinessChatType.Business -> MR.strings.button_add_friends
null -> MR.strings.button_add_members
}
AddMembersButton(addMembersTitleId, tint, onAddMembersClick)
}
if (activeSortedMembers.size > 8) {
SectionItemView(padding = PaddingValues(start = 14.dp, end = DEFAULT_PADDING_HALF)) {
MemberListSearchRowView(searchText)
}
}
SectionItemView(minHeight = 54.dp, padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
MemberRow(groupInfo.membership, user = true)
}
}
}
}
if (!groupInfo.nextConnectPrepared) {
items(filteredMembers.value, key = { it.groupMemberId }) { member ->
Divider()
val showMenu = remember { mutableStateOf(false) }
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) {
this@SectionItemViewLongClickable.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)
}
}
}
}
}
}
private fun LazyListScope.ContentItemsTab(
filteredChatItems: State<List<ChatItem>>,
scrollToItemId: MutableState<Long?>
) {
if (filteredChatItems.value.isEmpty()) {
item {
Box(
Modifier
.fillMaxWidth()
.padding(vertical = DEFAULT_PADDING * 2),
contentAlignment = Alignment.Center
) {
//TODO: this is just a temporary UI, if no item, tab will not be shown
Text(
"No items found",
color = MaterialTheme.colors.secondary
)
}
}
} else {
items(filteredChatItems.value, key = { it.id }) { chatItem ->
Divider()
SectionItemView(
click = { scrollToItemId.value = chatItem.id },
minHeight = 54.dp,
padding = PaddingValues(horizontal = DEFAULT_PADDING)
) {
GroupChatItemRow(chatItem)
}
}
}
}
@Composable
private fun tabTitle(tab: GroupInfoTab): String {
return when (tab) {
GroupInfoTab.Members -> stringResource(MR.strings.group_info_tab_members)
GroupInfoTab.Images -> stringResource(MR.strings.group_info_tab_images)
GroupInfoTab.Videos -> stringResource(MR.strings.group_info_tab_videos)
GroupInfoTab.Links -> stringResource(MR.strings.group_info_tab_links)
GroupInfoTab.Files -> stringResource(MR.strings.group_info_tab_files)
GroupInfoTab.Voices -> stringResource(MR.strings.group_info_tab_voices)
}
}
@Composable
private fun GroupChatItemRow(chatItem: ChatItem) {
Row(
Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
when (val content = chatItem.content.msgContent) {
is MsgContent.MCImage -> {
Icon(painterResource(MR.images.ic_image), null, Modifier.size(24.dp), tint = MaterialTheme.colors.secondary)
Spacer(Modifier.width(DEFAULT_PADDING))
Column(Modifier.weight(1f)) {
//TODO, This is just a temporary UI, needs to be adjusted
Text(
content.text.ifEmpty { "Image" },
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
getTimestampText(chatItem.meta.itemTs),
fontSize = 12.sp,
color = MaterialTheme.colors.secondary
)
}
}
is MsgContent.MCVideo -> {
Icon(painterResource(MR.images.ic_videocam), null, Modifier.size(24.dp), tint = MaterialTheme.colors.secondary)
Spacer(Modifier.width(DEFAULT_PADDING))
Column(Modifier.weight(1f)) {
//TODO, This is just a temporary UI, needs to be adjusted
Text(
content.text.ifEmpty { "Video" },
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
getTimestampText(chatItem.meta.itemTs),
fontSize = 12.sp,
color = MaterialTheme.colors.secondary
)
}
}
is MsgContent.MCLink -> {
Icon(painterResource(MR.images.ic_link), null, Modifier.size(24.dp), tint = MaterialTheme.colors.secondary)
Spacer(Modifier.width(DEFAULT_PADDING))
Column(Modifier.weight(1f)) {
//TODO, This is just a temporary UI, needs to be adjusted
Text(
content.preview.uri.ifEmpty { content.text },
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
getTimestampText(chatItem.meta.itemTs),
fontSize = 12.sp,
color = MaterialTheme.colors.secondary
)
}
}
is MsgContent.MCFile -> {
Icon(painterResource(MR.images.ic_draft_filled), null, Modifier.size(24.dp), tint = MaterialTheme.colors.secondary)
Spacer(Modifier.width(DEFAULT_PADDING))
Column(Modifier.weight(1f)) {
//TODO, This is just a temporary UI, needs to be adjusted
Text(
chatItem.file?.fileName ?: content.text.ifEmpty { "File" },
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
getTimestampText(chatItem.meta.itemTs),
fontSize = 12.sp,
color = MaterialTheme.colors.secondary
)
}
}
is MsgContent.MCVoice -> {
Icon(painterResource(MR.images.ic_mic_filled), null, Modifier.size(24.dp), tint = MaterialTheme.colors.secondary)
Spacer(Modifier.width(DEFAULT_PADDING))
Column(Modifier.weight(1f)) {
//TODO, This is just a temporary UI, needs to be adjusted
Text(
content.text.ifEmpty { generalGetString(MR.strings.voice_message) },
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
getTimestampText(chatItem.meta.itemTs),
fontSize = 12.sp,
color = MaterialTheme.colors.secondary
)
}
}
else -> {
Text(
generalGetString(MR.strings.unknown_message_format),
color = MaterialTheme.colors.secondary
)
}
}
}
}

View File

@@ -9,6 +9,7 @@ import SectionSpacer
import SectionTextFooter
import SectionView
import androidx.compose.animation.*
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.combinedClickable
@@ -161,11 +162,21 @@ fun ModalData.GroupChatInfoView(
)
}
},
openSettings = {
ModalManager.end.showCustomModal { close ->
GroupSettingsView(
chatModel,
rhId,
chat.id,
close
)
}
},
deleteGroup = { deleteGroupDialog(chat, groupInfo, chatModel, close) },
clearChat = { clearChatDialog(chat, close) },
leaveGroup = { leaveGroupDialog(rhId, groupInfo, chatModel, close) },
manageGroupLink = {
ModalManager.end.showModal { GroupLinkView(chatModel, rhId, groupInfo, groupLink, onGroupLinkUpdated) }
ModalManager.end.showModal { GroupLinkView(chatModel, rhId, groupInfo, groupLink, onGroupLinkUpdated) }
},
onSearchClicked = onSearchClicked,
deletingItems = deletingItems
@@ -228,7 +239,7 @@ fun leaveGroupDialog(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, cl
)
}
private fun removeMemberAlert(rhId: Long?, groupInfo: GroupInfo, mem: GroupMember) {
fun removeMemberAlert(rhId: Long?, groupInfo: GroupInfo, mem: GroupMember) {
val messageId = if (groupInfo.businessChat == null)
MR.strings.member_will_be_removed_from_group_cannot_be_undone
else
@@ -326,7 +337,7 @@ fun MuteButton(
InfoViewActionButton(
modifier = modifier,
icon = painterResource(nextNotificationMode.icon),
icon = painterResource(nextNotificationMode.icon),
title = generalGetString(nextNotificationMode.text(true)),
disabled = !groupInfo.ready,
disabledLook = !groupInfo.ready,
@@ -336,6 +347,22 @@ fun MuteButton(
)
}
@Composable
fun SettingsButton(
modifier: Modifier,
groupInfo: GroupInfo,
onClick: () -> Unit
) {
InfoViewActionButton(
modifier = modifier,
icon = painterResource(MR.images.ic_settings),
title = generalGetString(MR.strings.icon_descr_settings),
disabled = !groupInfo.ready,
disabledLook = !groupInfo.ready,
onClick = onClick
)
}
@Composable
fun AddGroupMembersButton(
modifier: Modifier,
@@ -344,7 +371,7 @@ fun AddGroupMembersButton(
) {
InfoViewActionButton(
modifier = modifier,
icon = if (groupInfo.incognito) painterResource(MR.images.ic_add_link) else painterResource(MR.images.ic_person_add_500),
icon = if (groupInfo.incognito) painterResource(MR.images.ic_add_link) else painterResource(MR.images.ic_person_add_500),
title = stringResource(MR.strings.action_button_add_members),
disabled = !groupInfo.ready,
disabledLook = !groupInfo.ready,
@@ -414,11 +441,12 @@ fun ModalData.GroupChatInfoLayout(
addOrEditWelcomeMessage: () -> Unit,
openMemberSupport: () -> Unit,
openPreferences: () -> Unit,
openSettings: () -> Unit,
deleteGroup: () -> Unit,
clearChat: () -> Unit,
leaveGroup: () -> Unit,
manageGroupLink: () -> Unit,
close: () -> Unit = { ModalManager.closeAllModalsEverywhere()},
close: () -> Unit = { ModalManager.closeAllModalsEverywhere() },
onSearchClicked: () -> Unit,
deletingItems: State<Boolean>
) {
@@ -434,219 +462,124 @@ fun ModalData.GroupChatInfoLayout(
if (s.isEmpty()) activeSortedMembers else activeSortedMembers.filter { m -> m.anyNameContains(s) }
}
}
val selectedTab = rememberSaveable { mutableStateOf(GroupInfoTab.Members) }
val filteredChatItems = remember(chat.chatItems, selectedTab.value) {
derivedStateOf {
when (selectedTab.value) {
GroupInfoTab.Members -> emptyList()
GroupInfoTab.Images -> chat.chatItems.filter {
it.content.msgContent is MsgContent.MCImage && it.meta.itemDeleted == null
}
GroupInfoTab.Videos -> chat.chatItems.filter {
it.content.msgContent is MsgContent.MCVideo && it.meta.itemDeleted == null
}
GroupInfoTab.Links -> chat.chatItems.filter {
it.content.msgContent is MsgContent.MCLink && it.meta.itemDeleted == null
}
GroupInfoTab.Files -> chat.chatItems.filter {
it.content.msgContent is MsgContent.MCFile && it.meta.itemDeleted == null
}
GroupInfoTab.Voices -> chat.chatItems.filter {
it.content.msgContent is MsgContent.MCVoice && it.meta.itemDeleted == null
}
}
}
}
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 = 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),
bottom = if (imePadding > 0.dp) {
imePadding + selectedItemsBarHeight
} else {
navBarPadding + selectedItemsBarHeight
}
)
}
) {
item {
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
GroupChatInfoHeader(chat.chatInfo, groupInfo)
LazyColumnWithScrollBar(
state = listState,
contentPadding = if (oneHandUI.value) {
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),
bottom = if (imePadding > 0.dp) {
imePadding + selectedItemsBarHeight
} else {
navBarPadding + selectedItemsBarHeight
}
)
}
LocalAliasEditor(chat.id, groupInfo.localAlias, isContact = false, updateValue = onLocalAliasChanged)
SectionSpacer()
Box(
Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
) {
item {
Row(
Modifier
.widthIn(max = if (groupInfo.canAddMembers) 320.dp else 230.dp)
.padding(horizontal = DEFAULT_PADDING),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
if (groupInfo.canAddMembers) {
SearchButton(modifier = Modifier.fillMaxWidth(0.33f), chat, groupInfo, close, onSearchClicked)
AddGroupMembersButton(modifier = Modifier.fillMaxWidth(0.5f), chat, groupInfo)
MuteButton(modifier = Modifier.fillMaxWidth(1f), chat, groupInfo)
} else {
SearchButton(modifier = Modifier.fillMaxWidth(0.5f), chat, groupInfo, close, onSearchClicked)
MuteButton(modifier = Modifier.fillMaxWidth(1f), chat, groupInfo)
}
GroupChatInfoHeader(chat.chatInfo, groupInfo)
}
}
SectionSpacer()
LocalAliasEditor(chat.id, groupInfo.localAlias, isContact = false, updateValue = onLocalAliasChanged)
var anyTopSectionRowShow = false
SectionView {
if (groupInfo.canAddMembers && groupInfo.businessChat == null) {
anyTopSectionRowShow = true
if (groupLink == null) {
CreateGroupLinkButton(manageGroupLink)
} else {
GroupLinkButton(manageGroupLink)
}
}
if (groupInfo.businessChat == null && groupInfo.membership.memberRole >= GroupMemberRole.Moderator) {
anyTopSectionRowShow = true
MemberSupportButton(chat, openMemberSupport)
}
if (groupInfo.canModerate) {
anyTopSectionRowShow = true
GroupReportsButton(chat) {
scope.launch {
showGroupReportsView(chatModel.chatId, scrollToItemId, chat.chatInfo)
}
}
}
if (
groupInfo.membership.memberActive &&
(groupInfo.membership.memberRole < GroupMemberRole.Moderator || groupInfo.membership.supportChat != null)
SectionSpacer()
Box(
Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
anyTopSectionRowShow = true
UserSupportChatButton(chat, groupInfo, scrollToItemId)
}
}
if (anyTopSectionRowShow) {
SectionDividerSpaced(maxBottomPadding = false)
}
SectionView {
if (groupInfo.isOwner && groupInfo.businessChat?.chatType == null) {
EditGroupProfileButton(editGroupProfile)
}
if (groupInfo.groupProfile.description != null || (groupInfo.isOwner && groupInfo.businessChat?.chatType == null)) {
AddOrEditWelcomeMessage(groupInfo.groupProfile.description, addOrEditWelcomeMessage)
}
val prefsTitleId = if (groupInfo.businessChat == null) MR.strings.group_preferences else MR.strings.chat_preferences
GroupPreferencesButton(prefsTitleId, openPreferences)
}
val footerId = if (groupInfo.businessChat == null) MR.strings.only_group_owners_can_change_prefs else MR.strings.only_chat_owners_can_change_prefs
SectionTextFooter(stringResource(footerId))
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
SectionView {
if (activeSortedMembers.filter { it.memberCurrent }.size <= SMALL_GROUPS_RCPS_MEM_LIMIT) {
SendReceiptsOption(currentUser, sendReceipts, setSendReceipts)
} else {
SendReceiptsOptionDisabled()
}
WallpaperButton {
ModalManager.end.showModal {
val chat = remember { derivedStateOf { chatModel.chats.value.firstOrNull { it.id == chat.id } } }
val c = chat.value
if (c != null) {
ChatWallpaperEditorModal(c)
}
}
}
ChatTTLOption(chatItemTTL, setChatItemTTL, deletingItems)
SectionTextFooter(stringResource(MR.strings.chat_ttl_options_footer))
}
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = true)
if (!groupInfo.nextConnectPrepared) {
SectionView(title = String.format(generalGetString(MR.strings.group_info_section_title_num_members), activeSortedMembers.count() + 1)) {
if (groupInfo.canAddMembers) {
val onAddMembersClick = if (chat.chatInfo.incognito) ::cantInviteIncognitoAlert else addMembers
val tint = if (chat.chatInfo.incognito) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
val addMembersTitleId = when (groupInfo.businessChat?.chatType) {
BusinessChatType.Customer -> MR.strings.button_add_team_members
BusinessChatType.Business -> MR.strings.button_add_friends
null -> MR.strings.button_add_members
}
AddMembersButton(addMembersTitleId, tint, onAddMembersClick)
}
if (activeSortedMembers.size > 8) {
SectionItemView(padding = PaddingValues(start = 14.dp, end = DEFAULT_PADDING_HALF)) {
MemberListSearchRowView(searchText)
}
}
SectionItemView(minHeight = 54.dp, padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
MemberRow(groupInfo.membership, user = true)
}
}
}
}
if (!groupInfo.nextConnectPrepared) {
items(filteredMembers.value, key = { it.groupMemberId }) { member ->
Divider()
val showMenu = remember { mutableStateOf(false) }
val canBeSelected = groupInfo.membership.memberRole >= member.memberRole && member.memberRole < GroupMemberRole.Moderator
SectionItemViewLongClickable(
click = {
if (selectedItems.value != null) {
if (canBeSelected) {
toggleItemSelection(member.groupMemberId, selectedItems)
}
Row(
Modifier
.widthIn(max = if (groupInfo.canAddMembers) 480.dp else 320.dp)
.padding(horizontal = DEFAULT_PADDING),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically
) {
if (groupInfo.canAddMembers) {
SearchButton(modifier = Modifier.weight(1f), chat, groupInfo, close, onSearchClicked)
AddGroupMembersButton(modifier = Modifier.weight(1f), chat, groupInfo)
MuteButton(modifier = Modifier.weight(1f), chat, groupInfo)
SettingsButton(modifier = Modifier.weight(1f), groupInfo, openSettings)
} 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)
SearchButton(modifier = Modifier.weight(1f), chat, groupInfo, close, onSearchClicked)
MuteButton(modifier = Modifier.weight(1f), chat, groupInfo)
SettingsButton(modifier = Modifier.weight(1f), groupInfo, openSettings)
}
}
}
}
}
item {
if (!groupInfo.nextConnectPrepared) {
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
}
SectionView {
ClearChatButton(clearChat)
if (groupInfo.canDelete) {
val titleId = if (groupInfo.businessChat == null) MR.strings.button_delete_group else MR.strings.button_delete_chat
DeleteGroupButton(titleId, deleteGroup)
}
if (groupInfo.membership.memberCurrentOrPending) {
val titleId = if (groupInfo.businessChat == null) MR.strings.button_leave_group else MR.strings.button_leave_chat
LeaveGroupButton(titleId, leaveGroup)
}
SectionSpacer()
}
if (developerTools) {
SectionDividerSpaced()
SectionView(title = stringResource(MR.strings.section_title_for_console)) {
InfoRow(stringResource(MR.strings.info_row_local_name), groupInfo.localDisplayName)
InfoRow(stringResource(MR.strings.info_row_database_id), groupInfo.apiId.toString())
GroupChatInfoTabs(
groupInfo = groupInfo,
activeSortedMembers = activeSortedMembers,
filteredMembers = filteredMembers,
searchText = searchText,
selectedItems = selectedItems,
scrollToItemId = scrollToItemId,
addMembers = addMembers,
showMemberInfo = showMemberInfo,
selectedTab = selectedTab,
filteredChatItems = filteredChatItems,
chat = chat,
groupLink = groupLink,
manageGroupLink = manageGroupLink,
openMemberSupport = openMemberSupport
)
item {
if (developerTools) {
SectionDividerSpaced()
SectionView(title = stringResource(MR.strings.section_title_for_console)) {
InfoRow(stringResource(MR.strings.info_row_local_name), groupInfo.localDisplayName)
InfoRow(stringResource(MR.strings.info_row_database_id), groupInfo.apiId.toString())
}
}
SectionBottomSpacer()
}
SectionBottomSpacer()
}
}
if (!oneHandUI.value) {
NavigationBarBackground(oneHandUI.value, oneHandUI.value)
}
@@ -779,7 +712,7 @@ private fun GroupChatInfoHeader(cInfo: ChatInfo, groupInfo: GroupInfo) {
}
@Composable
private fun MemberSupportButton(chat: Chat, onClick: () -> Unit) {
fun MemberSupportButton(chat: Chat, onClick: () -> Unit) {
SettingsActionItemWithContent(
painterResource(if (chat.supportUnreadCount > 0) MR.images.ic_flag_filled else MR.images.ic_flag),
stringResource(MR.strings.member_support),
@@ -796,7 +729,7 @@ private fun MemberSupportButton(chat: Chat, onClick: () -> Unit) {
}
@Composable
private fun GroupPreferencesButton(titleId: StringResource, onClick: () -> Unit) {
fun GroupPreferencesButton(titleId: StringResource, onClick: () -> Unit) {
SettingsActionItem(
painterResource(MR.images.ic_toggle_on),
stringResource(titleId),
@@ -805,7 +738,7 @@ private fun GroupPreferencesButton(titleId: StringResource, onClick: () -> Unit)
}
@Composable
private fun GroupReportsButton(chat: Chat, onClick: () -> Unit) {
fun GroupReportsButton(chat: Chat, onClick: () -> Unit) {
SettingsActionItemWithContent(
painterResource(if (chat.chatStats.reportsCount > 0) MR.images.ic_flag_filled else MR.images.ic_flag),
stringResource(MR.strings.group_reports_member_reports),
@@ -822,7 +755,7 @@ private fun GroupReportsButton(chat: Chat, onClick: () -> Unit) {
}
@Composable
private fun SendReceiptsOption(currentUser: User, state: State<SendReceipts>, onSelected: (SendReceipts) -> Unit) {
fun SendReceiptsOption(currentUser: User, state: State<SendReceipts>, onSelected: (SendReceipts) -> Unit) {
val values = remember {
mutableListOf(SendReceipts.Yes, SendReceipts.No, SendReceipts.UserDefault(currentUser.sendRcptsSmallGroups)).map { it to it.text }
}
@@ -853,7 +786,7 @@ fun SendReceiptsOptionDisabled() {
}
@Composable
private fun AddMembersButton(titleId: StringResource, tint: Color = MaterialTheme.colors.primary, onClick: () -> Unit) {
fun AddMembersButton(titleId: StringResource, tint: Color = MaterialTheme.colors.primary, onClick: () -> Unit) {
SettingsActionItem(
painterResource(MR.images.ic_add),
stringResource(titleId),
@@ -945,7 +878,7 @@ fun MemberVerifiedShield() {
}
@Composable
private fun DropDownMenuForMember(rhId: Long?, member: GroupMember, groupInfo: GroupInfo, selectedItems: MutableState<Set<Long>?>, showMenu: MutableState<Boolean>) {
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)
@@ -994,7 +927,7 @@ private fun DropDownMenuForMember(rhId: Long?, member: GroupMember, groupInfo: G
}
@Composable
private fun GroupLinkButton(onClick: () -> Unit) {
fun GroupLinkButton(onClick: () -> Unit) {
SettingsActionItem(
painterResource(MR.images.ic_link),
stringResource(MR.strings.group_link),
@@ -1004,7 +937,7 @@ private fun GroupLinkButton(onClick: () -> Unit) {
}
@Composable
private fun CreateGroupLinkButton(onClick: () -> Unit) {
fun CreateGroupLinkButton(onClick: () -> Unit) {
SettingsActionItem(
painterResource(MR.images.ic_add_link),
stringResource(MR.strings.create_group_link),
@@ -1024,7 +957,7 @@ fun EditGroupProfileButton(onClick: () -> Unit) {
}
@Composable
private fun AddOrEditWelcomeMessage(welcomeMessage: String?, onClick: () -> Unit) {
fun AddOrEditWelcomeMessage(welcomeMessage: String?, onClick: () -> Unit) {
val text = if (welcomeMessage == null) {
stringResource(MR.strings.button_add_welcome_message)
} else {
@@ -1039,7 +972,7 @@ private fun AddOrEditWelcomeMessage(welcomeMessage: String?, onClick: () -> Unit
}
@Composable
private fun LeaveGroupButton(titleId: StringResource, onClick: () -> Unit) {
fun LeaveGroupButton(titleId: StringResource, onClick: () -> Unit) {
SettingsActionItem(
painterResource(MR.images.ic_logout),
stringResource(titleId),
@@ -1050,7 +983,7 @@ private fun LeaveGroupButton(titleId: StringResource, onClick: () -> Unit) {
}
@Composable
private fun DeleteGroupButton(titleId: StringResource, onClick: () -> Unit) {
fun DeleteGroupButton(titleId: StringResource, onClick: () -> Unit) {
SettingsActionItem(
painterResource(MR.images.ic_delete),
stringResource(titleId),
@@ -1060,6 +993,7 @@ private fun DeleteGroupButton(titleId: StringResource, onClick: () -> Unit) {
)
}
@Composable
fun MemberListSearchRowView(
searchText: MutableState<TextFieldValue> = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) }
@@ -1155,7 +1089,9 @@ fun PreviewGroupChatInfoLayout() {
leaveGroup = {},
manageGroupLink = {},
onSearchClicked = {},
deletingItems = remember { mutableStateOf(true) }
deletingItems = remember { mutableStateOf(true) },
openSettings = { },
close = { }
)
}
}

View File

@@ -0,0 +1,186 @@
package chat.simplex.common.views.chat.group
import SectionBottomSpacer
import SectionDividerSpaced
import SectionTextFooter
import SectionView
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.graphics.Color
import dev.icerock.moko.resources.compose.stringResource
import chat.simplex.common.model.*
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.chat.*
import chat.simplex.common.views.chatlist.updateChatSettings
import chat.simplex.common.views.usersettings.SettingsActionItem
import chat.simplex.common.views.usersettings.SettingsActionItemWithContent
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.*
@Composable
fun GroupSettingsView(
m: ChatModel,
rhId: Long?,
chatId: String,
close: () -> Unit
) {
val chat = remember { derivedStateOf { m.getChat(chatId) } }
val c = chat.value
if (c == null || c.chatInfo !is ChatInfo.Group) return
val groupInfo = c.chatInfo.groupInfo
val currentUser = m.currentUser.value ?: return
val sendReceipts = remember { mutableStateOf(SendReceipts.fromBool(groupInfo.chatSettings.sendRcpts, currentUser.sendRcptsSmallGroups)) }
val chatItemTTL = rememberSaveable(groupInfo.id) { mutableStateOf(if (groupInfo.chatItemTTL != null) ChatItemTTL.fromSeconds(groupInfo.chatItemTTL) else null) }
val deletingItems = rememberSaveable(groupInfo.id) { mutableStateOf(false) }
fun setSendReceipts(sendRcpts: SendReceipts) {
val chatSettings = (c.chatInfo.chatSettings ?: ChatSettings.defaults).copy(sendRcpts = sendRcpts.bool)
updateChatSettings(c.remoteHostId, c.chatInfo, chatSettings, m)
sendReceipts.value = sendRcpts
}
fun setChatItemTTL(ttl: ChatItemTTL?) {
if (ttl == chatItemTTL.value) return
val previousChatTTL = chatItemTTL.value
chatItemTTL.value = ttl
setChatTTLAlert(m.chatsContext, c.remoteHostId, c.chatInfo, chatItemTTL, previousChatTTL, deletingItems)
}
ModalView(close = close) {
GroupSettingsLayout(
chat = c,
groupInfo = groupInfo,
currentUser = currentUser,
sendReceipts = sendReceipts,
setSendReceipts = ::setSendReceipts,
chatItemTTL = chatItemTTL,
setChatItemTTL = ::setChatItemTTL,
deletingItems = deletingItems,
editGroupProfile = {
ModalManager.end.showCustomModal { closeModal ->
GroupProfileView(rhId, groupInfo, m, closeModal)
}
},
addOrEditWelcomeMessage = {
ModalManager.end.showCustomModal { closeModal ->
GroupWelcomeView(m, rhId, groupInfo, closeModal)
}
},
openPreferences = {
ModalManager.end.showCustomModal { closeModal ->
GroupPreferencesView(m, rhId, chatId, closeModal)
}
},
clearChat = { clearChatDialog(c, close) },
deleteGroup = { deleteGroupDialog(c, groupInfo, m, close) },
leaveGroup = { leaveGroupDialog(rhId, groupInfo, m, close) },
close = close
)
}
}
@Composable
private fun GroupSettingsLayout(
chat: Chat,
groupInfo: GroupInfo,
currentUser: User,
sendReceipts: MutableState<SendReceipts>,
setSendReceipts: (SendReceipts) -> Unit,
chatItemTTL: MutableState<ChatItemTTL?>,
setChatItemTTL: (ChatItemTTL?) -> Unit,
deletingItems: MutableState<Boolean>,
editGroupProfile: () -> Unit,
addOrEditWelcomeMessage: () -> Unit,
openPreferences: () -> Unit,
clearChat: () -> Unit,
deleteGroup: () -> Unit,
leaveGroup: () -> Unit,
close: () -> Unit
) {
ColumnWithScrollBar {
AppBarTitle(stringResource(MR.strings.icon_descr_settings))
SectionView {
if (groupInfo.isOwner && groupInfo.businessChat?.chatType == null) {
EditGroupProfileButton(editGroupProfile)
}
if (groupInfo.groupProfile.description != null || (groupInfo.isOwner && groupInfo.businessChat?.chatType == null)) {
AddOrEditWelcomeMessage(groupInfo.groupProfile.description, addOrEditWelcomeMessage)
}
val prefsTitleId = if (groupInfo.businessChat == null) MR.strings.group_preferences else MR.strings.chat_preferences
GroupPreferencesButton(prefsTitleId, openPreferences)
}
val footerId = if (groupInfo.businessChat == null) MR.strings.only_group_owners_can_change_prefs else MR.strings.only_chat_owners_can_change_prefs
SectionTextFooter(stringResource(footerId))
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
SectionView {
val activeSortedMembers = remember { chatModel.groupMembers }.value
.filter { it.memberStatus != GroupMemberStatus.MemLeft && it.memberStatus != GroupMemberStatus.MemRemoved }
if (activeSortedMembers.filter { it.memberCurrent }.size <= SMALL_GROUPS_RCPS_MEM_LIMIT) {
SendReceiptsOption(currentUser, sendReceipts, setSendReceipts)
} else {
SendReceiptsOptionDisabled()
}
WallpaperButton {
ModalManager.end.showModal {
val chatState = remember { derivedStateOf { chatModel.chats.value.firstOrNull { it.id == chat.id } } }
val currentChat = chatState.value
if (currentChat != null) {
ChatWallpaperEditorModal(currentChat)
}
}
}
}
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
SectionView {
ChatTTLOption(chatItemTTL, setChatItemTTL, deletingItems)
SectionTextFooter(stringResource(MR.strings.chat_ttl_options_footer))
}
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
SectionView {
ClearChatButton(clearChat)
if (groupInfo.canDelete) {
val titleId = if (groupInfo.businessChat == null) MR.strings.button_delete_group else MR.strings.button_delete_chat
DeleteGroupButton(titleId, deleteGroup)
}
if (groupInfo.membership.memberCurrentOrPending) {
val titleId = if (groupInfo.businessChat == null) MR.strings.button_leave_group else MR.strings.button_leave_chat
LeaveGroupButton(titleId, leaveGroup)
}
}
SectionBottomSpacer()
}
}
@Composable
private fun WallpaperButton(onClick: () -> Unit) {
SettingsActionItem(
painterResource(MR.images.ic_image),
stringResource(MR.strings.settings_section_title_chat_theme),
click = onClick
)
}
@Composable
private fun ClearChatButton(onClick: () -> Unit) {
SettingsActionItem(
painterResource(MR.images.ic_settings_backup_restore),
stringResource(MR.strings.clear_chat_button),
click = onClick,
textColor = WarningOrange,
iconColor = WarningOrange,
)
}

View File

@@ -1789,6 +1789,12 @@
<string name="button_add_team_members">Add team members</string>
<string name="button_add_friends">Add friends</string>
<string name="group_info_section_title_num_members">%1$s MEMBERS</string>
<string name="group_info_tab_members">MEMBERS</string>
<string name="group_info_tab_images">IMAGES</string>
<string name="group_info_tab_videos">VIDEOS</string>
<string name="group_info_tab_links">LINKS</string>
<string name="group_info_tab_files">FILES</string>
<string name="group_info_tab_voices">VOICES</string>
<string name="group_info_member_you">you: %1$s</string>
<string name="button_delete_group">Delete group</string>
<string name="button_delete_chat">Delete chat</string>