From dd6da0783987ada50d3e0d9ef5d7c63b51603ac0 Mon Sep 17 00:00:00 2001 From: fakelog <48557356+fakelog@users.noreply.github.com> Date: Wed, 15 Apr 2026 22:27:29 +0500 Subject: [PATCH] Refactor current chat state decomposition into focused StateFlows Previously, ChatContent and chat viewers collected a single broad chat state, which caused the whole current chat screen to recompose on unrelated changes (messages, wallpaper, selection, pinned state, overlays, input, media viewers). Now state is split by responsibility: - chatUiState: chat chrome, metadata, topics, moderation, counters - messagesState: message list, loading, scroll commands, unread separator - appearanceState: typography, wallpaper, playback and download preferences - inputState: draft, reply/editing, mentions, inline bot and attach menu state - pinnedState: pinned message and pinned list state - mediaViewerState: image/video/web/mini app overlays - selectionState/searchState: message selection and in-chat search Benefits: - ChatContent subscribes only to the state slices each section actually uses - Media viewers and menus avoid recomposing on unrelated message or input updates - MainContent can render overlays without collecting the full chat state - Current chat state model now matches screen responsibilities and is easier to maintain Additionally, added a local auto-scroll guard in ChatContent to scope message-count driven scrolling to the active chat/topic and reduce redundant scroll triggers. --- .../main/java/org/monogram/app/MainContent.kt | 10 +- .../chats/currentChat/ChatComponent.kt | 165 ++++++ .../features/chats/currentChat/ChatContent.kt | 560 +++++++++--------- .../chats/currentChat/DefaultChatComponent.kt | 178 ++++++ .../chatContent/ChatContentBackground.kt | 2 +- .../chatContent/ChatContentList.kt | 208 ++++--- .../chatContent/ChatContentViewers.kt | 182 +++--- .../chatContent/ChatMessageOptionsMenu.kt | 52 +- 8 files changed, 886 insertions(+), 471 deletions(-) diff --git a/app/src/main/java/org/monogram/app/MainContent.kt b/app/src/main/java/org/monogram/app/MainContent.kt index 968a6a4b..075f0f55 100644 --- a/app/src/main/java/org/monogram/app/MainContent.kt +++ b/app/src/main/java/org/monogram/app/MainContent.kt @@ -172,9 +172,15 @@ fun MainContent( when (activeChild) { is RootComponent.Child.ChatDetailChild -> { - val chatState by activeChild.component.state.collectAsState() + val chatUiState by activeChild.component.chatUiState.collectAsState() + val appearanceState by activeChild.component.appearanceState.collectAsState() + val messagesState by activeChild.component.messagesState.collectAsState() + val mediaViewerState by activeChild.component.mediaViewerState.collectAsState() ChatContentViewers( - state = chatState, + chatUiState = chatUiState, + appearanceState = appearanceState, + messagesState = messagesState, + mediaViewerState = mediaViewerState, component = activeChild.component, localClipboard = localClipboard ) 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 65be35a0..b5730ee3 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 @@ -339,4 +347,161 @@ interface ChatComponent { val lastReadInboxMessageId: Long = 0L, val unreadSeparatorLastReadInboxMessageId: 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, + val unreadSeparatorCount: Int = 0, + val unreadSeparatorLastReadInboxMessageId: 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 isPlayerGesturesEnabled: Boolean = true, + val isPlayerDoubleTapSeekEnabled: Boolean = true, + val playerSeekDuration: Int = 10, + val isPlayerZoomEnabled: Boolean = true, + 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..2ade0929 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,23 +1400,28 @@ fun ChatContent( ) } - if (state.showBotCommands) { + if (chatUiState.showBotCommands) { BotCommandsSheet( - commands = state.botCommands, + commands = inputState.botCommands, onCommandClick = { component.onBotCommandClick(it) }, onDismiss = { component.onDismissBotCommands() } ) } - /*ChatContentViewers( - state = state, + ChatContentViewers( + chatUiState = chatUiState, + appearanceState = appearanceState, + messagesState = messagesState, + mediaViewerState = mediaViewerState, component = component, localClipboard = localClipboard - )*/ + ) selectedMessage?.let { msg -> ChatMessageOptionsMenu( - state = state, + chatUiState = chatUiState, + appearanceState = appearanceState, + pinnedState = pinnedState, component = component, selectedMessage = msg, menuOffset = menuOffset, @@ -1443,14 +1467,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 +1528,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 f6ac2413..94449e21 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 @@ -9,13 +9,18 @@ import com.arkivanov.mvikotlin.extensions.coroutines.labels import com.arkivanov.mvikotlin.extensions.coroutines.stateFlow import com.arkivanov.mvikotlin.main.store.DefaultStoreFactory import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.launch @@ -71,6 +76,7 @@ import org.monogram.presentation.settings.storage.CacheController import java.io.File import java.util.concurrent.ConcurrentHashMap +@OptIn(ExperimentalCoroutinesApi::class) class DefaultChatComponent( context: AppComponentContext, val chatId: Long, @@ -157,16 +163,188 @@ class DefaultChatComponent( ) ) + 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, + unreadSeparatorCount = unreadSeparatorCount, + unreadSeparatorLastReadInboxMessageId = unreadSeparatorLastReadInboxMessageId + ) + + 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, + isPlayerGesturesEnabled = isPlayerGesturesEnabled, + isPlayerDoubleTapSeekEnabled = isPlayerDoubleTapSeekEnabled, + playerSeekDuration = playerSeekDuration, + isPlayerZoomEnabled = isPlayerZoomEnabled, + 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(), component = this ).create() override val state: StateFlow = store.stateFlow + override val chatUiState: StateFlow = + state.selectState(state.value.toChatUiState()) { it.toChatUiState() } + override val selectionState: StateFlow = + state.selectState(state.value.toSelectionState()) { it.toSelectionState() } + override val searchState: StateFlow = + state.selectState(state.value.toSearchState()) { it.toSearchState() } + override val appearanceState: StateFlow = + state.selectState(state.value.toAppearanceState()) { it.toAppearanceState() } + override val messagesState: StateFlow = + state.selectState(state.value.toMessagesState()) { it.toMessagesState() } + override val inputState: StateFlow = + state.selectState(state.value.toInputState()) { it.toInputState() } + override val pinnedState: StateFlow = + state.selectState(state.value.toPinnedState()) { it.toPinnedState() } + override val mediaViewerState: StateFlow = + state.selectState(state.value.toMediaViewerState()) { it.toMediaViewerState() } private var availableWallpapers: List = emptyList() internal var allMembers: List = emptyList() + private inline fun StateFlow.selectState( + initialValue: T, + crossinline selector: (ChatComponent.State) -> T + ): StateFlow = map { selector(it) } + .distinctUntilChanged() + .stateIn(scope, SharingStarted.Eagerly, initialValue) + init { setupLifecycle() setupCollectors() 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 8c6d262e..2a67edef 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 @@ -91,7 +91,10 @@ import java.io.File @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, @@ -111,26 +114,27 @@ 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.unreadSeparatorCount, - state.unreadSeparatorLastReadInboxMessageId + messagesState.messages, + messagesState.unreadSeparatorCount, + messagesState.unreadSeparatorLastReadInboxMessageId ) { - if (isComments || state.unreadSeparatorCount <= 0) { + if (isComments || messagesState.unreadSeparatorCount <= 0) { null // suppress in thread/comments mode } else { val boundaryItem = findFirstUnreadBoundary( - messages = state.messages, + messages = messagesState.messages, groupedItems = groupedMessages, - firstUnreadMessageId = state.unreadSeparatorLastReadInboxMessageId + firstUnreadMessageId = messagesState.unreadSeparatorLastReadInboxMessageId ) 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.unreadSeparatorCount + unreadCount = messagesState.unreadSeparatorCount ) } } 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.unreadSeparatorCount + unreadCount = messagesState.unreadSeparatorCount ) } } - 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/ChatContentViewers.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentViewers.kt index a345eef7..80baccb0 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentViewers.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentViewers.kt @@ -20,22 +20,25 @@ import org.monogram.presentation.features.webview.InternalWebView @Composable fun ChatContentViewers( - state: ChatComponent.State, + chatUiState: ChatComponent.ChatUiState, + appearanceState: ChatComponent.AppearanceState, + messagesState: ChatComponent.MessagesState, + mediaViewerState: ChatComponent.MediaViewerState, component: ChatComponent, localClipboard: Clipboard ) { - InstantViewOverlay(state, component) - YouTubeOverlay(state, component, localClipboard) - MiniAppOverlay(state, component) - WebViewOverlay(state, component) - ImagesOverlay(state, component, localClipboard) - VideoOverlay(state, component, localClipboard) - InvoiceOverlay(state, component) - MiniAppTOSOverlay(state, component) + InstantViewOverlay(mediaViewerState, component) + YouTubeOverlay(chatUiState, messagesState, mediaViewerState, component, localClipboard) + MiniAppOverlay(chatUiState, mediaViewerState, component) + WebViewOverlay(mediaViewerState, component) + ImagesOverlay(chatUiState, appearanceState, messagesState, mediaViewerState, component, localClipboard) + VideoOverlay(chatUiState, appearanceState, messagesState, mediaViewerState, component, localClipboard) + InvoiceOverlay(chatUiState, mediaViewerState, component) + MiniAppTOSOverlay(mediaViewerState, component) } @Composable -private fun InstantViewOverlay(state: ChatComponent.State, component: ChatComponent) { +private fun InstantViewOverlay(state: ChatComponent.MediaViewerState, component: ChatComponent) { AnimatedVisibility( visible = state.instantViewUrl != null, enter = slideInVertically(initialOffsetY = { it }) + fadeIn(), @@ -55,21 +58,23 @@ private fun InstantViewOverlay(state: ChatComponent.State, component: ChatCompon @Composable private fun YouTubeOverlay( - state: ChatComponent.State, + chatUiState: ChatComponent.ChatUiState, + messagesState: ChatComponent.MessagesState, + mediaViewerState: ChatComponent.MediaViewerState, component: ChatComponent, localClipboard: Clipboard ) { AnimatedVisibility( - visible = state.youtubeUrl != null, + visible = mediaViewerState.youtubeUrl != null, enter = fadeIn(), exit = fadeOut() ) { - state.youtubeUrl?.let { url -> + mediaViewerState.youtubeUrl?.let { url -> YouTubeViewer( videoUrl = url, onDismiss = { component.onDismissYouTube() }, onForward = { - component.onForwardMessage(state.messages.find { + component.onForwardMessage(messagesState.messages.find { (it.content as? MessageContent.Text)?.text?.contains( url ) == true @@ -85,26 +90,30 @@ private fun YouTubeOverlay( ClipData.newPlainText("", AnnotatedString(it)) ) }, - isPipEnabled = !state.isInstalledFromGooglePlay + isPipEnabled = !chatUiState.isInstalledFromGooglePlay ) } } } @Composable -private fun MiniAppOverlay(state: ChatComponent.State, component: ChatComponent) { +private fun MiniAppOverlay( + chatUiState: ChatComponent.ChatUiState, + mediaViewerState: ChatComponent.MediaViewerState, + component: ChatComponent +) { AnimatedVisibility( - visible = state.miniAppUrl != null, + visible = mediaViewerState.miniAppUrl != null, enter = slideInVertically(initialOffsetY = { it }) + fadeIn(), exit = slideOutVertically(targetOffsetY = { it }) + fadeOut() ) { - if (state.miniAppUrl != null && state.miniAppName != null) { + if (mediaViewerState.miniAppUrl != null && mediaViewerState.miniAppName != null) { MiniAppViewer( - chatId = state.chatId, - botUserId = state.miniAppBotUserId, - baseUrl = state.miniAppUrl, - botName = state.chatTitle, - botAvatarPath = state.chatAvatar, + chatId = chatUiState.chatId, + botUserId = mediaViewerState.miniAppBotUserId, + baseUrl = mediaViewerState.miniAppUrl, + botName = chatUiState.chatTitle, + botAvatarPath = chatUiState.chatAvatar, webAppRepository = component.repositoryMessage, onDismiss = { component.onDismissMiniApp() } ) @@ -113,7 +122,7 @@ private fun MiniAppOverlay(state: ChatComponent.State, component: ChatComponent) } @Composable -private fun WebViewOverlay(state: ChatComponent.State, component: ChatComponent) { +private fun WebViewOverlay(state: ChatComponent.MediaViewerState, component: ChatComponent) { AnimatedVisibility( visible = state.webViewUrl != null, enter = slideInVertically(initialOffsetY = { it }) + fadeIn(), @@ -130,34 +139,41 @@ private fun WebViewOverlay(state: ChatComponent.State, component: ChatComponent) @Composable private fun ImagesOverlay( - state: ChatComponent.State, + chatUiState: ChatComponent.ChatUiState, + appearanceState: ChatComponent.AppearanceState, + messagesState: ChatComponent.MessagesState, + mediaViewerState: ChatComponent.MediaViewerState, component: ChatComponent, localClipboard: Clipboard ) { AnimatedVisibility( - visible = state.fullScreenImages != null, + visible = mediaViewerState.fullScreenImages != null, enter = fadeIn() + scaleIn(initialScale = 0.9f), exit = fadeOut() + scaleOut(targetScale = 0.9f) ) { - state.fullScreenImages?.let { images -> - val autoDownload = remember(state.autoDownloadWifi, state.autoDownloadRoaming, state.autoDownloadMobile) { + mediaViewerState.fullScreenImages?.let { images -> + val autoDownload = remember( + appearanceState.autoDownloadWifi, + appearanceState.autoDownloadRoaming, + appearanceState.autoDownloadMobile + ) { when { - component.downloadUtils.isWifiConnected() -> state.autoDownloadWifi - component.downloadUtils.isRoaming() -> state.autoDownloadRoaming - else -> state.autoDownloadMobile + component.downloadUtils.isWifiConnected() -> appearanceState.autoDownloadWifi + component.downloadUtils.isRoaming() -> appearanceState.autoDownloadRoaming + else -> appearanceState.autoDownloadMobile } } - val viewerItems = remember(images, state.fullScreenImageMessageIds, state.messages) { - if (state.fullScreenImageMessageIds.size == images.size) { - state.fullScreenImageMessageIds.mapIndexed { index, messageId -> - val message = state.messages.firstOrNull { it.id == messageId } + val viewerItems = remember(images, mediaViewerState.fullScreenImageMessageIds, messagesState.messages) { + if (mediaViewerState.fullScreenImageMessageIds.size == images.size) { + mediaViewerState.fullScreenImageMessageIds.mapIndexed { index, messageId -> + val message = messagesState.messages.firstOrNull { it.id == messageId } val resolvedPath = message?.displayMediaPathForViewer() ?: images[index] ViewerMediaItem(messageId = messageId, path = resolvedPath) } } else { images.map { path -> - val message = state.messages.firstOrNull { it.content.matchesDisplayPath(path) } + val message = messagesState.messages.firstOrNull { it.content.matchesDisplayPath(path) } ViewerMediaItem( messageId = message?.id ?: 0L, path = message?.displayMediaPathForViewer() ?: path @@ -168,24 +184,24 @@ private fun ImagesOverlay( val viewerImages = remember(viewerItems) { viewerItems.map { it.path } } val imageMessageIds = remember(viewerItems) { viewerItems.map { it.messageId } } - var currentImageIndex by remember(viewerImages, state.fullScreenStartIndex) { + var currentImageIndex by remember(viewerImages, mediaViewerState.fullScreenStartIndex) { mutableIntStateOf( - state.fullScreenStartIndex.coerceIn( + mediaViewerState.fullScreenStartIndex.coerceIn( 0, (viewerImages.lastIndex).coerceAtLeast(0) ) ) } - val currentViewerMessage = remember(currentImageIndex, imageMessageIds, state.messages) { + val currentViewerMessage = remember(currentImageIndex, imageMessageIds, messagesState.messages) { imageMessageIds.getOrNull(currentImageIndex) ?.takeIf { it != 0L } - ?.let { id -> state.messages.firstOrNull { it.id == id } } + ?.let { id -> messagesState.messages.firstOrNull { it.id == id } } } - val imageDownloadingStates = remember(imageMessageIds, state.messages) { + val imageDownloadingStates = remember(imageMessageIds, messagesState.messages) { imageMessageIds.map { id -> - val content = state.messages.firstOrNull { it.id == id }?.content + val content = messagesState.messages.firstOrNull { it.id == id }?.content when (content) { is MessageContent.Photo -> content.isDownloading else -> false @@ -193,9 +209,9 @@ private fun ImagesOverlay( } } - val imageDownloadProgressStates = remember(imageMessageIds, state.messages) { + val imageDownloadProgressStates = remember(imageMessageIds, messagesState.messages) { imageMessageIds.map { id -> - val content = state.messages.firstOrNull { it.id == id }?.content + val content = messagesState.messages.firstOrNull { it.id == id }?.content when (content) { is MessageContent.Photo -> content.downloadProgress else -> 0f @@ -206,7 +222,7 @@ private fun ImagesOverlay( if (viewerImages.isNotEmpty()) { ImageViewer( images = viewerImages, - startIndex = state.fullScreenStartIndex.coerceIn(0, viewerImages.lastIndex), + startIndex = mediaViewerState.fullScreenStartIndex.coerceIn(0, viewerImages.lastIndex), onDismiss = component::onDismissImages, autoDownload = autoDownload, onPageChanged = { index -> @@ -215,23 +231,23 @@ private fun ImagesOverlay( imageMessageIds.getOrNull(index + 1)?.takeIf { it != 0L }?.let(component::onDownloadHighRes) }, onForward = { path -> - val msg = currentViewerMessage ?: state.messages.find { it.content.matchesDisplayPath(path) } + val msg = currentViewerMessage ?: messagesState.messages.find { it.content.matchesDisplayPath(path) } msg?.let { component.onForwardMessage(it) } }, onDelete = { path -> - val msg = currentViewerMessage ?: state.messages.find { it.content.matchesDisplayPath(path) } + val msg = currentViewerMessage ?: messagesState.messages.find { it.content.matchesDisplayPath(path) } if (msg?.isOutgoing == true) { component.onDeleteMessage(msg, true) component.onDismissImages() } }, onCopyLink = { path -> - val msg = currentViewerMessage ?: state.messages.find { it.content.matchesDisplayPath(path) } + val msg = currentViewerMessage ?: messagesState.messages.find { it.content.matchesDisplayPath(path) } val link = if (msg != null) { - if (!state.isGroup && !state.isChannel) { - "tg://openmessage?user_id=${state.chatId}&message_id=${msg.id shr 20}" + if (!chatUiState.isGroup && !chatUiState.isChannel) { + "tg://openmessage?user_id=${chatUiState.chatId}&message_id=${msg.id shr 20}" } else { - "https://t.me/c/${state.chatId.toString().removePrefix("-100")}/${msg.id shr 20}" + "https://t.me/c/${chatUiState.chatId.toString().removePrefix("-100")}/${msg.id shr 20}" } } else { path @@ -241,7 +257,7 @@ private fun ImagesOverlay( ) }, onCopyText = { path -> - val msg = state.messages.find { + val msg = messagesState.messages.find { when (val content = it.content) { is MessageContent.Photo -> content.path == path is MessageContent.Video -> content.path == path @@ -262,7 +278,7 @@ private fun ImagesOverlay( } }, onVideoClick = { path -> - val msg = currentViewerMessage ?: state.messages.find { it.content.matchesDisplayPath(path) } + val msg = currentViewerMessage ?: messagesState.messages.find { it.content.matchesDisplayPath(path) } if (msg != null) { val mediaPath = msg.displayMediaPathForViewer() ?: path component.onOpenVideo( @@ -278,7 +294,7 @@ private fun ImagesOverlay( component.onOpenVideo(path = path, messageId = null, caption = null) } }, - captions = state.fullScreenCaptions, + captions = mediaViewerState.fullScreenCaptions, imageDownloadingStates = imageDownloadingStates, imageDownloadProgressStates = imageDownloadProgressStates, downloadUtils = component.downloadUtils @@ -290,12 +306,16 @@ private fun ImagesOverlay( @Composable private fun VideoOverlay( - state: ChatComponent.State, + chatUiState: ChatComponent.ChatUiState, + appearanceState: ChatComponent.AppearanceState, + messagesState: ChatComponent.MessagesState, + mediaViewerState: ChatComponent.MediaViewerState, component: ChatComponent, localClipboard: Clipboard ) { val videoVisible = - (state.fullScreenVideoPath != null || state.fullScreenVideoMessageId != null) && state.fullScreenImages == null + (mediaViewerState.fullScreenVideoPath != null || mediaViewerState.fullScreenVideoMessageId != null) && + mediaViewerState.fullScreenImages == null AnimatedVisibility( visible = videoVisible, @@ -303,11 +323,11 @@ private fun VideoOverlay( exit = fadeOut() + scaleOut(targetScale = 0.9f) ) { if (videoVisible) { - val messageId = state.fullScreenVideoMessageId - val path = state.fullScreenVideoPath + val messageId = mediaViewerState.fullScreenVideoMessageId + val path = mediaViewerState.fullScreenVideoPath - val msg = remember(messageId, path, state.messages) { - state.messages.find { it.id == messageId } ?: state.messages.find { + val msg = remember(messageId, path, messagesState.messages) { + messagesState.messages.find { it.id == messageId } ?: messagesState.messages.find { it.content.matchesDisplayPath(path ?: "") } } @@ -325,18 +345,18 @@ private fun VideoOverlay( VideoViewer( path = finalPath, onDismiss = component::onDismissVideo, - isGesturesEnabled = state.isPlayerGesturesEnabled, - isDoubleTapSeekEnabled = state.isPlayerDoubleTapSeekEnabled, - seekDuration = state.playerSeekDuration, - isZoomEnabled = state.isPlayerZoomEnabled, + isGesturesEnabled = appearanceState.isPlayerGesturesEnabled, + isDoubleTapSeekEnabled = appearanceState.isPlayerDoubleTapSeekEnabled, + seekDuration = appearanceState.playerSeekDuration, + isZoomEnabled = appearanceState.isPlayerZoomEnabled, onForward = { videoPath -> - val forwardMsg = state.messages.find { + val forwardMsg = messagesState.messages.find { it.content.matchesDisplayPath(videoPath) } forwardMsg?.let { component.onForwardMessage(it) } }, onDelete = { videoPath -> - val deleteMsg = state.messages.find { + val deleteMsg = messagesState.messages.find { it.content.matchesDisplayPath(videoPath) } if (deleteMsg?.isOutgoing == true) { @@ -345,15 +365,15 @@ private fun VideoOverlay( } }, onCopyLink = { videoPath -> - val linkMsg = state.messages.find { + val linkMsg = messagesState.messages.find { it.content.matchesDisplayPath(videoPath) } val link = if (linkMsg != null) { - if (!state.isGroup && !state.isChannel) { - "tg://openmessage?user_id=${state.chatId}&message_id=${linkMsg.id shr 20}" + if (!chatUiState.isGroup && !chatUiState.isChannel) { + "tg://openmessage?user_id=${chatUiState.chatId}&message_id=${linkMsg.id shr 20}" } else { "https://t.me/c/${ - state.chatId.toString().removePrefix("-100") + chatUiState.chatId.toString().removePrefix("-100") }/${linkMsg.id shr 20}" } } else { @@ -364,7 +384,7 @@ private fun VideoOverlay( ) }, onCopyText = { videoPath -> - val textMsg = state.messages.find { + val textMsg = messagesState.messages.find { it.content.matchesDisplayPath(videoPath) } val textToCopy = when (val content = textMsg?.content) { @@ -378,10 +398,10 @@ private fun VideoOverlay( ) } }, - onSaveGif = if (state.messages.any { (it.content as? MessageContent.Gif)?.path == finalPath }) { + onSaveGif = if (messagesState.messages.any { (it.content as? MessageContent.Gif)?.path == finalPath }) { { videoPath -> component.onAddToGifs(videoPath) } } else null, - caption = state.fullScreenVideoCaption, + caption = mediaViewerState.fullScreenVideoCaption, fileId = fileId, supportsStreaming = supportsStreaming, downloadUtils = component.downloadUtils @@ -393,12 +413,16 @@ private fun VideoOverlay( } @Composable -private fun InvoiceOverlay(state: ChatComponent.State, component: ChatComponent) { - if (state.invoiceSlug != null || state.invoiceMessageId != null) { +private fun InvoiceOverlay( + chatUiState: ChatComponent.ChatUiState, + mediaViewerState: ChatComponent.MediaViewerState, + component: ChatComponent +) { + if (mediaViewerState.invoiceSlug != null || mediaViewerState.invoiceMessageId != null) { InvoiceDialog( - slug = state.invoiceSlug, - chatId = state.chatId, - messageId = state.invoiceMessageId, + slug = mediaViewerState.invoiceSlug, + chatId = chatUiState.chatId, + messageId = mediaViewerState.invoiceMessageId, paymentRepository = component.repositoryMessage, fileRepository = component.repositoryMessage, onDismiss = { status -> component.onDismissInvoice(status) } @@ -407,7 +431,7 @@ private fun InvoiceOverlay(state: ChatComponent.State, component: ChatComponent) } @Composable -private fun MiniAppTOSOverlay(state: ChatComponent.State, component: ChatComponent) { +private fun MiniAppTOSOverlay(state: ChatComponent.MediaViewerState, component: ChatComponent) { MiniAppTOSBottomSheet( isVisible = state.showMiniAppTOS, onDismiss = { component.onDismissMiniAppTOS() }, 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(