From 44371d287d77c75cd7ed50cb46c6e2dfe04977e8 Mon Sep 17 00:00:00 2001 From: Avently <7953703+avently@users.noreply.github.com> Date: Sat, 4 Jan 2025 02:27:34 +0700 Subject: [PATCH] android, desktop: reports dashboard --- .../platform/ScrollableColumn.android.kt | 2 + .../kotlin/chat/simplex/common/App.kt | 4 +- .../common/platform/ScrollableColumn.kt | 2 + .../chat/simplex/common/views/TerminalView.kt | 4 +- .../simplex/common/views/chat/ChatView.kt | 179 ++++++++++++++---- .../views/chat/group/GroupChatInfoView.kt | 4 +- .../views/chat/group/GroupReportsView.kt | 87 +++++++++ .../common/views/chatlist/TagListView.kt | 2 +- .../commonMain/resources/MR/base/strings.xml | 5 + .../platform/ScrollableColumn.desktop.kt | 10 +- 10 files changed, 249 insertions(+), 50 deletions(-) create mode 100644 apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupReportsView.kt diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/ScrollableColumn.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/ScrollableColumn.android.kt index 60197f3851..b3d8e9b52f 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/ScrollableColumn.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/ScrollableColumn.android.kt @@ -29,6 +29,7 @@ actual fun LazyColumnWithScrollBar( flingBehavior: FlingBehavior, userScrollEnabled: Boolean, additionalBarOffset: State?, + additionalTopBar: State, chatBottomBar: State, fillMaxSize: Boolean, content: LazyListScope.() -> Unit @@ -92,6 +93,7 @@ actual fun LazyColumnWithScrollBarNoAppBar( flingBehavior: FlingBehavior, userScrollEnabled: Boolean, additionalBarOffset: State?, + additionalTopBar: State, chatBottomBar: State, content: LazyListScope.() -> Unit ) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt index fc17c49c7e..28f91226c7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt @@ -339,7 +339,7 @@ fun AndroidScreen(userPickerState: MutableStateFlow) { .graphicsLayer { translationX = maxWidth.toPx() - minOf(offset.value.dp, maxWidth).toPx() } ) Box2@{ currentChatId.value?.let { - ChatView(currentChatId, onComposed) + ChatView(currentChatId, reportsView = false, onComposed) } } } @@ -393,7 +393,7 @@ fun CenterPartOfScreen() { ModalManager.center.showInView() } } - else -> ChatView(currentChatId) {} + else -> ChatView(currentChatId, reportsView = false) {} } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/ScrollableColumn.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/ScrollableColumn.kt index b4e823bd45..e6d4514875 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/ScrollableColumn.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/ScrollableColumn.kt @@ -23,6 +23,7 @@ expect fun LazyColumnWithScrollBar( flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(), userScrollEnabled: Boolean = true, additionalBarOffset: State? = null, + additionalTopBar: State = remember { mutableStateOf(false) }, chatBottomBar: State = remember { mutableStateOf(true) }, // by default, this function will include .fillMaxSize() without you doing anything. If you don't need it, pass `false` here // maxSize (at least maxHeight) is needed for blur on appBars to work correctly @@ -42,6 +43,7 @@ expect fun LazyColumnWithScrollBarNoAppBar( flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(), userScrollEnabled: Boolean = true, additionalBarOffset: State? = null, + additionalTopBar: State = remember { mutableStateOf(false) }, chatBottomBar: State = remember { mutableStateOf(true) }, content: LazyListScope.() -> Unit ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt index 6a6db0da85..66ed433878 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt @@ -154,12 +154,12 @@ fun TerminalLog(floating: Boolean, composeViewHeight: State) { } } LazyColumnWithScrollBar ( - reverseLayout = true, + state = listState, contentPadding = PaddingValues( top = topPaddingToContent(false), bottom = composeViewHeight.value ), - state = listState, + reverseLayout = true, additionalBarOffset = composeViewHeight ) { items(reversedTerminalItems, key = { item -> item.id to item.createdAtNanos }) { item -> diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index 71e1a422a6..cf58267710 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -57,7 +57,7 @@ data class ItemSeparation(val timestamp: Boolean, val largeGap: Boolean, val dat @Composable // staleChatId means the id that was before chatModel.chatId becomes null. It's needed for Android only to make transition from chat // to chat list smooth. Otherwise, chat view will become blank right before the transition starts -fun ChatView(staleChatId: State, onComposed: suspend (chatId: String) -> Unit) { +fun ChatView(staleChatId: State, reportsView: Boolean, onComposed: suspend (chatId: String) -> Unit) { val remoteHostId = remember { derivedStateOf { chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.remoteHostId } } val showSearch = rememberSaveable { mutableStateOf(false) } val activeChatInfo = remember { derivedStateOf { chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.chatInfo } } @@ -69,6 +69,8 @@ fun ChatView(staleChatId: State, onComposed: suspend (chatId: String) - ModalManager.end.closeModals() } } else { + val showArchivedReports = remember { mutableStateOf(false) } + val groupReports = remember { derivedStateOf { GroupReports((activeChatInfo.value as? ChatInfo.Group)?.apiId?.toInt() ?: 0, reportsView, showArchivedReports.value) } } val searchText = rememberSaveable { mutableStateOf("") } val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get() val composeState = rememberSaveable(saver = ComposeState.saver()) { @@ -119,6 +121,15 @@ fun ChatView(staleChatId: State, onComposed: suspend (chatId: String) - val overrides = if (perChatTheme != null) ThemeManager.currentColors(null, perChatTheme, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) else null val fullDeleteAllowed = remember(chatInfo) { chatInfo.featureEnabled(ChatFeature.FullDelete) } SimpleXThemeOverride(overrides ?: CurrentColors.collectAsState().value) { + val onSearchValueChanged: (String) -> Unit = onSearchValueChanged@{ value -> + if (searchText.value == value) return@onSearchValueChanged + val c = chatModel.getChat(chatInfo.id) ?: return@onSearchValueChanged + if (chatModel.chatId.value != chatInfo.id) return@onSearchValueChanged + withBGApi { + apiFindMessages(c, value) + searchText.value = value + } + } ChatLayout( remoteHostId = remoteHostId, chatInfo = activeChatInfo, @@ -211,6 +222,7 @@ fun ChatView(staleChatId: State, onComposed: suspend (chatId: String) - ) } }, + groupReports, attachmentOption, attachmentBottomSheetState, searchText, @@ -278,6 +290,34 @@ fun ChatView(staleChatId: State, onComposed: suspend (chatId: String) - } } }, + showGroupReports = { + if (ModalManager.end.hasModalsOpen()) { + ModalManager.end.closeModals() + return@ChatLayout + } + hideKeyboard(view) + ModalManager.end.showCustomModal(true) { close -> + ModalView({}, showAppBar = false) { + val oneHandUI = remember { appPrefs.oneHandUI.state } + val chatInfo = remember { activeChatInfo }.value + if (chatInfo is ChatInfo.Group) { + GroupReportsView(staleChatId) + if (oneHandUI.value) { + StatusBarBackground() + } + Box(Modifier.align(if (oneHandUI.value) Alignment.BottomStart else Alignment.TopStart)) { + GroupReportsAppBar(groupReports, close, showArchived = { showHide -> + showArchivedReports.value = showHide + }, onSearchValueChanged) + } + } else { + LaunchedEffect(Unit) { + close() + } + } + } + } + }, showMemberInfo = { groupInfo: GroupInfo, member: GroupMember -> hideKeyboard(view) groupMembersJob.cancel() @@ -535,15 +575,7 @@ fun ChatView(staleChatId: State, onComposed: suspend (chatId: String) - } }, changeNtfsState = { enabled, currentValue -> toggleNotifications(chatRh, chatInfo, enabled, chatModel, currentValue) }, - onSearchValueChanged = { value -> - if (searchText.value == value) return@ChatLayout - val c = chatModel.getChat(chatInfo.id) ?: return@ChatLayout - if (chatModel.chatId.value != chatInfo.id) return@ChatLayout - withBGApi { - apiFindMessages(c, value) - searchText.value = value - } - }, + onSearchValueChanged = onSearchValueChanged, onComposed, developerTools = chatModel.controller.appPrefs.developerTools.get(), showViaProxy = chatModel.controller.appPrefs.showSentViaProxy.get(), @@ -603,6 +635,7 @@ fun ChatLayout( unreadCount: State, composeState: MutableState, composeView: (@Composable () -> Unit), + groupReports: State, attachmentOption: MutableState, attachmentBottomSheetState: ModalBottomSheetState, searchValue: State, @@ -611,6 +644,7 @@ fun ChatLayout( selectedChatItems: MutableState?>, back: () -> Unit, info: () -> Unit, + showGroupReports: () -> Unit, showMemberInfo: (GroupInfo, GroupMember) -> Unit, loadMessages: suspend (ChatId, ChatPagination, ActiveChatState, visibleItemIndexesNonReversed: () -> IntRange) -> Unit, deleteMessage: (Long, CIDeleteMode) -> Unit, @@ -685,7 +719,7 @@ fun ChatLayout( }) { ChatItemsList( remoteHostId, chatInfo, unreadCount, composeState, composeViewHeight, searchValue, - useLinkPreviews, linkMode, selectedChatItems, showMemberInfo, showChatInfo = info, loadMessages, deleteMessage, deleteMessages, + useLinkPreviews, linkMode, groupReports, selectedChatItems, showMemberInfo, showChatInfo = info, loadMessages, deleteMessage, deleteMessages, receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem, updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember, setReaction, showItemDetails, markItemsRead, markChatRead, remember { { onComposed(it) } }, developerTools, showViaProxy, @@ -693,29 +727,50 @@ fun ChatLayout( } } } - Box( - Modifier - .layoutId(CHAT_COMPOSE_LAYOUT_ID) - .align(Alignment.BottomCenter) - .imePadding() - .navigationBarsPadding() - .then(if (oneHandUI.value && chatBottomBar.value) Modifier.padding(bottom = AppBarHeight * fontSizeSqrtMultiplier) else Modifier) - ) { - composeView() + if (!groupReports.value.reportsView) { + Box( + Modifier + .layoutId(CHAT_COMPOSE_LAYOUT_ID) + .align(Alignment.BottomCenter) + .imePadding() + .navigationBarsPadding() + .then(if (oneHandUI.value && chatBottomBar.value) Modifier.padding(bottom = AppBarHeight * fontSizeSqrtMultiplier) else Modifier) + ) { + composeView() + } + } else { + // That's placeholder to take some space for bottom app bar in oneHandUI + Box( + Modifier + .layoutId(CHAT_COMPOSE_LAYOUT_ID) + .align(Alignment.BottomCenter) + .height(if (oneHandUI.value) AppBarHeight * fontSizeSqrtMultiplier else 0.dp) + ) } } if (oneHandUI.value && chatBottomBar.value) { - StatusBarBackground() + if (groupReports.value.showBar) { + GroupReportsToolbar(groupReports, withStatusBar = true, showGroupReports) + } else { + StatusBarBackground() + } } else { NavigationBarBackground(true, oneHandUI.value, noAlpha = true) } - Box(if (oneHandUI.value && chatBottomBar.value) Modifier.align(Alignment.BottomStart).imePadding() else Modifier) { - if (selectedChatItems.value == null) { - if (chatInfo != null) { - ChatInfoToolbar(chatInfo, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch) + Column(if (oneHandUI.value && chatBottomBar.value) Modifier.align(Alignment.BottomStart).imePadding() else Modifier) { + if (!groupReports.value.reportsView) { + Box { + if (selectedChatItems.value == null) { + if (chatInfo != null) { + ChatInfoToolbar(chatInfo, groupReports, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch) + } + } else { + SelectedItemsTopToolbar(selectedChatItems) + } } - } else { - SelectedItemsTopToolbar(selectedChatItems) + } + if (groupReports.value.showBar && (!oneHandUI.value || !chatBottomBar.value)) { + GroupReportsToolbar(groupReports, withStatusBar = false, showGroupReports) } } } @@ -726,6 +781,7 @@ fun ChatLayout( @Composable fun BoxScope.ChatInfoToolbar( chatInfo: ChatInfo, + groupReports: State, back: () -> Unit, info: () -> Unit, startCall: (CallMediaType) -> Unit, @@ -747,7 +803,7 @@ fun BoxScope.ChatInfoToolbar( showSearch.value = false } } - if (appPlatform.isAndroid) { + if (appPlatform.isAndroid && !groupReports.value.reportsView) { BackHandler(onBack = onBackClicked) } val barButtons = arrayListOf<@Composable RowScope.() -> Unit>() @@ -941,6 +997,40 @@ fun ChatInfoToolbarTitle(cInfo: ChatInfo, imageSize: Dp = 40.dp, iconColor: Colo } } +@Composable +private fun GroupReportsToolbar( + groupReports: State, + withStatusBar: Boolean, + showGroupReports: () -> Unit +) { + Box { + val statusBarPadding = if (withStatusBar) WindowInsets.statusBars.asPaddingValues().calculateTopPadding() else 0.dp + Row( + Modifier + .fillMaxWidth() + .height(AppBarHeight * fontSizeSqrtMultiplier + statusBarPadding) + .background(MaterialTheme.colors.background) + .clickable(onClick = showGroupReports) + .padding(top = statusBarPadding), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Icon(painterResource(MR.images.ic_flag), null, Modifier.size(22.dp), tint = MaterialTheme.colors.error) + Spacer(Modifier.width(4.dp)) + val reports = groupReports.value.activeReports + Text( + if (reports == 1) { + stringResource(MR.strings.group_reports_active_one) + } else { + stringResource(MR.strings.group_reports_active).format(reports) + }, + style = MaterialTheme.typography.button + ) + } + Divider(Modifier.align(Alignment.BottomStart)) + } +} + @Composable private fun ContactVerifiedShield() { Icon(painterResource(MR.images.ic_verified_user), null, Modifier.size(18.dp * fontSizeSqrtMultiplier).padding(end = 3.dp, top = 1.dp), tint = MaterialTheme.colors.secondary) @@ -956,6 +1046,7 @@ fun BoxScope.ChatItemsList( searchValue: State, useLinkPreviews: Boolean, linkMode: SimplexLinkMode, + groupReports: State, selectedChatItems: MutableState?>, showMemberInfo: (GroupInfo, GroupMember) -> Unit, showChatInfo: () -> Unit, @@ -987,7 +1078,7 @@ fun BoxScope.ChatItemsList( val reversedChatItems = remember { derivedStateOf { chatModel.chatItems.asReversed() } } val revealedItems = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(setOf()) } val mergedItems = remember { derivedStateOf { MergedItems.create(reversedChatItems.value, unreadCount, revealedItems.value, chatModel.chatState) } } - val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent(true).roundToPx() }) + val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent(chatView = !groupReports.value.reportsView, groupReports.value.showBar).roundToPx() }) /** determines height based on window info and static height of two AppBars. It's needed because in the first graphic frame height of * [composeViewHeight] is unknown, but we need to set scroll position for unread messages already so it will be correct before the first frame appears * */ @@ -1286,12 +1377,13 @@ fun BoxScope.ChatItemsList( LazyColumnWithScrollBar( Modifier.align(Alignment.BottomCenter), state = listState.value, - reverseLayout = true, contentPadding = PaddingValues( - top = topPaddingToContent(true), + top = topPaddingToContent(chatView = !groupReports.value.reportsView, groupReports.value.showBar), bottom = composeViewHeight.value ), + reverseLayout = true, additionalBarOffset = composeViewHeight, + additionalTopBar = remember { derivedStateOf { groupReports.value.showBar } }, chatBottomBar = remember { appPrefs.chatBottomBar.state } ) { val mergedItemsValue = mergedItems.value @@ -1335,8 +1427,8 @@ fun BoxScope.ChatItemsList( } } } - FloatingButtons(loadingMoreItems, animatedScrollingInProgress, mergedItems, unreadCount, maxHeight, composeViewHeight, searchValue, markChatRead, listState) - FloatingDate(Modifier.padding(top = 10.dp + topPaddingToContent(true)).align(Alignment.TopCenter), mergedItems, listState) + FloatingButtons(loadingMoreItems, animatedScrollingInProgress, mergedItems, unreadCount, maxHeight, composeViewHeight, searchValue, groupReports, markChatRead, listState) + FloatingDate(Modifier.padding(top = 10.dp + topPaddingToContent(chatView = !groupReports.value.reportsView, groupReports.value.showBar)).align(Alignment.TopCenter), mergedItems, listState, groupReports) LaunchedEffect(Unit) { snapshotFlow { listState.value.isScrollInProgress } @@ -1436,11 +1528,12 @@ fun BoxScope.FloatingButtons( maxHeight: State, composeViewHeight: State, searchValue: State, + groupReports: State, markChatRead: () -> Unit, listState: State ) { val scope = rememberCoroutineScope() - val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent(true).roundToPx() }) + val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent(chatView = !groupReports.value.reportsView, groupReports.value.showBar).roundToPx() }) val bottomUnreadCount = remember { derivedStateOf { if (unreadCount.value == 0) return@derivedStateOf 0 @@ -1486,7 +1579,7 @@ fun BoxScope.FloatingButtons( val showDropDown = remember { mutableStateOf(false) } TopEndFloatingButton( - Modifier.padding(end = DEFAULT_PADDING, top = 24.dp + topPaddingToContent(true)).align(Alignment.TopEnd), + Modifier.padding(end = DEFAULT_PADDING, top = 24.dp + topPaddingToContent(chatView = !groupReports.value.reportsView, groupReports.value.showBar)).align(Alignment.TopEnd), topUnreadCount, animatedScrollingInProgress, onClick = { @@ -1508,7 +1601,7 @@ fun BoxScope.FloatingButtons( DefaultDropdownMenu( showDropDown, modifier = Modifier.onSizeChanged { with(density) { width.value = it.width.toDp().coerceAtLeast(250.dp) } }, - offset = DpOffset(-DEFAULT_PADDING - width.value, 24.dp + fabSize + topPaddingToContent(true)) + offset = DpOffset(-DEFAULT_PADDING - width.value, 24.dp + fabSize + topPaddingToContent(chatView = !groupReports.value.reportsView, groupReports.value.showBar)) ) { ItemAction( generalGetString(MR.strings.mark_read), @@ -1659,13 +1752,14 @@ private fun TopEndFloatingButton( } @Composable -fun topPaddingToContent(chatView: Boolean): Dp { +fun topPaddingToContent(chatView: Boolean, additionalTopBar: Boolean = false): Dp { val oneHandUI = remember { appPrefs.oneHandUI.state } val chatBottomBar = remember { appPrefs.chatBottomBar.state } + val reportsPadding = if (additionalTopBar) AppBarHeight * fontSizeSqrtMultiplier else 0.dp return if (oneHandUI.value && (!chatView || chatBottomBar.value)) { - WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + reportsPadding } else { - AppBarHeight * fontSizeSqrtMultiplier + WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + AppBarHeight * fontSizeSqrtMultiplier + WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + reportsPadding } } @@ -1674,12 +1768,13 @@ private fun FloatingDate( modifier: Modifier, mergedItems: State, listState: State, + groupReports: State ) { val isNearBottom = remember(chatModel.chatId) { mutableStateOf(listState.value.firstVisibleItemIndex == 0) } val nearBottomIndex = remember(chatModel.chatId) { mutableStateOf(if (isNearBottom.value) -1 else 0) } val showDate = remember(chatModel.chatId) { mutableStateOf(false) } val density = LocalDensity.current.density - val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent(true).roundToPx() }) + val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent(chatView = !groupReports.value.reportsView, groupReports.value.showBar).roundToPx() }) val fontSizeSqrtMultiplier = fontSizeSqrtMultiplier val lastVisibleItemDate = remember { derivedStateOf { @@ -2439,6 +2534,7 @@ fun PreviewChatLayout() { unreadCount = unreadCount, composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) }, composeView = {}, + groupReports = remember { mutableStateOf(GroupReports(0, false)) }, attachmentOption = remember { mutableStateOf(null) }, attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden), searchValue, @@ -2447,6 +2543,7 @@ fun PreviewChatLayout() { selectedChatItems = remember { mutableStateOf(setOf()) }, back = {}, info = {}, + showGroupReports = {}, showMemberInfo = { _, _ -> }, loadMessages = { _, _, _, _ -> }, deleteMessage = { _, _ -> }, @@ -2512,6 +2609,7 @@ fun PreviewGroupChatLayout() { unreadCount = unreadCount, composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) }, composeView = {}, + groupReports = remember { mutableStateOf(GroupReports(0, false)) }, attachmentOption = remember { mutableStateOf(null) }, attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden), searchValue, @@ -2520,6 +2618,7 @@ fun PreviewGroupChatLayout() { selectedChatItems = remember { mutableStateOf(setOf()) }, back = {}, info = {}, + showGroupReports = {}, showMemberInfo = { _, _ -> }, loadMessages = { _, _, _, _ -> }, deleteMessage = { _, _ -> }, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt index 21d678ba50..9852c7f7d7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt @@ -309,12 +309,12 @@ fun ModalData.GroupChatInfoLayout( Box { val oneHandUI = remember { appPrefs.oneHandUI.state } LazyColumnWithScrollBar( + state = listState, contentPadding = if (oneHandUI.value) { PaddingValues(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + DEFAULT_PADDING + 5.dp, bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()) } else { PaddingValues(top = topPaddingToContent(false)) - }, - state = listState + } ) { item { Row( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupReportsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupReportsView.kt new file mode 100644 index 0000000000..bc9cb671b9 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupReportsView.kt @@ -0,0 +1,87 @@ +package chat.simplex.common.views.chat.group + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.height +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.platform.* +import chat.simplex.common.views.chat.ChatView +import chat.simplex.common.views.chat.item.ItemAction +import chat.simplex.common.views.helpers.* +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource + +data class GroupReports( + val activeReports: Int, + val reportsView: Boolean, + val showArchived: Boolean = false +) { + val showBar: Boolean = activeReports > 0 && !reportsView +} + +@Composable +fun GroupReportsView(staleChatId: State) { + ChatView(staleChatId, reportsView = true, onComposed = {}) +} + +@Composable +fun GroupReportsAppBar( + groupReports: State, + close: () -> Unit, + showArchived: (Boolean) -> Unit, + onSearchValueChanged: (String) -> Unit +) { + val oneHandUI = remember { appPrefs.oneHandUI.state } + val showMenu = remember { mutableStateOf(false) } + val showSearch = rememberSaveable { mutableStateOf(false) } + val onBackClicked = { + if (!showSearch.value) { + close() + } else { + onSearchValueChanged("") + showSearch.value = false + } + } + BackHandler(onBack = onBackClicked) + DefaultAppBar( + navigationButton = { NavigationButtonBack(onBackClicked) }, + fixedTitleText = stringResource(MR.strings.group_reports_member_reports), + onTitleClick = null, + onTop = !oneHandUI.value, + showSearch = showSearch.value, + onSearchValueChanged = onSearchValueChanged, + buttons = { + IconButton({ showMenu.value = true }) { + Icon(MoreVertFilled, stringResource(MR.strings.icon_descr_more_button), tint = MaterialTheme.colors.primary) + } + val onClosedAction = remember { mutableStateOf({}) } + DefaultDropdownMenu( + showMenu, + onClosed = onClosedAction + ) { + ItemAction(stringResource(MR.strings.search_verb), painterResource(MR.images.ic_search), onClick = { + showMenu.value = false + showSearch.value = true + }) + ItemAction( + if (groupReports.value.showArchived) stringResource(MR.strings.group_reports_hide_archived) else stringResource(MR.strings.group_reports_show_archived), + painterResource(MR.images.ic_add), + onClick = { + onClosedAction.value = { + showArchived(!groupReports.value.showArchived) + onClosedAction.value = {} + } + showMenu.value = false + } + ) + } + } + ) +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/TagListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/TagListView.kt index f8ddc16bde..7bbf4f4aa5 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/TagListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/TagListView.kt @@ -72,11 +72,11 @@ fun TagListView(rhId: Long?, chat: Chat? = null, close: () -> Unit, reorderMode: LazyColumnWithScrollBar( modifier = if (reorderMode) Modifier.dragContainer(dragDropState) else Modifier, + state = listState, contentPadding = PaddingValues( top = if (oneHandUI.value) WindowInsets.statusBars.asPaddingValues().calculateTopPadding() else topPaddingToContent, bottom = if (oneHandUI.value) WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier else 0.dp ), - state = listState, verticalArrangement = if (oneHandUI.value) Arrangement.Bottom else Arrangement.Top, ) { @Composable fun CreateList() { diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index e45ba24822..f866f94d52 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -424,6 +424,11 @@ Notes All Add list + 1 report + %d reports + Member reports + Show archived + Hide archived Share message… diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/ScrollableColumn.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/ScrollableColumn.desktop.kt index 785c3b40fa..3f5703365d 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/ScrollableColumn.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/ScrollableColumn.desktop.kt @@ -36,6 +36,7 @@ actual fun LazyColumnWithScrollBar( flingBehavior: FlingBehavior, userScrollEnabled: Boolean, additionalBarOffset: State?, + additionalTopBar: State, chatBottomBar: State, fillMaxSize: Boolean, content: LazyListScope.() -> Unit @@ -93,7 +94,7 @@ actual fun LazyColumnWithScrollBar( val modifier = if (fillMaxSize) Modifier.fillMaxSize().then(modifier) else modifier Box(Modifier.copyViewToAppBar(remember { appPrefs.appearanceBarsBlurRadius.state }.value, LocalAppBarHandler.current?.graphicsLayer).nestedScroll(connection)) { LazyColumn(modifier.then(scrollModifier), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content) - ScrollBar(reverseLayout, state, scrollBarAlpha, scrollJob, scrollBarDraggingState, additionalBarOffset, chatBottomBar) + ScrollBar(reverseLayout, state, scrollBarAlpha, scrollJob, scrollBarDraggingState, additionalBarOffset, additionalTopBar, chatBottomBar) } } @@ -108,6 +109,7 @@ actual fun LazyColumnWithScrollBarNoAppBar( flingBehavior: FlingBehavior, userScrollEnabled: Boolean, additionalBarOffset: State?, + additionalTopBar: State, chatBottomBar: State, content: LazyListScope.() -> Unit ) { @@ -135,7 +137,7 @@ actual fun LazyColumnWithScrollBarNoAppBar( val scrollBarDraggingState = remember { mutableStateOf(false) } Box { LazyColumn(modifier.then(scrollModifier), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content) - ScrollBar(reverseLayout, state, scrollBarAlpha, scrollJob, scrollBarDraggingState, additionalBarOffset, chatBottomBar) + ScrollBar(reverseLayout, state, scrollBarAlpha, scrollJob, scrollBarDraggingState, additionalBarOffset, additionalTopBar, chatBottomBar) } } @@ -147,11 +149,13 @@ private fun ScrollBar( scrollJob: MutableState, scrollBarDraggingState: MutableState, additionalBarHeight: State?, + additionalTopBar: State, chatBottomBar: State, ) { val oneHandUI = remember { appPrefs.oneHandUI.state } + val topBarPadding = if (additionalTopBar.value) AppBarHeight * fontSizeSqrtMultiplier else 0.dp val padding = if (additionalBarHeight != null) { - PaddingValues(top = if (oneHandUI.value && chatBottomBar.value) 0.dp else AppBarHeight * fontSizeSqrtMultiplier, bottom = additionalBarHeight.value) + PaddingValues(top = topBarPadding + if (oneHandUI.value && chatBottomBar.value) 0.dp else AppBarHeight * fontSizeSqrtMultiplier, bottom = additionalBarHeight.value) } else if (reverseLayout) { PaddingValues(bottom = AppBarHeight * fontSizeSqrtMultiplier) } else {