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