From cf8343d4f803c06e04025f1cc8861d3060e4a110 Mon Sep 17 00:00:00 2001 From: fakelog <48557356+fakelog@users.noreply.github.com> Date: Sun, 12 Apr 2026 22:16:26 +0500 Subject: [PATCH] Refactor current chat state decomposition into focused StateFlows Previously, a single broad state object caused unnecessary recompositions of the entire ChatContent screen on unrelated changes (input, search, selection, pinned state, viewers, overlays, appearance flags). Now state is split by responsibility: - chatUiState: top bar, screen-level flags, dialogs, topic/navigation state - messagesState: message list, loading, pagination, scroll commands and viewport state - selectionState: selected messages and selection mode - searchState: search query and search mode - inputState: draft, reply/edit state, bot commands, inline results, sticker sheet - pinnedState: pinned banner and pinned messages sheet - mediaViewerState: image/video/web/mini app viewers - appearanceState: wallpaper and message rendering preferences Benefits: - Unrelated updates no longer recompose the whole chat screen - Heavy UI like the message list subscribes to narrower flows, reducing noise - Screen state aligns better with actual UI responsibilities, improving maintainability - Background, message list and message options no longer depend on the monolithic state directly Additionally, moved ChatContent subtrees to the focused state models so the UI no longer collects the broad component state at the root. --- .../chats/currentChat/ChatComponent.kt | 159 +++++ .../features/chats/currentChat/ChatContent.kt | 551 +++++++++--------- .../chats/currentChat/DefaultChatComponent.kt | 183 ++++++ .../chatContent/ChatContentBackground.kt | 2 +- .../chatContent/ChatContentList.kt | 204 ++++--- .../chatContent/ChatMessageOptionsMenu.kt | 52 +- 6 files changed, 766 insertions(+), 385 deletions(-) diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/ChatComponent.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/ChatComponent.kt index 57e20406..295060cf 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/ChatComponent.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/ChatComponent.kt @@ -32,6 +32,14 @@ interface ChatComponent { val appPreferences: AppPreferences val stickerRepository: StickerRepository val state: StateFlow + val chatUiState: StateFlow + val selectionState: StateFlow + val searchState: StateFlow + val appearanceState: StateFlow + val messagesState: StateFlow + val inputState: StateFlow + val pinnedState: StateFlow + val mediaViewerState: StateFlow val repositoryMessage: MessageRepository val downloadUtils: IDownloadUtils @@ -337,4 +345,155 @@ interface ChatComponent { val scheduledMessages: List = emptyList(), val lastReadInboxMessageId: Long = 0L, ) + + @Stable + data class MessagesState( + val chatId: Long = 0L, + val messages: List = emptyList(), + val isLoading: Boolean = false, + val isLoadingOlder: Boolean = false, + val isLoadingNewer: Boolean = false, + val scrollToMessageId: Long? = null, + val pendingScrollCommand: ChatScrollCommand? = null, + val highlightedMessageId: Long? = null, + val isAtBottom: Boolean = true, + val currentScrollMessageId: Long = 0L, + val lastScrollPosition: Long = 0L, + val lastSavedViewport: ChatViewportCacheEntry? = null, + val isLatestLoaded: Boolean = true, + val isOldestLoaded: Boolean = false, + val lastReadInboxMessageId: Long = 0L + ) + + @Stable + data class ChatUiState( + val chatId: Long = 0L, + val chatTitle: String = "Chat", + val chatAvatar: String? = null, + val chatPersonalAvatar: String? = null, + val chatEmojiStatus: String? = null, + val isGroup: Boolean = false, + val isChannel: Boolean = false, + val isSecretChat: Boolean = false, + val isOnline: Boolean = false, + val isVerified: Boolean = false, + val isSponsor: Boolean = false, + val canWrite: Boolean = false, + val isAdmin: Boolean = false, + val permissions: ChatPermissionsModel = ChatPermissionsModel(), + val slowModeDelay: Int = 0, + val slowModeDelayExpiresIn: Double = 0.0, + val isCurrentUserRestricted: Boolean = false, + val restrictedUntilDate: Int = 0, + val memberCount: Int = 0, + val onlineCount: Int = 0, + val unreadCount: Int = 0, + val unreadMentionCount: Int = 0, + val unreadReactionCount: Int = 0, + val userStatus: String? = null, + val typingAction: String? = null, + val pollVoters: List = emptyList(), + val showPollVoters: Boolean = false, + val isPollVotersLoading: Boolean = false, + val viewAsTopics: Boolean = false, + val topics: List = emptyList(), + val currentTopicId: Long? = null, + val rootMessage: MessageModel? = null, + val isLoadingTopics: Boolean = false, + val isWhitelistedInAdBlock: Boolean = false, + val isMuted: Boolean = false, + val showReportDialog: Boolean = false, + val showBotCommands: Boolean = false, + val currentUser: UserModel? = null, + val otherUser: UserModel? = null, + val isMember: Boolean = true, + val restrictUserId: Long? = null, + val isInstalledFromGooglePlay: Boolean = true + ) + + @Stable + data class MessageSelectionState( + val selectedMessageIds: Set = emptySet() + ) + + @Stable + data class SearchState( + val isSearchActive: Boolean = false, + val searchQuery: String = "" + ) + + @Stable + data class AppearanceState( + val fontSize: Float = 16f, + val letterSpacing: Float = 0f, + val bubbleRadius: Float = 18f, + val stickerSize: Float = 200f, + val wallpaper: String? = null, + val wallpaperModel: WallpaperModel? = null, + val isWallpaperBlurred: Boolean = false, + val wallpaperBlurIntensity: Int = 20, + val isWallpaperMoving: Boolean = false, + val wallpaperDimming: Int = 0, + val isWallpaperGrayscale: Boolean = false, + val autoDownloadMobile: Boolean = true, + val autoDownloadWifi: Boolean = true, + val autoDownloadRoaming: Boolean = false, + val autoDownloadFiles: Boolean = false, + val autoplayGifs: Boolean = true, + val autoplayVideos: Boolean = true, + val showLinkPreviews: Boolean = true, + val isChatAnimationsEnabled: Boolean = true + ) + + @Stable + data class InputState( + val chatId: Long = 0L, + val replyMessage: MessageModel? = null, + val editingMessage: MessageModel? = null, + val draftText: String = "", + val selectedStickerSet: StickerSetModel? = null, + val isBot: Boolean = false, + val botCommands: List = emptyList(), + val botMenuButton: BotMenuButtonModel = BotMenuButtonModel.Default, + val mentionSuggestions: List = emptyList(), + val inlineBotResults: InlineBotResultsModel? = null, + val currentInlineBotUsername: String? = null, + val currentInlineQuery: String? = null, + val isInlineBotLoading: Boolean = false, + val attachMenuBots: List = emptyList(), + val scheduledMessages: List = emptyList() + ) + + @Stable + data class PinnedState( + val pinnedMessage: MessageModel? = null, + val allPinnedMessages: List = emptyList(), + val showPinnedMessagesList: Boolean = false, + val isLoadingPinnedMessages: Boolean = false, + val pinnedMessageCount: Int = 0, + val pinnedMessageIndex: Int = 0 + ) + + @Stable + data class MediaViewerState( + val instantViewUrl: String? = null, + val youtubeUrl: String? = null, + val miniAppUrl: String? = null, + val miniAppName: String? = null, + val miniAppBotUserId: Long = 0L, + val showMiniAppTOS: Boolean = false, + val miniAppTOSBotUserId: Long = 0L, + val miniAppTOSUrl: String? = null, + val miniAppTOSName: String? = null, + val webViewUrl: String? = null, + val fullScreenImages: List? = null, + val fullScreenImageMessageIds: List = emptyList(), + val fullScreenCaptions: List = emptyList(), + val fullScreenStartIndex: Int = 0, + val fullScreenVideoMessageId: Long? = null, + val fullScreenVideoPath: String? = null, + val fullScreenVideoCaption: String? = null, + val invoiceSlug: String? = null, + val invoiceMessageId: Long? = null + ) } diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/ChatContent.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/ChatContent.kt index 2978b9a2..752fb0e7 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/ChatContent.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/ChatContent.kt @@ -57,6 +57,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -105,24 +106,8 @@ import org.monogram.presentation.R import org.monogram.presentation.core.ui.ConfirmationSheet import org.monogram.presentation.core.ui.ExpressiveDefaults import org.monogram.presentation.core.util.LocalTabletInterfaceEnabled -import org.monogram.presentation.features.chats.currentChat.chatContent.ChatContentBackground -import org.monogram.presentation.features.chats.currentChat.chatContent.ChatContentList -import org.monogram.presentation.features.chats.currentChat.chatContent.ChatContentTopBar -import org.monogram.presentation.features.chats.currentChat.chatContent.ChatContentTopBarUiState -import org.monogram.presentation.features.chats.currentChat.chatContent.ChatMessageOptionsMenu -import org.monogram.presentation.features.chats.currentChat.chatContent.GroupedMessageItem -import org.monogram.presentation.features.chats.currentChat.chatContent.ReportChatDialog -import org.monogram.presentation.features.chats.currentChat.chatContent.RestrictUserSheet -import org.monogram.presentation.features.chats.currentChat.chatContent.chatContentLeadingItemsCount -import org.monogram.presentation.features.chats.currentChat.chatContent.groupMessagesByAlbum -import org.monogram.presentation.features.chats.currentChat.chatContent.groupedIndexToLazyIndex -import org.monogram.presentation.features.chats.currentChat.chatContent.lazyIndexToGroupedIndex -import org.monogram.presentation.features.chats.currentChat.components.AdvancedCircularRecorderScreen -import org.monogram.presentation.features.chats.currentChat.components.ChatInputBar -import org.monogram.presentation.features.chats.currentChat.components.ChatInputBarActions -import org.monogram.presentation.features.chats.currentChat.components.ChatInputBarState -import org.monogram.presentation.features.chats.currentChat.components.MessageListShimmer -import org.monogram.presentation.features.chats.currentChat.components.StickerSetSheet +import org.monogram.presentation.features.chats.currentChat.chatContent.* +import org.monogram.presentation.features.chats.currentChat.components.* import org.monogram.presentation.features.chats.currentChat.components.chats.BotCommandsSheet import org.monogram.presentation.features.chats.currentChat.components.chats.LocalLinkHandler import org.monogram.presentation.features.chats.currentChat.components.chats.PollVotersSheet @@ -141,7 +126,14 @@ fun ChatContent( component: ChatComponent, isOverlay: Boolean = false, ) { - val state by component.state.collectAsState() + val chatUiState by component.chatUiState.collectAsState() + val selectionState by component.selectionState.collectAsState() + val searchState by component.searchState.collectAsState() + val appearanceState by component.appearanceState.collectAsState() + val messagesState by component.messagesState.collectAsState() + val inputState by component.inputState.collectAsState() + val pinnedState by component.pinnedState.collectAsState() + val mediaViewerState by component.mediaViewerState.collectAsState() val scrollState = rememberLazyListState() val context = LocalContext.current val density = LocalDensity.current @@ -163,7 +155,7 @@ fun ChatContent( var selectedMessageId by rememberSaveable { mutableStateOf(null) } val transformedMessageTexts = remember { mutableStateMapOf() } val originalMessageTexts = remember { mutableStateMapOf() } - val latestMessagesState = rememberUpdatedState(state.messages) + val latestMessagesState = rememberUpdatedState(messagesState.messages) val selectedMessageIdState = rememberUpdatedState(selectedMessageId) val displayMessages by remember { derivedStateOf { @@ -215,17 +207,18 @@ fun ChatContent( } } } - val isComments = state.rootMessage != null - val isForumList = state.viewAsTopics && state.currentTopicId == null + val isComments = chatUiState.rootMessage != null + val isForumList = chatUiState.viewAsTopics && chatUiState.currentTopicId == null var showScrollToBottomButton by remember { mutableStateOf(false) } - - val isAnyViewerOpen = state.fullScreenImages != null || - state.fullScreenVideoPath != null || - state.fullScreenVideoMessageId != null || - state.youtubeUrl != null || - state.instantViewUrl != null || - state.miniAppUrl != null || - state.webViewUrl != null || + var lastAutoScrollMessageCount by remember(chatUiState.chatId, chatUiState.currentTopicId) { mutableIntStateOf(0) } + + val isAnyViewerOpen = mediaViewerState.fullScreenImages != null || + mediaViewerState.fullScreenVideoPath != null || + mediaViewerState.fullScreenVideoMessageId != null || + mediaViewerState.youtubeUrl != null || + mediaViewerState.instantViewUrl != null || + mediaViewerState.miniAppUrl != null || + mediaViewerState.webViewUrl != null || editingPhotoPath != null || editingVideoPath != null || isRecordingVideo @@ -237,19 +230,19 @@ fun ChatContent( val leadingItems = chatContentLeadingItemsCount( isComments = isComments, showNavPadding = false, - isLoadingOlder = state.isLoadingOlder, - isLoadingNewer = state.isLoadingNewer, - isAtBottom = state.isAtBottom, + isLoadingOlder = messagesState.isLoadingOlder, + isLoadingNewer = messagesState.isLoadingNewer, + isAtBottom = messagesState.isAtBottom, hasMessages = groupedMessages.isNotEmpty() ) val targetIndex = groupedIndexToLazyIndex(index, leadingItems) - scrollState.scrollToMessageIndex( - index = targetIndex, - align = ScrollAlign.Center, - animated = state.isChatAnimationsEnabled, - staged = true - ) + scrollState.scrollToMessageIndex( + index = targetIndex, + align = ScrollAlign.Center, + animated = appearanceState.isChatAnimationsEnabled, + staged = true + ) } } else { component.onPinnedMessageClick(msg) @@ -258,14 +251,14 @@ fun ChatContent( LaunchedEffect(Unit) { isVisible = true - if (state.fullScreenVideoPath != null || state.fullScreenVideoMessageId != null) { + if (mediaViewerState.fullScreenVideoPath != null || mediaViewerState.fullScreenVideoMessageId != null) { component.onDismissVideo() } } - LaunchedEffect(state.messages) { + LaunchedEffect(messagesState.messages) { if (transformedMessageTexts.isEmpty() && originalMessageTexts.isEmpty()) return@LaunchedEffect - val ids = state.messages.map { it.id }.toSet() + val ids = messagesState.messages.map { it.id }.toSet() transformedMessageTexts.keys.toList().forEach { id -> if (id !in ids) { transformedMessageTexts.remove(id) @@ -276,22 +269,22 @@ fun ChatContent( // Initial Loading Delay logic LaunchedEffect( - state.isLoading, - state.messages.isEmpty(), - state.viewAsTopics, - state.currentTopicId, - state.isLoadingTopics, - state.rootMessage + messagesState.isLoading, + messagesState.messages.isEmpty(), + chatUiState.viewAsTopics, + chatUiState.currentTopicId, + chatUiState.isLoadingTopics, + chatUiState.rootMessage ) { - val isActuallyLoading = if (state.viewAsTopics && state.currentTopicId == null) { - state.isLoadingTopics && state.topics.isEmpty() - } else if (state.currentTopicId != null) { - state.isLoading && state.messages.isEmpty() && state.rootMessage == null + val isActuallyLoading = if (chatUiState.viewAsTopics && chatUiState.currentTopicId == null) { + chatUiState.isLoadingTopics && chatUiState.topics.isEmpty() + } else if (chatUiState.currentTopicId != null) { + messagesState.isLoading && messagesState.messages.isEmpty() && chatUiState.rootMessage == null } else { - state.isLoading && state.messages.isEmpty() + messagesState.isLoading && messagesState.messages.isEmpty() } if (isActuallyLoading) { - if (state.isChatAnimationsEnabled) delay(200) + if (appearanceState.isChatAnimationsEnabled) delay(200) showInitialLoading = true } else { showInitialLoading = false @@ -299,15 +292,15 @@ fun ChatContent( } // Unified command-based scrolling: restore, jump, bottom. - LaunchedEffect(state.pendingScrollCommand, isComments) { - val command = state.pendingScrollCommand ?: return@LaunchedEffect + LaunchedEffect(messagesState.pendingScrollCommand, isComments) { + val command = messagesState.pendingScrollCommand ?: return@LaunchedEffect val leadingItems = chatContentLeadingItemsCount( isComments = isComments, showNavPadding = false, - isLoadingOlder = state.isLoadingOlder, - isLoadingNewer = state.isLoadingNewer, - isAtBottom = state.isAtBottom, + isLoadingOlder = messagesState.isLoadingOlder, + isLoadingNewer = messagesState.isLoadingNewer, + isAtBottom = messagesState.isAtBottom, hasMessages = groupedMessages.isNotEmpty() ) @@ -353,7 +346,7 @@ fun ChatContent( scrollState.scrollToMessageIndex( index = targetIndex, align = command.align, - animated = command.animated && state.isChatAnimationsEnabled, + animated = command.animated && appearanceState.isChatAnimationsEnabled, staged = true ) } @@ -363,7 +356,7 @@ fun ChatContent( is ChatScrollCommand.ScrollToBottom -> { scrollState.scrollToChatBottomStaged( isComments = isComments, - animated = command.animated && state.isChatAnimationsEnabled + animated = command.animated && appearanceState.isChatAnimationsEnabled ) component.onScrollCommandConsumed() } @@ -382,12 +375,12 @@ fun ChatContent( BottomVisibilitySnapshot( isAtBottom = scrollState.isAtBottom( isComments = isComments, - isLatestLoaded = state.isLatestLoaded + isLatestLoaded = messagesState.isLatestLoaded ), isNearBottom = scrollState.isNearBottom( isComments = isComments ), - unreadCount = state.unreadCount + unreadCount = chatUiState.unreadCount ) } .distinctUntilChanged() @@ -418,20 +411,20 @@ fun ChatContent( scrollState, groupedMessages, isComments, - state.isLatestLoaded, - state.isLoadingOlder, - state.isLoadingNewer, - state.isAtBottom + messagesState.isLatestLoaded, + messagesState.isLoadingOlder, + messagesState.isLoadingNewer, + messagesState.isAtBottom ) { snapshotFlow { buildViewportSnapshot( scrollState = scrollState, groupedMessages = groupedMessages, isComments = isComments, - isLatestLoaded = state.isLatestLoaded, - isLoadingOlder = state.isLoadingOlder, - isLoadingNewer = state.isLoadingNewer, - isAtBottom = state.isAtBottom, + isLatestLoaded = messagesState.isLatestLoaded, + isLoadingOlder = messagesState.isLoadingOlder, + isLoadingNewer = messagesState.isLoadingNewer, + isAtBottom = messagesState.isAtBottom, showNavPadding = false ) } @@ -447,21 +440,21 @@ fun ChatContent( scrollState, groupedMessages, isComments, - state.currentTopicId, - state.isLatestLoaded, - state.isLoadingOlder, - state.isLoadingNewer, - state.isAtBottom + chatUiState.currentTopicId, + messagesState.isLatestLoaded, + messagesState.isLoadingOlder, + messagesState.isLoadingNewer, + messagesState.isAtBottom ) { onDispose { val viewport = buildViewportSnapshot( scrollState = scrollState, groupedMessages = groupedMessages, isComments = isComments, - isLatestLoaded = state.isLatestLoaded, - isLoadingOlder = state.isLoadingOlder, - isLoadingNewer = state.isLoadingNewer, - isAtBottom = state.isAtBottom, + isLatestLoaded = messagesState.isLatestLoaded, + isLoadingOlder = messagesState.isLoadingOlder, + isLoadingNewer = messagesState.isLoadingNewer, + isAtBottom = messagesState.isAtBottom, showNavPadding = false ) if (viewport != null) { @@ -471,7 +464,7 @@ fun ChatContent( } // Performance: Update visible range for repository - LaunchedEffect(scrollState, groupedMessages, state.rootMessage) { + LaunchedEffect(scrollState, groupedMessages, chatUiState.rootMessage) { snapshotFlow { scrollState.layoutInfo.visibleItemsInfo } .map { visibleItems -> val visibleIds = LinkedHashSet() @@ -481,7 +474,7 @@ fun ChatContent( val maxIndex = visibleItems.maxOf { it.index } visibleItems.forEach { item -> - val groupedIndex = if (state.rootMessage != null) item.index - 1 else item.index + val groupedIndex = if (chatUiState.rootMessage != null) item.index - 1 else item.index groupedMessages.getOrNull(groupedIndex)?.let { grouped -> when (grouped) { is GroupedMessageItem.Single -> visibleIds.add(grouped.message.id) @@ -496,7 +489,7 @@ fun ChatContent( val nearbyEnd = maxIndex + 5 for (index in nearbyStart..nearbyEnd) { if (index in minIndex..maxIndex) continue - val groupedIndex = if (state.rootMessage != null) index - 1 else index + val groupedIndex = if (chatUiState.rootMessage != null) index - 1 else index groupedMessages.getOrNull(groupedIndex)?.let { grouped -> when (grouped) { is GroupedMessageItem.Single -> nearbyIds.add(grouped.message.id) @@ -521,22 +514,27 @@ fun ChatContent( // Auto-scroll to bottom when new messages arrive and we are already at the bottom val messageCount = groupedMessages.size - LaunchedEffect(messageCount, state.isLatestLoaded) { - if (isComments) return@LaunchedEffect + LaunchedEffect(messageCount, messagesState.isLatestLoaded, isComments) { + val previousMessageCount = lastAutoScrollMessageCount + lastAutoScrollMessageCount = messageCount + + if (isComments || previousMessageCount == 0 || messageCount <= previousMessageCount) { + return@LaunchedEffect + } val isAtBottomNow = scrollState.isAtBottom( isComments = isComments, - isLatestLoaded = state.isLatestLoaded + isLatestLoaded = messagesState.isLatestLoaded ) - if ((state.isAtBottom || isAtBottomNow) && - !state.isLoading && - !state.isLoadingOlder && - !state.isLoadingNewer && + if ((messagesState.isAtBottom || isAtBottomNow) && + !messagesState.isLoading && + !messagesState.isLoadingOlder && + !messagesState.isLoadingNewer && !scrollState.isScrollInProgress ) { scrollState.scrollToChatBottomStaged( isComments = isComments, - animated = state.isChatAnimationsEnabled + animated = appearanceState.isChatAnimationsEnabled ) } } @@ -550,8 +548,8 @@ fun ChatContent( } } - LaunchedEffect(state.showBotCommands, isRecordingVideo) { - if (state.showBotCommands || isRecordingVideo) { + LaunchedEffect(chatUiState.showBotCommands, isRecordingVideo) { + if (chatUiState.showBotCommands || isRecordingVideo) { focusManager.clearFocus(force = true) keyboardController?.hide() } @@ -580,47 +578,47 @@ fun ChatContent( val contentAlpha by animateFloatAsState( - targetValue = if (isVisible || !state.isChatAnimationsEnabled || isOverlay) 1f else 0f, - animationSpec = if (state.isChatAnimationsEnabled && !isOverlay) tween(300) else snap(), + targetValue = if (isVisible || !appearanceState.isChatAnimationsEnabled || isOverlay) 1f else 0f, + animationSpec = if (appearanceState.isChatAnimationsEnabled && !isOverlay) tween(300) else snap(), label = "ContentAlpha" ) val contentOffset by animateDpAsState( - targetValue = if (isVisible || !state.isChatAnimationsEnabled || isOverlay) 0.dp else 20.dp, - animationSpec = if (state.isChatAnimationsEnabled && !isOverlay) tween(300) else snap(), + targetValue = if (isVisible || !appearanceState.isChatAnimationsEnabled || isOverlay) 0.dp else 20.dp, + animationSpec = if (appearanceState.isChatAnimationsEnabled && !isOverlay) tween(300) else snap(), label = "ContentOffset" ) val showInputBar by remember( - state.isMember, - state.isChannel, - state.isGroup, - state.canWrite, - state.currentTopicId, - state.selectedMessageIds, - state.viewAsTopics, + chatUiState.isMember, + chatUiState.isChannel, + chatUiState.isGroup, + chatUiState.canWrite, + chatUiState.currentTopicId, + selectionState.selectedMessageIds, + chatUiState.viewAsTopics, isRecordingVideo ) { derivedStateOf { - (state.isMember || !state.isChannel && !state.isGroup) && - (state.canWrite || state.currentTopicId != null) && + (chatUiState.isMember || !chatUiState.isChannel && !chatUiState.isGroup) && + (chatUiState.canWrite || chatUiState.currentTopicId != null) && !isRecordingVideo && - state.selectedMessageIds.isEmpty() && - (!state.viewAsTopics || state.currentTopicId != null) + selectionState.selectedMessageIds.isEmpty() && + (!chatUiState.viewAsTopics || chatUiState.currentTopicId != null) } } var containerSize by remember { mutableStateOf(IntSize.Zero) } - var renderPinnedMessagesList by rememberSaveable { mutableStateOf(state.showPinnedMessagesList) } + var renderPinnedMessagesList by rememberSaveable { mutableStateOf(pinnedState.showPinnedMessagesList) } var pendingPinnedSheetAction by remember { mutableStateOf<(() -> Unit)?>(null) } - LaunchedEffect(state.showPinnedMessagesList) { - if (state.showPinnedMessagesList) { + LaunchedEffect(pinnedState.showPinnedMessagesList) { + if (pinnedState.showPinnedMessagesList) { renderPinnedMessagesList = true } } val requestPinnedMessagesListDismiss = { - if (state.showPinnedMessagesList) { + if (pinnedState.showPinnedMessagesList) { component.onDismissPinnedMessages() } } @@ -629,105 +627,105 @@ fun ChatContent( editingPhotoPath, editingVideoPath, selectedMessageId, - state.selectedMessageIds, - state.currentTopicId, - state.showBotCommands, - state.restrictUserId, - state.showPinnedMessagesList, - state.fullScreenImages, - state.fullScreenVideoPath, - state.fullScreenVideoMessageId, - state.miniAppUrl, - state.webViewUrl, - state.instantViewUrl, - state.youtubeUrl + selectionState.selectedMessageIds, + chatUiState.currentTopicId, + chatUiState.showBotCommands, + chatUiState.restrictUserId, + pinnedState.showPinnedMessagesList, + mediaViewerState.fullScreenImages, + mediaViewerState.fullScreenVideoPath, + mediaViewerState.fullScreenVideoMessageId, + mediaViewerState.miniAppUrl, + mediaViewerState.webViewUrl, + mediaViewerState.instantViewUrl, + mediaViewerState.youtubeUrl ) { derivedStateOf { editingPhotoPath != null || editingVideoPath != null || selectedMessageId != null || - state.selectedMessageIds.isNotEmpty() || - state.currentTopicId != null || - state.showBotCommands || - state.restrictUserId != null || - state.showPinnedMessagesList || - state.fullScreenImages != null || - state.fullScreenVideoPath != null || - state.fullScreenVideoMessageId != null || - state.miniAppUrl != null || - state.webViewUrl != null || - state.instantViewUrl != null || - state.youtubeUrl != null + selectionState.selectedMessageIds.isNotEmpty() || + chatUiState.currentTopicId != null || + chatUiState.showBotCommands || + chatUiState.restrictUserId != null || + pinnedState.showPinnedMessagesList || + mediaViewerState.fullScreenImages != null || + mediaViewerState.fullScreenVideoPath != null || + mediaViewerState.fullScreenVideoMessageId != null || + mediaViewerState.miniAppUrl != null || + mediaViewerState.webViewUrl != null || + mediaViewerState.instantViewUrl != null || + mediaViewerState.youtubeUrl != null } } - val selectedCount = state.selectedMessageIds.size - val selectedMessageIdSet by remember(state.selectedMessageIds) { - derivedStateOf { state.selectedMessageIds.toHashSet() } + val selectedCount = selectionState.selectedMessageIds.size + val selectedMessageIdSet by remember(selectionState.selectedMessageIds) { + derivedStateOf { selectionState.selectedMessageIds.toHashSet() } } - val canRevokeSelected by remember(state.messages, selectedMessageIdSet) { + val canRevokeSelected by remember(messagesState.messages, selectedMessageIdSet) { derivedStateOf { if (selectedMessageIdSet.isEmpty()) { false } else { - state.messages.any { it.id in selectedMessageIdSet && it.canBeDeletedForAllUsers } + messagesState.messages.any { it.id in selectedMessageIdSet && it.canBeDeletedForAllUsers } } } } val topBarUiState = remember( - state.currentTopicId, - state.rootMessage, - state.isGroup, - state.isChannel, - state.isAdmin, - state.permissions, - state.otherUser, - state.currentUser, - state.typingAction, - state.memberCount, - state.onlineCount, - state.topics, - state.chatTitle, - state.chatAvatar, - state.chatPersonalAvatar, - state.chatEmojiStatus, - state.isOnline, - state.isVerified, - state.isSponsor, - state.isWhitelistedInAdBlock, - state.isInstalledFromGooglePlay, - state.isMuted, - state.isSearchActive, - state.searchQuery, - state.pinnedMessage, - state.pinnedMessageCount + chatUiState.currentTopicId, + chatUiState.rootMessage, + chatUiState.isGroup, + chatUiState.isChannel, + chatUiState.isAdmin, + chatUiState.permissions, + chatUiState.otherUser, + chatUiState.currentUser, + chatUiState.typingAction, + chatUiState.memberCount, + chatUiState.onlineCount, + chatUiState.topics, + chatUiState.chatTitle, + chatUiState.chatAvatar, + chatUiState.chatPersonalAvatar, + chatUiState.chatEmojiStatus, + chatUiState.isOnline, + chatUiState.isVerified, + chatUiState.isSponsor, + chatUiState.isWhitelistedInAdBlock, + chatUiState.isInstalledFromGooglePlay, + chatUiState.isMuted, + searchState.isSearchActive, + searchState.searchQuery, + pinnedState.pinnedMessage, + pinnedState.pinnedMessageCount ) { ChatContentTopBarUiState( - currentTopicId = state.currentTopicId, - rootMessage = state.rootMessage, - isGroup = state.isGroup, - isChannel = state.isChannel, - isAdmin = state.isAdmin, - permissions = state.permissions, - otherUser = state.otherUser, - currentUser = state.currentUser, - typingAction = state.typingAction, - memberCount = state.memberCount, - onlineCount = state.onlineCount, - topics = state.topics, - chatTitle = state.chatTitle, - chatAvatar = state.chatAvatar, - chatPersonalAvatar = state.chatPersonalAvatar, - chatEmojiStatus = state.chatEmojiStatus, - isOnline = state.isOnline, - isVerified = state.isVerified, - isSponsor = state.isSponsor, - isWhitelistedInAdBlock = state.isWhitelistedInAdBlock, - isInstalledFromGooglePlay = state.isInstalledFromGooglePlay, - isMuted = state.isMuted, - isSearchActive = state.isSearchActive, - searchQuery = state.searchQuery, - pinnedMessage = state.pinnedMessage, - pinnedMessageCount = state.pinnedMessageCount + currentTopicId = chatUiState.currentTopicId, + rootMessage = chatUiState.rootMessage, + isGroup = chatUiState.isGroup, + isChannel = chatUiState.isChannel, + isAdmin = chatUiState.isAdmin, + permissions = chatUiState.permissions, + otherUser = chatUiState.otherUser, + currentUser = chatUiState.currentUser, + typingAction = chatUiState.typingAction, + memberCount = chatUiState.memberCount, + onlineCount = chatUiState.onlineCount, + topics = chatUiState.topics, + chatTitle = chatUiState.chatTitle, + chatAvatar = chatUiState.chatAvatar, + chatPersonalAvatar = chatUiState.chatPersonalAvatar, + chatEmojiStatus = chatUiState.chatEmojiStatus, + isOnline = chatUiState.isOnline, + isVerified = chatUiState.isVerified, + isSponsor = chatUiState.isSponsor, + isWhitelistedInAdBlock = chatUiState.isWhitelistedInAdBlock, + isInstalledFromGooglePlay = chatUiState.isInstalledFromGooglePlay, + isMuted = chatUiState.isMuted, + isSearchActive = searchState.isSearchActive, + searchQuery = searchState.searchQuery, + pinnedMessage = pinnedState.pinnedMessage, + pinnedMessageCount = pinnedState.pinnedMessageCount ) } @@ -752,7 +750,7 @@ fun ChatContent( translationY = contentOffset.toPx() } ) { - ChatContentBackground(state = state) + ChatContentBackground(state = appearanceState) } if (isTablet) { @@ -783,7 +781,7 @@ fun ChatContent( contentAlpha = contentAlpha, onBack = { keyboardController?.hide() - if (state.currentTopicId != null) { + if (chatUiState.currentTopicId != null) { component.onTopicClick(0) } else { component.onBackClicked() @@ -799,36 +797,54 @@ fun ChatContent( }, bottomBar = { if (showInputBar) { - val inputBarState = - remember(state, pendingMediaPaths, pendingDocumentPaths) { + val inputReplyMarkup = remember(messagesState.messages) { + messagesState.messages.firstOrNull { it.replyMarkup is ReplyMarkupModel.ShowKeyboard }?.replyMarkup + } + val inputBarState = remember( + inputState, + pendingMediaPaths, + pendingDocumentPaths, + inputReplyMarkup, + chatUiState.topics, + chatUiState.currentTopicId, + chatUiState.permissions, + chatUiState.slowModeDelay, + chatUiState.slowModeDelayExpiresIn, + chatUiState.isCurrentUserRestricted, + chatUiState.restrictedUntilDate, + chatUiState.isAdmin, + chatUiState.isChannel, + chatUiState.currentUser, + chatUiState.isSecretChat + ) { ChatInputBarState( - replyMessage = state.replyMessage, - editingMessage = state.editingMessage, - draftText = state.draftText, + replyMessage = inputState.replyMessage, + editingMessage = inputState.editingMessage, + draftText = inputState.draftText, pendingMediaPaths = pendingMediaPaths, pendingDocumentPaths = pendingDocumentPaths, - isClosed = state.topics.find { it.id.toLong() == state.currentTopicId }?.isClosed + isClosed = chatUiState.topics.find { it.id.toLong() == chatUiState.currentTopicId }?.isClosed ?: false, - permissions = state.permissions, - slowModeDelay = state.slowModeDelay, - slowModeDelayExpiresIn = state.slowModeDelayExpiresIn, - isCurrentUserRestricted = state.isCurrentUserRestricted, - restrictedUntilDate = state.restrictedUntilDate, - isAdmin = state.isAdmin, - isChannel = state.isChannel, - isBot = state.isBot, - botCommands = state.botCommands, - botMenuButton = state.botMenuButton, - replyMarkup = state.messages.firstOrNull { it.replyMarkup is ReplyMarkupModel.ShowKeyboard }?.replyMarkup, - mentionSuggestions = state.mentionSuggestions, - inlineBotResults = state.inlineBotResults, - currentInlineBotUsername = state.currentInlineBotUsername, - currentInlineQuery = state.currentInlineQuery, - isInlineBotLoading = state.isInlineBotLoading, - attachBots = state.attachMenuBots, - scheduledMessages = state.scheduledMessages, - isPremiumUser = state.currentUser?.isPremium == true, - isSecretChat = state.isSecretChat + permissions = chatUiState.permissions, + slowModeDelay = chatUiState.slowModeDelay, + slowModeDelayExpiresIn = chatUiState.slowModeDelayExpiresIn, + isCurrentUserRestricted = chatUiState.isCurrentUserRestricted, + restrictedUntilDate = chatUiState.restrictedUntilDate, + isAdmin = chatUiState.isAdmin, + isChannel = chatUiState.isChannel, + isBot = inputState.isBot, + botCommands = inputState.botCommands, + botMenuButton = inputState.botMenuButton, + replyMarkup = inputReplyMarkup, + mentionSuggestions = inputState.mentionSuggestions, + inlineBotResults = inputState.inlineBotResults, + currentInlineBotUsername = inputState.currentInlineBotUsername, + currentInlineQuery = inputState.currentInlineQuery, + isInlineBotLoading = inputState.isInlineBotLoading, + attachBots = inputState.attachMenuBots, + scheduledMessages = inputState.scheduledMessages, + isPremiumUser = chatUiState.currentUser?.isPremium == true, + isSecretChat = chatUiState.isSecretChat ) } @@ -924,14 +940,14 @@ fun ChatContent( component.onReplyMarkupButtonClick( 0, it, - if (state.isBot) state.chatId else 0L + if (inputState.isBot) inputState.chatId else 0L ) }, onOpenMiniApp = { url, name -> component.onOpenMiniApp( url, name, - if (state.isBot) state.chatId else 0L + if (inputState.isBot) inputState.chatId else 0L ) }, onMentionQueryChange = { component.onMentionQueryChange(it) }, @@ -968,7 +984,7 @@ fun ChatContent( appPreferences = component.appPreferences, stickerRepository = component.stickerRepository ) - } else if (!state.isMember && (state.isChannel || state.isGroup)) { + } else if (!chatUiState.isMember && (chatUiState.isChannel || chatUiState.isGroup)) { Box( modifier = Modifier .fillMaxWidth() @@ -1196,7 +1212,10 @@ fun ChatContent( ChatContentList( showNavPadding = false, - state = state, + chatUiState = chatUiState, + appearanceState = appearanceState, + messagesState = messagesState, + selectionState = selectionState, component = component, scrollState = scrollState, groupedMessages = groupedMessages, @@ -1240,7 +1259,7 @@ fun ChatContent( } AnimatedVisibility( - visible = state.unreadCount > 0, + visible = chatUiState.unreadCount > 0, enter = scaleIn() + fadeIn(), exit = scaleOut() + fadeOut(), modifier = Modifier @@ -1253,7 +1272,7 @@ fun ChatContent( shadowElevation = 4.dp ) { AnimatedContent( - targetState = state.unreadCount, + targetState = chatUiState.unreadCount, transitionSpec = { if (targetState > initialState) { (slideInVertically { height -> height } + fadeIn()).togetherWith( @@ -1310,8 +1329,8 @@ fun ChatContent( .background(MaterialTheme.colorScheme.surface) ) { MessageListShimmer( - isGroup = state.isGroup, - isChannel = state.isChannel + isGroup = chatUiState.isGroup, + isChannel = chatUiState.isChannel ) } } @@ -1324,22 +1343,22 @@ fun ChatContent( // Modals & Overlays if (renderPinnedMessagesList) { PinnedMessagesListSheet( - isVisible = state.showPinnedMessagesList, - allPinnedMessages = state.allPinnedMessages, - pinnedMessageCount = state.pinnedMessageCount, - isLoadingPinnedMessages = state.isLoadingPinnedMessages, - isGroup = state.isGroup, - isChannel = state.isChannel, - fontSize = state.fontSize, - letterSpacing = state.letterSpacing, - bubbleRadius = state.bubbleRadius, - stickerSize = state.stickerSize, - autoDownloadMobile = state.autoDownloadMobile, - autoDownloadWifi = state.autoDownloadWifi, - autoDownloadRoaming = state.autoDownloadRoaming, - autoDownloadFiles = state.autoDownloadFiles, - autoplayGifs = state.autoplayGifs, - autoplayVideos = state.autoplayVideos, + isVisible = pinnedState.showPinnedMessagesList, + allPinnedMessages = pinnedState.allPinnedMessages, + pinnedMessageCount = pinnedState.pinnedMessageCount, + isLoadingPinnedMessages = pinnedState.isLoadingPinnedMessages, + isGroup = chatUiState.isGroup, + isChannel = chatUiState.isChannel, + fontSize = appearanceState.fontSize, + letterSpacing = appearanceState.letterSpacing, + bubbleRadius = appearanceState.bubbleRadius, + stickerSize = appearanceState.stickerSize, + autoDownloadMobile = appearanceState.autoDownloadMobile, + autoDownloadWifi = appearanceState.autoDownloadWifi, + autoDownloadRoaming = appearanceState.autoDownloadRoaming, + autoDownloadFiles = appearanceState.autoDownloadFiles, + autoplayGifs = appearanceState.autoplayGifs, + autoplayVideos = appearanceState.autoplayVideos, onDismissRequest = requestPinnedMessagesListDismiss, onHidden = { renderPinnedMessagesList = false @@ -1361,7 +1380,7 @@ fun ChatContent( ) } - state.selectedStickerSet?.let { stickerSet -> + inputState.selectedStickerSet?.let { stickerSet -> StickerSetSheet( stickerSet = stickerSet, onDismiss = { component.onDismissStickerSet() }, @@ -1369,10 +1388,10 @@ fun ChatContent( ) } - if (state.showPollVoters) { + if (chatUiState.showPollVoters) { PollVotersSheet( - voters = state.pollVoters, - isLoading = state.isPollVotersLoading, + voters = chatUiState.pollVoters, + isLoading = chatUiState.isPollVotersLoading, onUserClick = { component.onDismissVoters() component.toProfile(it) @@ -1381,9 +1400,9 @@ fun ChatContent( ) } - if (state.showBotCommands) { + if (chatUiState.showBotCommands) { BotCommandsSheet( - commands = state.botCommands, + commands = inputState.botCommands, onCommandClick = { component.onBotCommandClick(it) }, onDismiss = { component.onDismissBotCommands() } ) @@ -1397,7 +1416,9 @@ fun ChatContent( selectedMessage?.let { msg -> ChatMessageOptionsMenu( - state = state, + chatUiState = chatUiState, + appearanceState = appearanceState, + pinnedState = pinnedState, component = component, selectedMessage = msg, menuOffset = menuOffset, @@ -1443,14 +1464,14 @@ fun ChatContent( ) } - if (state.showReportDialog) { + if (chatUiState.showReportDialog) { ReportChatDialog( onDismiss = { component.onDismissReportDialog() }, onReasonSelected = { component.onReportReasonSelected(it) } ) } - if (state.restrictUserId != null) { + if (chatUiState.restrictUserId != null) { RestrictUserSheet( onDismiss = { component.onDismissRestrictDialog() }, onConfirm = { permissions, untilDate -> component.onConfirmRestrict(permissions, untilDate) } @@ -1504,18 +1525,18 @@ fun ChatContent( BackHandler(enabled = isCustomBackHandlingEnabled) { if (editingPhotoPath != null) editingPhotoPath = null else if (editingVideoPath != null) editingVideoPath = null - else if (state.selectedMessageIds.isNotEmpty()) component.onClearSelection() + else if (selectionState.selectedMessageIds.isNotEmpty()) component.onClearSelection() else if (selectedMessageId != null) selectedMessageId = null - else if (state.showBotCommands) component.onDismissBotCommands() - else if (state.restrictUserId != null) component.onDismissRestrictDialog() - else if (state.showPinnedMessagesList && !isAnyViewerOpen) requestPinnedMessagesListDismiss() - else if (state.fullScreenImages != null) component.onDismissImages() - else if (state.fullScreenVideoPath != null || state.fullScreenVideoMessageId != null) component.onDismissVideo() - else if (state.instantViewUrl != null) component.onDismissInstantView() - else if (state.youtubeUrl != null) component.onDismissYouTube() - else if (state.miniAppUrl != null) component.onDismissMiniApp() - else if (state.webViewUrl != null) component.onDismissWebView() - else if (state.currentTopicId != null) component.onTopicClick(0) + else if (chatUiState.showBotCommands) component.onDismissBotCommands() + else if (chatUiState.restrictUserId != null) component.onDismissRestrictDialog() + else if (pinnedState.showPinnedMessagesList && !isAnyViewerOpen) requestPinnedMessagesListDismiss() + else if (mediaViewerState.fullScreenImages != null) component.onDismissImages() + else if (mediaViewerState.fullScreenVideoPath != null || mediaViewerState.fullScreenVideoMessageId != null) component.onDismissVideo() + else if (mediaViewerState.instantViewUrl != null) component.onDismissInstantView() + else if (mediaViewerState.youtubeUrl != null) component.onDismissYouTube() + else if (mediaViewerState.miniAppUrl != null) component.onDismissMiniApp() + else if (mediaViewerState.webViewUrl != null) component.onDismissWebView() + else if (chatUiState.currentTopicId != null) component.onTopicClick(0) } } } diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/DefaultChatComponent.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/DefaultChatComponent.kt index b7391827..78754df7 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/DefaultChatComponent.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/DefaultChatComponent.kt @@ -155,6 +155,173 @@ class DefaultChatComponent( lastReadInboxMessageId = 0L ) ) + private val _messagesState = MutableStateFlow( + _state.value.run { + toMessagesState() + } + ) + private val _chatUiState = MutableStateFlow(_state.value.toChatUiState()) + private val _selectionState = MutableStateFlow(_state.value.toSelectionState()) + private val _searchState = MutableStateFlow(_state.value.toSearchState()) + private val _appearanceState = MutableStateFlow(_state.value.toAppearanceState()) + private val _inputState = MutableStateFlow( + _state.value.run { + toInputState() + } + ) + private val _pinnedState = MutableStateFlow( + _state.value.run { + toPinnedState() + } + ) + private val _mediaViewerState = MutableStateFlow( + _state.value.run { + toMediaViewerState() + } + ) + + private fun ChatComponent.State.toMessagesState() = ChatComponent.MessagesState( + chatId = chatId, + messages = messages, + isLoading = isLoading, + isLoadingOlder = isLoadingOlder, + isLoadingNewer = isLoadingNewer, + scrollToMessageId = scrollToMessageId, + pendingScrollCommand = pendingScrollCommand, + highlightedMessageId = highlightedMessageId, + isAtBottom = isAtBottom, + currentScrollMessageId = currentScrollMessageId, + lastScrollPosition = lastScrollPosition, + lastSavedViewport = lastSavedViewport, + isLatestLoaded = isLatestLoaded, + isOldestLoaded = isOldestLoaded, + lastReadInboxMessageId = lastReadInboxMessageId + ) + + private fun ChatComponent.State.toChatUiState() = ChatComponent.ChatUiState( + chatId = chatId, + chatTitle = chatTitle, + chatAvatar = chatAvatar, + chatPersonalAvatar = chatPersonalAvatar, + chatEmojiStatus = chatEmojiStatus, + isGroup = isGroup, + isChannel = isChannel, + isSecretChat = isSecretChat, + isOnline = isOnline, + isVerified = isVerified, + isSponsor = isSponsor, + canWrite = canWrite, + isAdmin = isAdmin, + permissions = permissions, + slowModeDelay = slowModeDelay, + slowModeDelayExpiresIn = slowModeDelayExpiresIn, + isCurrentUserRestricted = isCurrentUserRestricted, + restrictedUntilDate = restrictedUntilDate, + memberCount = memberCount, + onlineCount = onlineCount, + unreadCount = unreadCount, + unreadMentionCount = unreadMentionCount, + unreadReactionCount = unreadReactionCount, + userStatus = userStatus, + typingAction = typingAction, + pollVoters = pollVoters, + showPollVoters = showPollVoters, + isPollVotersLoading = isPollVotersLoading, + viewAsTopics = viewAsTopics, + topics = topics, + currentTopicId = currentTopicId, + rootMessage = rootMessage, + isLoadingTopics = isLoadingTopics, + isWhitelistedInAdBlock = isWhitelistedInAdBlock, + isMuted = isMuted, + showReportDialog = showReportDialog, + showBotCommands = showBotCommands, + currentUser = currentUser, + otherUser = otherUser, + isMember = isMember, + restrictUserId = restrictUserId, + isInstalledFromGooglePlay = isInstalledFromGooglePlay + ) + + private fun ChatComponent.State.toSelectionState() = ChatComponent.MessageSelectionState( + selectedMessageIds = selectedMessageIds + ) + + private fun ChatComponent.State.toSearchState() = ChatComponent.SearchState( + isSearchActive = isSearchActive, + searchQuery = searchQuery + ) + + private fun ChatComponent.State.toAppearanceState() = ChatComponent.AppearanceState( + fontSize = fontSize, + letterSpacing = letterSpacing, + bubbleRadius = bubbleRadius, + stickerSize = stickerSize, + wallpaper = wallpaper, + wallpaperModel = wallpaperModel, + isWallpaperBlurred = isWallpaperBlurred, + wallpaperBlurIntensity = wallpaperBlurIntensity, + isWallpaperMoving = isWallpaperMoving, + wallpaperDimming = wallpaperDimming, + isWallpaperGrayscale = isWallpaperGrayscale, + autoDownloadMobile = autoDownloadMobile, + autoDownloadWifi = autoDownloadWifi, + autoDownloadRoaming = autoDownloadRoaming, + autoDownloadFiles = autoDownloadFiles, + autoplayGifs = autoplayGifs, + autoplayVideos = autoplayVideos, + showLinkPreviews = showLinkPreviews, + isChatAnimationsEnabled = isChatAnimationsEnabled + ) + + private fun ChatComponent.State.toInputState() = ChatComponent.InputState( + chatId = chatId, + replyMessage = replyMessage, + editingMessage = editingMessage, + draftText = draftText, + selectedStickerSet = selectedStickerSet, + isBot = isBot, + botCommands = botCommands, + botMenuButton = botMenuButton, + mentionSuggestions = mentionSuggestions, + inlineBotResults = inlineBotResults, + currentInlineBotUsername = currentInlineBotUsername, + currentInlineQuery = currentInlineQuery, + isInlineBotLoading = isInlineBotLoading, + attachMenuBots = attachMenuBots, + scheduledMessages = scheduledMessages + ) + + private fun ChatComponent.State.toPinnedState() = ChatComponent.PinnedState( + pinnedMessage = pinnedMessage, + allPinnedMessages = allPinnedMessages, + showPinnedMessagesList = showPinnedMessagesList, + isLoadingPinnedMessages = isLoadingPinnedMessages, + pinnedMessageCount = pinnedMessageCount, + pinnedMessageIndex = pinnedMessageIndex + ) + + private fun ChatComponent.State.toMediaViewerState() = ChatComponent.MediaViewerState( + instantViewUrl = instantViewUrl, + youtubeUrl = youtubeUrl, + miniAppUrl = miniAppUrl, + miniAppName = miniAppName, + miniAppBotUserId = miniAppBotUserId, + showMiniAppTOS = showMiniAppTOS, + miniAppTOSBotUserId = miniAppTOSBotUserId, + miniAppTOSUrl = miniAppTOSUrl, + miniAppTOSName = miniAppTOSName, + webViewUrl = webViewUrl, + fullScreenImages = fullScreenImages, + fullScreenImageMessageIds = fullScreenImageMessageIds, + fullScreenCaptions = fullScreenCaptions, + fullScreenStartIndex = fullScreenStartIndex, + fullScreenVideoMessageId = fullScreenVideoMessageId, + fullScreenVideoPath = fullScreenVideoPath, + fullScreenVideoCaption = fullScreenVideoCaption, + invoiceSlug = invoiceSlug, + invoiceMessageId = invoiceMessageId + ) private val store = ChatStoreFactory( storeFactory = DefaultStoreFactory(), @@ -162,6 +329,14 @@ class DefaultChatComponent( ).create() override val state: StateFlow = store.stateFlow + override val chatUiState: StateFlow = _chatUiState + override val selectionState: StateFlow = _selectionState + override val searchState: StateFlow = _searchState + override val appearanceState: StateFlow = _appearanceState + override val messagesState: StateFlow = _messagesState + override val inputState: StateFlow = _inputState + override val pinnedState: StateFlow = _pinnedState + override val mediaViewerState: StateFlow = _mediaViewerState private var availableWallpapers: List = emptyList() internal var allMembers: List = emptyList() @@ -236,6 +411,14 @@ class DefaultChatComponent( } _state.onEach { + _messagesState.value = it.toMessagesState() + _chatUiState.value = it.toChatUiState() + _selectionState.value = it.toSelectionState() + _searchState.value = it.toSearchState() + _appearanceState.value = it.toAppearanceState() + _inputState.value = it.toInputState() + _pinnedState.value = it.toPinnedState() + _mediaViewerState.value = it.toMediaViewerState() store.accept(ChatStore.Intent.UpdateState(it)) }.launchIn(scope) } diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentBackground.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentBackground.kt index d576b029..02bafaa9 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentBackground.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentBackground.kt @@ -17,7 +17,7 @@ import java.io.File @Composable fun ChatContentBackground( - state: ChatComponent.State, + state: ChatComponent.AppearanceState, modifier: Modifier = Modifier ) { val wallpaper = state.wallpaperModel diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentList.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentList.kt index 8372cfe1..fd3e675a 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentList.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentList.kt @@ -92,7 +92,10 @@ import kotlin.compareTo @OptIn(ExperimentalFoundationApi::class) @Composable fun ChatContentList( - state: ChatComponent.State, + chatUiState: ChatComponent.ChatUiState, + appearanceState: ChatComponent.AppearanceState, + messagesState: ChatComponent.MessagesState, + selectionState: ChatComponent.MessageSelectionState, component: ChatComponent, scrollState: LazyListState, groupedMessages: List, @@ -112,25 +115,26 @@ fun ChatContentList( downloadUtils: IDownloadUtils, isAnyViewerOpen: Boolean = false ) { - val isComments = state.rootMessage != null + val isComments = chatUiState.rootMessage != null val isScrolling by remember(scrollState) { derivedStateOf { scrollState.isScrollInProgress } } - val latestState by rememberUpdatedState(state) + val latestMessagesState by rememberUpdatedState(messagesState) + val latestChatUiState by rememberUpdatedState(chatUiState) var lastOlderLoadTriggerUptimeMs by remember { mutableLongStateOf(0L) } var lastNewerLoadTriggerUptimeMs by remember { mutableLongStateOf(0L) } val loadTriggerThrottleMs = 350L val unreadBoundaryIndex = remember( isComments, groupedMessages, - state.messages, - state.lastReadInboxMessageId + messagesState.messages, + messagesState.lastReadInboxMessageId ) { if (isComments) { null // suppress in thread/comments mode } else { val boundaryItem = findFirstUnreadBoundary( - messages = state.messages, + messages = messagesState.messages, groupedItems = groupedMessages, - firstUnreadMessageId = state.lastReadInboxMessageId + firstUnreadMessageId = messagesState.lastReadInboxMessageId ) boundaryItem?.let { target -> groupedMessages.indexOfFirst { it.firstMessageId == target.firstMessageId } @@ -154,8 +158,9 @@ fun ChatContentList( } .distinctUntilChanged() .collect { (firstVisibleIndex, lastVisibleIndex) -> - val currentState = latestState - if (currentState.isLoading || currentState.isLoadingOlder || currentState.isLoadingNewer) return@collect + val currentMessagesState = latestMessagesState + val currentChatUiState = latestChatUiState + if (currentMessagesState.isLoading || currentMessagesState.isLoadingOlder || currentMessagesState.isLoadingNewer) return@collect val nearStart = firstVisibleIndex <= 2 val nearEnd = lastVisibleIndex >= (groupedMessages.size - 3).coerceAtLeast(0) @@ -164,24 +169,24 @@ fun ChatContentList( if (isComments) { if (!scrollState.isScrollInProgress) return@collect - if (nearStart && !currentState.isOldestLoaded) { + if (nearStart && !currentMessagesState.isOldestLoaded) { if (now - lastOlderLoadTriggerUptimeMs >= loadTriggerThrottleMs) { lastOlderLoadTriggerUptimeMs = now component.loadMore() } - } else if (nearEnd && !currentState.isLatestLoaded) { + } else if (nearEnd && !currentMessagesState.isLatestLoaded) { if (now - lastNewerLoadTriggerUptimeMs >= loadTriggerThrottleMs) { lastNewerLoadTriggerUptimeMs = now component.loadNewer() } } } else { - if (nearEnd && !currentState.isOldestLoaded) { + if (nearEnd && !currentMessagesState.isOldestLoaded) { if (now - lastOlderLoadTriggerUptimeMs >= loadTriggerThrottleMs) { lastOlderLoadTriggerUptimeMs = now component.loadMore() } - } else if (nearStart && !currentState.isAtBottom && !currentState.isLatestLoaded) { + } else if (nearStart && !currentMessagesState.isAtBottom && !currentMessagesState.isLatestLoaded) { if (now - lastNewerLoadTriggerUptimeMs >= loadTriggerThrottleMs) { lastNewerLoadTriggerUptimeMs = now component.loadNewer() @@ -191,9 +196,9 @@ fun ChatContentList( } } - if (state.viewAsTopics && state.currentTopicId == null) { + if (chatUiState.viewAsTopics && chatUiState.currentTopicId == null) { TopicsList( - topics = state.topics, + topics = chatUiState.topics, onTopicClick = { component.onTopicClick(it.id) }, modifier = modifier ) @@ -208,13 +213,13 @@ fun ChatContentList( reverseLayout = !isComments, contentPadding = PaddingValues(vertical = 8.dp) ) { - if (isComments && state.isLoadingOlder && groupedMessages.isNotEmpty()) { + if (isComments && messagesState.isLoadingOlder && groupedMessages.isNotEmpty()) { item(key = "loading_older_top") { PagingLoadingIndicator() } } - if (!isComments && state.isLoadingNewer && !state.isAtBottom && groupedMessages.isNotEmpty()) { + if (!isComments && messagesState.isLoadingNewer && !messagesState.isAtBottom && groupedMessages.isNotEmpty()) { item(key = "loading_newer_bottom") { PagingLoadingIndicator() } @@ -223,18 +228,19 @@ fun ChatContentList( if (isComments) { item(key = "root_header") { RootMessageSection( - state, - component, - onPhotoClick, - onPhotoDownload, - onVideoClick, - onDocumentClick, - onAudioClick, - onMessageOptionsClick, - onGoToReply, - onViaBotClick, - toProfile, - downloadUtils, + chatUiState = chatUiState, + appearanceState = appearanceState, + component = component, + onPhotoClick = onPhotoClick, + onPhotoDownload = onPhotoDownload, + onVideoClick = onVideoClick, + onDocumentClick = onDocumentClick, + onAudioClick = onAudioClick, + onMessageOptionsClick = onMessageOptionsClick, + onGoToReply = onGoToReply, + onViaBotClick = onViaBotClick, + toProfile = toProfile, + downloadUtils = downloadUtils, isAnyViewerOpen = isAnyViewerOpen ) } @@ -259,12 +265,14 @@ fun ChatContentList( MessageRowItem( item = item, - state = state, + chatUiState = chatUiState, + appearanceState = appearanceState, + messagesState = messagesState, component = component, olderMsg = olderMsg, newerMsg = newerMsg, - isSelected = isItemSelected(item, state.selectedMessageIds), - isSelectionMode = state.selectedMessageIds.isNotEmpty(), + isSelected = isItemSelected(item, selectionState.selectedMessageIds), + isSelectionMode = selectionState.selectedMessageIds.isNotEmpty(), selectedMessageId = selectedMessageId, onPhotoClick = onPhotoClick, onPhotoDownload = onPhotoDownload, @@ -280,7 +288,7 @@ fun ChatContentList( downloadUtils = downloadUtils, isAnyViewerOpen = isAnyViewerOpen, showUnreadSeparator = index == unreadBoundaryIndex, - unreadCount = state.unreadCount + unreadCount = chatUiState.unreadCount ) } } else { @@ -315,12 +323,14 @@ fun ChatContentList( MessageRowItem( item = item, - state = state, + chatUiState = chatUiState, + appearanceState = appearanceState, + messagesState = messagesState, component = component, olderMsg = olderMsg, newerMsg = newerMsg, - isSelected = isItemSelected(item, state.selectedMessageIds), - isSelectionMode = state.selectedMessageIds.isNotEmpty(), + isSelected = isItemSelected(item, selectionState.selectedMessageIds), + isSelectionMode = selectionState.selectedMessageIds.isNotEmpty(), selectedMessageId = selectedMessageId, onPhotoClick = onPhotoClick, onPhotoDownload = onPhotoDownload, @@ -336,24 +346,24 @@ fun ChatContentList( downloadUtils = downloadUtils, isAnyViewerOpen = isAnyViewerOpen, showUnreadSeparator = index == unreadBoundaryIndex, - unreadCount = state.unreadCount + unreadCount = chatUiState.unreadCount ) } } - if (isComments && state.isLoadingNewer && groupedMessages.isNotEmpty()) { + if (isComments && messagesState.isLoadingNewer && groupedMessages.isNotEmpty()) { item(key = "loading_newer_bottom") { PagingLoadingIndicator() } } - if (!isComments && state.isLoadingOlder && groupedMessages.isNotEmpty()) { + if (!isComments && messagesState.isLoadingOlder && groupedMessages.isNotEmpty()) { item(key = "loading_older_top") { PagingLoadingIndicator() } } - if (state.isLoading && groupedMessages.isNotEmpty() && !state.isLoadingOlder && !state.isLoadingNewer) { + if (messagesState.isLoading && groupedMessages.isNotEmpty() && !messagesState.isLoadingOlder && !messagesState.isLoadingNewer) { item(key = "loading_indicator") { PagingLoadingIndicator() } @@ -396,7 +406,9 @@ private fun PagingLoadingIndicator() { @Composable private fun MessageRowItem( item: GroupedMessageItem, - state: ChatComponent.State, + chatUiState: ChatComponent.ChatUiState, + appearanceState: ChatComponent.AppearanceState, + messagesState: ChatComponent.MessagesState, component: ChatComponent, olderMsg: MessageModel?, newerMsg: MessageModel?, @@ -423,7 +435,7 @@ private fun MessageRowItem( if (item is GroupedMessageItem.Single) item.message else (item as GroupedMessageItem.Album).messages.last() } - val shouldAnimateEntry = state.isChatAnimationsEnabled && !isScrolling + val shouldAnimateEntry = appearanceState.isChatAnimationsEnabled && !isScrolling val scale = remember(mainMsg.id) { Animatable( @@ -504,7 +516,9 @@ private fun MessageRowItem( MessageBubbleSwitcher( item = item, - state = state, + chatUiState = chatUiState, + appearanceState = appearanceState, + messagesState = messagesState, component = component, olderMsg = olderMsg, newerMsg = newerMsg, @@ -531,7 +545,9 @@ private fun MessageRowItem( @Composable private fun MessageBubbleSwitcher( item: GroupedMessageItem, - state: ChatComponent.State, + chatUiState: ChatComponent.ChatUiState, + appearanceState: ChatComponent.AppearanceState, + messagesState: ChatComponent.MessagesState, component: ChatComponent, olderMsg: MessageModel?, newerMsg: MessageModel?, @@ -550,8 +566,8 @@ private fun MessageBubbleSwitcher( downloadUtils: IDownloadUtils, isAnyViewerOpen: Boolean = false ) { - val isChannel = state.isChannel && state.currentTopicId == null - val isTopicClosed = state.topics.find { it.id.toLong() == state.currentTopicId }?.isClosed?: false + val isChannel = chatUiState.isChannel && chatUiState.currentTopicId == null + val isTopicClosed = chatUiState.topics.find { it.id.toLong() == chatUiState.currentTopicId }?.isClosed ?: false when (item) { is GroupedMessageItem.Single -> { @@ -562,10 +578,10 @@ private fun MessageBubbleSwitcher( msg = item.message, olderMsg = olderMsg, newerMsg = newerMsg, - autoplayGifs = state.autoplayGifs, - autoplayVideos = state.autoplayVideos, - autoDownloadFiles = state.autoDownloadFiles, - highlighted = state.highlightedMessageId == item.message.id, + autoplayGifs = appearanceState.autoplayGifs, + autoplayVideos = appearanceState.autoplayVideos, + autoDownloadFiles = appearanceState.autoDownloadFiles, + highlighted = messagesState.highlightedMessageId == item.message.id, onHighlightConsumed = { component.onHighlightConsumed() }, onPhotoClick = { if (isSelectionMode) component.onToggleMessageSelection(it.id) else handlePhotoClick( @@ -640,16 +656,16 @@ private fun MessageBubbleSwitcher( it ) }, - fontSize = state.fontSize, - letterSpacing = state.letterSpacing, - bubbleRadius = state.bubbleRadius, - stickerSize = state.stickerSize, + fontSize = appearanceState.fontSize, + letterSpacing = appearanceState.letterSpacing, + bubbleRadius = appearanceState.bubbleRadius, + stickerSize = appearanceState.stickerSize, shouldReportPosition = item.message.id == selectedMessageId, onPositionChange = { _, pos, size -> onMessagePositionChange(pos, size) }, onCommentsClick = { component.onCommentsClick(it) }, toProfile = toProfile, onViaBotClick = onViaBotClick, - canReply = state.canWrite && !isSelectionMode, + canReply = chatUiState.canWrite && !isSelectionMode, onReplySwipe = { component.onReplyMessage(it) }, onYouTubeClick = { component.onOpenYouTube(it) }, onInstantViewClick = { component.onOpenInstantView(it) }, @@ -661,19 +677,19 @@ private fun MessageBubbleSwitcher( msg = item.message, olderMsg = olderMsg, newerMsg = newerMsg, - isGroup = state.isGroup || state.currentTopicId != null, - fontSize = state.fontSize, - letterSpacing = state.letterSpacing, - bubbleRadius = state.bubbleRadius, - stSize = state.stickerSize, - autoDownloadMobile = state.autoDownloadMobile, - autoDownloadWifi = state.autoDownloadWifi, - autoDownloadRoaming = state.autoDownloadRoaming, - autoDownloadFiles = state.autoDownloadFiles, - autoplayGifs = state.autoplayGifs, - autoplayVideos = state.autoplayVideos, - showLinkPreviews = state.showLinkPreviews, - highlighted = state.highlightedMessageId == item.message.id, + isGroup = chatUiState.isGroup || chatUiState.currentTopicId != null, + fontSize = appearanceState.fontSize, + letterSpacing = appearanceState.letterSpacing, + bubbleRadius = appearanceState.bubbleRadius, + stSize = appearanceState.stickerSize, + autoDownloadMobile = appearanceState.autoDownloadMobile, + autoDownloadWifi = appearanceState.autoDownloadWifi, + autoDownloadRoaming = appearanceState.autoDownloadRoaming, + autoDownloadFiles = appearanceState.autoDownloadFiles, + autoplayGifs = appearanceState.autoplayGifs, + autoplayVideos = appearanceState.autoplayVideos, + showLinkPreviews = appearanceState.showLinkPreviews, + highlighted = messagesState.highlightedMessageId == item.message.id, onHighlightConsumed = { component.onHighlightConsumed() }, onPhotoClick = { if (isSelectionMode) component.onToggleMessageSelection(it.id) else handlePhotoClick( @@ -754,7 +770,7 @@ private fun MessageBubbleSwitcher( onPositionChange = { _, pos, size -> onMessagePositionChange(pos, size) }, toProfile = toProfile, onViaBotClick = onViaBotClick, - canReply = state.canWrite && !isSelectionMode && (!isTopicClosed || state.isAdmin), + canReply = chatUiState.canWrite && !isSelectionMode && (!isTopicClosed || chatUiState.isAdmin), onReplySwipe = { component.onReplyMessage(it) }, swipeEnabled = !isSelectionMode, downloadUtils = downloadUtils, @@ -768,13 +784,13 @@ private fun MessageBubbleSwitcher( messages = item.messages, olderMsg = olderMsg, newerMsg = newerMsg, - isGroup = state.isGroup || state.currentTopicId != null, + isGroup = chatUiState.isGroup || chatUiState.currentTopicId != null, isChannel = isChannel, - autoplayGifs = state.autoplayGifs, - autoplayVideos = state.autoplayVideos, - autoDownloadMobile = state.autoDownloadMobile, - autoDownloadWifi = state.autoDownloadWifi, - autoDownloadRoaming = state.autoDownloadRoaming, + autoplayGifs = appearanceState.autoplayGifs, + autoplayVideos = appearanceState.autoplayVideos, + autoDownloadMobile = appearanceState.autoDownloadMobile, + autoDownloadWifi = appearanceState.autoDownloadWifi, + autoDownloadRoaming = appearanceState.autoDownloadRoaming, onPhotoClick = { if (isSelectionMode) component.onToggleMessageSelection(it.id) else handleAlbumPhotoClick( it, @@ -822,7 +838,7 @@ private fun MessageBubbleSwitcher( onCommentsClick = { component.onCommentsClick(it) }, toProfile = toProfile, onViaBotClick = onViaBotClick, - canReply = state.canWrite && !isSelectionMode && (!isTopicClosed || state.isAdmin), + canReply = chatUiState.canWrite && !isSelectionMode && (!isTopicClosed || chatUiState.isAdmin), onReplySwipe = { component.onReplyMessage(it) }, swipeEnabled = !isSelectionMode, downloadUtils = downloadUtils, @@ -857,7 +873,8 @@ private fun SelectionIndicator(isSelected: Boolean, modifier: Modifier = Modifie @Composable private fun RootMessageSection( - state: ChatComponent.State, + chatUiState: ChatComponent.ChatUiState, + appearanceState: ChatComponent.AppearanceState, component: ChatComponent, onPhotoClick: (MessageModel, List, List, List, Int) -> Unit, onPhotoDownload: (Int) -> Unit, @@ -871,17 +888,17 @@ private fun RootMessageSection( downloadUtils: IDownloadUtils, isAnyViewerOpen: Boolean = false ) { - val root = state.rootMessage ?: return + val root = chatUiState.rootMessage ?: return Column( modifier = Modifier .fillMaxWidth() .padding(bottom = 4.dp) ) { - if (state.isChannel) { + if (chatUiState.isChannel) { ChannelMessageBubbleContainer( msg = root, olderMsg = null, newerMsg = null, - autoplayGifs = state.autoplayGifs, autoplayVideos = state.autoplayVideos, - autoDownloadFiles = state.autoDownloadFiles, + autoplayGifs = appearanceState.autoplayGifs, autoplayVideos = appearanceState.autoplayVideos, + autoDownloadFiles = appearanceState.autoDownloadFiles, onPhotoClick = { handlePhotoClick(it, onPhotoClick) }, onDownloadPhoto = onPhotoDownload, onVideoClick = { handleVideoClick(it, onVideoClick) }, @@ -897,10 +914,10 @@ private fun RootMessageSection( onRetractVote = { component.onRetractVote(it) }, onShowVoters = { id, opt -> component.onShowVoters(id, opt) }, onClosePoll = { component.onClosePoll(it) }, - fontSize = state.fontSize, - letterSpacing = state.letterSpacing, - bubbleRadius = state.bubbleRadius, - stickerSize = state.stickerSize, + fontSize = appearanceState.fontSize, + letterSpacing = appearanceState.letterSpacing, + bubbleRadius = appearanceState.bubbleRadius, + stickerSize = appearanceState.stickerSize, onCommentsClick = {}, showComments = false, toProfile = toProfile, onViaBotClick = onViaBotClick, @@ -911,14 +928,14 @@ private fun RootMessageSection( ) } else { MessageBubbleContainer( - msg = root, olderMsg = null, newerMsg = null, isGroup = state.isGroup, - fontSize = state.fontSize, - letterSpacing = state.letterSpacing, - bubbleRadius = state.bubbleRadius, - stSize = state.stickerSize, - autoDownloadMobile = state.autoDownloadMobile, autoDownloadWifi = state.autoDownloadWifi, - autoDownloadRoaming = state.autoDownloadRoaming, autoDownloadFiles = state.autoDownloadFiles, - autoplayGifs = state.autoplayGifs, autoplayVideos = state.autoplayVideos, + msg = root, olderMsg = null, newerMsg = null, isGroup = chatUiState.isGroup, + fontSize = appearanceState.fontSize, + letterSpacing = appearanceState.letterSpacing, + bubbleRadius = appearanceState.bubbleRadius, + stSize = appearanceState.stickerSize, + autoDownloadMobile = appearanceState.autoDownloadMobile, autoDownloadWifi = appearanceState.autoDownloadWifi, + autoDownloadRoaming = appearanceState.autoDownloadRoaming, autoDownloadFiles = appearanceState.autoDownloadFiles, + autoplayGifs = appearanceState.autoplayGifs, autoplayVideos = appearanceState.autoplayVideos, onPhotoClick = { handlePhotoClick(it, onPhotoClick) }, onDownloadPhoto = onPhotoDownload, onVideoClick = { handleVideoClick(it, onVideoClick) }, @@ -1272,4 +1289,3 @@ fun TopicItem( } } } - diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatMessageOptionsMenu.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatMessageOptionsMenu.kt index a19d3279..0e1344af 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatMessageOptionsMenu.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatMessageOptionsMenu.kt @@ -36,7 +36,9 @@ import java.util.Locale @Composable fun ChatMessageOptionsMenu( - state: ChatComponent.State, + chatUiState: ChatComponent.ChatUiState, + appearanceState: ChatComponent.AppearanceState, + pinnedState: ChatComponent.PinnedState, component: ChatComponent, selectedMessage: MessageModel, menuOffset: Offset, @@ -54,14 +56,14 @@ fun ChatMessageOptionsMenu( ) { val nativeClipboard = localClipboard.nativeClipboard val messageRepository: MessageAiRepository = koinInject() - val canCheckViewersList = remember(state.isChannel, state.isGroup, state.memberCount) { - !state.isChannel && (!state.isGroup || state.memberCount in 1 until 100) + val canCheckViewersList = remember(chatUiState.isChannel, chatUiState.isGroup, chatUiState.memberCount) { + !chatUiState.isChannel && (!chatUiState.isGroup || chatUiState.memberCount in 1 until 100) } val messageViewsCount = remember(selectedMessage.viewCount, selectedMessage.views) { selectedMessage.viewCount ?: selectedMessage.views } - val shouldShowViewsInfo = remember(state.isChannel, messageViewsCount) { - state.isChannel && (messageViewsCount ?: 0) > 0 + val shouldShowViewsInfo = remember(chatUiState.isChannel, messageViewsCount) { + chatUiState.isChannel && (messageViewsCount ?: 0) > 0 } val index = groupedMessages.indexOfFirst { item -> @@ -105,10 +107,10 @@ fun ChatMessageOptionsMenu( } } - val canShowViewersList = remember(state.memberCount, state.isChannel, selectedMessage) { - state.memberCount in 1 until 100 && + val canShowViewersList = remember(chatUiState.memberCount, chatUiState.isChannel, selectedMessage) { + chatUiState.memberCount in 1 until 100 && selectedMessage.isOutgoing && - (selectedMessage.canGetReadReceipts || selectedMessage.canGetViewers || !state.isChannel) + (selectedMessage.canGetReadReceipts || selectedMessage.canGetViewers || !chatUiState.isChannel) } suspend fun reloadViewers() { @@ -141,7 +143,7 @@ fun ChatMessageOptionsMenu( val splitOffset = remember(selectedMessage, menuMessageSize, density, shouldShowSeparatePost) { if (!shouldShowSeparatePost) return@remember null - val isChannel = state.isChannel && state.currentTopicId == null + val isChannel = chatUiState.isChannel && chatUiState.currentTopicId == null val width = menuMessageSize.width.toFloat() when (val content = selectedMessage.content) { @@ -204,18 +206,18 @@ fun ChatMessageOptionsMenu( } val senderIsUser = selectedMessage.senderId > 0L - val canModerateInChat = (state.isGroup || state.isChannel) && state.isAdmin + val canModerateInChat = (chatUiState.isGroup || chatUiState.isChannel) && chatUiState.isAdmin val canBlockUser = !selectedMessage.isOutgoing && senderIsUser && - (canModerateInChat || (!state.isGroup && !state.isChannel)) - val canRestrictUser = canBlockUser && (state.isGroup || state.isChannel) && state.isAdmin - val isOtherUserDialog = state.otherUser?.id?.let { it != state.currentUser?.id } == true + (canModerateInChat || (!chatUiState.isGroup && !chatUiState.isChannel)) + val canRestrictUser = canBlockUser && (chatUiState.isGroup || chatUiState.isChannel) && chatUiState.isAdmin + val isOtherUserDialog = chatUiState.otherUser?.id?.let { it != chatUiState.currentUser?.id } == true val canReportMessage = !selectedMessage.isOutgoing && ( - state.isGroup || state.isChannel || + chatUiState.isGroup || chatUiState.isChannel || isOtherUserDialog ) - val canCopyLink = state.isGroup || state.isChannel - val canPinMessages = state.isAdmin || state.permissions.canPinMessages - val isPremiumUser = state.currentUser?.isPremium == true + val canCopyLink = chatUiState.isGroup || chatUiState.isChannel + val canPinMessages = chatUiState.isAdmin || chatUiState.permissions.canPinMessages + val isPremiumUser = chatUiState.currentUser?.isPremium == true val canUseTelegramSummary = isPremiumUser && !canRestoreOriginalText && canSummarize(selectedMessage) val canUseTelegramTranslator = @@ -230,9 +232,9 @@ fun ChatMessageOptionsMenu( } MessageOptionsMenu( message = menuMessage.copy(readDate = messageWithReadDate.readDate), - canWrite = state.canWrite, + canWrite = chatUiState.canWrite, canPinMessages = canPinMessages, - isPinned = selectedMessage.id == state.pinnedMessage?.id, + isPinned = selectedMessage.id == pinnedState.pinnedMessage?.id, messageOffset = menuOffset, messageSize = menuMessageSize, clickOffset = clickOffset, @@ -261,14 +263,14 @@ fun ChatMessageOptionsMenu( scope.launch { reloadViewers() } }, onViewerClick = { component.toProfile(it) }, - bubbleRadius = state.bubbleRadius, + bubbleRadius = appearanceState.bubbleRadius, splitOffset = splitOffset, onReply = { component.onReplyMessage(selectedMessage) onDismiss() }, onPin = { - if (selectedMessage.id == state.pinnedMessage?.id) component.onUnpinMessage(selectedMessage) else component.onPinMessage( + if (selectedMessage.id == pinnedState.pinnedMessage?.id) component.onUnpinMessage(selectedMessage) else component.onPinMessage( selectedMessage ) onDismiss() @@ -290,10 +292,10 @@ fun ChatMessageOptionsMenu( onDismiss() }, onCopyLink = { - val link = if (!state.isGroup && !state.isChannel) { - "tg://openmessage?user_id=${state.chatId}&message_id=${selectedMessage.id shr 20}" + val link = if (!chatUiState.isGroup && !chatUiState.isChannel) { + "tg://openmessage?user_id=${chatUiState.chatId}&message_id=${selectedMessage.id shr 20}" } else { - "https://t.me/c/${state.chatId.toString().removePrefix("-100")}/${selectedMessage.id shr 20}" + "https://t.me/c/${chatUiState.chatId.toString().removePrefix("-100")}/${selectedMessage.id shr 20}" } nativeClipboard.setPrimaryClip( @@ -367,7 +369,7 @@ fun ChatMessageOptionsMenu( }, onTelegramTranslator = { telegramAiScope.launch { - val languageCode = state.currentUser?.languageCode?.takeIf { it.isNotBlank() } + val languageCode = chatUiState.currentUser?.languageCode?.takeIf { it.isNotBlank() } ?: Locale.getDefault().language coRunCatching { messageRepository.translateMessage(