From efa079ba98c36573141a9372bcaff44519c65bb8 Mon Sep 17 00:00:00 2001 From: Artur Skubei <41114720+gdlbo@users.noreply.github.com> Date: Fri, 24 Apr 2026 15:55:05 +0300 Subject: [PATCH 1/8] implement comprehensive message search with advanced filters and navigation - implement `SearchMessages.kt` to handle message search logic, including debounced queries, pagination, and multi-criteria filtering (sender, date range) - add advanced search UI components including `SearchNavigationPanel`, `SearchFilterTray`, `SearchSenderPickerOverlay`, and `SearchResultsListOverlay` - support filtering search results by specific sender and date ranges (Today, Last 7 days, Last 30 days, or custom picker) - update `MessageRepository` and `MessageRemoteDataSource` to support `senderId` and `threadId` in search requests - enhance `ChatTopBar` with a search toggle and integrate new search intents into `ChatStore` and `DefaultChatComponent` - add localized string resources for search states, date labels, and result formatting in English and Russian - ensure search state is reset when switching chat topics or message threads - implement automatic "load more" logic for search results during scrolling and navigation - refine `TdMessageRemoteDataSource` to correctly resolve topic IDs for forum and thread-based searches within the TDLib fork context --- .../remote/MessageRemoteDataSource.kt | 13 +- .../remote/TdMessageRemoteDataSource.kt | 59 +- .../data/repository/MessageRepositoryImpl.kt | 12 +- .../domain/repository/MessageRepository.kt | 3 +- .../chats/currentChat/ChatComponent.kt | 15 + .../features/chats/currentChat/ChatContent.kt | 977 +++++++++++++++++- .../features/chats/currentChat/ChatStore.kt | 8 + .../chats/currentChat/ChatStoreFactory.kt | 28 +- .../chats/currentChat/DefaultChatComponent.kt | 13 + .../currentChat/components/ChatTopBar.kt | 7 + .../chats/currentChat/impl/MessageLoading.kt | 6 + .../chats/currentChat/impl/SearchMessages.kt | 458 ++++++++ .../src/main/res/values-ru-rRU/string.xml | 6 + presentation/src/main/res/values/string.xml | 10 + 14 files changed, 1569 insertions(+), 46 deletions(-) create mode 100644 presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/SearchMessages.kt diff --git a/data/src/main/java/org/monogram/data/datasource/remote/MessageRemoteDataSource.kt b/data/src/main/java/org/monogram/data/datasource/remote/MessageRemoteDataSource.kt index 02c3b53a..00d1f0f8 100644 --- a/data/src/main/java/org/monogram/data/datasource/remote/MessageRemoteDataSource.kt +++ b/data/src/main/java/org/monogram/data/datasource/remote/MessageRemoteDataSource.kt @@ -47,7 +47,15 @@ interface MessageRemoteDataSource { suspend fun getMessageThread(chatId: Long, messageId: Long): TdApi.MessageThreadInfo? suspend fun getMessages(chatId: Long, fromMessageId: Long, offset: Int, limit: Int, threadId: Long?): TdApi.Messages? suspend fun getChatHistory(chatId: Long, fromMessageId: Long, offset: Int, limit: Int): TdApi.Messages? - suspend fun searchChatMessages(chatId: Long, query: String, fromMessageId: Long, limit: Int, filter: TdApi.SearchMessagesFilter, threadId: Long?): TdApi.FoundChatMessages? + suspend fun searchChatMessages( + chatId: Long, + query: String, + fromMessageId: Long, + limit: Int, + filter: TdApi.SearchMessagesFilter, + threadId: Long?, + senderId: Long? = null + ): TdApi.FoundChatMessages? suspend fun getChatPinnedMessage(chatId: Long): TdApi.Message? suspend fun getPollVoters(chatId: Long, messageId: Long, optionId: Int, offset: Int, limit: Int): TdApi.PollVoters? suspend fun getMessageViewers(chatId: Long, messageId: Long): TdApi.MessageViewers? @@ -163,7 +171,8 @@ interface MessageRemoteDataSource { query: String, fromMessageId: Long, limit: Int, - threadId: Long? + threadId: Long?, + senderId: Long? = null ): SearchChatMessagesResult suspend fun getPollVotersModels( diff --git a/data/src/main/java/org/monogram/data/datasource/remote/TdMessageRemoteDataSource.kt b/data/src/main/java/org/monogram/data/datasource/remote/TdMessageRemoteDataSource.kt index 16b0f6f4..4500e4c0 100644 --- a/data/src/main/java/org/monogram/data/datasource/remote/TdMessageRemoteDataSource.kt +++ b/data/src/main/java/org/monogram/data/datasource/remote/TdMessageRemoteDataSource.kt @@ -118,6 +118,8 @@ class TdMessageRemoteDataSource( private suspend fun safeExecute(function: TdApi.Function): T? { return try { gateway.execute(function) + } catch (e: CancellationException) { + throw e } catch (e: Exception) { Log.e("TdMessageRemote", "Error executing ${function.javaClass.simpleName}", e) null @@ -370,12 +372,20 @@ class TdMessageRemoteDataSource( } } - override suspend fun searchChatMessages(chatId: Long, query: String, fromMessageId: Long, limit: Int, filter: TdApi.SearchMessagesFilter, threadId: Long?): TdApi.FoundChatMessages? { + override suspend fun searchChatMessages( + chatId: Long, + query: String, + fromMessageId: Long, + limit: Int, + filter: TdApi.SearchMessagesFilter, + threadId: Long?, + senderId: Long? + ): TdApi.FoundChatMessages? { val request = TdApi.SearchChatMessages().apply { this.chatId = chatId - this.topicId = if (threadId != null) TdApi.MessageTopicForum(threadId.toInt()) else null + this.topicId = resolveSearchTopicId(chatId, threadId) this.query = query - this.senderId = null + this.senderId = senderId?.let(TdApi::MessageSenderUser) this.fromMessageId = fromMessageId this.offset = 0 this.limit = limit @@ -384,8 +394,23 @@ class TdMessageRemoteDataSource( return safeExecute(request) } - override suspend fun searchMessages(chatId: Long, query: String, fromMessageId: Long, limit: Int, threadId: Long?): SearchChatMessagesResult { - val result = searchChatMessages(chatId, query, fromMessageId, limit, TdApi.SearchMessagesFilterEmpty(), threadId) + override suspend fun searchMessages( + chatId: Long, + query: String, + fromMessageId: Long, + limit: Int, + threadId: Long?, + senderId: Long? + ): SearchChatMessagesResult { + val result = searchChatMessages( + chatId = chatId, + query = query, + fromMessageId = fromMessageId, + limit = limit, + filter = TdApi.SearchMessagesFilterEmpty(), + threadId = threadId, + senderId = senderId + ) if (result != null) { val chat = getChat(chatId) val lastReadInbox = chat?.lastReadInboxMessageId ?: 0L @@ -395,16 +420,36 @@ class TdMessageRemoteDataSource( scope.async { try { withTimeout(5000) { messageMapper.mapMessageToModelSync(msg, lastReadInbox, lastReadOutbox, isChatOpen = true) } + } catch (e: CancellationException) { + throw e } catch (e: Exception) { Log.e("TdMessageRemote", "Error mapping search message ${msg.id}", e) createFallbackMessage(msg) } } }.awaitAll() - return SearchChatMessagesResult(models, result.totalCount, result.nextFromMessageId) + val nextCursor = result.nextFromMessageId.takeIf { it != 0L } + ?: models.lastOrNull()?.id + ?: 0L + return SearchChatMessagesResult( + messages = models, + totalCount = result.totalCount, + nextFromMessageId = if (models.size < result.totalCount) nextCursor else 0L + ) } else return SearchChatMessagesResult(emptyList(), 0, 0L) } + private suspend fun resolveSearchTopicId(chatId: Long, threadId: Long?): TdApi.MessageTopic? { + if (threadId == null || threadId == 0L) return null + + val chat = getChat(chatId) + return if (chat?.viewAsTopics == true) { + TdApi.MessageTopicForum(threadId.toInt()) + } else { + TdApi.MessageTopicThread(threadId) + } + } + private suspend fun loadMessages(chatId: Long, fromMessageId: Long, offset: Int, limit: Int, threadId: Long? = null): List = withContext(dispatcherProvider.io) { val historyResult = getChatHistoryInternal(chatId, fromMessageId, offset, limit, threadId) ?: throw IllegalStateException( @@ -423,6 +468,8 @@ class TdMessageRemoteDataSource( async { try { withTimeout(5000) { messageMapper.mapMessageToModelSync(msg, lastReadInbox, lastReadOutbox, isChatOpen = true) } + } catch (e: CancellationException) { + throw e } catch (e: Exception) { Log.e("TdMessageRemote", "Error mapping message ${msg.id}", e) createFallbackMessage(msg) diff --git a/data/src/main/java/org/monogram/data/repository/MessageRepositoryImpl.kt b/data/src/main/java/org/monogram/data/repository/MessageRepositoryImpl.kt index b814d2b4..c0d68010 100644 --- a/data/src/main/java/org/monogram/data/repository/MessageRepositoryImpl.kt +++ b/data/src/main/java/org/monogram/data/repository/MessageRepositoryImpl.kt @@ -842,9 +842,17 @@ class MessageRepositoryImpl( query: String, fromMessageId: Long, limit: Int, - threadId: Long? + threadId: Long?, + senderId: Long? ): SearchChatMessagesResult = withContext(dispatcherProvider.io) { - messageRemoteDataSource.searchMessages(chatId, query, fromMessageId, limit, threadId) + messageRemoteDataSource.searchMessages( + chatId, + query, + fromMessageId, + limit, + threadId, + senderId + ) } override fun updateVisibleRange(chatId: Long, visibleMessageIds: List, nearbyMessageIds: List) { diff --git a/domain/src/main/java/org/monogram/domain/repository/MessageRepository.kt b/domain/src/main/java/org/monogram/domain/repository/MessageRepository.kt index 20c5026e..fa249e18 100644 --- a/domain/src/main/java/org/monogram/domain/repository/MessageRepository.kt +++ b/domain/src/main/java/org/monogram/domain/repository/MessageRepository.kt @@ -184,7 +184,8 @@ interface MessageRepository : query: String, fromMessageId: Long = 0, limit: Int = 50, - threadId: Long? = null + threadId: Long? = null, + senderId: Long? = null ): SearchChatMessagesResult fun updateVisibleRange(chatId: Long, visibleMessageIds: List, nearbyMessageIds: List) 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 679424d5..ee85729b 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 @@ -174,6 +174,12 @@ interface ChatComponent { fun onToggleMute() fun onSearchToggle() fun onSearchQueryChange(query: String) + fun onSearchNextResult() + fun onSearchPreviousResult() + fun onSearchResultClick(index: Int) + fun onLoadMoreSearchResults() + fun onSearchSenderChange(user: UserModel?) + fun onSearchDateRangeChange(fromEpochSeconds: Int?, toEpochSeconds: Int?) fun onClearHistory() fun onDeleteChat() fun onReport() @@ -321,6 +327,15 @@ interface ChatComponent { val isMuted: Boolean = false, val isSearchActive: Boolean = false, val searchQuery: String = "", + val isSearchingMessages: Boolean = false, + val searchResults: List = emptyList(), + val searchResultsTotalCount: Int = 0, + val selectedSearchResultIndex: Int = -1, + val searchNextFromMessageId: Long = 0L, + val searchSender: UserModel? = null, + val searchAvailableSenders: List = emptyList(), + val searchDateFromEpochSeconds: Int? = null, + val searchDateToEpochSeconds: Int? = null, val showReportDialog: Boolean = false, val isBot: Boolean = false, val botCommands: List = emptyList(), 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 12bacc0d..c60014ba 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 @@ -1,5 +1,6 @@ package org.monogram.presentation.features.chats.currentChat +import android.app.DatePickerDialog import androidx.activity.compose.BackHandler import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest @@ -19,26 +20,37 @@ import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.animation.togetherWith import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.animateScrollBy import androidx.compose.foundation.gestures.scrollBy import androidx.compose.foundation.interaction.collectIsDraggedAsState +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.List import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.rounded.Block import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults @@ -50,6 +62,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -84,6 +97,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -104,7 +118,9 @@ import org.monogram.domain.models.ForwardInfo import org.monogram.domain.models.MessageContent import org.monogram.domain.models.MessageModel import org.monogram.domain.models.ReplyMarkupModel +import org.monogram.domain.models.UserModel import org.monogram.presentation.R +import org.monogram.presentation.core.ui.AvatarForChat import org.monogram.presentation.core.ui.ConfirmationSheet import org.monogram.presentation.core.ui.ExpressiveDefaults import org.monogram.presentation.core.util.LocalTabletInterfaceEnabled @@ -137,6 +153,10 @@ import java.io.File import java.io.FileOutputStream import java.net.URLEncoder import java.nio.charset.StandardCharsets +import java.time.Instant +import java.time.LocalDate +import java.time.ZoneId +import java.time.format.DateTimeFormatter import kotlin.math.abs @OptIn(ExperimentalMaterial3ExpressiveApi::class) @@ -233,10 +253,61 @@ fun ChatContent( val isComments = state.rootMessage != null val isForumList = state.viewAsTopics && state.currentTopicId == null var showScrollToBottomButton by remember { mutableStateOf(false) } + var showAllSearchResults by rememberSaveable( + state.chatId, + state.currentTopicId, + state.isSearchActive, + state.searchQuery + ) { + mutableStateOf(false) + } + var showSearchSenderPicker by rememberSaveable( + state.chatId, + state.currentTopicId, + state.isSearchActive + ) { + mutableStateOf(false) + } + var showSearchFilters by rememberSaveable( + state.chatId, + state.currentTopicId, + state.isSearchActive + ) { + mutableStateOf(false) + } var hasUserScrolledAwayFromBottom by rememberSaveable(state.chatId, state.currentTopicId) { mutableStateOf(false) } val isDragged by scrollState.interactionSource.collectIsDraggedAsState() + val canLoadMoreSearchResults by remember( + state.searchNextFromMessageId, + state.searchResults.size, + state.searchResultsTotalCount + ) { + derivedStateOf { + state.searchResults.size < state.searchResultsTotalCount || + state.searchNextFromMessageId != 0L + } + } + val searchSenderCandidates by remember(state.searchAvailableSenders, state.otherUser) { + derivedStateOf { + buildList { + addAll(state.searchAvailableSenders) + state.otherUser?.let(::add) + }.distinctBy(UserModel::id) + } + } + val hasSearchFiltersApplied by remember( + state.searchSender, + state.searchDateFromEpochSeconds, + state.searchDateToEpochSeconds + ) { + derivedStateOf { + state.searchSender != null || + state.searchDateFromEpochSeconds != null || + state.searchDateToEpochSeconds != null + } + } val isAnyViewerOpen = state.fullScreenImages != null || state.fullScreenVideoPath != null || @@ -770,11 +841,13 @@ fun ChatContent( state.currentTopicId, state.selectedMessageIds, state.viewAsTopics, + state.isSearchActive, isRecordingVideo ) { derivedStateOf { (state.canWrite || state.isCurrentUserRestricted) && !isRecordingVideo && + !state.isSearchActive && state.selectedMessageIds.isEmpty() && (!state.viewAsTopics || state.currentTopicId != null) } @@ -790,10 +863,12 @@ fun ChatContent( state.selectedMessageIds, state.viewAsTopics, state.currentTopicId, + state.isSearchActive, isRecordingVideo ) { derivedStateOf { !showInputBar && + !state.isSearchActive && !state.isMember && (state.isChannel || state.isGroup) && !state.canWrite && @@ -814,6 +889,16 @@ fun ChatContent( } } + LaunchedEffect(state.isSearchActive) { + if (state.isSearchActive) { + showSearchFilters = false + showSearchSenderPicker = false + if (state.showPinnedMessagesList) { + component.onDismissPinnedMessages() + } + } + } + val requestPinnedMessagesListDismiss = { if (state.showPinnedMessagesList) { component.onDismissPinnedMessages() @@ -835,7 +920,8 @@ fun ChatContent( state.miniAppUrl, state.webViewUrl, state.instantViewUrl, - state.youtubeUrl + state.youtubeUrl, + state.isSearchActive ) { derivedStateOf { editingPhotoPath != null || @@ -852,7 +938,8 @@ fun ChatContent( state.miniAppUrl != null || state.webViewUrl != null || state.instantViewUrl != null || - state.youtubeUrl != null + state.youtubeUrl != null || + state.isSearchActive } } val selectedCount = state.selectedMessageIds.size @@ -921,8 +1008,8 @@ fun ChatContent( isMuted = state.isMuted, isSearchActive = state.isSearchActive, searchQuery = state.searchQuery, - pinnedMessage = state.pinnedMessage, - pinnedMessageCount = state.pinnedMessageCount + pinnedMessage = if (state.isSearchActive) null else state.pinnedMessage, + pinnedMessageCount = if (state.isSearchActive) 0 else state.pinnedMessageCount ) } @@ -975,27 +1062,32 @@ fun ChatContent( topOverlayHeight = with(density) { it.height.toDp() } } ) { - ChatContentTopBar( - topBarState = topBarUiState, - selectedCount = selectedCount, - canRevokeSelected = canRevokeSelected, - component = component, - contentAlpha = contentAlpha, - onBack = { - keyboardController?.hide() - if (state.currentTopicId != null) { - component.onTopicClick(0) - } else { - component.onBackClicked() - } - }, - onOpenMenu = { - keyboardController?.hide() - focusManager.clearFocus(force = true) - }, - onPinnedMessageClick = { msg -> scrollToMessageState.value(msg) }, - showBack = !isTablet - ) + Column( + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + ChatContentTopBar( + topBarState = topBarUiState, + selectedCount = selectedCount, + canRevokeSelected = canRevokeSelected, + component = component, + contentAlpha = contentAlpha, + onBack = { + keyboardController?.hide() + if (state.currentTopicId != null) { + component.onTopicClick(0) + } else { + component.onBackClicked() + } + }, + onOpenMenu = { + keyboardController?.hide() + focusManager.clearFocus(force = true) + }, + onPinnedMessageClick = { msg -> scrollToMessageState.value(msg) }, + showBack = !isTablet + ) + + } } }, bottomBar = { @@ -1461,7 +1553,213 @@ fun ChatContent( ) AnimatedVisibility( - visible = showScrollToBottomButton, + visible = state.isSearchActive, + enter = fadeIn(animationSpec = tween(220)) + + slideInVertically( + animationSpec = tween(280), + initialOffsetY = { it / 3 } + ) + + scaleIn( + animationSpec = tween(220), + initialScale = 0.96f + ), + exit = fadeOut(animationSpec = tween(160)) + + slideOutVertically( + animationSpec = tween(180), + targetOffsetY = { it / 4 } + ), + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(horizontal = 12.dp, vertical = 16.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .windowInsetsPadding(WindowInsets.navigationBars), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + AnimatedVisibility( + visible = showAllSearchResults && state.searchResults.isNotEmpty(), + enter = fadeIn(animationSpec = tween(180)) + + slideInVertically( + animationSpec = tween(240), + initialOffsetY = { it / 8 } + ), + exit = fadeOut(animationSpec = tween(140)) + + slideOutVertically( + animationSpec = tween(180), + targetOffsetY = { it / 10 } + ) + ) { + SearchResultsListOverlay( + query = state.searchQuery, + results = state.searchResults, + selectedIndex = state.selectedSearchResultIndex, + isSearching = state.isSearchingMessages, + canLoadMore = canLoadMoreSearchResults, + onLoadMore = component::onLoadMoreSearchResults, + onResultClick = { index -> + showAllSearchResults = false + component.onSearchResultClick(index) + } + ) + } + + AnimatedVisibility( + visible = showSearchFilters && showSearchSenderPicker, + enter = fadeIn(animationSpec = tween(180)) + + slideInVertically( + animationSpec = tween(220), + initialOffsetY = { it / 6 } + ) + + scaleIn( + animationSpec = tween(200), + initialScale = 0.98f + ), + exit = fadeOut(animationSpec = tween(140)) + + slideOutVertically( + animationSpec = tween(160), + targetOffsetY = { it / 8 } + ) + ) { + SearchSenderPickerOverlay( + selectedSenderId = state.searchSender?.id, + senders = searchSenderCandidates, + onSelectSender = { user -> + showSearchSenderPicker = false + component.onSearchSenderChange(user) + } + ) + } + + AnimatedVisibility( + visible = showSearchFilters, + enter = fadeIn(animationSpec = tween(180)) + + slideInVertically( + animationSpec = tween(220), + initialOffsetY = { it / 6 } + ) + + scaleIn( + animationSpec = tween(200), + initialScale = 0.98f + ), + exit = fadeOut(animationSpec = tween(140)) + + slideOutVertically( + animationSpec = tween(160), + targetOffsetY = { it / 8 } + ) + ) { + SearchFilterTray( + selectedSender = state.searchSender, + fromEpochSeconds = state.searchDateFromEpochSeconds, + toEpochSeconds = state.searchDateToEpochSeconds, + onToggleSenderPicker = { + showSearchSenderPicker = !showSearchSenderPicker + }, + onApplyToday = { + val now = LocalDate.now() + component.onSearchDateRangeChange( + now.atStartOfDay(ZoneId.systemDefault()) + .toEpochSecond().toInt(), + now.plusDays(1) + .atStartOfDay(ZoneId.systemDefault()) + .toEpochSecond().toInt() - 1 + ) + }, + onApplyLastDays = { days -> + val now = LocalDate.now() + val from = now.minusDays((days - 1).toLong()) + component.onSearchDateRangeChange( + from.atStartOfDay(ZoneId.systemDefault()) + .toEpochSecond().toInt(), + now.plusDays(1) + .atStartOfDay(ZoneId.systemDefault()) + .toEpochSecond().toInt() - 1 + ) + }, + onResetDateRange = { + component.onSearchDateRangeChange(null, null) + }, + onPickFromDate = { + showSearchDatePicker( + context = context, + initialEpochSeconds = state.searchDateFromEpochSeconds, + onDateSelected = { date -> + val nextFrom = + toStartOfDayEpochSeconds(date) + val nextTo = state.searchDateToEpochSeconds + ?.let(::epochSecondsToLocalDate) + ?.let { currentTo -> + if (currentTo.isBefore(date)) { + toEndOfDayEpochSeconds(date) + } else { + toEndOfDayEpochSeconds(currentTo) + } + } + component.onSearchDateRangeChange( + nextFrom, + nextTo + ) + } + ) + }, + onPickToDate = { + showSearchDatePicker( + context = context, + initialEpochSeconds = state.searchDateToEpochSeconds, + onDateSelected = { date -> + val nextTo = toEndOfDayEpochSeconds(date) + val nextFrom = + state.searchDateFromEpochSeconds + ?.let(::epochSecondsToLocalDate) + ?.let { currentFrom -> + if (currentFrom.isAfter(date)) { + toStartOfDayEpochSeconds( + date + ) + } else { + toStartOfDayEpochSeconds( + currentFrom + ) + } + } + component.onSearchDateRangeChange( + nextFrom, + nextTo + ) + } + ) + } + ) + } + + SearchNavigationPanel( + query = state.searchQuery, + results = state.searchResults, + totalCount = state.searchResultsTotalCount, + selectedIndex = state.selectedSearchResultIndex, + isSearching = state.isSearchingMessages, + showAllResults = showAllSearchResults, + filtersExpanded = showSearchFilters, + hasFiltersApplied = hasSearchFiltersApplied, + onPrevious = component::onSearchPreviousResult, + onNext = component::onSearchNextResult, + onToggleShowAll = { + showAllSearchResults = !showAllSearchResults + }, + onToggleFilters = { + val nextExpanded = !showSearchFilters + showSearchFilters = nextExpanded + if (!nextExpanded) { + showSearchSenderPicker = false + } + } + ) + } + } + + AnimatedVisibility( + visible = showScrollToBottomButton && !state.isSearchActive, enter = slideInVertically(initialOffsetY = { it / 2 }) + fadeIn(), exit = slideOutVertically(targetOffsetY = { it / 2 }) + fadeOut(), modifier = Modifier @@ -1766,6 +2064,7 @@ fun ChatContent( else if (state.youtubeUrl != null) component.onDismissYouTube() else if (state.miniAppUrl != null) component.onDismissMiniApp() else if (state.webViewUrl != null) component.onDismissWebView() + else if (state.isSearchActive) component.onSearchToggle() else if (state.currentTopicId != null) component.onTopicClick(0) } } @@ -1784,6 +2083,632 @@ private fun MessageModel.extractTextContent(): String? { } } +@Composable +private fun SearchNavigationPanel( + query: String, + results: List, + totalCount: Int, + selectedIndex: Int, + isSearching: Boolean, + showAllResults: Boolean, + filtersExpanded: Boolean, + hasFiltersApplied: Boolean, + onPrevious: () -> Unit, + onNext: () -> Unit, + onToggleShowAll: () -> Unit, + onToggleFilters: () -> Unit +) { + val hasResults = results.isNotEmpty() + val selectedPosition = (selectedIndex + 1).takeIf { selectedIndex in results.indices } ?: 0 + val listIconRotation by animateFloatAsState( + targetValue = if (showAllResults) 90f else 0f, + animationSpec = tween(220), + label = "SearchListRotation" + ) + val statusText = when { + isSearching -> stringResource(R.string.search_results_loading) + query.isBlank() -> stringResource(R.string.no_results_found) + else -> stringResource(R.string.no_search_results_format, query) + } + val counterText = stringResource( + R.string.search_results_position_format, + selectedPosition, + totalCount.coerceAtLeast(results.size) + ) + + Surface( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(24.dp), + tonalElevation = 10.dp, + shadowElevation = 14.dp, + color = MaterialTheme.colorScheme.surface.copy(alpha = 0.98f) + ) { + Column( + modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + if (!hasResults) { + AnimatedContent( + targetState = statusText, + transitionSpec = { fadeIn(tween(180)) togetherWith fadeOut(tween(120)) }, + label = "SearchStatusText" + ) { text -> + Text( + text = text, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.fillMaxWidth() + ) + } + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Surface( + onClick = onToggleFilters, + shape = CircleShape, + color = if (hasFiltersApplied || filtersExpanded) { + MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.92f) + } else { + MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.75f) + }, + tonalElevation = 2.dp + ) { + Box( + modifier = Modifier.padding(horizontal = 10.dp, vertical = 9.dp), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Default.MoreVert, + contentDescription = null, + tint = if (hasFiltersApplied || filtersExpanded) { + MaterialTheme.colorScheme.onPrimaryContainer + } else { + MaterialTheme.colorScheme.onSurfaceVariant + } + ) + } + } + + Surface( + onClick = onToggleShowAll, + shape = CircleShape, + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.75f), + tonalElevation = 2.dp + ) { + Box( + modifier = Modifier.padding(9.dp), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.List, + contentDescription = null, + tint = if (showAllResults) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + modifier = Modifier.graphicsLayer { + rotationZ = listIconRotation + } + ) + } + } + + Surface( + onClick = onPrevious, + shape = CircleShape, + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.75f), + tonalElevation = 2.dp + ) { + Box( + modifier = Modifier.padding(9.dp), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Default.KeyboardArrowDown, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface + ) + } + } + + Surface( + modifier = Modifier.weight(1f), + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.92f), + tonalElevation = 2.dp + ) { + AnimatedContent( + targetState = counterText, + transitionSpec = { + (fadeIn(tween(180)) + slideInVertically { it / 3 }) togetherWith + (fadeOut(tween(120)) + slideOutVertically { -it / 3 }) + }, + label = "SearchCounter" + ) { animatedCounter -> + Text( + text = animatedCounter, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = Modifier.padding(horizontal = 12.dp, vertical = 9.dp), + maxLines = 1 + ) + } + } + + Surface( + onClick = onNext, + shape = CircleShape, + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.75f), + tonalElevation = 2.dp + ) { + Box( + modifier = Modifier.padding(9.dp), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Default.KeyboardArrowUp, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface + ) + } + } + } + } + } +} + +@Composable +private fun SearchFilterTray( + selectedSender: UserModel?, + fromEpochSeconds: Int?, + toEpochSeconds: Int?, + onToggleSenderPicker: () -> Unit, + onApplyToday: () -> Unit, + onApplyLastDays: (Int) -> Unit, + onResetDateRange: () -> Unit, + onPickFromDate: () -> Unit, + onPickToDate: () -> Unit +) { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(24.dp), + tonalElevation = 10.dp, + shadowElevation = 14.dp, + color = MaterialTheme.colorScheme.surface.copy(alpha = 0.98f) + ) { + Column( + modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + SearchSenderChip( + selectedSender = selectedSender, + onClick = onToggleSenderPicker + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + SearchMiniChip( + label = stringResource(R.string.search_date_all), + isActive = fromEpochSeconds == null && toEpochSeconds == null, + modifier = Modifier.weight(1f), + onClick = onResetDateRange + ) + SearchMiniChip( + label = stringResource(R.string.preview_date_today), + isActive = isTodayRange(fromEpochSeconds, toEpochSeconds), + modifier = Modifier.weight(1f), + onClick = onApplyToday + ) + SearchMiniChip( + label = stringResource(R.string.search_date_last_7_days), + isActive = matchesLastDaysRange(fromEpochSeconds, toEpochSeconds, 7), + modifier = Modifier.weight(1f), + onClick = { onApplyLastDays(7) } + ) + SearchMiniChip( + label = stringResource(R.string.search_date_last_30_days), + isActive = matchesLastDaysRange(fromEpochSeconds, toEpochSeconds, 30), + modifier = Modifier.weight(1f), + onClick = { onApplyLastDays(30) } + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + SearchRangeChip( + modifier = Modifier.weight(1f), + label = stringResource(R.string.search_date_from), + value = fromEpochSeconds?.let(::formatSearchDate), + onClick = onPickFromDate + ) + SearchRangeChip( + modifier = Modifier.weight(1f), + label = stringResource(R.string.search_date_to), + value = toEpochSeconds?.let(::formatSearchDate), + onClick = onPickToDate + ) + } + } + } +} + +@Composable +private fun SearchSenderPickerOverlay( + selectedSenderId: Long?, + senders: List, + onSelectSender: (UserModel?) -> Unit +) { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(22.dp), + tonalElevation = 10.dp, + shadowElevation = 12.dp, + color = MaterialTheme.colorScheme.surface.copy(alpha = 0.985f) + ) { + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 280.dp) + .padding(horizontal = 4.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + item("all_senders") { + SearchSenderRow( + title = stringResource(R.string.search_sender_all), + subtitle = stringResource(R.string.search_section_messages), + avatarPath = null, + isSelected = selectedSenderId == null, + onClick = { onSelectSender(null) } + ) + } + + itemsIndexed(senders, key = { _, user -> user.id }) { _, user -> + SearchSenderRow( + title = formatSearchSenderLabel(user), + subtitle = user.username?.takeIf { it.isNotBlank() }?.let { "@$it" }, + avatarPath = user.avatarPath, + isSelected = selectedSenderId == user.id, + onClick = { onSelectSender(user) } + ) + } + } + } +} + +@Composable +private fun SearchSenderRow( + title: String, + subtitle: String?, + avatarPath: String?, + isSelected: Boolean, + onClick: () -> Unit +) { + Surface( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 6.dp) + .clickable(onClick = onClick), + shape = RoundedCornerShape(18.dp), + color = if (isSelected) { + MaterialTheme.colorScheme.primaryContainer + } else { + MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.28f) + }, + tonalElevation = if (isSelected) 2.dp else 0.dp + ) { + Row( + modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + AvatarForChat( + path = avatarPath, + name = title, + size = 32.dp + ) + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + Text( + text = title, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + subtitle?.let { + Text( + text = it, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + } + } +} + +private fun formatSearchSenderLabel(user: UserModel): String { + return listOfNotNull( + user.firstName.takeIf { it.isNotBlank() }, + user.lastName?.takeIf { it.isNotBlank() } + ).joinToString(" ").ifBlank { + user.username?.takeIf { it.isNotBlank() } ?: user.id.toString() + } +} + +@Composable +private fun SearchSenderChip( + selectedSender: UserModel?, + onClick: () -> Unit +) { + val label = selectedSender?.let(::formatSearchSenderLabel) + ?: stringResource(R.string.search_sender_all) + + Surface( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick), + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.34f), + tonalElevation = 1.dp + ) { + Row( + modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + AvatarForChat( + path = selectedSender?.avatarPath, + name = label, + size = 30.dp + ) + Text( + text = label, + modifier = Modifier.weight(1f), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } +} + +@Composable +private fun SearchMiniChip( + label: String, + isActive: Boolean, + modifier: Modifier = Modifier, + onClick: () -> Unit +) { + Surface( + modifier = modifier.clickable(onClick = onClick), + shape = RoundedCornerShape(14.dp), + color = if (isActive) { + MaterialTheme.colorScheme.primaryContainer + } else { + MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.28f) + }, + tonalElevation = if (isActive) 2.dp else 0.dp + ) { + Box( + modifier = Modifier.padding(horizontal = 10.dp, vertical = 7.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = label, + style = MaterialTheme.typography.labelMedium, + color = if (isActive) { + MaterialTheme.colorScheme.onPrimaryContainer + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } +} + +@Composable +private fun SearchRangeChip( + label: String, + value: String?, + modifier: Modifier = Modifier, + onClick: () -> Unit +) { + Surface( + modifier = modifier.clickable(onClick = onClick), + shape = RoundedCornerShape(16.dp), + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.28f), + tonalElevation = 0.dp + ) { + Column( + modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + Text( + text = label, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1 + ) + Text( + text = value ?: stringResource(R.string.cd_select_date), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } +} + +private fun isTodayRange(fromEpochSeconds: Int?, toEpochSeconds: Int?): Boolean { + val today = LocalDate.now() + return fromEpochSeconds?.let(::epochSecondsToLocalDate) == today && + toEpochSeconds?.let(::epochSecondsToLocalDate) == today +} + +private fun matchesLastDaysRange(fromEpochSeconds: Int?, toEpochSeconds: Int?, days: Int): Boolean { + val today = LocalDate.now() + return fromEpochSeconds?.let(::epochSecondsToLocalDate) == today.minusDays((days - 1).toLong()) && + toEpochSeconds?.let(::epochSecondsToLocalDate) == today +} + +private fun showSearchDatePicker( + context: android.content.Context, + initialEpochSeconds: Int?, + onDateSelected: (LocalDate) -> Unit +) { + val initialDate = initialEpochSeconds?.let(::epochSecondsToLocalDate) ?: LocalDate.now() + DatePickerDialog( + context, + { _, year, month, dayOfMonth -> + onDateSelected(LocalDate.of(year, month + 1, dayOfMonth)) + }, + initialDate.year, + initialDate.monthValue - 1, + initialDate.dayOfMonth + ).show() +} + +private fun epochSecondsToLocalDate(epochSeconds: Int): LocalDate { + return Instant.ofEpochSecond(epochSeconds.toLong()) + .atZone(ZoneId.systemDefault()) + .toLocalDate() +} + +private fun toStartOfDayEpochSeconds(date: LocalDate): Int { + return date.atStartOfDay(ZoneId.systemDefault()).toEpochSecond().toInt() +} + +private fun toEndOfDayEpochSeconds(date: LocalDate): Int { + return date.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toEpochSecond().toInt() - 1 +} + +private fun formatSearchDate(epochSeconds: Int): String { + return epochSecondsToLocalDate(epochSeconds).format(DateTimeFormatter.ofPattern("dd.MM.yyyy")) +} + +@Composable +private fun SearchResultsListOverlay( + query: String, + results: List, + selectedIndex: Int, + isSearching: Boolean, + canLoadMore: Boolean, + onLoadMore: () -> Unit, + onResultClick: (Int) -> Unit +) { + val listState = rememberLazyListState() + + LaunchedEffect(listState, results.size, canLoadMore, isSearching) { + if (!canLoadMore || isSearching) return@LaunchedEffect + + snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index } + .filterNotNull() + .distinctUntilChanged() + .collectLatest { lastVisibleIndex -> + if (lastVisibleIndex >= results.lastIndex - 4) { + onLoadMore() + } + } + } + + Surface( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(22.dp), + tonalElevation = 10.dp, + shadowElevation = 12.dp, + color = MaterialTheme.colorScheme.surface.copy(alpha = 0.985f) + ) { + Column( + modifier = Modifier.padding(horizontal = 10.dp, vertical = 10.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + LazyColumn( + state = listState, + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 340.dp), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + itemsIndexed(results, key = { _, message -> message.id }) { index, message -> + val preview = message.extractTextContent() + ?.replace('\n', ' ') + ?.trim() + ?.takeIf { it.isNotBlank() } + ?: message.senderName.ifBlank { query } + val sender = message.senderName.ifBlank { + stringResource(R.string.search_section_messages) + } + val isSelected = index == selectedIndex + + Surface( + modifier = Modifier + .fillMaxWidth() + .clickable { onResultClick(index) }, + shape = RoundedCornerShape(18.dp), + color = if (isSelected) { + MaterialTheme.colorScheme.primaryContainer + } else { + MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.32f) + }, + tonalElevation = if (isSelected) 2.dp else 0.dp + ) { + Column( + modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp), + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + Text( + text = sender, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.primary + ) + Text( + text = preview, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 2 + ) + } + } + } + } + + if (canLoadMore || isSearching) { + TextButton( + onClick = onLoadMore, + enabled = !isSearching, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) { + Text( + text = if (isSearching) { + stringResource(R.string.search_results_loading) + } else { + stringResource(R.string.action_show_more) + } + ) + } + } + } + } +} + private fun MessageModel.withUpdatedTextContent(newText: String): MessageModel { val updatedContent = when (val c = content) { is MessageContent.Text -> c.copy(text = newText, entities = emptyList(), webPage = null) diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/ChatStore.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/ChatStore.kt index 2e48a83b..13b2065d 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/ChatStore.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/ChatStore.kt @@ -11,6 +11,7 @@ import org.monogram.domain.models.MessageEntity import org.monogram.domain.models.MessageModel import org.monogram.domain.models.MessageSendOptions import org.monogram.domain.models.PollDraft +import org.monogram.domain.models.UserModel import java.io.File interface ChatStore : Store { @@ -144,6 +145,13 @@ interface ChatStore : Store component.handleAddToAdBlockWhitelist() is Intent.RemoveFromAdBlockWhitelist -> component.handleRemoveFromAdBlockWhitelist() is Intent.ToggleMute -> component.handleToggleMute() - - is Intent.SearchToggle -> component._state.update { - it.copy( - isSearchActive = !it.isSearchActive, - searchQuery = "" - ) - } - - is Intent.SearchQueryChange -> component._state.update { it.copy(searchQuery = intent.query) } + is Intent.SearchToggle -> component.handleSearchToggle() + is Intent.SearchQueryChange -> component.handleSearchQueryChange(intent.query) + is Intent.SearchNextResult -> component.handleSearchNextResult() + is Intent.SearchPreviousResult -> component.handleSearchPreviousResult() + is Intent.SearchResultClick -> component.handleSearchResultClick(intent.index) + is Intent.LoadMoreSearchResults -> component.loadMoreSearchResults() + is Intent.SearchSenderChange -> component.handleSearchSenderChange(intent.user) + is Intent.SearchDateRangeChange -> component.handleSearchDateRangeChange( + intent.fromEpochSeconds, + intent.toEpochSeconds + ) is Intent.ClearHistory -> component.handleClearHistory() is Intent.DeleteChat -> component.handleDeleteChat() 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 6f90e6e3..c9370455 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 @@ -116,6 +116,7 @@ class DefaultChatComponent( var draftSaveJob: Job? = null private var autoLoadJob: Job? = null private var mentionJob: Job? = null + internal var searchJob: Job? = null internal val reactionUpdateSuppressedUntil = ConcurrentHashMap() internal val remappedMessageIds = ConcurrentHashMap() internal val mediaDownloadRetryCount = ConcurrentHashMap() @@ -312,6 +313,7 @@ class DefaultChatComponent( try { allMembers = chatInfoRepository.getChatMembers(chatId, 0, 200, ChatMembersFilter.Recent) .map { it.user } + _state.update { it.copy(searchAvailableSenders = allMembers) } } catch (e: Exception) { Log.e("DefaultChatComponent", "Failed to load members", e) } @@ -591,6 +593,17 @@ class DefaultChatComponent( override fun onSearchToggle() = store.accept(ChatStore.Intent.SearchToggle) override fun onSearchQueryChange(query: String) = store.accept(ChatStore.Intent.SearchQueryChange(query)) + override fun onSearchNextResult() = store.accept(ChatStore.Intent.SearchNextResult) + override fun onSearchPreviousResult() = store.accept(ChatStore.Intent.SearchPreviousResult) + override fun onSearchResultClick(index: Int) = + store.accept(ChatStore.Intent.SearchResultClick(index)) + + override fun onLoadMoreSearchResults() = store.accept(ChatStore.Intent.LoadMoreSearchResults) + override fun onSearchSenderChange(user: UserModel?) = + store.accept(ChatStore.Intent.SearchSenderChange(user)) + + override fun onSearchDateRangeChange(fromEpochSeconds: Int?, toEpochSeconds: Int?) = + store.accept(ChatStore.Intent.SearchDateRangeChange(fromEpochSeconds, toEpochSeconds)) override fun onClearHistory() = store.accept(ChatStore.Intent.ClearHistory) override fun onDeleteChat() = store.accept(ChatStore.Intent.DeleteChat) diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/ChatTopBar.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/ChatTopBar.kt index b2e9320a..111260e7 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/ChatTopBar.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/ChatTopBar.kt @@ -30,6 +30,7 @@ import androidx.compose.material.icons.automirrored.rounded.PlaylistAddCheck import androidx.compose.material.icons.automirrored.rounded.VolumeOff import androidx.compose.material.icons.automirrored.rounded.VolumeUp import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.rounded.Block import androidx.compose.material.icons.rounded.CleaningServices import androidx.compose.material.icons.rounded.Close @@ -310,6 +311,12 @@ fun ChatTopBar( } }, actions = { + IconButton(onClick = onSearchToggle) { + Icon( + Icons.Default.Search, + contentDescription = stringResource(R.string.action_search) + ) + } IconButton(onClick = { onMenu() showMenu = true diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/MessageLoading.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/MessageLoading.kt index d666ea3b..85515a63 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/MessageLoading.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/MessageLoading.kt @@ -699,6 +699,8 @@ internal fun DefaultChatComponent.scrollToMessageInternal(messageId: Long) { animated = true ) ) + } catch (e: CancellationException) { + throw e } catch (e: Exception) { Log.e("DefaultChatComponent", "Failed to scroll to message", e) } finally { @@ -746,6 +748,8 @@ internal fun DefaultChatComponent.scrollToBottomInternal() { scrollCommand = ChatScrollCommand.ScrollToBottom(animated = true) ) } + } catch (e: CancellationException) { + throw e } catch (e: Exception) { Log.e("DefaultChatComponent", "Failed to scroll to bottom", e) } finally { @@ -1408,6 +1412,7 @@ internal fun DefaultChatComponent.loadDraft() { internal fun DefaultChatComponent.handleTopicClick(topicId: Int) { val id = if (topicId == 0) null else topicId.toLong() + resetSearchState(isSearchActive = false) _state.update { it.copy( currentTopicId = id, @@ -1428,6 +1433,7 @@ internal fun DefaultChatComponent.handleCommentsClick(messageId: Long) { scope.launch { val message = _state.value.messages.find { it.id == messageId } val threadContext = repositoryMessage.getMessageThreadContext(chatId, messageId) + resetSearchState(isSearchActive = false) _state.update { it.copy( currentTopicId = messageId, diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/SearchMessages.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/SearchMessages.kt new file mode 100644 index 00000000..93b6b3f3 --- /dev/null +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/SearchMessages.kt @@ -0,0 +1,458 @@ +package org.monogram.presentation.features.chats.currentChat.impl + +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.monogram.domain.models.MessageModel +import org.monogram.domain.models.UserModel +import org.monogram.presentation.features.chats.currentChat.ChatScrollCommand +import org.monogram.presentation.features.chats.currentChat.DefaultChatComponent +import org.monogram.presentation.features.chats.currentChat.ScrollAlign + +private const val SEARCH_DEBOUNCE_MS = 250L +private const val SEARCH_PAGE_SIZE = 20 +private const val SEARCH_FETCH_ATTEMPTS = 8 + +private fun hasDateFilter(fromEpochSeconds: Int?, toEpochSeconds: Int?): Boolean { + return fromEpochSeconds != null || toEpochSeconds != null +} + +private fun DefaultChatComponent.hasSearchCriteria(state: org.monogram.presentation.features.chats.currentChat.ChatComponent.State): Boolean { + return state.searchQuery.isNotBlank() || + state.searchSender != null || + state.searchDateFromEpochSeconds != null || + state.searchDateToEpochSeconds != null +} + +private fun DefaultChatComponent.hasMoreSearchResults(state: org.monogram.presentation.features.chats.currentChat.ChatComponent.State): Boolean { + return state.searchResults.size < state.searchResultsTotalCount || + state.searchNextFromMessageId != 0L +} + +internal fun DefaultChatComponent.handleSearchToggle() { + searchJob?.cancel() + val isSearchActive = _state.value.isSearchActive + _state.update { + it.copy( + isSearchActive = !isSearchActive, + searchQuery = "", + isSearchingMessages = false, + searchResults = emptyList(), + searchResultsTotalCount = 0, + selectedSearchResultIndex = -1, + searchNextFromMessageId = 0L, + searchSender = null, + searchDateFromEpochSeconds = null, + searchDateToEpochSeconds = null + ) + } +} + +internal fun DefaultChatComponent.resetSearchState(isSearchActive: Boolean = _state.value.isSearchActive) { + searchJob?.cancel() + _state.update { + it.copy( + isSearchActive = isSearchActive, + searchQuery = "", + isSearchingMessages = false, + searchResults = emptyList(), + searchResultsTotalCount = 0, + selectedSearchResultIndex = -1, + searchNextFromMessageId = 0L, + searchSender = null, + searchDateFromEpochSeconds = null, + searchDateToEpochSeconds = null + ) + } +} + +internal fun DefaultChatComponent.handleSearchQueryChange(query: String) { + searchJob?.cancel() + _state.update { it.copy(searchQuery = query) } + + val currentState = _state.value + if ( + query.isBlank() && + currentState.searchSender == null && + currentState.searchDateFromEpochSeconds == null && + currentState.searchDateToEpochSeconds == null + ) { + _state.update { + it.copy( + isSearchingMessages = false, + searchResults = emptyList(), + searchResultsTotalCount = 0, + selectedSearchResultIndex = -1, + searchNextFromMessageId = 0L + ) + } + return + } + + startSearch(query = query, sender = currentState.searchSender, withDebounce = true) +} + +internal fun DefaultChatComponent.handleSearchSenderChange(user: UserModel?) { + searchJob?.cancel() + val currentQuery = _state.value.searchQuery + _state.update { it.copy(searchSender = user) } + + if ( + currentQuery.isBlank() && + user == null && + _state.value.searchDateFromEpochSeconds == null && + _state.value.searchDateToEpochSeconds == null + ) { + _state.update { + it.copy( + isSearchingMessages = false, + searchResults = emptyList(), + searchResultsTotalCount = 0, + selectedSearchResultIndex = -1, + searchNextFromMessageId = 0L + ) + } + return + } + + startSearch(query = currentQuery, sender = user, withDebounce = false) +} + +internal fun DefaultChatComponent.handleSearchDateRangeChange( + fromEpochSeconds: Int?, + toEpochSeconds: Int? +) { + searchJob?.cancel() + _state.update { + it.copy( + searchDateFromEpochSeconds = fromEpochSeconds, + searchDateToEpochSeconds = toEpochSeconds + ) + } + + val updatedState = _state.value + if ( + updatedState.searchQuery.isBlank() && + updatedState.searchSender == null && + fromEpochSeconds == null && + toEpochSeconds == null + ) { + _state.update { + it.copy( + isSearchingMessages = false, + searchResults = emptyList(), + searchResultsTotalCount = 0, + selectedSearchResultIndex = -1, + searchNextFromMessageId = 0L + ) + } + return + } + + startSearch( + query = updatedState.searchQuery, + sender = updatedState.searchSender, + fromEpochSeconds = fromEpochSeconds, + toEpochSeconds = toEpochSeconds, + withDebounce = false + ) +} + +internal fun DefaultChatComponent.handleSearchNextResult() { + val currentState = _state.value + val results = currentState.searchResults + if (results.isEmpty()) return + + val currentIndex = currentState.selectedSearchResultIndex.takeIf { it in results.indices } ?: 0 + val nextIndex = currentIndex + 1 + + if (nextIndex < results.size) { + handleSearchResultClick(nextIndex) + return + } + + val canLoadMore = hasMoreSearchResults(currentState) + if (!canLoadMore) { + handleSearchResultClick(0) + return + } + + scope.launch { + val previousSize = _state.value.searchResults.size + val updatedResults = appendMoreSearchResults() ?: return@launch + val targetIndex = previousSize.coerceAtMost(updatedResults.lastIndex) + if (targetIndex >= 0) { + handleSearchResultClick(targetIndex) + } + } +} + +internal fun DefaultChatComponent.handleSearchPreviousResult() { + val results = _state.value.searchResults + if (results.isEmpty()) return + val currentIndex = _state.value.selectedSearchResultIndex.takeIf { it in results.indices } ?: 0 + val previousIndex = if (currentIndex == 0) results.lastIndex else currentIndex - 1 + handleSearchResultClick(previousIndex) +} + +internal fun DefaultChatComponent.handleSearchResultClick(index: Int) { + val results = _state.value.searchResults + if (index !in results.indices) return + _state.update { it.copy(selectedSearchResultIndex = index) } + scrollToSearchResult(index, results) + + val stateAfterSelection = _state.value + val isNearLoadedTail = index >= results.lastIndex - 2 + if (isNearLoadedTail && hasMoreSearchResults(stateAfterSelection) && !stateAfterSelection.isSearchingMessages) { + scope.launch { + appendMoreSearchResults() + } + } +} + +internal fun DefaultChatComponent.loadMoreSearchResults() { + searchJob?.cancel() + searchJob = scope.launch { + appendMoreSearchResults() + } +} + +private suspend fun DefaultChatComponent.appendMoreSearchResults(): List? { + val currentState = _state.value + if (currentState.isSearchingMessages) return currentState.searchResults + if (!hasSearchCriteria(currentState)) return currentState.searchResults + if (!hasMoreSearchResults(currentState)) return currentState.searchResults + + val targetChatId = activeThreadChatId() + val targetThreadId = activeThreadId() + val query = currentState.searchQuery.trim() + val senderId = currentState.searchSender?.id + val fromEpochSeconds = currentState.searchDateFromEpochSeconds + val toEpochSeconds = currentState.searchDateToEpochSeconds + val isDateFiltered = hasDateFilter(fromEpochSeconds, toEpochSeconds) + + return try { + _state.update { it.copy(isSearchingMessages = true) } + + var requestCursor: Long? = currentState.searchNextFromMessageId.takeIf { it != 0L } + var latestState = currentState + var mergedList = currentState.searchResults + var totalCount = currentState.searchResultsTotalCount + var shouldStopPaging = false + + repeat(SEARCH_FETCH_ATTEMPTS) { + if (shouldStopPaging) return@repeat + val fromMessageId = requestCursor + ?: latestState.searchResults.lastOrNull()?.id + ?: 0L + + val result = repositoryMessage.searchMessages( + chatId = targetChatId, + query = query, + fromMessageId = fromMessageId, + limit = SEARCH_PAGE_SIZE, + threadId = targetThreadId, + senderId = senderId + ) + val filteredPage = result.messages.filterByDateRange(fromEpochSeconds, toEpochSeconds) + + val stillRelevant = _state.value.isSearchActive && + _state.value.searchQuery.trim() == query && + _state.value.searchSender?.id == senderId && + _state.value.searchDateFromEpochSeconds == fromEpochSeconds && + _state.value.searchDateToEpochSeconds == toEpochSeconds && + activeThreadChatId() == targetChatId && + activeThreadId() == targetThreadId + if (!stillRelevant) return null + + val mergedResults = + LinkedHashMap(mergedList.size + filteredPage.size) + mergedList.forEach { mergedResults[it.id] = it } + filteredPage.forEach { mergedResults[it.id] = it } + val resolvedNextCursor = result.nextFromMessageId.takeIf { it != 0L } + ?: result.messages.lastOrNull()?.id + ?: 0L + val shouldKeepPaging = resolvedNextCursor != 0L + val updatedResults = mergedResults.values.toList() + val didGrow = updatedResults.size > mergedList.size + totalCount = if (isDateFiltered) { + if (shouldKeepPaging) { + maxOf(totalCount, updatedResults.size + 1) + } else { + updatedResults.size + } + } else { + maxOf(totalCount, result.totalCount, updatedResults.size) + } + + mergedList = updatedResults + latestState = latestState.copy( + searchResults = updatedResults, + searchResultsTotalCount = totalCount, + searchNextFromMessageId = if (shouldKeepPaging) resolvedNextCursor else 0L + ) + + shouldStopPaging = (!shouldKeepPaging) || + resolvedNextCursor == fromMessageId || + (!isDateFiltered && didGrow) || + (isDateFiltered && didGrow && filteredPage.isNotEmpty()) + requestCursor = resolvedNextCursor.takeIf { !shouldStopPaging } + } + + _state.update { + if ( + it.isSearchActive && + it.searchQuery.trim() == query && + it.searchSender?.id == senderId && + it.searchDateFromEpochSeconds == fromEpochSeconds && + it.searchDateToEpochSeconds == toEpochSeconds && + activeThreadChatId() == targetChatId && + activeThreadId() == targetThreadId + ) { + latestState.copy(isSearchingMessages = false) + } else { + it + } + } + mergedList + } catch (e: CancellationException) { + _state.update { it.copy(isSearchingMessages = false) } + throw e + } catch (_: Exception) { + _state.update { it.copy(isSearchingMessages = false) } + null + } +} + +private fun DefaultChatComponent.startSearch( + query: String, + sender: UserModel?, + fromEpochSeconds: Int? = _state.value.searchDateFromEpochSeconds, + toEpochSeconds: Int? = _state.value.searchDateToEpochSeconds, + withDebounce: Boolean +) { + val targetChatId = activeThreadChatId() + val targetThreadId = activeThreadId() + searchJob = scope.launch { + try { + _state.update { it.copy(isSearchingMessages = true) } + if (withDebounce) { + delay(SEARCH_DEBOUNCE_MS) + } + + val normalizedQuery = query.trim() + val senderId = sender?.id + val result = repositoryMessage.searchMessages( + chatId = targetChatId, + query = normalizedQuery, + fromMessageId = 0L, + limit = SEARCH_PAGE_SIZE, + threadId = targetThreadId, + senderId = senderId + ) + var filteredMessages = + result.messages.filterByDateRange(fromEpochSeconds, toEpochSeconds) + var nextCursor = result.nextFromMessageId + val isDateFiltered = hasDateFilter(fromEpochSeconds, toEpochSeconds) + var attempts = 0 + + while (filteredMessages.size < SEARCH_PAGE_SIZE && nextCursor != 0L && attempts < SEARCH_FETCH_ATTEMPTS) { + attempts++ + val nextResult = repositoryMessage.searchMessages( + chatId = targetChatId, + query = normalizedQuery, + fromMessageId = nextCursor, + limit = SEARCH_PAGE_SIZE, + threadId = targetThreadId, + senderId = senderId + ) + filteredMessages = (filteredMessages + nextResult.messages.filterByDateRange( + fromEpochSeconds, + toEpochSeconds + )) + .distinctBy(MessageModel::id) + val resolvedNextCursor = nextResult.nextFromMessageId.takeIf { it != 0L } + ?: nextResult.messages.lastOrNull()?.id + ?: 0L + if (resolvedNextCursor == nextCursor) break + nextCursor = resolvedNextCursor + } + + val resolvedTotalCount = if (isDateFiltered) { + if (nextCursor != 0L) { + filteredMessages.size + 1 + } else { + filteredMessages.size + } + } else { + maxOf(result.totalCount, filteredMessages.size) + } + + val stillRelevant = _state.value.isSearchActive && + _state.value.searchQuery.trim() == normalizedQuery && + _state.value.searchSender?.id == senderId && + _state.value.searchDateFromEpochSeconds == fromEpochSeconds && + _state.value.searchDateToEpochSeconds == toEpochSeconds && + activeThreadChatId() == targetChatId && + activeThreadId() == targetThreadId + if (!stillRelevant) return@launch + + _state.update { + it.copy( + isSearchingMessages = false, + searchResults = filteredMessages, + searchResultsTotalCount = resolvedTotalCount, + selectedSearchResultIndex = if (filteredMessages.isNotEmpty()) 0 else -1, + searchNextFromMessageId = nextCursor + ) + } + + if (filteredMessages.isNotEmpty()) { + scrollToSearchResult(0, filteredMessages) + } + } catch (e: CancellationException) { + throw e + } catch (_: Exception) { + _state.update { + if (it.searchQuery == query && it.searchSender?.id == sender?.id) { + it.copy(isSearchingMessages = false) + } else { + it + } + } + } + } +} + +private fun List.filterByDateRange( + fromEpochSeconds: Int?, + toEpochSeconds: Int? +): List { + return filter { message -> + val isAfterStart = fromEpochSeconds == null || message.date >= fromEpochSeconds + val isBeforeEnd = toEpochSeconds == null || message.date <= toEpochSeconds + isAfterStart && isBeforeEnd + } +} + +private fun DefaultChatComponent.scrollToSearchResult(index: Int, results: List) { + val message = results.getOrNull(index) ?: return + val state = _state.value + val isAlreadyLoaded = state.messages.any { it.id == message.id } + if (isAlreadyLoaded) { + _state.update { + it.copy( + highlightedMessageId = message.id, + pendingScrollCommand = ChatScrollCommand.JumpToMessage( + messageId = message.id, + highlight = true, + align = ScrollAlign.Center, + animated = true + ) + ) + } + } else { + scrollToMessage(message.id) + } +} diff --git a/presentation/src/main/res/values-ru-rRU/string.xml b/presentation/src/main/res/values-ru-rRU/string.xml index a7ef509f..2c9b60f8 100644 --- a/presentation/src/main/res/values-ru-rRU/string.xml +++ b/presentation/src/main/res/values-ru-rRU/string.xml @@ -158,6 +158,12 @@ Показать аккаунты Поиск сообщений… + Все авторы + Все даты + С + По + + 30д Очистить Без звука Подтверждённый аккаунт diff --git a/presentation/src/main/res/values/string.xml b/presentation/src/main/res/values/string.xml index 773f15da..352ffd67 100644 --- a/presentation/src/main/res/values/string.xml +++ b/presentation/src/main/res/values/string.xml @@ -180,6 +180,16 @@ Search messages... + Searching... + %1$d / %2$d + Show all + Hide results + All senders + All dates + From + To + 7d + 30d Clear Muted Verified From 34eb81afba504490c2a370640992a31d8c261525 Mon Sep 17 00:00:00 2001 From: Artur Skubei <41114720+gdlbo@users.noreply.github.com> Date: Fri, 24 Apr 2026 16:39:17 +0300 Subject: [PATCH 2/8] refactor chat content logic into specialized helper files and state managers - extract chat-specific side effects, scroll coordination, and UI state derivation into standalone internal files (`ChatContentEffects.kt`, `ChatContentScrollCoordinator.kt`, `ChatContentDerivedState.kt`) - introduce `rememberChatContentPermissionState`, `rememberChatMessageListState`, and `rememberChatTopBarUiState` to encapsulate complex UI state derivation - move input bar state and action handling to `ChatContentInputConfiguration.kt` - externalize search-related UI components into `ChatContentSearchOverlay.kt` and `rememberChatSearchUiState` - centralize modal sheets and overlay logic (pinned messages, stickers, poll voters, bot commands, and editors) in `ChatContentOverlays.kt` - isolate message content manipulation utilities in `ChatContentMessageUtils.kt` - clean up `ChatContent.kt` by replacing inline logic with the new specialized state and effect helpers for improved maintainability and performance --- .../features/chats/currentChat/ChatContent.kt | 2396 ++--------------- .../chatContent/ChatContentDerivedState.kt | 415 +++ .../chatContent/ChatContentEffects.kt | 374 +++ .../ChatContentInputConfiguration.kt | 172 ++ .../chatContent/ChatContentMessageUtils.kt | 35 + .../chatContent/ChatContentOverlays.kt | 204 ++ .../ChatContentScrollCoordinator.kt | 242 ++ .../chatContent/ChatContentSearchOverlay.kt | 883 ++++++ 8 files changed, 2554 insertions(+), 2167 deletions(-) create mode 100644 presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentDerivedState.kt create mode 100644 presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentEffects.kt create mode 100644 presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentInputConfiguration.kt create mode 100644 presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentMessageUtils.kt create mode 100644 presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentOverlays.kt create mode 100644 presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentScrollCoordinator.kt create mode 100644 presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentSearchOverlay.kt 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 c60014ba..011604e7 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 @@ -1,7 +1,5 @@ package org.monogram.presentation.features.chats.currentChat -import android.app.DatePickerDialog -import androidx.activity.compose.BackHandler import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts @@ -20,38 +18,25 @@ import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.animation.togetherWith import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.gestures.animateScrollBy -import androidx.compose.foundation.gestures.scrollBy import androidx.compose.foundation.interaction.collectIsDraggedAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.List import androidx.compose.material.icons.filled.KeyboardArrowDown -import androidx.compose.material.icons.filled.KeyboardArrowUp -import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material.icons.rounded.Block import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @@ -62,12 +47,9 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -78,7 +60,6 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset @@ -97,67 +78,45 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.toSize import androidx.compose.ui.zIndex import androidx.window.core.layout.WindowWidthSizeClass -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import kotlinx.coroutines.withTimeoutOrNull -import org.monogram.domain.models.ChatViewportCacheEntry import org.monogram.domain.models.ForwardInfo import org.monogram.domain.models.MessageContent import org.monogram.domain.models.MessageModel -import org.monogram.domain.models.ReplyMarkupModel -import org.monogram.domain.models.UserModel import org.monogram.presentation.R -import org.monogram.presentation.core.ui.AvatarForChat -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.ChatContentEffects import org.monogram.presentation.features.chats.currentChat.chatContent.ChatContentList +import org.monogram.presentation.features.chats.currentChat.chatContent.ChatContentOverlays +import org.monogram.presentation.features.chats.currentChat.chatContent.ChatContentSearchOverlay 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.ChatMessageListUiState -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.extractTextContent 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.chatContent.rememberChatChromeState +import org.monogram.presentation.features.chats.currentChat.chatContent.rememberChatContentPermissionState +import org.monogram.presentation.features.chats.currentChat.chatContent.rememberChatInputBarActions +import org.monogram.presentation.features.chats.currentChat.chatContent.rememberChatInputBarState +import org.monogram.presentation.features.chats.currentChat.chatContent.rememberChatMessageListState +import org.monogram.presentation.features.chats.currentChat.chatContent.rememberChatSearchUiState +import org.monogram.presentation.features.chats.currentChat.chatContent.rememberChatTopBarUiState +import org.monogram.presentation.features.chats.currentChat.chatContent.scrollToMessageIndex +import org.monogram.presentation.features.chats.currentChat.chatContent.withUpdatedTextContent 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.MessageListShimmer -import org.monogram.presentation.features.chats.currentChat.components.StickerSetSheet -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 -import org.monogram.presentation.features.chats.currentChat.components.inputbar.ChatInputBarActions -import org.monogram.presentation.features.chats.currentChat.components.inputbar.ChatInputBarState -import org.monogram.presentation.features.chats.currentChat.components.pins.PinnedMessagesListSheet -import org.monogram.presentation.features.chats.currentChat.editor.photo.PhotoEditorScreen -import org.monogram.presentation.features.chats.currentChat.editor.video.VideoEditorScreen import java.io.File import java.io.FileOutputStream -import java.net.URLEncoder -import java.nio.charset.StandardCharsets -import java.time.Instant -import java.time.LocalDate -import java.time.ZoneId -import java.time.format.DateTimeFormatter -import kotlin.math.abs @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable @@ -235,7 +194,6 @@ fun ChatContent( val groupedMessages by remember { derivedStateOf { groupMessagesByAlbum(displayMessages) } } - val latestUiState = rememberUpdatedState(state) val groupedMessageIndexById by remember(groupedMessages) { derivedStateOf { buildMap { @@ -279,35 +237,7 @@ fun ChatContent( mutableStateOf(false) } val isDragged by scrollState.interactionSource.collectIsDraggedAsState() - val canLoadMoreSearchResults by remember( - state.searchNextFromMessageId, - state.searchResults.size, - state.searchResultsTotalCount - ) { - derivedStateOf { - state.searchResults.size < state.searchResultsTotalCount || - state.searchNextFromMessageId != 0L - } - } - val searchSenderCandidates by remember(state.searchAvailableSenders, state.otherUser) { - derivedStateOf { - buildList { - addAll(state.searchAvailableSenders) - state.otherUser?.let(::add) - }.distinctBy(UserModel::id) - } - } - val hasSearchFiltersApplied by remember( - state.searchSender, - state.searchDateFromEpochSeconds, - state.searchDateToEpochSeconds - ) { - derivedStateOf { - state.searchSender != null || - state.searchDateFromEpochSeconds != null || - state.searchDateToEpochSeconds != null - } - } + val searchUiState = rememberChatSearchUiState(state) val isAnyViewerOpen = state.fullScreenImages != null || state.fullScreenVideoPath != null || @@ -346,330 +276,6 @@ fun ChatContent( } }) - LaunchedEffect(Unit) { - isVisible = true - if (state.fullScreenVideoPath != null || state.fullScreenVideoMessageId != null) { - component.onDismissVideo() - } - } - - LaunchedEffect(state.messages) { - if (transformedMessageTexts.isEmpty() && originalMessageTexts.isEmpty()) return@LaunchedEffect - val ids = state.messages.map { it.id }.toSet() - transformedMessageTexts.keys.toList().forEach { id -> - if (id !in ids) { - transformedMessageTexts.remove(id) - originalMessageTexts.remove(id) - } - } - } - - // Initial Loading Delay logic - LaunchedEffect( - state.isLoading, - state.messages.isEmpty(), - state.viewAsTopics, - state.currentTopicId, - state.isLoadingTopics, - state.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 - } else { - state.isLoading && state.messages.isEmpty() - } - if (isActuallyLoading) { - if (state.isChatAnimationsEnabled) delay(200) - showInitialLoading = true - } else { - showInitialLoading = false - } - } - - // Unified command-based scrolling: restore, jump, bottom. - LaunchedEffect(state.pendingScrollCommand, isComments) { - val command = state.pendingScrollCommand ?: return@LaunchedEffect - - val leadingItems = chatContentLeadingItemsCount( - isComments = isComments, - showNavPadding = false, - isLoadingOlder = state.isLoadingOlder, - isLoadingNewer = state.isLoadingNewer, - isAtBottom = state.isAtBottom, - hasMessages = groupedMessages.isNotEmpty() - ) - - when (command) { - is ChatScrollCommand.RestoreViewport -> { - if (command.atBottom || command.anchorMessageId == null) { - scrollState.scrollToChatBottomStaged( - isComments = isComments, - animated = false - ) - } else { - val groupedIndex = groupedMessageIndexById[command.anchorMessageId] - ?: awaitGroupedIndex( - messageId = command.anchorMessageId, - groupedMessageIndexByIdProvider = { groupedMessageIndexById } - ) - ?: -1 - if (groupedIndex >= 0) { - val targetIndex = groupedIndexToLazyIndex(groupedIndex, leadingItems) - scrollState.restoreViewportAtIndex( - targetIndex = targetIndex, - anchorOffsetPx = command.anchorOffsetPx - ) - } else { - scrollState.scrollToChatBottomStaged( - isComments = isComments, - animated = false - ) - } - } - component.onScrollCommandConsumed() - } - - is ChatScrollCommand.JumpToMessage -> { - val groupedIndex = groupedMessageIndexById[command.messageId] - ?: awaitGroupedIndex( - messageId = command.messageId, - groupedMessageIndexByIdProvider = { groupedMessageIndexById } - ) - ?: -1 - if (groupedIndex >= 0) { - val targetIndex = groupedIndexToLazyIndex(groupedIndex, leadingItems) - scrollState.scrollToMessageIndex( - index = targetIndex, - align = command.align, - animated = command.animated && state.isChatAnimationsEnabled, - staged = true - ) - } - component.onScrollCommandConsumed() - } - - is ChatScrollCommand.ScrollToBottom -> { - scrollState.scrollToChatBottomStaged( - isComments = isComments, - animated = command.animated && state.isChatAnimationsEnabled - ) - component.onScrollCommandConsumed() - } - - is ChatScrollCommand.ScrollToStart -> { - scrollState.scrollToChatStartStaged( - animated = command.animated && state.isChatAnimationsEnabled - ) - component.onScrollCommandConsumed() - } - } - } - - // Unified bottom-status + bottom-button controller with hysteresis/debounce for smoothness. - LaunchedEffect( - scrollState, - isComments, - isForumList, - showInitialLoading, - isDragged - ) { - var lastReportedBottomState: Boolean? = null - snapshotFlow { - BottomVisibilitySnapshot( - isAtBottom = scrollState.isAtBottom( - isComments = isComments, - isLatestLoaded = state.isLatestLoaded - ), - isNearBottom = scrollState.isNearBottom( - isComments = isComments - ), - unreadCount = state.unreadCount - ) - } - .distinctUntilChanged() - .collectLatest { snapshot -> - if (lastReportedBottomState != snapshot.isAtBottom) { - component.onBottomReached(snapshot.isAtBottom) - lastReportedBottomState = snapshot.isAtBottom - } - - if (snapshot.isNearBottom) { - hasUserScrolledAwayFromBottom = false - } else if (isDragged) { - hasUserScrolledAwayFromBottom = true - } - - val shouldShow = !isForumList && - !showInitialLoading && - (snapshot.unreadCount > 0 || (hasUserScrolledAwayFromBottom && !snapshot.isNearBottom)) - - if (shouldShow) { - showScrollToBottomButton = true - } else { - delay(120) - val keepVisible = snapshot.unreadCount > 0 || - (hasUserScrolledAwayFromBottom && !snapshot.isNearBottom) - if (!keepVisible) { - showScrollToBottomButton = false - } - } - } - } - - // Save full viewport (anchor + pixel offset) for precise restore after reopen. - LaunchedEffect( - scrollState, - groupedMessages, - isComments, - state.isLatestLoaded, - state.isLoadingOlder, - state.isLoadingNewer, - state.isAtBottom - ) { - snapshotFlow { - buildViewportSnapshot( - scrollState = scrollState, - groupedMessages = groupedMessages, - isComments = isComments, - isLatestLoaded = state.isLatestLoaded, - isLoadingOlder = state.isLoadingOlder, - isLoadingNewer = state.isLoadingNewer, - isAtBottom = state.isAtBottom, - showNavPadding = false - ) - } - .filterNotNull() - .distinctUntilChanged() - .debounce(120) - .collect { viewport -> - component.updateViewport(viewport) - } - } - - DisposableEffect( - scrollState, - groupedMessages, - isComments, - state.currentTopicId, - state.isLatestLoaded, - state.isLoadingOlder, - state.isLoadingNewer, - state.isAtBottom - ) { - onDispose { - val viewport = buildViewportSnapshot( - scrollState = scrollState, - groupedMessages = groupedMessages, - isComments = isComments, - isLatestLoaded = state.isLatestLoaded, - isLoadingOlder = state.isLoadingOlder, - isLoadingNewer = state.isLoadingNewer, - isAtBottom = state.isAtBottom, - showNavPadding = false - ) - if (viewport != null) { - component.updateViewport(viewport) - } - } - } - - // Performance: Update visible range for repository - LaunchedEffect(scrollState, groupedMessages) { - snapshotFlow { scrollState.layoutInfo.visibleItemsInfo } - .map { visibleItems -> - val currentState = latestUiState.value - val leadingItemsCount = chatContentLeadingItemsCount( - isComments = currentState.rootMessage != null, - showNavPadding = false, - isLoadingOlder = currentState.isLoadingOlder, - isLoadingNewer = currentState.isLoadingNewer, - isAtBottom = currentState.isAtBottom, - hasMessages = groupedMessages.isNotEmpty() - ) - val visibleIds = LinkedHashSet() - val nearbyIds = LinkedHashSet() - if (visibleItems.isNotEmpty()) { - val minIndex = visibleItems.minOf { it.index } - val maxIndex = visibleItems.maxOf { it.index } - - visibleItems.forEach { item -> - val groupedIndex = lazyIndexToGroupedIndex(item.index, leadingItemsCount) - groupedMessages.getOrNull(groupedIndex)?.let { grouped -> - when (grouped) { - is GroupedMessageItem.Single -> visibleIds.add(grouped.message.id) - is GroupedMessageItem.Album -> grouped.messages.forEach { message -> - visibleIds.add(message.id) - } - } - } - } - - val nearbyStart = (minIndex - 5).coerceAtLeast(0) - val nearbyEnd = maxIndex + 5 - for (index in nearbyStart..nearbyEnd) { - if (index in minIndex..maxIndex) continue - val groupedIndex = lazyIndexToGroupedIndex(index, leadingItemsCount) - groupedMessages.getOrNull(groupedIndex)?.let { grouped -> - when (grouped) { - is GroupedMessageItem.Single -> nearbyIds.add(grouped.message.id) - is GroupedMessageItem.Album -> grouped.messages.forEach { message -> - nearbyIds.add(message.id) - } - } - } - } - } - val visibleIdList = visibleIds.toList() - visibleIdList to nearbyIds.filterNot(visibleIds::contains) - } - .distinctUntilChanged() - .debounce(100) - .collect { (visibleIds, nearbyIds) -> - (component as? DefaultChatComponent)?.let { - it.repositoryMessage.updateVisibleRange(it.chatId, visibleIds, nearbyIds) - } - } - } - - // 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 - - val isAtBottomNow = scrollState.isAtBottom( - isComments = isComments, - isLatestLoaded = state.isLatestLoaded - ) - if ((state.isAtBottom || isAtBottomNow) && - !state.isLoading && - !state.isLoadingOlder && - !state.isLoadingNewer && - !scrollState.isScrollInProgress - ) { - scrollState.scrollToChatBottomStaged( - isComments = isComments, - animated = state.isChatAnimationsEnabled - ) - } - } - - // Scroll Management - LaunchedEffect(isDragged) { - if (isDragged) { - focusManager.clearFocus() - keyboardController?.hide() - } - } - - LaunchedEffect(state.showBotCommands, isRecordingVideo) { - if (state.showBotCommands || isRecordingVideo) { - focusManager.clearFocus(force = true) - keyboardController?.hide() - } - } - // Pick Media Result val pickMedia = rememberLauncherForActivityResult(ActivityResultContracts.PickMultipleVisualMedia()) { uris -> val albumPaths = mutableListOf() @@ -691,7 +297,6 @@ fun ChatContent( if (albumPaths.isNotEmpty()) pendingDocumentPaths = emptyList() } - val shouldAnimateContentEntrance = state.isChatAnimationsEnabled && isOverlay val contentAlpha by animateFloatAsState( targetValue = if (isVisible || !shouldAnimateContentEntrance) 1f else 0f, @@ -704,314 +309,60 @@ fun ChatContent( label = "ContentOffset" ) - val canWriteText by remember(state.isAdmin, state.permissions.canSendBasicMessages) { - derivedStateOf { state.isAdmin || state.permissions.canSendBasicMessages } - } - val canSendPhotos by remember(state.isAdmin, state.permissions.canSendPhotos) { - derivedStateOf { state.isAdmin || state.permissions.canSendPhotos } - } - val canSendVideos by remember(state.isAdmin, state.permissions.canSendVideos) { - derivedStateOf { state.isAdmin || state.permissions.canSendVideos } - } - val canSendDocuments by remember(state.isAdmin, state.permissions.canSendDocuments) { - derivedStateOf { state.isAdmin || state.permissions.canSendDocuments } - } - val canSendAudios by remember(state.isAdmin, state.permissions.canSendAudios) { - derivedStateOf { state.isAdmin || state.permissions.canSendAudios } - } - val canUseMediaPicker by remember(canSendPhotos, canSendVideos) { - derivedStateOf { canSendPhotos || canSendVideos } - } - val canUseDocumentPicker by remember(canSendDocuments, canSendAudios) { - derivedStateOf { canSendDocuments || canSendAudios } - } - val canSendPolls by remember(state.isAdmin, state.permissions.canSendPolls) { - derivedStateOf { state.isAdmin || state.permissions.canSendPolls } - } - val canOpenAttachSheet by remember( - canUseMediaPicker, - canUseDocumentPicker, - canSendPolls, - state.attachMenuBots - ) { - derivedStateOf { canUseMediaPicker || canUseDocumentPicker || canSendPolls || state.attachMenuBots.isNotEmpty() } - } - val canSendStickers by remember(state.isAdmin, state.permissions.canSendOtherMessages) { - derivedStateOf { state.isAdmin || state.permissions.canSendOtherMessages } - } - val canSendVoice by remember(state.isAdmin, state.permissions.canSendVoiceNotes) { - derivedStateOf { state.isAdmin || state.permissions.canSendVoiceNotes } - } - val canSendVideoNotes by remember(state.isAdmin, state.permissions.canSendVideoNotes) { - derivedStateOf { state.isAdmin || state.permissions.canSendVideoNotes } - } - val canSendAnything by remember( - canWriteText, - canOpenAttachSheet, - canSendStickers, - canSendVoice, - canSendVideoNotes, - canSendPolls - ) { - derivedStateOf { - canWriteText || canOpenAttachSheet || canSendStickers || canSendVoice || canSendVideoNotes || canSendPolls - } - } - - val messageListState = remember( - state.chatId, - state.currentTopicId, - displayMessages, - state.selectedMessageIds, - state.unreadSeparatorCount, - state.unreadSeparatorLastReadInboxMessageId, - state.viewAsTopics, - state.topics, - state.rootMessage, - state.isLoading, - state.isLoadingOlder, - state.isLoadingNewer, - state.isAtBottom, - state.isLatestLoaded, - state.isOldestLoaded, - state.isGroup, - state.isChannel, - state.isAdmin, - state.canWrite, - canSendAnything, - state.highlightedMessageId, - state.fontSize, - state.letterSpacing, - state.bubbleRadius, - state.stickerSize, - state.autoDownloadMobile, - state.autoDownloadWifi, - state.autoDownloadRoaming, - state.autoDownloadFiles, - state.autoplayGifs, - state.autoplayVideos, - state.showLinkPreviews, - state.isChatAnimationsEnabled, - showInitialLoading, - state.pendingScrollCommand - ) { - ChatMessageListUiState( - chatId = state.chatId, - currentTopicId = state.currentTopicId, - messages = displayMessages, - selectedMessageIds = state.selectedMessageIds, - unreadSeparatorCount = state.unreadSeparatorCount, - unreadSeparatorLastReadInboxMessageId = state.unreadSeparatorLastReadInboxMessageId, - viewAsTopics = state.viewAsTopics, - topics = state.topics, - rootMessage = state.rootMessage, - isLoading = state.isLoading, - isLoadingOlder = state.isLoadingOlder, - isLoadingNewer = state.isLoadingNewer, - isAtBottom = state.isAtBottom, - isLatestLoaded = state.isLatestLoaded, - isOldestLoaded = state.isOldestLoaded, - isGroup = state.isGroup, - isChannel = state.isChannel, - isAdmin = state.isAdmin, - canWrite = state.canWrite, - canSendAnything = canSendAnything, - highlightedMessageId = state.highlightedMessageId, - 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, - showLinkPreviews = state.showLinkPreviews, - isChatAnimationsEnabled = state.isChatAnimationsEnabled, - suppressEntryAnimations = showInitialLoading || state.pendingScrollCommand != null - ) - } - - val showInputBar by remember( - state.isChannel, - state.isGroup, - state.canWrite, - state.isCurrentUserRestricted, - state.currentTopicId, - state.selectedMessageIds, - state.viewAsTopics, - state.isSearchActive, - isRecordingVideo - ) { - derivedStateOf { - (state.canWrite || state.isCurrentUserRestricted) && - !isRecordingVideo && - !state.isSearchActive && - state.selectedMessageIds.isEmpty() && - (!state.viewAsTopics || state.currentTopicId != null) - } - } - - val showJoinButton by remember( - showInputBar, - state.isMember, - state.isChannel, - state.isGroup, - state.canWrite, - state.isCurrentUserRestricted, - state.selectedMessageIds, - state.viewAsTopics, - state.currentTopicId, - state.isSearchActive, - isRecordingVideo - ) { - derivedStateOf { - !showInputBar && - !state.isSearchActive && - !state.isMember && - (state.isChannel || state.isGroup) && - !state.canWrite && - !state.isCurrentUserRestricted && - !isRecordingVideo && - state.selectedMessageIds.isEmpty() && - (!state.viewAsTopics || state.currentTopicId != null) - } - } + val permissionState = rememberChatContentPermissionState(state) + val messageListState = rememberChatMessageListState( + state = state, + displayMessages = displayMessages, + canSendAnything = permissionState.canSendAnything, + showInitialLoading = showInitialLoading + ) + val chromeState = rememberChatChromeState( + state = state, + isRecordingVideo = isRecordingVideo, + editingPhotoPath = editingPhotoPath, + editingVideoPath = editingVideoPath, + selectedMessageId = selectedMessageId + ) var containerSize by remember { mutableStateOf(IntSize.Zero) } var renderPinnedMessagesList by rememberSaveable { mutableStateOf(state.showPinnedMessagesList) } var pendingPinnedSheetAction by remember { mutableStateOf<(() -> Unit)?>(null) } - LaunchedEffect(state.showPinnedMessagesList) { - if (state.showPinnedMessagesList) { - renderPinnedMessagesList = true - } - } - - LaunchedEffect(state.isSearchActive) { - if (state.isSearchActive) { - showSearchFilters = false - showSearchSenderPicker = false - if (state.showPinnedMessagesList) { - component.onDismissPinnedMessages() - } - } - } + ChatContentEffects( + component = component, + state = state, + scrollState = scrollState, + groupedMessages = groupedMessages, + groupedMessageIndexById = groupedMessageIndexById, + isComments = isComments, + isForumList = isForumList, + isDragged = isDragged, + isRecordingVideo = isRecordingVideo, + showInitialLoading = showInitialLoading, + hasUserScrolledAwayFromBottom = hasUserScrolledAwayFromBottom, + transformedMessageTexts = transformedMessageTexts, + originalMessageTexts = originalMessageTexts, + onVisible = { + isVisible = true + }, + onShowInitialLoadingChanged = { showInitialLoading = it }, + onHasUserScrolledAwayFromBottomChanged = { hasUserScrolledAwayFromBottom = it }, + onShowScrollToBottomButtonChanged = { showScrollToBottomButton = it }, + onHideKeyboardAndClearFocus = { force -> + focusManager.clearFocus(force = force) + keyboardController?.hide() + }, + onRenderPinnedMessagesListChanged = { renderPinnedMessagesList = it }, + onSearchFiltersChanged = { showSearchFilters = it }, + onSearchSenderPickerChanged = { showSearchSenderPicker = it } + ) val requestPinnedMessagesListDismiss = { if (state.showPinnedMessagesList) { component.onDismissPinnedMessages() } } - - val isCustomBackHandlingEnabled by remember( - 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, - state.isSearchActive - ) { - 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 || - state.isSearchActive - } - } - val selectedCount = state.selectedMessageIds.size - val selectedMessageIdSet by remember(state.selectedMessageIds) { - derivedStateOf { state.selectedMessageIds.toHashSet() } - } - val canRevokeSelected by remember(state.messages, selectedMessageIdSet) { - derivedStateOf { - if (selectedMessageIdSet.isEmpty()) { - false - } else { - state.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 - ) { - 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 = if (state.isSearchActive) null else state.pinnedMessage, - pinnedMessageCount = if (state.isSearchActive) 0 else state.pinnedMessageCount - ) - } + val topBarUiState = rememberChatTopBarUiState(state) CompositionLocalProvider(LocalLinkHandler provides { component.onLinkClick(it) }) { val statusBarHeight = with(density) { WindowInsets.statusBars.getTop(this).toDp() } @@ -1067,8 +418,8 @@ fun ChatContent( ) { ChatContentTopBar( topBarState = topBarUiState, - selectedCount = selectedCount, - canRevokeSelected = canRevokeSelected, + selectedCount = chromeState.selectedCount, + canRevokeSelected = chromeState.canRevokeSelected, component = component, contentAlpha = contentAlpha, onBack = { @@ -1091,196 +442,46 @@ fun ChatContent( } }, bottomBar = { - if (showInputBar) { - val inputBarState = - remember(state, pendingMediaPaths, pendingDocumentPaths) { - ChatInputBarState( - replyMessage = state.replyMessage, - editingMessage = state.editingMessage, - draftText = state.draftText, - pendingMediaPaths = pendingMediaPaths, - pendingDocumentPaths = pendingDocumentPaths, - isClosed = state.topics.find { it.id.toLong() == state.currentTopicId }?.isClosed - ?: false, - permissions = state.effectiveInputPermissions - ?: 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 - ) - } + if (chromeState.showInputBar) { + val inputBarState = rememberChatInputBarState( + state = state, + pendingMediaPaths = pendingMediaPaths, + pendingDocumentPaths = pendingDocumentPaths + ) - val inputBarActions = - remember(component, pendingMediaPaths, pendingDocumentPaths) { - ChatInputBarActions( - onSend = { text, entities, options -> - component.onSendMessage( - text, - entities, - options - ) - }, - onStickerClick = { component.onSendSticker(it) }, - onGifClick = { component.onSendGif(it) }, - onAttachClick = { - pickMedia.launch( - PickVisualMediaRequest( - ActivityResultContracts.PickVisualMedia.ImageAndVideo - ) - ) - }, - onCameraClick = { - keyboardController?.hide() - focusManager.clearFocus(force = true) - isRecordingVideo = true - }, - onSendVoice = { path, duration, waveform -> - component.onSendVoice(path, duration, waveform) - }, - onCancelReply = { component.onCancelReply() }, - onCancelEdit = { component.onCancelEdit() }, - onSaveEdit = { t, e -> component.onSaveEditedMessage(t, e) }, - onDraftChange = { component.onDraftChange(it) }, - onTyping = { component.onTyping() }, - onCancelMedia = { pendingMediaPaths = emptyList() }, - onSendMedia = { paths, caption, captionEntities, options -> - if (options.sendAsDocument) { - if (paths.size > 1) { - component.onSendAlbum( - paths, - caption, - captionEntities, - options - ) - } else { - paths.firstOrNull()?.let { - component.onSendDocument( - it, - caption, - captionEntities, - options - ) - } - } - } else if (paths.size > 1) { - component.onSendAlbum( - paths, - caption, - captionEntities, - options - ) - } else { - paths.firstOrNull()?.let { - if (it.endsWith(".mp4")) component.onSendVideo( - it, - caption, - captionEntities, - options - ) - else component.onSendPhoto( - it, - caption, - captionEntities, - options - ) - } - } - pendingMediaPaths = emptyList() - pendingDocumentPaths = emptyList() - }, - onSendDocuments = { paths, caption, captionEntities, options -> - paths.forEachIndexed { index, path -> - component.onSendDocument( - path, - caption = if (index == 0) caption else "", - captionEntities = if (index == 0) captionEntities else emptyList(), - sendOptions = options - ) - } - pendingDocumentPaths = emptyList() - pendingMediaPaths = emptyList() - }, - onMediaOrderChange = { - pendingMediaPaths = it - if (it.isNotEmpty()) { - pendingDocumentPaths = emptyList() - } - }, - onDocumentOrderChange = { - pendingDocumentPaths = it - if (it.isNotEmpty()) { - pendingMediaPaths = emptyList() - } - }, - onMediaClick = { path -> - if (path.endsWith(".mp4")) { - editingVideoPath = path - } else { - editingPhotoPath = path - } - }, - onShowBotCommands = { - keyboardController?.hide() - focusManager.clearFocus(force = true) - component.onShowBotCommands() - }, - onReplyMarkupButtonClick = { - component.onReplyMarkupButtonClick( - 0, - it, - if (state.isBot) state.chatId else 0L - ) - }, - onOpenMiniApp = { url, name -> - component.onOpenMiniApp( - url, - name, - if (state.isBot) state.chatId else 0L - ) - }, - onMentionQueryChange = { component.onMentionQueryChange(it) }, - onInlineQueryChange = { bot, query -> - component.onInlineQueryChange(bot, query) - }, - onLoadMoreInlineResults = { offset -> - component.onLoadMoreInlineResults(offset) - }, - onSendInlineResult = { resultId -> component.onSendInlineResult(resultId) }, - onInlineSwitchPm = { botUsername, parameter -> - val encodedParameter = URLEncoder.encode( - parameter, - StandardCharsets.UTF_8.name() + val inputBarActions = rememberChatInputBarActions( + component = component, + state = state, + pendingMediaPaths = pendingMediaPaths, + pendingDocumentPaths = pendingDocumentPaths, + onPickMedia = { + pickMedia.launch( + PickVisualMediaRequest( + ActivityResultContracts.PickVisualMedia.ImageAndVideo ) - component.onLinkClick("https://t.me/$botUsername?start=$encodedParameter") - }, - onAttachBotClick = { bot -> - component.onOpenAttachBot(bot.botUserId, bot.name) - }, - onSendPoll = { poll -> - component.onSendPoll(poll) - }, - onRefreshScheduledMessages = { component.onRefreshScheduledMessages() }, - onEditScheduledMessage = { message -> component.onEditMessage(message) }, - onDeleteScheduledMessage = { message -> component.onDeleteMessage(message) }, - onSendScheduledNow = { message -> component.onSendScheduledNow(message) } - ) - } + ) + }, + onHideKeyboardAndClearFocus = { + keyboardController?.hide() + focusManager.clearFocus(force = true) + }, + onStartRecordingVideo = { + isRecordingVideo = true + }, + onSetPendingMediaPaths = { paths -> + pendingMediaPaths = paths + }, + onSetPendingDocumentPaths = { paths -> + pendingDocumentPaths = paths + }, + onEditMediaPath = { path -> + if (path.endsWith(".mp4")) { + editingVideoPath = path + } else { + editingPhotoPath = path + } + } + ) ChatInputBar( state = inputBarState, @@ -1288,7 +489,7 @@ fun ChatContent( appPreferences = component.appPreferences, stickerRepository = component.stickerRepository ) - } else if (showJoinButton) { + } else if (chromeState.showJoinButton) { Box( modifier = Modifier .fillMaxWidth() @@ -1316,7 +517,7 @@ fun ChatContent( Box( modifier = Modifier .fillMaxSize() - .padding(bottom = if (!state.canWrite && !showJoinButton) 0.dp else padding.calculateBottomPadding()) + .padding(bottom = if (!state.canWrite && !chromeState.showJoinButton) 0.dp else padding.calculateBottomPadding()) .consumeWindowInsets(padding) .onGloballyPositioned { coordinates -> contentRect = Rect( @@ -1549,214 +750,58 @@ fun ChatContent( onForwardOriginClick = onForwardOriginClickStable, downloadUtils = component.downloadUtils, isAnyViewerOpen = isAnyViewerOpen, - bottomContentPadding = if (state.rootMessage != null && (showInputBar || showJoinButton)) 120.dp else 8.dp + bottomContentPadding = if (state.rootMessage != null && (chromeState.showInputBar || chromeState.showJoinButton)) 120.dp else 8.dp ) AnimatedVisibility( visible = state.isSearchActive, - enter = fadeIn(animationSpec = tween(220)) + - slideInVertically( - animationSpec = tween(280), - initialOffsetY = { it / 3 } - ) + - scaleIn( - animationSpec = tween(220), - initialScale = 0.96f - ), - exit = fadeOut(animationSpec = tween(160)) + - slideOutVertically( - animationSpec = tween(180), - targetOffsetY = { it / 4 } - ), modifier = Modifier .align(Alignment.BottomCenter) .padding(horizontal = 12.dp, vertical = 16.dp) ) { - Column( - modifier = Modifier - .fillMaxWidth() - .windowInsetsPadding(WindowInsets.navigationBars), - verticalArrangement = Arrangement.spacedBy(10.dp) - ) { - AnimatedVisibility( - visible = showAllSearchResults && state.searchResults.isNotEmpty(), - enter = fadeIn(animationSpec = tween(180)) + - slideInVertically( - animationSpec = tween(240), - initialOffsetY = { it / 8 } - ), - exit = fadeOut(animationSpec = tween(140)) + - slideOutVertically( - animationSpec = tween(180), - targetOffsetY = { it / 10 } - ) - ) { - SearchResultsListOverlay( - query = state.searchQuery, - results = state.searchResults, - selectedIndex = state.selectedSearchResultIndex, - isSearching = state.isSearchingMessages, - canLoadMore = canLoadMoreSearchResults, - onLoadMore = component::onLoadMoreSearchResults, - onResultClick = { index -> - showAllSearchResults = false - component.onSearchResultClick(index) - } - ) - } - - AnimatedVisibility( - visible = showSearchFilters && showSearchSenderPicker, - enter = fadeIn(animationSpec = tween(180)) + - slideInVertically( - animationSpec = tween(220), - initialOffsetY = { it / 6 } - ) + - scaleIn( - animationSpec = tween(200), - initialScale = 0.98f - ), - exit = fadeOut(animationSpec = tween(140)) + - slideOutVertically( - animationSpec = tween(160), - targetOffsetY = { it / 8 } - ) - ) { - SearchSenderPickerOverlay( - selectedSenderId = state.searchSender?.id, - senders = searchSenderCandidates, - onSelectSender = { user -> - showSearchSenderPicker = false - component.onSearchSenderChange(user) - } - ) - } - - AnimatedVisibility( - visible = showSearchFilters, - enter = fadeIn(animationSpec = tween(180)) + - slideInVertically( - animationSpec = tween(220), - initialOffsetY = { it / 6 } - ) + - scaleIn( - animationSpec = tween(200), - initialScale = 0.98f - ), - exit = fadeOut(animationSpec = tween(140)) + - slideOutVertically( - animationSpec = tween(160), - targetOffsetY = { it / 8 } - ) - ) { - SearchFilterTray( - selectedSender = state.searchSender, - fromEpochSeconds = state.searchDateFromEpochSeconds, - toEpochSeconds = state.searchDateToEpochSeconds, - onToggleSenderPicker = { - showSearchSenderPicker = !showSearchSenderPicker - }, - onApplyToday = { - val now = LocalDate.now() - component.onSearchDateRangeChange( - now.atStartOfDay(ZoneId.systemDefault()) - .toEpochSecond().toInt(), - now.plusDays(1) - .atStartOfDay(ZoneId.systemDefault()) - .toEpochSecond().toInt() - 1 - ) - }, - onApplyLastDays = { days -> - val now = LocalDate.now() - val from = now.minusDays((days - 1).toLong()) - component.onSearchDateRangeChange( - from.atStartOfDay(ZoneId.systemDefault()) - .toEpochSecond().toInt(), - now.plusDays(1) - .atStartOfDay(ZoneId.systemDefault()) - .toEpochSecond().toInt() - 1 - ) - }, - onResetDateRange = { - component.onSearchDateRangeChange(null, null) - }, - onPickFromDate = { - showSearchDatePicker( - context = context, - initialEpochSeconds = state.searchDateFromEpochSeconds, - onDateSelected = { date -> - val nextFrom = - toStartOfDayEpochSeconds(date) - val nextTo = state.searchDateToEpochSeconds - ?.let(::epochSecondsToLocalDate) - ?.let { currentTo -> - if (currentTo.isBefore(date)) { - toEndOfDayEpochSeconds(date) - } else { - toEndOfDayEpochSeconds(currentTo) - } - } - component.onSearchDateRangeChange( - nextFrom, - nextTo - ) - } - ) - }, - onPickToDate = { - showSearchDatePicker( - context = context, - initialEpochSeconds = state.searchDateToEpochSeconds, - onDateSelected = { date -> - val nextTo = toEndOfDayEpochSeconds(date) - val nextFrom = - state.searchDateFromEpochSeconds - ?.let(::epochSecondsToLocalDate) - ?.let { currentFrom -> - if (currentFrom.isAfter(date)) { - toStartOfDayEpochSeconds( - date - ) - } else { - toStartOfDayEpochSeconds( - currentFrom - ) - } - } - component.onSearchDateRangeChange( - nextFrom, - nextTo - ) - } - ) - } - ) - } - - SearchNavigationPanel( - query = state.searchQuery, - results = state.searchResults, - totalCount = state.searchResultsTotalCount, - selectedIndex = state.selectedSearchResultIndex, - isSearching = state.isSearchingMessages, - showAllResults = showAllSearchResults, - filtersExpanded = showSearchFilters, - hasFiltersApplied = hasSearchFiltersApplied, - onPrevious = component::onSearchPreviousResult, - onNext = component::onSearchNextResult, - onToggleShowAll = { - showAllSearchResults = !showAllSearchResults - }, - onToggleFilters = { - val nextExpanded = !showSearchFilters - showSearchFilters = nextExpanded - if (!nextExpanded) { - showSearchSenderPicker = false - } - } - ) - } - } + ChatContentSearchOverlay( + context = context, + query = state.searchQuery, + results = state.searchResults, + totalCount = state.searchResultsTotalCount, + selectedIndex = state.selectedSearchResultIndex, + isSearching = state.isSearchingMessages, + canLoadMore = searchUiState.canLoadMoreSearchResults, + showAllResults = showAllSearchResults, + showSearchFilters = showSearchFilters, + showSearchSenderPicker = showSearchSenderPicker, + hasFiltersApplied = searchUiState.hasSearchFiltersApplied, + selectedSender = state.searchSender, + searchSenderCandidates = searchUiState.searchSenderCandidates, + fromEpochSeconds = state.searchDateFromEpochSeconds, + toEpochSeconds = state.searchDateToEpochSeconds, + onLoadMore = component::onLoadMoreSearchResults, + onResultClick = { index -> + showAllSearchResults = false + component.onSearchResultClick(index) + }, + onPrevious = component::onSearchPreviousResult, + onNext = component::onSearchNextResult, + onToggleShowAll = { + showAllSearchResults = !showAllSearchResults + }, + onToggleFilters = { + val nextExpanded = !showSearchFilters + showSearchFilters = nextExpanded + if (!nextExpanded) { + showSearchSenderPicker = false + } + }, + onToggleSenderPicker = { + showSearchSenderPicker = !showSearchSenderPicker + }, + onSelectSender = { user -> + showSearchSenderPicker = false + component.onSearchSenderChange(user) + }, + onApplyDateRange = component::onSearchDateRangeChange + ) + } AnimatedVisibility( visible = showScrollToBottomButton && !state.isSearchActive, @@ -1868,1083 +913,100 @@ 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, - onDismissRequest = requestPinnedMessagesListDismiss, - onHidden = { - renderPinnedMessagesList = false - pendingPinnedSheetAction?.invoke() - pendingPinnedSheetAction = null - }, - onMessageClick = { - pendingPinnedSheetAction = { scrollToMessageState.value(it) } - requestPinnedMessagesListDismiss() - }, - onUnpin = { component.onUnpinMessage(it) }, - onReplyClick = { - pendingPinnedSheetAction = { scrollToMessageState.value(it) } - requestPinnedMessagesListDismiss() - }, - onReactionClick = { id, r -> component.onSendReaction(id, r) }, - downloadUtils = component.downloadUtils, - isAnyViewerOpen = isAnyViewerOpen - ) - } - - state.selectedStickerSet?.let { stickerSet -> - StickerSetSheet( - stickerSet = stickerSet, - onDismiss = { component.onDismissStickerSet() }, - onStickerClick = { _, path -> component.onSendSticker(path) } - ) - } - - if (state.showPollVoters) { - PollVotersSheet( - voters = state.pollVoters, - isLoading = state.isPollVotersLoading, - onUserClick = { - component.onDismissVoters() - component.toProfile(it) - }, - onDismiss = { component.onDismissVoters() } - ) - } - - if (state.showBotCommands) { - BotCommandsSheet( - commands = state.botCommands, - onCommandClick = { component.onBotCommandClick(it) }, - onDismiss = { component.onDismissBotCommands() } - ) - } - - /*ChatContentViewers( + ChatContentOverlays( state = state, component = component, - localClipboard = localClipboard - )*/ - - selectedMessage?.let { msg -> - ChatMessageOptionsMenu( - state = state, - component = component, - selectedMessage = msg, - menuOffset = menuOffset, - menuMessageSize = menuMessageSize, - clickOffset = clickOffset, - contentRect = contentRect, - groupedMessages = groupedMessages, - downloadUtils = component.downloadUtils, - localClipboard = localClipboard, - canRestoreOriginalText = originalMessageTexts.containsKey(msg.id), - onApplyTransformedText = { newText -> - val originalText = msg.extractTextContent() - if (!originalText.isNullOrBlank() && !originalMessageTexts.containsKey(msg.id)) { - originalMessageTexts[msg.id] = originalText - } - transformedMessageTexts[msg.id] = newText - }, - onRestoreOriginalText = { - if (!originalMessageTexts.containsKey(msg.id)) { - return@ChatMessageOptionsMenu - } - transformedMessageTexts.remove(msg.id) - originalMessageTexts.remove(msg.id) - }, - onBlockRequest = { userId -> - pendingBlockUserId = userId - }, - onDismiss = { selectedMessageId = null } - ) - } - - pendingBlockUserId?.let { userId -> - ConfirmationSheet( - icon = Icons.Rounded.Block, - title = stringResource(R.string.block_user_title), - description = stringResource(R.string.block_user_confirmation), - confirmText = stringResource(R.string.action_block), - onConfirm = { - component.onBlockUser(userId) - pendingBlockUserId = null - }, - onDismiss = { pendingBlockUserId = null } - ) - } - - if (state.showReportDialog) { - ReportChatDialog( - onDismiss = { component.onDismissReportDialog() }, - onReasonSelected = { component.onReportReasonSelected(it) } - ) - } - - if (state.restrictUserId != null) { - RestrictUserSheet( - onDismiss = { component.onDismissRestrictDialog() }, - onConfirm = { permissions, untilDate -> component.onConfirmRestrict(permissions, untilDate) } - ) - } - - editingPhotoPath?.let { path -> - Box( - modifier = Modifier - .fillMaxSize() - .zIndex(20f) - ) { - PhotoEditorScreen( - imagePath = path, - onClose = { editingPhotoPath = null }, - onSave = { newPath -> - val newList = pendingMediaPaths.toMutableList() - val index = newList.indexOf(path) - if (index != -1) { - newList[index] = newPath - pendingMediaPaths = newList - } - editingPhotoPath = null - } - ) - } - } - - editingVideoPath?.let { path -> - Box( - modifier = Modifier - .fillMaxSize() - .zIndex(20f) - ) { - VideoEditorScreen( - videoPath = path, - onClose = { editingVideoPath = null }, - onSave = { newPath -> - val newList = pendingMediaPaths.toMutableList() - val index = newList.indexOf(path) - if (index != -1) { - newList[index] = newPath - pendingMediaPaths = newList - } - editingVideoPath = null - } - ) - } - } - - BackHandler(enabled = isCustomBackHandlingEnabled) { - if (editingPhotoPath != null) editingPhotoPath = null - else if (editingVideoPath != null) editingVideoPath = null - else if (state.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.isSearchActive) component.onSearchToggle() - else if (state.currentTopicId != null) component.onTopicClick(0) - } - } - } -} - -private fun MessageModel.extractTextContent(): String? { - return when (val c = content) { - is MessageContent.Text -> c.text - is MessageContent.Photo -> c.caption - is MessageContent.Video -> c.caption - is MessageContent.Gif -> c.caption - is MessageContent.Document -> c.caption - is MessageContent.Audio -> c.caption - else -> null - } -} - -@Composable -private fun SearchNavigationPanel( - query: String, - results: List, - totalCount: Int, - selectedIndex: Int, - isSearching: Boolean, - showAllResults: Boolean, - filtersExpanded: Boolean, - hasFiltersApplied: Boolean, - onPrevious: () -> Unit, - onNext: () -> Unit, - onToggleShowAll: () -> Unit, - onToggleFilters: () -> Unit -) { - val hasResults = results.isNotEmpty() - val selectedPosition = (selectedIndex + 1).takeIf { selectedIndex in results.indices } ?: 0 - val listIconRotation by animateFloatAsState( - targetValue = if (showAllResults) 90f else 0f, - animationSpec = tween(220), - label = "SearchListRotation" - ) - val statusText = when { - isSearching -> stringResource(R.string.search_results_loading) - query.isBlank() -> stringResource(R.string.no_results_found) - else -> stringResource(R.string.no_search_results_format, query) - } - val counterText = stringResource( - R.string.search_results_position_format, - selectedPosition, - totalCount.coerceAtLeast(results.size) - ) - - Surface( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(24.dp), - tonalElevation = 10.dp, - shadowElevation = 14.dp, - color = MaterialTheme.colorScheme.surface.copy(alpha = 0.98f) - ) { - Column( - modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - if (!hasResults) { - AnimatedContent( - targetState = statusText, - transitionSpec = { fadeIn(tween(180)) togetherWith fadeOut(tween(120)) }, - label = "SearchStatusText" - ) { text -> - Text( - text = text, - style = MaterialTheme.typography.labelLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.fillMaxWidth() - ) - } - } - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Surface( - onClick = onToggleFilters, - shape = CircleShape, - color = if (hasFiltersApplied || filtersExpanded) { - MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.92f) - } else { - MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.75f) - }, - tonalElevation = 2.dp - ) { - Box( - modifier = Modifier.padding(horizontal = 10.dp, vertical = 9.dp), - contentAlignment = Alignment.Center - ) { - Icon( - imageVector = Icons.Default.MoreVert, - contentDescription = null, - tint = if (hasFiltersApplied || filtersExpanded) { - MaterialTheme.colorScheme.onPrimaryContainer - } else { - MaterialTheme.colorScheme.onSurfaceVariant - } - ) - } - } - - Surface( - onClick = onToggleShowAll, - shape = CircleShape, - color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.75f), - tonalElevation = 2.dp - ) { - Box( - modifier = Modifier.padding(9.dp), - contentAlignment = Alignment.Center - ) { - Icon( - imageVector = Icons.AutoMirrored.Filled.List, - contentDescription = null, - tint = if (showAllResults) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, - modifier = Modifier.graphicsLayer { - rotationZ = listIconRotation - } - ) - } - } - - Surface( - onClick = onPrevious, - shape = CircleShape, - color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.75f), - tonalElevation = 2.dp - ) { - Box( - modifier = Modifier.padding(9.dp), - contentAlignment = Alignment.Center - ) { - Icon( - imageVector = Icons.Default.KeyboardArrowDown, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface - ) - } - } - - Surface( - modifier = Modifier.weight(1f), - shape = RoundedCornerShape(18.dp), - color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.92f), - tonalElevation = 2.dp - ) { - AnimatedContent( - targetState = counterText, - transitionSpec = { - (fadeIn(tween(180)) + slideInVertically { it / 3 }) togetherWith - (fadeOut(tween(120)) + slideOutVertically { -it / 3 }) - }, - label = "SearchCounter" - ) { animatedCounter -> - Text( - text = animatedCounter, - style = MaterialTheme.typography.labelLarge, - color = MaterialTheme.colorScheme.onPrimaryContainer, - modifier = Modifier.padding(horizontal = 12.dp, vertical = 9.dp), - maxLines = 1 - ) + localClipboard = localClipboard, + groupedMessages = groupedMessages, + isAnyViewerOpen = isAnyViewerOpen, + renderPinnedMessagesList = renderPinnedMessagesList, + requestPinnedMessagesListDismiss = requestPinnedMessagesListDismiss, + onPinnedSheetHidden = { + renderPinnedMessagesList = false + pendingPinnedSheetAction?.invoke() + pendingPinnedSheetAction = null + }, + onPinnedMessageClick = { + pendingPinnedSheetAction = { scrollToMessageState.value(it) } + requestPinnedMessagesListDismiss() + }, + selectedMessage = selectedMessage, + menuOffset = menuOffset, + menuMessageSize = menuMessageSize, + clickOffset = clickOffset, + contentRect = contentRect, + canRestoreOriginalText = selectedMessage?.let { msg -> + originalMessageTexts.containsKey(msg.id) + } == true, + onApplyTransformedText = { newText -> + val msg = selectedMessage ?: return@ChatContentOverlays + val originalText = msg.extractTextContent() + if (!originalText.isNullOrBlank() && !originalMessageTexts.containsKey(msg.id)) { + originalMessageTexts[msg.id] = originalText } - } - - Surface( - onClick = onNext, - shape = CircleShape, - color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.75f), - tonalElevation = 2.dp - ) { - Box( - modifier = Modifier.padding(9.dp), - contentAlignment = Alignment.Center - ) { - Icon( - imageVector = Icons.Default.KeyboardArrowUp, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface - ) + transformedMessageTexts[msg.id] = newText + }, + onRestoreOriginalText = { + val msg = selectedMessage ?: return@ChatContentOverlays + if (!originalMessageTexts.containsKey(msg.id)) { + return@ChatContentOverlays } - } - } - } - } -} - -@Composable -private fun SearchFilterTray( - selectedSender: UserModel?, - fromEpochSeconds: Int?, - toEpochSeconds: Int?, - onToggleSenderPicker: () -> Unit, - onApplyToday: () -> Unit, - onApplyLastDays: (Int) -> Unit, - onResetDateRange: () -> Unit, - onPickFromDate: () -> Unit, - onPickToDate: () -> Unit -) { - Surface( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(24.dp), - tonalElevation = 10.dp, - shadowElevation = 14.dp, - color = MaterialTheme.colorScheme.surface.copy(alpha = 0.98f) - ) { - Column( - modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - SearchSenderChip( - selectedSender = selectedSender, - onClick = onToggleSenderPicker - ) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(6.dp) - ) { - SearchMiniChip( - label = stringResource(R.string.search_date_all), - isActive = fromEpochSeconds == null && toEpochSeconds == null, - modifier = Modifier.weight(1f), - onClick = onResetDateRange - ) - SearchMiniChip( - label = stringResource(R.string.preview_date_today), - isActive = isTodayRange(fromEpochSeconds, toEpochSeconds), - modifier = Modifier.weight(1f), - onClick = onApplyToday - ) - SearchMiniChip( - label = stringResource(R.string.search_date_last_7_days), - isActive = matchesLastDaysRange(fromEpochSeconds, toEpochSeconds, 7), - modifier = Modifier.weight(1f), - onClick = { onApplyLastDays(7) } - ) - SearchMiniChip( - label = stringResource(R.string.search_date_last_30_days), - isActive = matchesLastDaysRange(fromEpochSeconds, toEpochSeconds, 30), - modifier = Modifier.weight(1f), - onClick = { onApplyLastDays(30) } - ) - } - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - SearchRangeChip( - modifier = Modifier.weight(1f), - label = stringResource(R.string.search_date_from), - value = fromEpochSeconds?.let(::formatSearchDate), - onClick = onPickFromDate - ) - SearchRangeChip( - modifier = Modifier.weight(1f), - label = stringResource(R.string.search_date_to), - value = toEpochSeconds?.let(::formatSearchDate), - onClick = onPickToDate - ) - } - } - } -} - -@Composable -private fun SearchSenderPickerOverlay( - selectedSenderId: Long?, - senders: List, - onSelectSender: (UserModel?) -> Unit -) { - Surface( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(22.dp), - tonalElevation = 10.dp, - shadowElevation = 12.dp, - color = MaterialTheme.colorScheme.surface.copy(alpha = 0.985f) - ) { - LazyColumn( - modifier = Modifier - .fillMaxWidth() - .heightIn(max = 280.dp) - .padding(horizontal = 4.dp, vertical = 8.dp), - verticalArrangement = Arrangement.spacedBy(2.dp) - ) { - item("all_senders") { - SearchSenderRow( - title = stringResource(R.string.search_sender_all), - subtitle = stringResource(R.string.search_section_messages), - avatarPath = null, - isSelected = selectedSenderId == null, - onClick = { onSelectSender(null) } - ) - } - - itemsIndexed(senders, key = { _, user -> user.id }) { _, user -> - SearchSenderRow( - title = formatSearchSenderLabel(user), - subtitle = user.username?.takeIf { it.isNotBlank() }?.let { "@$it" }, - avatarPath = user.avatarPath, - isSelected = selectedSenderId == user.id, - onClick = { onSelectSender(user) } - ) - } - } - } -} - -@Composable -private fun SearchSenderRow( - title: String, - subtitle: String?, - avatarPath: String?, - isSelected: Boolean, - onClick: () -> Unit -) { - Surface( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 6.dp) - .clickable(onClick = onClick), - shape = RoundedCornerShape(18.dp), - color = if (isSelected) { - MaterialTheme.colorScheme.primaryContainer - } else { - MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.28f) - }, - tonalElevation = if (isSelected) 2.dp else 0.dp - ) { - Row( - modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp), - horizontalArrangement = Arrangement.spacedBy(10.dp), - verticalAlignment = Alignment.CenterVertically - ) { - AvatarForChat( - path = avatarPath, - name = title, - size = 32.dp - ) - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(2.dp) - ) { - Text( - text = title, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - subtitle?.let { - Text( - text = it, - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - } - } - } -} - -private fun formatSearchSenderLabel(user: UserModel): String { - return listOfNotNull( - user.firstName.takeIf { it.isNotBlank() }, - user.lastName?.takeIf { it.isNotBlank() } - ).joinToString(" ").ifBlank { - user.username?.takeIf { it.isNotBlank() } ?: user.id.toString() - } -} - -@Composable -private fun SearchSenderChip( - selectedSender: UserModel?, - onClick: () -> Unit -) { - val label = selectedSender?.let(::formatSearchSenderLabel) - ?: stringResource(R.string.search_sender_all) - - Surface( - modifier = Modifier - .fillMaxWidth() - .clickable(onClick = onClick), - shape = RoundedCornerShape(18.dp), - color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.34f), - tonalElevation = 1.dp - ) { - Row( - modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp), - horizontalArrangement = Arrangement.spacedBy(10.dp), - verticalAlignment = Alignment.CenterVertically - ) { - AvatarForChat( - path = selectedSender?.avatarPath, - name = label, - size = 30.dp - ) - Text( - text = label, - modifier = Modifier.weight(1f), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - } -} - -@Composable -private fun SearchMiniChip( - label: String, - isActive: Boolean, - modifier: Modifier = Modifier, - onClick: () -> Unit -) { - Surface( - modifier = modifier.clickable(onClick = onClick), - shape = RoundedCornerShape(14.dp), - color = if (isActive) { - MaterialTheme.colorScheme.primaryContainer - } else { - MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.28f) - }, - tonalElevation = if (isActive) 2.dp else 0.dp - ) { - Box( - modifier = Modifier.padding(horizontal = 10.dp, vertical = 7.dp), - contentAlignment = Alignment.Center - ) { - Text( - text = label, - style = MaterialTheme.typography.labelMedium, - color = if (isActive) { - MaterialTheme.colorScheme.onPrimaryContainer - } else { - MaterialTheme.colorScheme.onSurfaceVariant + transformedMessageTexts.remove(msg.id) + originalMessageTexts.remove(msg.id) }, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - } -} - -@Composable -private fun SearchRangeChip( - label: String, - value: String?, - modifier: Modifier = Modifier, - onClick: () -> Unit -) { - Surface( - modifier = modifier.clickable(onClick = onClick), - shape = RoundedCornerShape(16.dp), - color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.28f), - tonalElevation = 0.dp - ) { - Column( - modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp), - verticalArrangement = Arrangement.spacedBy(2.dp) - ) { - Text( - text = label, - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 1 - ) - Text( - text = value ?: stringResource(R.string.cd_select_date), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurface, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - } -} - -private fun isTodayRange(fromEpochSeconds: Int?, toEpochSeconds: Int?): Boolean { - val today = LocalDate.now() - return fromEpochSeconds?.let(::epochSecondsToLocalDate) == today && - toEpochSeconds?.let(::epochSecondsToLocalDate) == today -} - -private fun matchesLastDaysRange(fromEpochSeconds: Int?, toEpochSeconds: Int?, days: Int): Boolean { - val today = LocalDate.now() - return fromEpochSeconds?.let(::epochSecondsToLocalDate) == today.minusDays((days - 1).toLong()) && - toEpochSeconds?.let(::epochSecondsToLocalDate) == today -} - -private fun showSearchDatePicker( - context: android.content.Context, - initialEpochSeconds: Int?, - onDateSelected: (LocalDate) -> Unit -) { - val initialDate = initialEpochSeconds?.let(::epochSecondsToLocalDate) ?: LocalDate.now() - DatePickerDialog( - context, - { _, year, month, dayOfMonth -> - onDateSelected(LocalDate.of(year, month + 1, dayOfMonth)) - }, - initialDate.year, - initialDate.monthValue - 1, - initialDate.dayOfMonth - ).show() -} - -private fun epochSecondsToLocalDate(epochSeconds: Int): LocalDate { - return Instant.ofEpochSecond(epochSeconds.toLong()) - .atZone(ZoneId.systemDefault()) - .toLocalDate() -} - -private fun toStartOfDayEpochSeconds(date: LocalDate): Int { - return date.atStartOfDay(ZoneId.systemDefault()).toEpochSecond().toInt() -} - -private fun toEndOfDayEpochSeconds(date: LocalDate): Int { - return date.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toEpochSecond().toInt() - 1 -} - -private fun formatSearchDate(epochSeconds: Int): String { - return epochSecondsToLocalDate(epochSeconds).format(DateTimeFormatter.ofPattern("dd.MM.yyyy")) -} - -@Composable -private fun SearchResultsListOverlay( - query: String, - results: List, - selectedIndex: Int, - isSearching: Boolean, - canLoadMore: Boolean, - onLoadMore: () -> Unit, - onResultClick: (Int) -> Unit -) { - val listState = rememberLazyListState() - - LaunchedEffect(listState, results.size, canLoadMore, isSearching) { - if (!canLoadMore || isSearching) return@LaunchedEffect - - snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index } - .filterNotNull() - .distinctUntilChanged() - .collectLatest { lastVisibleIndex -> - if (lastVisibleIndex >= results.lastIndex - 4) { - onLoadMore() - } - } - } - - Surface( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(22.dp), - tonalElevation = 10.dp, - shadowElevation = 12.dp, - color = MaterialTheme.colorScheme.surface.copy(alpha = 0.985f) - ) { - Column( - modifier = Modifier.padding(horizontal = 10.dp, vertical = 10.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - LazyColumn( - state = listState, - modifier = Modifier - .fillMaxWidth() - .heightIn(max = 340.dp), - verticalArrangement = Arrangement.spacedBy(6.dp) - ) { - itemsIndexed(results, key = { _, message -> message.id }) { index, message -> - val preview = message.extractTextContent() - ?.replace('\n', ' ') - ?.trim() - ?.takeIf { it.isNotBlank() } - ?: message.senderName.ifBlank { query } - val sender = message.senderName.ifBlank { - stringResource(R.string.search_section_messages) + onDismissMessageOptions = { selectedMessageId = null }, + pendingBlockUserId = pendingBlockUserId, + onRequestBlockUser = { userId -> + pendingBlockUserId = userId + }, + onConfirmBlockUser = { userId -> + component.onBlockUser(userId) + pendingBlockUserId = null + }, + onDismissBlockUser = { pendingBlockUserId = null }, + editingPhotoPath = editingPhotoPath, + onClosePhotoEditor = { editingPhotoPath = null }, + onSavePhotoEditor = { newPath -> + val path = editingPhotoPath ?: return@ChatContentOverlays + val newList = pendingMediaPaths.toMutableList() + val index = newList.indexOf(path) + if (index != -1) { + newList[index] = newPath + pendingMediaPaths = newList } - val isSelected = index == selectedIndex - - Surface( - modifier = Modifier - .fillMaxWidth() - .clickable { onResultClick(index) }, - shape = RoundedCornerShape(18.dp), - color = if (isSelected) { - MaterialTheme.colorScheme.primaryContainer - } else { - MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.32f) - }, - tonalElevation = if (isSelected) 2.dp else 0.dp - ) { - Column( - modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp), - verticalArrangement = Arrangement.spacedBy(2.dp) - ) { - Text( - text = sender, - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.primary - ) - Text( - text = preview, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface, - maxLines = 2 - ) - } + editingPhotoPath = null + }, + editingVideoPath = editingVideoPath, + onCloseVideoEditor = { editingVideoPath = null }, + onSaveVideoEditor = { newPath -> + val path = editingVideoPath ?: return@ChatContentOverlays + val newList = pendingMediaPaths.toMutableList() + val index = newList.indexOf(path) + if (index != -1) { + newList[index] = newPath + pendingMediaPaths = newList } + editingVideoPath = null + }, + isCustomBackHandlingEnabled = chromeState.isCustomBackHandlingEnabled, + onBack = { + if (editingPhotoPath != null) editingPhotoPath = null + else if (editingVideoPath != null) editingVideoPath = null + else if (state.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.isSearchActive) component.onSearchToggle() + else if (state.currentTopicId != null) component.onTopicClick(0) } - } - - if (canLoadMore || isSearching) { - TextButton( - onClick = onLoadMore, - enabled = !isSearching, - modifier = Modifier.align(Alignment.CenterHorizontally) - ) { - Text( - text = if (isSearching) { - stringResource(R.string.search_results_loading) - } else { - stringResource(R.string.action_show_more) - } - ) - } - } - } - } -} - -private fun MessageModel.withUpdatedTextContent(newText: String): MessageModel { - val updatedContent = when (val c = content) { - is MessageContent.Text -> c.copy(text = newText, entities = emptyList(), webPage = null) - is MessageContent.Photo -> c.copy(caption = newText, entities = emptyList()) - is MessageContent.Video -> c.copy(caption = newText, entities = emptyList()) - is MessageContent.Gif -> c.copy(caption = newText, entities = emptyList()) - is MessageContent.Document -> c.copy(caption = newText, entities = emptyList()) - is MessageContent.Audio -> c.copy(caption = newText, entities = emptyList()) - else -> return this - } - return copy(content = updatedContent) -} - -private suspend fun LazyListState.scrollToMessageIndex( - index: Int, - align: ScrollAlign, - animated: Boolean, - staged: Boolean -) { - val total = layoutInfo.totalItemsCount - if (total <= 0) return - - val boundedIndex = index.coerceIn(0, total - 1) - val distance = abs(firstVisibleItemIndex - boundedIndex) - - if (staged && distance > 20) { - val coarseIndex = when { - boundedIndex > firstVisibleItemIndex -> (boundedIndex - 10).coerceAtLeast(0) - boundedIndex < firstVisibleItemIndex -> (boundedIndex + 10).coerceAtMost(total - 1) - else -> boundedIndex - } - scrollToItem(coarseIndex) - } - - scrollToItem(boundedIndex) - - val itemInfo = layoutInfo.visibleItemsInfo.firstOrNull { it.index == boundedIndex } ?: return - val viewportStart = layoutInfo.viewportStartOffset - val viewportEnd = layoutInfo.viewportEndOffset - val viewportCenter = (viewportStart + viewportEnd) / 2 - - val targetPosition = when (align) { - ScrollAlign.Start -> viewportStart - ScrollAlign.Center -> viewportCenter - (itemInfo.size / 2) - ScrollAlign.End -> viewportEnd - itemInfo.size - } - val delta = (itemInfo.offset - targetPosition).toFloat() - - if (abs(delta) > 1f) { - if (animated) { - animateScrollBy(delta) - } else { - scrollBy(delta) - } - } -} - -private data class BottomVisibilitySnapshot( - val isAtBottom: Boolean, - val isNearBottom: Boolean, - val unreadCount: Int -) - -private fun LazyListState.isAtBottom( - isComments: Boolean, - isLatestLoaded: Boolean -): Boolean { - if (!isLatestLoaded) return false - - val info = layoutInfo - val visible = info.visibleItemsInfo - if (visible.isEmpty()) return true - - return if (isComments) { - val lastVisible = visible.last() - lastVisible.index >= info.totalItemsCount - 1 && - abs((info.viewportEndOffset - (lastVisible.offset + lastVisible.size)).toFloat()) <= 40f - } else { - val firstVisible = visible.first() - firstVisible.index == 0 && - abs((firstVisible.offset - info.viewportStartOffset).toFloat()) <= 40f - } -} - -private fun LazyListState.isNearBottom(isComments: Boolean): Boolean { - val info = layoutInfo - val visible = info.visibleItemsInfo - if (visible.isEmpty()) return true - - return if (isComments) { - val lastVisible = visible.last() - val distance = abs((info.viewportEndOffset - (lastVisible.offset + lastVisible.size)).toFloat()) - lastVisible.index >= info.totalItemsCount - 2 && distance <= 240f - } else { - val firstVisible = visible.first() - val distance = abs((firstVisible.offset - info.viewportStartOffset).toFloat()) - firstVisible.index <= 1 && distance <= 240f - } -} - -private suspend fun LazyListState.scrollToChatBottomStaged( - isComments: Boolean, - animated: Boolean -) { - val total = layoutInfo.totalItemsCount - if (total <= 0) return - - val targetIndex = if (isComments) total - 1 else 0 - val distance = abs(firstVisibleItemIndex - targetIndex) - - if (distance > 24) { - val coarse = if (isComments) { - (targetIndex - 8).coerceAtLeast(0) - } else { - (targetIndex + 8).coerceAtMost(total - 1) - } - scrollToItem(coarse) - } - - if (animated) { - animateScrollToItem(targetIndex) - } else { - scrollToItem(targetIndex) - } - - val targetInfo = layoutInfo.visibleItemsInfo.firstOrNull { it.index == targetIndex } - if (targetInfo != null) { - val delta = if (isComments) { - ((targetInfo.offset + targetInfo.size) - layoutInfo.viewportEndOffset).toFloat() - } else { - (targetInfo.offset - layoutInfo.viewportStartOffset).toFloat() - } - if (abs(delta) > 1f) { - scrollBy(delta) - } - } - - scrollToItem(targetIndex) -} - -private suspend fun LazyListState.scrollToChatStartStaged( - animated: Boolean -) { - val total = layoutInfo.totalItemsCount - if (total <= 0) return - - if (animated) { - animateScrollToItem(0) - } else { - scrollToItem(0) - } - - val targetInfo = layoutInfo.visibleItemsInfo.firstOrNull { it.index == 0 } - if (targetInfo != null) { - val delta = (targetInfo.offset - layoutInfo.viewportStartOffset).toFloat() - if (abs(delta) > 1f) { - scrollBy(delta) + ) } } - - scrollToItem(0) -} - -private suspend fun awaitGroupedIndex( - messageId: Long, - groupedMessageIndexByIdProvider: () -> Map, - timeoutMs: Long = 1200L -): Int? { - return withTimeoutOrNull(timeoutMs) { - snapshotFlow { groupedMessageIndexByIdProvider()[messageId] } - .filterNotNull() - .first() - } -} - -private suspend fun LazyListState.restoreViewportAtIndex( - targetIndex: Int, - anchorOffsetPx: Int -) { - val total = layoutInfo.totalItemsCount - if (total <= 0) return - val boundedIndex = targetIndex.coerceIn(0, total - 1) - - scrollToItem(boundedIndex) - val itemInfo = layoutInfo.visibleItemsInfo.firstOrNull { it.index == boundedIndex } ?: return - val viewportStart = layoutInfo.viewportStartOffset - val desiredOffset = viewportStart + anchorOffsetPx - val delta = (itemInfo.offset - desiredOffset).toFloat() - - if (abs(delta) > 1f) { - scrollBy(delta) - } -} - -private fun buildViewportSnapshot( - scrollState: LazyListState, - groupedMessages: List, - isComments: Boolean, - isLatestLoaded: Boolean, - isLoadingOlder: Boolean, - isLoadingNewer: Boolean, - isAtBottom: Boolean, - showNavPadding: Boolean -): ChatViewportCacheEntry? { - if (groupedMessages.isEmpty()) { - return ChatViewportCacheEntry(atBottom = true) - } - - val atBottomNow = scrollState.isAtBottom( - isComments = isComments, - isLatestLoaded = isLatestLoaded - ) - if (atBottomNow) { - return ChatViewportCacheEntry(atBottom = true) - } - - val leadingItems = chatContentLeadingItemsCount( - isComments = isComments, - showNavPadding = showNavPadding, - isLoadingOlder = isLoadingOlder, - isLoadingNewer = isLoadingNewer, - isAtBottom = isAtBottom, - hasMessages = groupedMessages.isNotEmpty() - ) - val info = scrollState.layoutInfo - val anchorItem = info.visibleItemsInfo.firstOrNull { itemInfo -> - val groupedIndex = lazyIndexToGroupedIndex(itemInfo.index, leadingItems) - groupedIndex in groupedMessages.indices - } ?: return null - - val groupedIndex = lazyIndexToGroupedIndex(anchorItem.index, leadingItems) - val anchorMessageId = groupedMessages.getOrNull(groupedIndex)?.firstMessageId ?: return null - - return ChatViewportCacheEntry( - anchorMessageId = anchorMessageId, - anchorOffsetPx = anchorItem.offset - info.viewportStartOffset, - atBottom = false - ) } diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentDerivedState.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentDerivedState.kt new file mode 100644 index 00000000..10951782 --- /dev/null +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentDerivedState.kt @@ -0,0 +1,415 @@ +package org.monogram.presentation.features.chats.currentChat.chatContent + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import org.monogram.domain.models.UserModel +import org.monogram.presentation.features.chats.currentChat.ChatComponent + +@Immutable +internal data class ChatContentPermissionState( + val canWriteText: Boolean, + val canSendAnything: Boolean +) + +@Immutable +internal data class ChatContentSearchUiState( + val canLoadMoreSearchResults: Boolean, + val searchSenderCandidates: List, + val hasSearchFiltersApplied: Boolean +) + +@Immutable +internal data class ChatContentChromeState( + val showInputBar: Boolean, + val showJoinButton: Boolean, + val isCustomBackHandlingEnabled: Boolean, + val selectedCount: Int, + val canRevokeSelected: Boolean +) + +@Composable +internal fun rememberChatContentPermissionState( + state: ChatComponent.State +): ChatContentPermissionState { + val canWriteText by remember(state.isAdmin, state.permissions.canSendBasicMessages) { + derivedStateOf { state.isAdmin || state.permissions.canSendBasicMessages } + } + val canSendPhotos by remember(state.isAdmin, state.permissions.canSendPhotos) { + derivedStateOf { state.isAdmin || state.permissions.canSendPhotos } + } + val canSendVideos by remember(state.isAdmin, state.permissions.canSendVideos) { + derivedStateOf { state.isAdmin || state.permissions.canSendVideos } + } + val canSendDocuments by remember(state.isAdmin, state.permissions.canSendDocuments) { + derivedStateOf { state.isAdmin || state.permissions.canSendDocuments } + } + val canSendAudios by remember(state.isAdmin, state.permissions.canSendAudios) { + derivedStateOf { state.isAdmin || state.permissions.canSendAudios } + } + val canUseMediaPicker by remember(canSendPhotos, canSendVideos) { + derivedStateOf { canSendPhotos || canSendVideos } + } + val canUseDocumentPicker by remember(canSendDocuments, canSendAudios) { + derivedStateOf { canSendDocuments || canSendAudios } + } + val canSendPolls by remember(state.isAdmin, state.permissions.canSendPolls) { + derivedStateOf { state.isAdmin || state.permissions.canSendPolls } + } + val canOpenAttachSheet by remember( + canUseMediaPicker, + canUseDocumentPicker, + canSendPolls, + state.attachMenuBots + ) { + derivedStateOf { + canUseMediaPicker || canUseDocumentPicker || canSendPolls || state.attachMenuBots.isNotEmpty() + } + } + val canSendStickers by remember(state.isAdmin, state.permissions.canSendOtherMessages) { + derivedStateOf { state.isAdmin || state.permissions.canSendOtherMessages } + } + val canSendVoice by remember(state.isAdmin, state.permissions.canSendVoiceNotes) { + derivedStateOf { state.isAdmin || state.permissions.canSendVoiceNotes } + } + val canSendVideoNotes by remember(state.isAdmin, state.permissions.canSendVideoNotes) { + derivedStateOf { state.isAdmin || state.permissions.canSendVideoNotes } + } + val canSendAnything by remember( + canWriteText, + canOpenAttachSheet, + canSendStickers, + canSendVoice, + canSendVideoNotes, + canSendPolls + ) { + derivedStateOf { + canWriteText || canOpenAttachSheet || canSendStickers || canSendVoice || canSendVideoNotes || canSendPolls + } + } + + return remember(canWriteText, canSendAnything) { + ChatContentPermissionState( + canWriteText = canWriteText, + canSendAnything = canSendAnything + ) + } +} + +@Composable +internal fun rememberChatMessageListState( + state: ChatComponent.State, + displayMessages: List, + canSendAnything: Boolean, + showInitialLoading: Boolean +): ChatMessageListUiState { + return remember( + state.chatId, + state.currentTopicId, + displayMessages, + state.selectedMessageIds, + state.unreadSeparatorCount, + state.unreadSeparatorLastReadInboxMessageId, + state.viewAsTopics, + state.topics, + state.rootMessage, + state.isLoading, + state.isLoadingOlder, + state.isLoadingNewer, + state.isAtBottom, + state.isLatestLoaded, + state.isOldestLoaded, + state.isGroup, + state.isChannel, + state.isAdmin, + state.canWrite, + canSendAnything, + state.highlightedMessageId, + state.fontSize, + state.letterSpacing, + state.bubbleRadius, + state.stickerSize, + state.autoDownloadMobile, + state.autoDownloadWifi, + state.autoDownloadRoaming, + state.autoDownloadFiles, + state.autoplayGifs, + state.autoplayVideos, + state.showLinkPreviews, + state.isChatAnimationsEnabled, + showInitialLoading, + state.pendingScrollCommand + ) { + ChatMessageListUiState( + chatId = state.chatId, + currentTopicId = state.currentTopicId, + messages = displayMessages, + selectedMessageIds = state.selectedMessageIds, + unreadSeparatorCount = state.unreadSeparatorCount, + unreadSeparatorLastReadInboxMessageId = state.unreadSeparatorLastReadInboxMessageId, + viewAsTopics = state.viewAsTopics, + topics = state.topics, + rootMessage = state.rootMessage, + isLoading = state.isLoading, + isLoadingOlder = state.isLoadingOlder, + isLoadingNewer = state.isLoadingNewer, + isAtBottom = state.isAtBottom, + isLatestLoaded = state.isLatestLoaded, + isOldestLoaded = state.isOldestLoaded, + isGroup = state.isGroup, + isChannel = state.isChannel, + isAdmin = state.isAdmin, + canWrite = state.canWrite, + canSendAnything = canSendAnything, + highlightedMessageId = state.highlightedMessageId, + 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, + showLinkPreviews = state.showLinkPreviews, + isChatAnimationsEnabled = state.isChatAnimationsEnabled, + suppressEntryAnimations = showInitialLoading || state.pendingScrollCommand != null + ) + } +} + +@Composable +internal fun rememberChatTopBarUiState( + state: ChatComponent.State +): ChatContentTopBarUiState { + return 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 + ) { + 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 = if (state.isSearchActive) null else state.pinnedMessage, + pinnedMessageCount = if (state.isSearchActive) 0 else state.pinnedMessageCount + ) + } +} + +@Composable +internal fun rememberChatSearchUiState( + state: ChatComponent.State +): ChatContentSearchUiState { + val canLoadMoreSearchResults by remember( + state.searchNextFromMessageId, + state.searchResults.size, + state.searchResultsTotalCount + ) { + derivedStateOf { + state.searchResults.size < state.searchResultsTotalCount || + state.searchNextFromMessageId != 0L + } + } + val searchSenderCandidates by remember(state.searchAvailableSenders, state.otherUser) { + derivedStateOf { + buildList { + addAll(state.searchAvailableSenders) + state.otherUser?.let(::add) + }.distinctBy(UserModel::id) + } + } + val hasSearchFiltersApplied by remember( + state.searchSender, + state.searchDateFromEpochSeconds, + state.searchDateToEpochSeconds + ) { + derivedStateOf { + state.searchSender != null || + state.searchDateFromEpochSeconds != null || + state.searchDateToEpochSeconds != null + } + } + + return remember( + canLoadMoreSearchResults, + searchSenderCandidates, + hasSearchFiltersApplied + ) { + ChatContentSearchUiState( + canLoadMoreSearchResults = canLoadMoreSearchResults, + searchSenderCandidates = searchSenderCandidates, + hasSearchFiltersApplied = hasSearchFiltersApplied + ) + } +} + +@Composable +internal fun rememberChatChromeState( + state: ChatComponent.State, + isRecordingVideo: Boolean, + editingPhotoPath: String?, + editingVideoPath: String?, + selectedMessageId: Long? +): ChatContentChromeState { + val showInputBar by remember( + state.isChannel, + state.isGroup, + state.canWrite, + state.isCurrentUserRestricted, + state.currentTopicId, + state.selectedMessageIds, + state.viewAsTopics, + state.isSearchActive, + isRecordingVideo + ) { + derivedStateOf { + (state.canWrite || state.isCurrentUserRestricted) && + !isRecordingVideo && + !state.isSearchActive && + state.selectedMessageIds.isEmpty() && + (!state.viewAsTopics || state.currentTopicId != null) + } + } + + val showJoinButton by remember( + showInputBar, + state.isMember, + state.isChannel, + state.isGroup, + state.canWrite, + state.isCurrentUserRestricted, + state.selectedMessageIds, + state.viewAsTopics, + state.currentTopicId, + state.isSearchActive, + isRecordingVideo + ) { + derivedStateOf { + !showInputBar && + !state.isSearchActive && + !state.isMember && + (state.isChannel || state.isGroup) && + !state.canWrite && + !state.isCurrentUserRestricted && + !isRecordingVideo && + state.selectedMessageIds.isEmpty() && + (!state.viewAsTopics || state.currentTopicId != null) + } + } + + val isCustomBackHandlingEnabled by remember( + 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, + state.isSearchActive + ) { + 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 || + state.isSearchActive + } + } + + val selectedCount = state.selectedMessageIds.size + val selectedMessageIdSet by remember(state.selectedMessageIds) { + derivedStateOf { state.selectedMessageIds.toHashSet() } + } + val canRevokeSelected by remember(state.messages, selectedMessageIdSet) { + derivedStateOf { + if (selectedMessageIdSet.isEmpty()) { + false + } else { + state.messages.any { it.id in selectedMessageIdSet && it.canBeDeletedForAllUsers } + } + } + } + + return remember( + showInputBar, + showJoinButton, + isCustomBackHandlingEnabled, + selectedCount, + canRevokeSelected + ) { + ChatContentChromeState( + showInputBar = showInputBar, + showJoinButton = showJoinButton, + isCustomBackHandlingEnabled = isCustomBackHandlingEnabled, + selectedCount = selectedCount, + canRevokeSelected = canRevokeSelected + ) + } +} diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentEffects.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentEffects.kt new file mode 100644 index 00000000..56534deb --- /dev/null +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentEffects.kt @@ -0,0 +1,374 @@ +package org.monogram.presentation.features.chats.currentChat.chatContent + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.snapshotFlow +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map +import org.monogram.presentation.features.chats.currentChat.ChatComponent +import org.monogram.presentation.features.chats.currentChat.ChatScrollCommand +import org.monogram.presentation.features.chats.currentChat.DefaultChatComponent + +@Composable +internal fun ChatContentEffects( + component: ChatComponent, + state: ChatComponent.State, + scrollState: LazyListState, + groupedMessages: List, + groupedMessageIndexById: Map, + isComments: Boolean, + isForumList: Boolean, + isDragged: Boolean, + isRecordingVideo: Boolean, + showInitialLoading: Boolean, + hasUserScrolledAwayFromBottom: Boolean, + transformedMessageTexts: MutableMap, + originalMessageTexts: MutableMap, + onVisible: () -> Unit, + onShowInitialLoadingChanged: (Boolean) -> Unit, + onHasUserScrolledAwayFromBottomChanged: (Boolean) -> Unit, + onShowScrollToBottomButtonChanged: (Boolean) -> Unit, + onHideKeyboardAndClearFocus: (Boolean) -> Unit, + onRenderPinnedMessagesListChanged: (Boolean) -> Unit, + onSearchFiltersChanged: (Boolean) -> Unit, + onSearchSenderPickerChanged: (Boolean) -> Unit +) { + val latestUiState = rememberUpdatedState(state) + + LaunchedEffect(Unit) { + onVisible() + if (state.fullScreenVideoPath != null || state.fullScreenVideoMessageId != null) { + component.onDismissVideo() + } + } + + LaunchedEffect(state.messages) { + if (transformedMessageTexts.isEmpty() && originalMessageTexts.isEmpty()) return@LaunchedEffect + val ids = state.messages.map { it.id }.toSet() + transformedMessageTexts.keys.toList().forEach { id -> + if (id !in ids) { + transformedMessageTexts.remove(id) + originalMessageTexts.remove(id) + } + } + } + + LaunchedEffect( + state.isLoading, + state.messages.isEmpty(), + state.viewAsTopics, + state.currentTopicId, + state.isLoadingTopics, + state.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 + } else { + state.isLoading && state.messages.isEmpty() + } + if (isActuallyLoading) { + if (state.isChatAnimationsEnabled) delay(200) + onShowInitialLoadingChanged(true) + } else { + onShowInitialLoadingChanged(false) + } + } + + LaunchedEffect(state.pendingScrollCommand, isComments) { + val command = state.pendingScrollCommand ?: return@LaunchedEffect + + val leadingItems = chatContentLeadingItemsCount( + isComments = isComments, + showNavPadding = false, + isLoadingOlder = state.isLoadingOlder, + isLoadingNewer = state.isLoadingNewer, + isAtBottom = state.isAtBottom, + hasMessages = groupedMessages.isNotEmpty() + ) + + when (command) { + is ChatScrollCommand.RestoreViewport -> { + if (command.atBottom || command.anchorMessageId == null) { + scrollState.scrollToChatBottomStaged( + isComments = isComments, + animated = false + ) + } else { + val groupedIndex = groupedMessageIndexById[command.anchorMessageId] + ?: awaitGroupedIndex( + messageId = command.anchorMessageId, + groupedMessageIndexByIdProvider = { groupedMessageIndexById } + ) + ?: -1 + if (groupedIndex >= 0) { + val targetIndex = groupedIndexToLazyIndex(groupedIndex, leadingItems) + scrollState.restoreViewportAtIndex( + targetIndex = targetIndex, + anchorOffsetPx = command.anchorOffsetPx + ) + } else { + scrollState.scrollToChatBottomStaged( + isComments = isComments, + animated = false + ) + } + } + component.onScrollCommandConsumed() + } + + is ChatScrollCommand.JumpToMessage -> { + val groupedIndex = groupedMessageIndexById[command.messageId] + ?: awaitGroupedIndex( + messageId = command.messageId, + groupedMessageIndexByIdProvider = { groupedMessageIndexById } + ) + ?: -1 + if (groupedIndex >= 0) { + val targetIndex = groupedIndexToLazyIndex(groupedIndex, leadingItems) + scrollState.scrollToMessageIndex( + index = targetIndex, + align = command.align, + animated = command.animated && state.isChatAnimationsEnabled, + staged = true + ) + } + component.onScrollCommandConsumed() + } + + is ChatScrollCommand.ScrollToBottom -> { + scrollState.scrollToChatBottomStaged( + isComments = isComments, + animated = command.animated && state.isChatAnimationsEnabled + ) + component.onScrollCommandConsumed() + } + + is ChatScrollCommand.ScrollToStart -> { + scrollState.scrollToChatStartStaged( + animated = command.animated && state.isChatAnimationsEnabled + ) + component.onScrollCommandConsumed() + } + } + } + + LaunchedEffect( + scrollState, + isComments, + isForumList, + showInitialLoading, + isDragged, + hasUserScrolledAwayFromBottom + ) { + var lastReportedBottomState: Boolean? = null + snapshotFlow { + val currentState = latestUiState.value + BottomVisibilitySnapshot( + isAtBottom = scrollState.isAtBottom( + isComments = isComments, + isLatestLoaded = currentState.isLatestLoaded + ), + isNearBottom = scrollState.isNearBottom(isComments = isComments), + unreadCount = currentState.unreadCount + ) + } + .distinctUntilChanged() + .collectLatest { snapshot -> + if (lastReportedBottomState != snapshot.isAtBottom) { + component.onBottomReached(snapshot.isAtBottom) + lastReportedBottomState = snapshot.isAtBottom + } + + if (snapshot.isNearBottom) { + onHasUserScrolledAwayFromBottomChanged(false) + } else if (isDragged) { + onHasUserScrolledAwayFromBottomChanged(true) + } + + val shouldShow = !isForumList && + !showInitialLoading && + (snapshot.unreadCount > 0 || (hasUserScrolledAwayFromBottom && !snapshot.isNearBottom)) + + if (shouldShow) { + onShowScrollToBottomButtonChanged(true) + } else { + delay(120) + val keepVisible = snapshot.unreadCount > 0 || + (hasUserScrolledAwayFromBottom && !snapshot.isNearBottom) + if (!keepVisible) { + onShowScrollToBottomButtonChanged(false) + } + } + } + } + + LaunchedEffect( + scrollState, + groupedMessages, + isComments, + state.isLatestLoaded, + state.isLoadingOlder, + state.isLoadingNewer, + state.isAtBottom + ) { + snapshotFlow { + buildViewportSnapshot( + scrollState = scrollState, + groupedMessages = groupedMessages, + isComments = isComments, + isLatestLoaded = state.isLatestLoaded, + isLoadingOlder = state.isLoadingOlder, + isLoadingNewer = state.isLoadingNewer, + isAtBottom = state.isAtBottom, + showNavPadding = false + ) + } + .filterNotNull() + .distinctUntilChanged() + .debounce(120) + .collect { viewport -> + component.updateViewport(viewport) + } + } + + DisposableEffect( + scrollState, + groupedMessages, + isComments, + state.currentTopicId, + state.isLatestLoaded, + state.isLoadingOlder, + state.isLoadingNewer, + state.isAtBottom + ) { + onDispose { + val viewport = buildViewportSnapshot( + scrollState = scrollState, + groupedMessages = groupedMessages, + isComments = isComments, + isLatestLoaded = state.isLatestLoaded, + isLoadingOlder = state.isLoadingOlder, + isLoadingNewer = state.isLoadingNewer, + isAtBottom = state.isAtBottom, + showNavPadding = false + ) + if (viewport != null) { + component.updateViewport(viewport) + } + } + } + + LaunchedEffect(scrollState, groupedMessages) { + snapshotFlow { scrollState.layoutInfo.visibleItemsInfo } + .map { visibleItems -> + val currentState = latestUiState.value + val leadingItemsCount = chatContentLeadingItemsCount( + isComments = currentState.rootMessage != null, + showNavPadding = false, + isLoadingOlder = currentState.isLoadingOlder, + isLoadingNewer = currentState.isLoadingNewer, + isAtBottom = currentState.isAtBottom, + hasMessages = groupedMessages.isNotEmpty() + ) + val visibleIds = LinkedHashSet() + val nearbyIds = LinkedHashSet() + if (visibleItems.isNotEmpty()) { + val minIndex = visibleItems.minOf { it.index } + val maxIndex = visibleItems.maxOf { it.index } + + visibleItems.forEach { item -> + val groupedIndex = lazyIndexToGroupedIndex(item.index, leadingItemsCount) + groupedMessages.getOrNull(groupedIndex)?.let { grouped -> + when (grouped) { + is GroupedMessageItem.Single -> visibleIds.add(grouped.message.id) + is GroupedMessageItem.Album -> grouped.messages.forEach { message -> + visibleIds.add(message.id) + } + } + } + } + + val nearbyStart = (minIndex - 5).coerceAtLeast(0) + val nearbyEnd = maxIndex + 5 + for (index in nearbyStart..nearbyEnd) { + if (index in minIndex..maxIndex) continue + val groupedIndex = lazyIndexToGroupedIndex(index, leadingItemsCount) + groupedMessages.getOrNull(groupedIndex)?.let { grouped -> + when (grouped) { + is GroupedMessageItem.Single -> nearbyIds.add(grouped.message.id) + is GroupedMessageItem.Album -> grouped.messages.forEach { message -> + nearbyIds.add(message.id) + } + } + } + } + } + val visibleIdList = visibleIds.toList() + visibleIdList to nearbyIds.filterNot(visibleIds::contains) + } + .distinctUntilChanged() + .debounce(100) + .collect { (visibleIds, nearbyIds) -> + (component as? DefaultChatComponent)?.let { + it.repositoryMessage.updateVisibleRange(it.chatId, visibleIds, nearbyIds) + } + } + } + + LaunchedEffect(groupedMessages.size, state.isLatestLoaded) { + if (isComments) return@LaunchedEffect + + val isAtBottomNow = scrollState.isAtBottom( + isComments = isComments, + isLatestLoaded = state.isLatestLoaded + ) + if ((state.isAtBottom || isAtBottomNow) && + !state.isLoading && + !state.isLoadingOlder && + !state.isLoadingNewer && + !scrollState.isScrollInProgress + ) { + scrollState.scrollToChatBottomStaged( + isComments = isComments, + animated = state.isChatAnimationsEnabled + ) + } + } + + LaunchedEffect(isDragged) { + if (isDragged) { + onHideKeyboardAndClearFocus(false) + } + } + + LaunchedEffect(state.showBotCommands, isRecordingVideo) { + if (state.showBotCommands || isRecordingVideo) { + onHideKeyboardAndClearFocus(true) + } + } + + LaunchedEffect(state.showPinnedMessagesList) { + if (state.showPinnedMessagesList) { + onRenderPinnedMessagesListChanged(true) + } + } + + LaunchedEffect(state.isSearchActive) { + if (state.isSearchActive) { + onSearchFiltersChanged(false) + onSearchSenderPickerChanged(false) + if (state.showPinnedMessagesList) { + component.onDismissPinnedMessages() + } + } + } +} diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentInputConfiguration.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentInputConfiguration.kt new file mode 100644 index 00000000..8bc14a3a --- /dev/null +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentInputConfiguration.kt @@ -0,0 +1,172 @@ +package org.monogram.presentation.features.chats.currentChat.chatContent + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import org.monogram.domain.models.ReplyMarkupModel +import org.monogram.presentation.features.chats.currentChat.ChatComponent +import org.monogram.presentation.features.chats.currentChat.components.inputbar.ChatInputBarActions +import org.monogram.presentation.features.chats.currentChat.components.inputbar.ChatInputBarState +import java.net.URLEncoder +import java.nio.charset.StandardCharsets + +@Composable +internal fun rememberChatInputBarState( + state: ChatComponent.State, + pendingMediaPaths: List, + pendingDocumentPaths: List +): ChatInputBarState { + return remember(state, pendingMediaPaths, pendingDocumentPaths) { + ChatInputBarState( + replyMessage = state.replyMessage, + editingMessage = state.editingMessage, + draftText = state.draftText, + pendingMediaPaths = pendingMediaPaths, + pendingDocumentPaths = pendingDocumentPaths, + isClosed = state.topics.find { it.id.toLong() == state.currentTopicId }?.isClosed + ?: false, + permissions = state.effectiveInputPermissions ?: 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 + ) + } +} + +@Composable +internal fun rememberChatInputBarActions( + component: ChatComponent, + state: ChatComponent.State, + pendingMediaPaths: List, + pendingDocumentPaths: List, + onPickMedia: () -> Unit, + onHideKeyboardAndClearFocus: () -> Unit, + onStartRecordingVideo: () -> Unit, + onSetPendingMediaPaths: (List) -> Unit, + onSetPendingDocumentPaths: (List) -> Unit, + onEditMediaPath: (String) -> Unit +): ChatInputBarActions { + return remember(component, state, pendingMediaPaths, pendingDocumentPaths) { + ChatInputBarActions( + onSend = { text, entities, options -> + component.onSendMessage(text, entities, options) + }, + onStickerClick = component::onSendSticker, + onGifClick = component::onSendGif, + onAttachClick = onPickMedia, + onCameraClick = { + onHideKeyboardAndClearFocus() + onStartRecordingVideo() + }, + onSendVoice = component::onSendVoice, + onCancelReply = component::onCancelReply, + onCancelEdit = component::onCancelEdit, + onSaveEdit = component::onSaveEditedMessage, + onDraftChange = component::onDraftChange, + onTyping = component::onTyping, + onCancelMedia = { onSetPendingMediaPaths(emptyList()) }, + onSendMedia = { paths, caption, captionEntities, options -> + if (options.sendAsDocument) { + if (paths.size > 1) { + component.onSendAlbum(paths, caption, captionEntities, options) + } else { + paths.firstOrNull()?.let { + component.onSendDocument(it, caption, captionEntities, options) + } + } + } else if (paths.size > 1) { + component.onSendAlbum(paths, caption, captionEntities, options) + } else { + paths.firstOrNull()?.let { + if (it.endsWith(".mp4")) { + component.onSendVideo(it, caption, captionEntities, options) + } else { + component.onSendPhoto(it, caption, captionEntities, options) + } + } + } + onSetPendingMediaPaths(emptyList()) + onSetPendingDocumentPaths(emptyList()) + }, + onSendDocuments = { paths, caption, captionEntities, options -> + paths.forEachIndexed { index, path -> + component.onSendDocument( + path, + caption = if (index == 0) caption else "", + captionEntities = if (index == 0) captionEntities else emptyList(), + sendOptions = options + ) + } + onSetPendingDocumentPaths(emptyList()) + onSetPendingMediaPaths(emptyList()) + }, + onMediaOrderChange = { + onSetPendingMediaPaths(it) + if (it.isNotEmpty()) { + onSetPendingDocumentPaths(emptyList()) + } + }, + onDocumentOrderChange = { + onSetPendingDocumentPaths(it) + if (it.isNotEmpty()) { + onSetPendingMediaPaths(emptyList()) + } + }, + onMediaClick = onEditMediaPath, + onShowBotCommands = { + onHideKeyboardAndClearFocus() + component.onShowBotCommands() + }, + onReplyMarkupButtonClick = { + component.onReplyMarkupButtonClick( + 0, + it, + if (state.isBot) state.chatId else 0L + ) + }, + onOpenMiniApp = { url, name -> + component.onOpenMiniApp( + url, + name, + if (state.isBot) state.chatId else 0L + ) + }, + onMentionQueryChange = component::onMentionQueryChange, + onInlineQueryChange = component::onInlineQueryChange, + onLoadMoreInlineResults = component::onLoadMoreInlineResults, + onSendInlineResult = component::onSendInlineResult, + onInlineSwitchPm = { botUsername, parameter -> + val encodedParameter = URLEncoder.encode( + parameter, + StandardCharsets.UTF_8.name() + ) + component.onLinkClick("https://t.me/$botUsername?start=$encodedParameter") + }, + onAttachBotClick = { bot -> + component.onOpenAttachBot(bot.botUserId, bot.name) + }, + onSendPoll = component::onSendPoll, + onRefreshScheduledMessages = component::onRefreshScheduledMessages, + onEditScheduledMessage = component::onEditMessage, + onDeleteScheduledMessage = component::onDeleteMessage, + onSendScheduledNow = component::onSendScheduledNow + ) + } +} diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentMessageUtils.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentMessageUtils.kt new file mode 100644 index 00000000..55c80d4c --- /dev/null +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentMessageUtils.kt @@ -0,0 +1,35 @@ +package org.monogram.presentation.features.chats.currentChat.chatContent + +import org.monogram.domain.models.MessageContent +import org.monogram.domain.models.MessageModel + +internal fun MessageModel.extractTextContent(): String? { + return when (val currentContent = content) { + is MessageContent.Text -> currentContent.text + is MessageContent.Photo -> currentContent.caption + is MessageContent.Video -> currentContent.caption + is MessageContent.Gif -> currentContent.caption + is MessageContent.Document -> currentContent.caption + is MessageContent.Audio -> currentContent.caption + else -> null + } +} + +internal fun MessageModel.withUpdatedTextContent(newText: String): MessageModel { + val updatedContent = when (val currentContent = content) { + is MessageContent.Text -> currentContent.copy( + text = newText, + entities = emptyList(), + webPage = null + ) + + is MessageContent.Photo -> currentContent.copy(caption = newText, entities = emptyList()) + is MessageContent.Video -> currentContent.copy(caption = newText, entities = emptyList()) + is MessageContent.Gif -> currentContent.copy(caption = newText, entities = emptyList()) + is MessageContent.Document -> currentContent.copy(caption = newText, entities = emptyList()) + is MessageContent.Audio -> currentContent.copy(caption = newText, entities = emptyList()) + else -> return this + } + + return copy(content = updatedContent) +} diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentOverlays.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentOverlays.kt new file mode 100644 index 00000000..06de8aee --- /dev/null +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentOverlays.kt @@ -0,0 +1,204 @@ +package org.monogram.presentation.features.chats.currentChat.chatContent + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Block +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.platform.Clipboard +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.zIndex +import org.monogram.domain.models.ChatPermissionsModel +import org.monogram.domain.models.MessageModel +import org.monogram.presentation.R +import org.monogram.presentation.core.ui.ConfirmationSheet +import org.monogram.presentation.features.chats.currentChat.ChatComponent +import org.monogram.presentation.features.chats.currentChat.components.StickerSetSheet +import org.monogram.presentation.features.chats.currentChat.components.chats.BotCommandsSheet +import org.monogram.presentation.features.chats.currentChat.components.chats.PollVotersSheet +import org.monogram.presentation.features.chats.currentChat.components.pins.PinnedMessagesListSheet +import org.monogram.presentation.features.chats.currentChat.editor.photo.PhotoEditorScreen +import org.monogram.presentation.features.chats.currentChat.editor.video.VideoEditorScreen + +@Composable +internal fun ChatContentOverlays( + state: ChatComponent.State, + component: ChatComponent, + localClipboard: Clipboard, + groupedMessages: List, + isAnyViewerOpen: Boolean, + renderPinnedMessagesList: Boolean, + requestPinnedMessagesListDismiss: () -> Unit, + onPinnedSheetHidden: () -> Unit, + onPinnedMessageClick: (MessageModel) -> Unit, + selectedMessage: MessageModel?, + menuOffset: Offset, + menuMessageSize: IntSize, + clickOffset: Offset, + contentRect: Rect, + canRestoreOriginalText: Boolean, + onApplyTransformedText: (String) -> Unit, + onRestoreOriginalText: () -> Unit, + onDismissMessageOptions: () -> Unit, + pendingBlockUserId: Long?, + onRequestBlockUser: (Long) -> Unit, + onConfirmBlockUser: (Long) -> Unit, + onDismissBlockUser: () -> Unit, + editingPhotoPath: String?, + onClosePhotoEditor: () -> Unit, + onSavePhotoEditor: (String) -> Unit, + editingVideoPath: String?, + onCloseVideoEditor: () -> Unit, + onSaveVideoEditor: (String) -> Unit, + isCustomBackHandlingEnabled: Boolean, + onBack: () -> Unit +) { + 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, + onDismissRequest = requestPinnedMessagesListDismiss, + onHidden = onPinnedSheetHidden, + onMessageClick = onPinnedMessageClick, + onUnpin = component::onUnpinMessage, + onReplyClick = onPinnedMessageClick, + onReactionClick = { id, reaction -> + component.onSendReaction(id, reaction) + }, + downloadUtils = component.downloadUtils, + isAnyViewerOpen = isAnyViewerOpen + ) + } + + state.selectedStickerSet?.let { stickerSet -> + StickerSetSheet( + stickerSet = stickerSet, + onDismiss = component::onDismissStickerSet, + onStickerClick = { _, path -> component.onSendSticker(path) } + ) + } + + if (state.showPollVoters) { + PollVotersSheet( + voters = state.pollVoters, + isLoading = state.isPollVotersLoading, + onUserClick = { + component.onDismissVoters() + component.toProfile(it) + }, + onDismiss = component::onDismissVoters + ) + } + + if (state.showBotCommands) { + BotCommandsSheet( + commands = state.botCommands, + onCommandClick = component::onBotCommandClick, + onDismiss = component::onDismissBotCommands + ) + } + + ChatContentViewers( + state = state, + component = component, + localClipboard = localClipboard + ) + + selectedMessage?.let { msg -> + ChatMessageOptionsMenu( + state = state, + component = component, + selectedMessage = msg, + menuOffset = menuOffset, + menuMessageSize = menuMessageSize, + clickOffset = clickOffset, + contentRect = contentRect, + groupedMessages = groupedMessages, + downloadUtils = component.downloadUtils, + localClipboard = localClipboard, + canRestoreOriginalText = canRestoreOriginalText, + onApplyTransformedText = onApplyTransformedText, + onRestoreOriginalText = onRestoreOriginalText, + onBlockRequest = onRequestBlockUser, + onDismiss = onDismissMessageOptions + ) + } + + pendingBlockUserId?.let { userId -> + ConfirmationSheet( + icon = Icons.Rounded.Block, + title = stringResource(R.string.block_user_title), + description = stringResource(R.string.block_user_confirmation), + confirmText = stringResource(R.string.action_block), + onConfirm = { onConfirmBlockUser(userId) }, + onDismiss = onDismissBlockUser + ) + } + + if (state.showReportDialog) { + ReportChatDialog( + onDismiss = component::onDismissReportDialog, + onReasonSelected = component::onReportReasonSelected + ) + } + + if (state.restrictUserId != null) { + RestrictUserSheet( + onDismiss = component::onDismissRestrictDialog, + onConfirm = { permissions: ChatPermissionsModel, untilDate: Int -> + component.onConfirmRestrict(permissions, untilDate) + } + ) + } + + editingPhotoPath?.let { path -> + Box( + modifier = Modifier + .fillMaxSize() + .zIndex(20f) + ) { + PhotoEditorScreen( + imagePath = path, + onClose = onClosePhotoEditor, + onSave = onSavePhotoEditor + ) + } + } + + editingVideoPath?.let { path -> + Box( + modifier = Modifier + .fillMaxSize() + .zIndex(20f) + ) { + VideoEditorScreen( + videoPath = path, + onClose = onCloseVideoEditor, + onSave = onSaveVideoEditor + ) + } + } + + BackHandler(enabled = isCustomBackHandlingEnabled) { + onBack() + } +} diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentScrollCoordinator.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentScrollCoordinator.kt new file mode 100644 index 00000000..2e349043 --- /dev/null +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentScrollCoordinator.kt @@ -0,0 +1,242 @@ +package org.monogram.presentation.features.chats.currentChat.chatContent + +import androidx.compose.foundation.gestures.animateScrollBy +import androidx.compose.foundation.gestures.scrollBy +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.snapshotFlow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withTimeoutOrNull +import org.monogram.domain.models.ChatViewportCacheEntry +import org.monogram.presentation.features.chats.currentChat.ScrollAlign +import kotlin.math.abs + +@Immutable +internal data class BottomVisibilitySnapshot( + val isAtBottom: Boolean, + val isNearBottom: Boolean, + val unreadCount: Int +) + +internal suspend fun LazyListState.scrollToMessageIndex( + index: Int, + align: ScrollAlign, + animated: Boolean, + staged: Boolean +) { + val total = layoutInfo.totalItemsCount + if (total <= 0) return + + val boundedIndex = index.coerceIn(0, total - 1) + val distance = abs(firstVisibleItemIndex - boundedIndex) + + if (staged && distance > 20) { + val coarseIndex = when { + boundedIndex > firstVisibleItemIndex -> (boundedIndex - 10).coerceAtLeast(0) + boundedIndex < firstVisibleItemIndex -> (boundedIndex + 10).coerceAtMost(total - 1) + else -> boundedIndex + } + scrollToItem(coarseIndex) + } + + scrollToItem(boundedIndex) + + val itemInfo = layoutInfo.visibleItemsInfo.firstOrNull { it.index == boundedIndex } ?: return + val viewportStart = layoutInfo.viewportStartOffset + val viewportEnd = layoutInfo.viewportEndOffset + val viewportCenter = (viewportStart + viewportEnd) / 2 + + val targetPosition = when (align) { + ScrollAlign.Start -> viewportStart + ScrollAlign.Center -> viewportCenter - (itemInfo.size / 2) + ScrollAlign.End -> viewportEnd - itemInfo.size + } + val delta = (itemInfo.offset - targetPosition).toFloat() + + if (abs(delta) > 1f) { + if (animated) { + animateScrollBy(delta) + } else { + scrollBy(delta) + } + } +} + +internal fun LazyListState.isAtBottom( + isComments: Boolean, + isLatestLoaded: Boolean +): Boolean { + if (!isLatestLoaded) return false + + val info = layoutInfo + val visible = info.visibleItemsInfo + if (visible.isEmpty()) return true + + return if (isComments) { + val lastVisible = visible.last() + lastVisible.index >= info.totalItemsCount - 1 && + abs((info.viewportEndOffset - (lastVisible.offset + lastVisible.size)).toFloat()) <= 40f + } else { + val firstVisible = visible.first() + firstVisible.index == 0 && + abs((firstVisible.offset - info.viewportStartOffset).toFloat()) <= 40f + } +} + +internal fun LazyListState.isNearBottom(isComments: Boolean): Boolean { + val info = layoutInfo + val visible = info.visibleItemsInfo + if (visible.isEmpty()) return true + + return if (isComments) { + val lastVisible = visible.last() + val distance = + abs((info.viewportEndOffset - (lastVisible.offset + lastVisible.size)).toFloat()) + lastVisible.index >= info.totalItemsCount - 2 && distance <= 240f + } else { + val firstVisible = visible.first() + val distance = abs((firstVisible.offset - info.viewportStartOffset).toFloat()) + firstVisible.index <= 1 && distance <= 240f + } +} + +internal suspend fun LazyListState.scrollToChatBottomStaged( + isComments: Boolean, + animated: Boolean +) { + val total = layoutInfo.totalItemsCount + if (total <= 0) return + + val targetIndex = if (isComments) total - 1 else 0 + val distance = abs(firstVisibleItemIndex - targetIndex) + + if (distance > 24) { + val coarse = if (isComments) { + (targetIndex - 8).coerceAtLeast(0) + } else { + (targetIndex + 8).coerceAtMost(total - 1) + } + scrollToItem(coarse) + } + + if (animated) { + animateScrollToItem(targetIndex) + } else { + scrollToItem(targetIndex) + } + + val targetInfo = layoutInfo.visibleItemsInfo.firstOrNull { it.index == targetIndex } + if (targetInfo != null) { + val delta = if (isComments) { + ((targetInfo.offset + targetInfo.size) - layoutInfo.viewportEndOffset).toFloat() + } else { + (targetInfo.offset - layoutInfo.viewportStartOffset).toFloat() + } + if (abs(delta) > 1f) { + scrollBy(delta) + } + } + + scrollToItem(targetIndex) +} + +internal suspend fun LazyListState.scrollToChatStartStaged( + animated: Boolean +) { + val total = layoutInfo.totalItemsCount + if (total <= 0) return + + if (animated) { + animateScrollToItem(0) + } else { + scrollToItem(0) + } + + val targetInfo = layoutInfo.visibleItemsInfo.firstOrNull { it.index == 0 } + if (targetInfo != null) { + val delta = (targetInfo.offset - layoutInfo.viewportStartOffset).toFloat() + if (abs(delta) > 1f) { + scrollBy(delta) + } + } + + scrollToItem(0) +} + +internal suspend fun awaitGroupedIndex( + messageId: Long, + groupedMessageIndexByIdProvider: () -> Map, + timeoutMs: Long = 1200L +): Int? { + return withTimeoutOrNull(timeoutMs) { + snapshotFlow { groupedMessageIndexByIdProvider()[messageId] } + .filterNotNull() + .first() + } +} + +internal suspend fun LazyListState.restoreViewportAtIndex( + targetIndex: Int, + anchorOffsetPx: Int +) { + val total = layoutInfo.totalItemsCount + if (total <= 0) return + val boundedIndex = targetIndex.coerceIn(0, total - 1) + + scrollToItem(boundedIndex) + val itemInfo = layoutInfo.visibleItemsInfo.firstOrNull { it.index == boundedIndex } ?: return + val viewportStart = layoutInfo.viewportStartOffset + val desiredOffset = viewportStart + anchorOffsetPx + val delta = (itemInfo.offset - desiredOffset).toFloat() + + if (abs(delta) > 1f) { + scrollBy(delta) + } +} + +internal fun buildViewportSnapshot( + scrollState: LazyListState, + groupedMessages: List, + isComments: Boolean, + isLatestLoaded: Boolean, + isLoadingOlder: Boolean, + isLoadingNewer: Boolean, + isAtBottom: Boolean, + showNavPadding: Boolean +): ChatViewportCacheEntry? { + if (groupedMessages.isEmpty()) { + return ChatViewportCacheEntry(atBottom = true) + } + + val atBottomNow = scrollState.isAtBottom( + isComments = isComments, + isLatestLoaded = isLatestLoaded + ) + if (atBottomNow) { + return ChatViewportCacheEntry(atBottom = true) + } + + val leadingItems = chatContentLeadingItemsCount( + isComments = isComments, + showNavPadding = showNavPadding, + isLoadingOlder = isLoadingOlder, + isLoadingNewer = isLoadingNewer, + isAtBottom = isAtBottom, + hasMessages = groupedMessages.isNotEmpty() + ) + val info = scrollState.layoutInfo + val anchorItem = info.visibleItemsInfo.firstOrNull { itemInfo -> + val groupedIndex = lazyIndexToGroupedIndex(itemInfo.index, leadingItems) + groupedIndex in groupedMessages.indices + } ?: return null + + val groupedIndex = lazyIndexToGroupedIndex(anchorItem.index, leadingItems) + val anchorMessageId = groupedMessages.getOrNull(groupedIndex)?.firstMessageId ?: return null + + return ChatViewportCacheEntry( + anchorMessageId = anchorMessageId, + anchorOffsetPx = anchorItem.offset - info.viewportStartOffset, + atBottom = false + ) +} diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentSearchOverlay.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentSearchOverlay.kt new file mode 100644 index 00000000..8e47f035 --- /dev/null +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentSearchOverlay.kt @@ -0,0 +1,883 @@ +package org.monogram.presentation.features.chats.currentChat.chatContent + +import android.app.DatePickerDialog +import android.content.Context +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.List +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull +import org.monogram.domain.models.MessageModel +import org.monogram.domain.models.UserModel +import org.monogram.presentation.R +import org.monogram.presentation.core.ui.AvatarForChat +import java.time.Instant +import java.time.LocalDate +import java.time.ZoneId +import java.time.format.DateTimeFormatter + +@Composable +internal fun ChatContentSearchOverlay( + context: Context, + query: String, + results: List, + totalCount: Int, + selectedIndex: Int, + isSearching: Boolean, + canLoadMore: Boolean, + showAllResults: Boolean, + showSearchFilters: Boolean, + showSearchSenderPicker: Boolean, + hasFiltersApplied: Boolean, + selectedSender: UserModel?, + searchSenderCandidates: List, + fromEpochSeconds: Int?, + toEpochSeconds: Int?, + onLoadMore: () -> Unit, + onResultClick: (Int) -> Unit, + onPrevious: () -> Unit, + onNext: () -> Unit, + onToggleShowAll: () -> Unit, + onToggleFilters: () -> Unit, + onToggleSenderPicker: () -> Unit, + onSelectSender: (UserModel?) -> Unit, + onApplyDateRange: (Int?, Int?) -> Unit, + modifier: Modifier = Modifier +) { + AnimatedVisibility( + visible = true, + enter = fadeIn(animationSpec = tween(220)) + + slideInVertically( + animationSpec = tween(280), + initialOffsetY = { it / 3 } + ) + + scaleIn( + animationSpec = tween(220), + initialScale = 0.96f + ), + exit = fadeOut(animationSpec = tween(160)) + + slideOutVertically( + animationSpec = tween(180), + targetOffsetY = { it / 4 } + ), + modifier = modifier + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .windowInsetsPadding(WindowInsets.navigationBars), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + AnimatedVisibility( + visible = showAllResults && results.isNotEmpty(), + enter = fadeIn(animationSpec = tween(180)) + + slideInVertically( + animationSpec = tween(240), + initialOffsetY = { it / 8 } + ), + exit = fadeOut(animationSpec = tween(140)) + + slideOutVertically( + animationSpec = tween(180), + targetOffsetY = { it / 10 } + ) + ) { + SearchResultsListOverlay( + query = query, + results = results, + selectedIndex = selectedIndex, + isSearching = isSearching, + canLoadMore = canLoadMore, + onLoadMore = onLoadMore, + onResultClick = onResultClick + ) + } + + AnimatedVisibility( + visible = showSearchFilters && showSearchSenderPicker, + enter = fadeIn(animationSpec = tween(180)) + + slideInVertically( + animationSpec = tween(220), + initialOffsetY = { it / 6 } + ) + + scaleIn( + animationSpec = tween(200), + initialScale = 0.98f + ), + exit = fadeOut(animationSpec = tween(140)) + + slideOutVertically( + animationSpec = tween(160), + targetOffsetY = { it / 8 } + ) + ) { + SearchSenderPickerOverlay( + selectedSenderId = selectedSender?.id, + senders = searchSenderCandidates, + onSelectSender = onSelectSender + ) + } + + AnimatedVisibility( + visible = showSearchFilters, + enter = fadeIn(animationSpec = tween(180)) + + slideInVertically( + animationSpec = tween(220), + initialOffsetY = { it / 6 } + ) + + scaleIn( + animationSpec = tween(200), + initialScale = 0.98f + ), + exit = fadeOut(animationSpec = tween(140)) + + slideOutVertically( + animationSpec = tween(160), + targetOffsetY = { it / 8 } + ) + ) { + SearchFilterTray( + selectedSender = selectedSender, + fromEpochSeconds = fromEpochSeconds, + toEpochSeconds = toEpochSeconds, + onToggleSenderPicker = onToggleSenderPicker, + onApplyToday = { + val now = LocalDate.now() + onApplyDateRange( + toStartOfDayEpochSeconds(now), + toEndOfDayEpochSeconds(now) + ) + }, + onApplyLastDays = { days -> + val now = LocalDate.now() + val from = now.minusDays((days - 1).toLong()) + onApplyDateRange( + toStartOfDayEpochSeconds(from), + toEndOfDayEpochSeconds(now) + ) + }, + onResetDateRange = { onApplyDateRange(null, null) }, + onPickFromDate = { + showSearchDatePicker( + context = context, + initialEpochSeconds = fromEpochSeconds, + onDateSelected = { date -> + val nextFrom = toStartOfDayEpochSeconds(date) + val nextTo = toEpochSeconds + ?.let(::epochSecondsToLocalDate) + ?.let { currentTo -> + if (currentTo.isBefore(date)) { + toEndOfDayEpochSeconds(date) + } else { + toEndOfDayEpochSeconds(currentTo) + } + } + onApplyDateRange(nextFrom, nextTo) + } + ) + }, + onPickToDate = { + showSearchDatePicker( + context = context, + initialEpochSeconds = toEpochSeconds, + onDateSelected = { date -> + val nextTo = toEndOfDayEpochSeconds(date) + val nextFrom = fromEpochSeconds + ?.let(::epochSecondsToLocalDate) + ?.let { currentFrom -> + if (currentFrom.isAfter(date)) { + toStartOfDayEpochSeconds(date) + } else { + toStartOfDayEpochSeconds(currentFrom) + } + } + onApplyDateRange(nextFrom, nextTo) + } + ) + } + ) + } + + SearchNavigationPanel( + query = query, + results = results, + totalCount = totalCount, + selectedIndex = selectedIndex, + isSearching = isSearching, + showAllResults = showAllResults, + filtersExpanded = showSearchFilters, + hasFiltersApplied = hasFiltersApplied, + onPrevious = onPrevious, + onNext = onNext, + onToggleShowAll = onToggleShowAll, + onToggleFilters = onToggleFilters + ) + } + } +} + +@Composable +private fun SearchNavigationPanel( + query: String, + results: List, + totalCount: Int, + selectedIndex: Int, + isSearching: Boolean, + showAllResults: Boolean, + filtersExpanded: Boolean, + hasFiltersApplied: Boolean, + onPrevious: () -> Unit, + onNext: () -> Unit, + onToggleShowAll: () -> Unit, + onToggleFilters: () -> Unit +) { + val hasResults = results.isNotEmpty() + val selectedPosition = (selectedIndex + 1).takeIf { selectedIndex in results.indices } ?: 0 + val listIconRotation = animateFloatAsState( + targetValue = if (showAllResults) 90f else 0f, + animationSpec = tween(220), + label = "SearchListRotation" + ) + val statusText = when { + isSearching -> stringResource(R.string.search_results_loading) + query.isBlank() -> stringResource(R.string.no_results_found) + else -> stringResource(R.string.no_search_results_format, query) + } + val counterText = stringResource( + R.string.search_results_position_format, + selectedPosition, + totalCount.coerceAtLeast(results.size) + ) + + Surface( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(24.dp), + tonalElevation = 10.dp, + shadowElevation = 14.dp, + color = MaterialTheme.colorScheme.surface.copy(alpha = 0.98f) + ) { + Column( + modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + if (!hasResults) { + AnimatedContent( + targetState = statusText, + transitionSpec = { fadeIn(tween(180)) togetherWith fadeOut(tween(120)) }, + label = "SearchStatusText" + ) { text -> + Text( + text = text, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.fillMaxWidth() + ) + } + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Surface( + onClick = onToggleFilters, + shape = CircleShape, + color = if (hasFiltersApplied || filtersExpanded) { + MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.92f) + } else { + MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.75f) + }, + tonalElevation = 2.dp + ) { + Box( + modifier = Modifier.padding(horizontal = 10.dp, vertical = 9.dp), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Default.MoreVert, + contentDescription = null, + tint = if (hasFiltersApplied || filtersExpanded) { + MaterialTheme.colorScheme.onPrimaryContainer + } else { + MaterialTheme.colorScheme.onSurfaceVariant + } + ) + } + } + + Surface( + onClick = onToggleShowAll, + shape = CircleShape, + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.75f), + tonalElevation = 2.dp + ) { + Box( + modifier = Modifier.padding(9.dp), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.List, + contentDescription = null, + tint = if (showAllResults) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + modifier = Modifier.graphicsLayer { + rotationZ = listIconRotation.value + } + ) + } + } + + Surface( + onClick = onPrevious, + shape = CircleShape, + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.75f), + tonalElevation = 2.dp + ) { + Box( + modifier = Modifier.padding(9.dp), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Default.KeyboardArrowDown, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface + ) + } + } + + Surface( + modifier = Modifier.weight(1f), + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.92f), + tonalElevation = 2.dp + ) { + AnimatedContent( + targetState = counterText, + transitionSpec = { + (fadeIn(tween(180)) + slideInVertically { it / 3 }) togetherWith + (fadeOut(tween(120)) + slideOutVertically { -it / 3 }) + }, + label = "SearchCounter" + ) { animatedCounter -> + Text( + text = animatedCounter, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = Modifier.padding(horizontal = 12.dp, vertical = 9.dp), + maxLines = 1 + ) + } + } + + Surface( + onClick = onNext, + shape = CircleShape, + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.75f), + tonalElevation = 2.dp + ) { + Box( + modifier = Modifier.padding(9.dp), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Default.KeyboardArrowUp, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface + ) + } + } + } + } + } +} + +@Composable +private fun SearchFilterTray( + selectedSender: UserModel?, + fromEpochSeconds: Int?, + toEpochSeconds: Int?, + onToggleSenderPicker: () -> Unit, + onApplyToday: () -> Unit, + onApplyLastDays: (Int) -> Unit, + onResetDateRange: () -> Unit, + onPickFromDate: () -> Unit, + onPickToDate: () -> Unit +) { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(24.dp), + tonalElevation = 10.dp, + shadowElevation = 14.dp, + color = MaterialTheme.colorScheme.surface.copy(alpha = 0.98f) + ) { + Column( + modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + SearchSenderChip( + selectedSender = selectedSender, + onClick = onToggleSenderPicker + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + SearchMiniChip( + label = stringResource(R.string.search_date_all), + isActive = fromEpochSeconds == null && toEpochSeconds == null, + modifier = Modifier.weight(1f), + onClick = onResetDateRange + ) + SearchMiniChip( + label = stringResource(R.string.preview_date_today), + isActive = isTodayRange(fromEpochSeconds, toEpochSeconds), + modifier = Modifier.weight(1f), + onClick = onApplyToday + ) + SearchMiniChip( + label = stringResource(R.string.search_date_last_7_days), + isActive = matchesLastDaysRange(fromEpochSeconds, toEpochSeconds, 7), + modifier = Modifier.weight(1f), + onClick = { onApplyLastDays(7) } + ) + SearchMiniChip( + label = stringResource(R.string.search_date_last_30_days), + isActive = matchesLastDaysRange(fromEpochSeconds, toEpochSeconds, 30), + modifier = Modifier.weight(1f), + onClick = { onApplyLastDays(30) } + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + SearchRangeChip( + modifier = Modifier.weight(1f), + label = stringResource(R.string.search_date_from), + value = fromEpochSeconds?.let(::formatSearchDate), + onClick = onPickFromDate + ) + SearchRangeChip( + modifier = Modifier.weight(1f), + label = stringResource(R.string.search_date_to), + value = toEpochSeconds?.let(::formatSearchDate), + onClick = onPickToDate + ) + } + } + } +} + +@Composable +private fun SearchSenderPickerOverlay( + selectedSenderId: Long?, + senders: List, + onSelectSender: (UserModel?) -> Unit +) { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(22.dp), + tonalElevation = 10.dp, + shadowElevation = 12.dp, + color = MaterialTheme.colorScheme.surface.copy(alpha = 0.985f) + ) { + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 280.dp) + .padding(horizontal = 4.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + item("all_senders") { + SearchSenderRow( + title = stringResource(R.string.search_sender_all), + subtitle = stringResource(R.string.search_section_messages), + avatarPath = null, + isSelected = selectedSenderId == null, + onClick = { onSelectSender(null) } + ) + } + + itemsIndexed(senders, key = { _, user -> user.id }) { _, user -> + SearchSenderRow( + title = formatSearchSenderLabel(user), + subtitle = user.username?.takeIf { it.isNotBlank() }?.let { "@$it" }, + avatarPath = user.avatarPath, + isSelected = selectedSenderId == user.id, + onClick = { onSelectSender(user) } + ) + } + } + } +} + +@Composable +private fun SearchSenderRow( + title: String, + subtitle: String?, + avatarPath: String?, + isSelected: Boolean, + onClick: () -> Unit +) { + Surface( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 6.dp) + .clickable(onClick = onClick), + shape = RoundedCornerShape(18.dp), + color = if (isSelected) { + MaterialTheme.colorScheme.primaryContainer + } else { + MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.28f) + }, + tonalElevation = if (isSelected) 2.dp else 0.dp + ) { + Row( + modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + AvatarForChat( + path = avatarPath, + name = title, + size = 32.dp + ) + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + Text( + text = title, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + subtitle?.let { + Text( + text = it, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + } + } +} + +private fun formatSearchSenderLabel(user: UserModel): String { + return listOfNotNull( + user.firstName.takeIf { it.isNotBlank() }, + user.lastName?.takeIf { it.isNotBlank() } + ).joinToString(" ").ifBlank { + user.username?.takeIf { it.isNotBlank() } ?: user.id.toString() + } +} + +@Composable +private fun SearchSenderChip( + selectedSender: UserModel?, + onClick: () -> Unit +) { + val label = selectedSender?.let(::formatSearchSenderLabel) + ?: stringResource(R.string.search_sender_all) + + Surface( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick), + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.34f), + tonalElevation = 1.dp + ) { + Row( + modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + AvatarForChat( + path = selectedSender?.avatarPath, + name = label, + size = 30.dp + ) + Text( + text = label, + modifier = Modifier.weight(1f), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } +} + +@Composable +private fun SearchMiniChip( + label: String, + isActive: Boolean, + modifier: Modifier = Modifier, + onClick: () -> Unit +) { + Surface( + modifier = modifier.clickable(onClick = onClick), + shape = RoundedCornerShape(14.dp), + color = if (isActive) { + MaterialTheme.colorScheme.primaryContainer + } else { + MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.28f) + }, + tonalElevation = if (isActive) 2.dp else 0.dp + ) { + Box( + modifier = Modifier.padding(horizontal = 10.dp, vertical = 7.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = label, + style = MaterialTheme.typography.labelMedium, + color = if (isActive) { + MaterialTheme.colorScheme.onPrimaryContainer + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } +} + +@Composable +private fun SearchRangeChip( + label: String, + value: String?, + modifier: Modifier = Modifier, + onClick: () -> Unit +) { + Surface( + modifier = modifier.clickable(onClick = onClick), + shape = RoundedCornerShape(16.dp), + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.28f), + tonalElevation = 0.dp + ) { + Column( + modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + Text( + text = label, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1 + ) + Text( + text = value ?: stringResource(R.string.cd_select_date), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } +} + +private fun isTodayRange(fromEpochSeconds: Int?, toEpochSeconds: Int?): Boolean { + val today = LocalDate.now() + return fromEpochSeconds?.let(::epochSecondsToLocalDate) == today && + toEpochSeconds?.let(::epochSecondsToLocalDate) == today +} + +private fun matchesLastDaysRange(fromEpochSeconds: Int?, toEpochSeconds: Int?, days: Int): Boolean { + val today = LocalDate.now() + return fromEpochSeconds?.let(::epochSecondsToLocalDate) == today.minusDays((days - 1).toLong()) && + toEpochSeconds?.let(::epochSecondsToLocalDate) == today +} + +private fun showSearchDatePicker( + context: Context, + initialEpochSeconds: Int?, + onDateSelected: (LocalDate) -> Unit +) { + val initialDate = initialEpochSeconds?.let(::epochSecondsToLocalDate) ?: LocalDate.now() + DatePickerDialog( + context, + { _, year, month, dayOfMonth -> + onDateSelected(LocalDate.of(year, month + 1, dayOfMonth)) + }, + initialDate.year, + initialDate.monthValue - 1, + initialDate.dayOfMonth + ).show() +} + +private fun epochSecondsToLocalDate(epochSeconds: Int): LocalDate { + return Instant.ofEpochSecond(epochSeconds.toLong()) + .atZone(ZoneId.systemDefault()) + .toLocalDate() +} + +private fun toStartOfDayEpochSeconds(date: LocalDate): Int { + return date.atStartOfDay(ZoneId.systemDefault()).toEpochSecond().toInt() +} + +private fun toEndOfDayEpochSeconds(date: LocalDate): Int { + return date.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toEpochSecond().toInt() - 1 +} + +private fun formatSearchDate(epochSeconds: Int): String { + return epochSecondsToLocalDate(epochSeconds).format(DateTimeFormatter.ofPattern("dd.MM.yyyy")) +} + +@Composable +private fun SearchResultsListOverlay( + query: String, + results: List, + selectedIndex: Int, + isSearching: Boolean, + canLoadMore: Boolean, + onLoadMore: () -> Unit, + onResultClick: (Int) -> Unit +) { + val listState = rememberLazyListState() + + LaunchedEffect(listState, results.size, canLoadMore, isSearching) { + if (!canLoadMore || isSearching) return@LaunchedEffect + + snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index } + .filterNotNull() + .distinctUntilChanged() + .collectLatest { lastVisibleIndex -> + if (lastVisibleIndex >= results.lastIndex - 4) { + onLoadMore() + } + } + } + + Surface( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(22.dp), + tonalElevation = 10.dp, + shadowElevation = 12.dp, + color = MaterialTheme.colorScheme.surface.copy(alpha = 0.985f) + ) { + Column( + modifier = Modifier.padding(horizontal = 10.dp, vertical = 10.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + LazyColumn( + state = listState, + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 340.dp), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + itemsIndexed(results, key = { _, message -> message.id }) { index, message -> + val preview = message.extractTextContent() + ?.replace('\n', ' ') + ?.trim() + ?.takeIf { it.isNotBlank() } + ?: message.senderName.ifBlank { query } + val sender = message.senderName.ifBlank { + stringResource(R.string.search_section_messages) + } + val isSelected = index == selectedIndex + + Surface( + modifier = Modifier + .fillMaxWidth() + .clickable { onResultClick(index) }, + shape = RoundedCornerShape(18.dp), + color = if (isSelected) { + MaterialTheme.colorScheme.primaryContainer + } else { + MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.32f) + }, + tonalElevation = if (isSelected) 2.dp else 0.dp + ) { + Column( + modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp), + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + Text( + text = sender, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.primary + ) + Text( + text = preview, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 2 + ) + } + } + } + } + + if (canLoadMore || isSearching) { + TextButton( + onClick = onLoadMore, + enabled = !isSearching, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) { + Text( + text = if (isSearching) { + stringResource(R.string.search_results_loading) + } else { + stringResource(R.string.action_show_more) + } + ) + } + } + } + } +} From 917d92bd11623d261964a99f583b4f5e8501e104 Mon Sep 17 00:00:00 2001 From: Artur Skubei <41114720+gdlbo@users.noreply.github.com> Date: Fri, 24 Apr 2026 17:14:51 +0300 Subject: [PATCH 3/8] cleanup chats folders --- .../java/org/monogram/app/MainActivity.kt | 2 +- .../main/java/org/monogram/app/MainContent.kt | 4 +- .../app/components/ChatConfirmJoinSheet.kt | 2 +- .../monogram/app/components/RenderChild.kt | 6 +- .../java/org/monogram/app/di/AppModule.kt | 4 +- .../media}/AlphaVideoPlayer.kt | 2 +- .../components => core/media}/AvatarPlayer.kt | 3 +- .../monogram/presentation/core/ui/Avatar.kt | 2 +- .../presentation/core/ui/AvatarHeader.kt | 2 +- .../presentation/core/ui/SectionHeader.kt | 4 +- .../presentation/core/ui/SettingsTextField.kt | 102 ++++++++++++ .../core/util/CompositionLocal.kt | 2 +- .../monogram/presentation/di/AppContainer.kt | 4 +- .../presentation/di/KoinAppContainer.kt | 4 +- .../auth/components/PhoneInputScreen.kt | 2 +- .../AutoDownloadSuppression.kt | 2 +- .../ChatComponent.kt | 2 +- .../ChatContent.kt | 51 +++--- .../ChatScrollModels.kt | 2 +- .../ChatStore.kt | 2 +- .../ChatStoreFactory.kt | 156 +++++++++--------- .../DefaultChatComponent.kt | 22 +-- .../editor/photo/PhotoEditorScreen.kt | 6 +- .../editor/photo/PhotoEditorUtils.kt | 2 +- .../editor/photo/components/ColorSelector.kt | 2 +- .../editor/photo/components/DrawControls.kt | 2 +- .../editor/photo/components/EditorTopBar.kt | 2 +- .../editor/photo/components/FilterControls.kt | 6 +- .../photo/components/TextEntryDialog.kt | 2 +- .../photo/components/TransformControls.kt | 2 +- .../editor/photo/crop/CropEditorState.kt | 2 +- .../editor/photo/crop/CropGeometry.kt | 2 +- .../editor/photo/crop/CropOverlay.kt | 2 +- .../editor/video/VideoEditorScreen.kt | 16 +- .../editor/video/VideoEditorUtils.kt | 2 +- .../components/VideoCompressionControls.kt | 4 +- .../video/components/VideoFilterControls.kt | 6 +- .../video/components/VideoTextControls.kt | 2 +- .../video/components/VideoTrimControls.kt | 6 +- .../logic/bots}/BotOperations.kt | 4 +- .../logic/chat}/ChatInfo.kt | 4 +- .../logic/files}/FileOperations.kt | 4 +- .../logic/message-actions}/MessageActions.kt | 10 +- .../message-actions}/MessageOperations.kt | 4 +- .../logic/message-loading}/MessageLoading.kt | 10 +- .../message-selection}/MessageSelection.kt | 4 +- .../logic/miniapp}/MiniAppOperations.kt | 4 +- .../logic/pinned}/PinnedMessages.kt | 8 +- .../logic/polls}/PollOperations.kt | 4 +- .../logic/preferences}/Preferences.kt | 4 +- .../logic/search}/SearchMessages.kt | 12 +- .../logic/stickers}/Stickers.kt | 4 +- .../logic/thread}/ThreadContext.kt | 6 +- .../ui}/AdvancedCircularRecorderScreen.kt | 2 +- .../ui}/AlbumMessageBubbleContainer.kt | 13 +- .../ui}/ChannelMessageBubbleContainer.kt | 21 +-- .../ui}/ChatInputBar.kt | 48 +++--- .../ui}/ChatTopBar.kt | 2 +- .../ui}/CompactMediaMosaic.kt | 18 +- .../ui}/DateSeparator.kt | 2 +- .../ui}/FastReplyIndicator.kt | 2 +- .../ui}/InlineVideoPlayer.kt | 4 +- .../ui}/MessageBubbleContainer.kt | 35 ++-- .../ui}/MessageListShimmer.kt | 2 +- .../ui}/SenderGrouping.kt | 2 +- .../ui}/ServiceMessage.kt | 2 +- .../ui}/StickerSetSheet.kt | 2 +- .../ui}/UnreadMessagesSeparator.kt | 2 +- .../ui}/VoicePlayer.kt | 2 +- .../ui/channel}/ChannelAlbumMessageBubble.kt | 20 +-- .../ui/channel}/ChannelCommentsButton.kt | 2 +- .../ui/channel}/ChannelGifMessageBubble.kt | 24 +-- .../channel}/ChannelMessageBubbleContainer.kt | 19 ++- .../ui/channel}/ChannelMessageUtils.kt | 2 +- .../ui/channel}/ChannelPhotoMessageBubble.kt | 22 +-- .../ui/channel}/ChannelPollMessageBubble.kt | 4 +- .../ui/channel}/ChannelTextMessageBubble.kt | 18 +- .../ui/channel}/ChannelVideoMessageBubble.kt | 26 +-- .../ui/channel}/ChannelVoiceMessageBubble.kt | 12 +- .../ui/content}/ChatContentBackground.kt | 5 +- .../ui/content}/ChatContentDerivedState.kt | 5 +- .../ui/content}/ChatContentEffects.kt | 9 +- .../content}/ChatContentInputConfiguration.kt | 9 +- .../ui/content}/ChatContentList.kt | 17 +- .../ui/content}/ChatContentMessageUtils.kt | 3 +- .../ui/content}/ChatContentOverlays.kt | 17 +- .../content}/ChatContentScrollCoordinator.kt | 5 +- .../ui/content}/ChatContentSearchOverlay.kt | 3 +- .../ui/content}/ChatContentTopBar.kt | 10 +- .../ui/content}/ChatContentUtils.kt | 3 +- .../ui/content}/ChatContentViewers.kt | 5 +- .../ui/content}/ChatMessageOptionsMenu.kt | 5 +- .../ui/content}/DeleteMessagesSheet.kt | 3 +- .../ui/content}/ReportChatDialog.kt | 5 +- .../ui/content}/RestrictUserSheet.kt | 3 +- .../inputbar/ChatInputBarComposerSection.kt | 4 +- .../ui}/inputbar/ChatInputBarContract.kt | 2 +- .../ui}/inputbar/ChatInputBarHelpers.kt | 2 +- .../ui}/inputbar/ClosedTopicBar.kt | 2 +- .../ui}/inputbar/EmojiInsertUtils.kt | 2 +- .../FullScreenEditorFindReplaceBar.kt | 4 +- .../FullScreenEditorMarkdownPreview.kt | 2 +- .../ui}/inputbar/FullScreenEditorSheet.kt | 6 +- .../FullScreenEditorStorageAndSearch.kt | 2 +- .../FullScreenEditorTemplatesSheet.kt | 4 +- .../ui}/inputbar/InlineBotResults.kt | 2 +- .../ui}/inputbar/InputBarLeadingIcons.kt | 2 +- .../ui}/inputbar/InputBarSendButton.kt | 2 +- .../ui}/inputbar/InputPreviewSection.kt | 6 +- .../ui}/inputbar/InputTextField.kt | 4 +- .../ui}/inputbar/InputTextFieldContainer.kt | 2 +- .../ui}/inputbar/KeyboardMarkupView.kt | 2 +- .../ui}/inputbar/MentionSuggestions.kt | 2 +- .../ui}/inputbar/RecordingUI.kt | 2 +- .../ui}/inputbar/RestrictedInputBar.kt | 2 +- .../ui}/inputbar/SchedulePickers.kt | 2 +- .../ui}/inputbar/ScheduledMessagesSheet.kt | 2 +- .../ui}/inputbar/SendOptionsPopup.kt | 2 +- .../ui}/inputbar/SlowModeInputBar.kt | 2 +- .../ui}/inputbar/VoiceRecorder.kt | 2 +- .../ui/message}/AudioMessageBubble.kt | 4 +- .../ui/message}/BotCommandItem.kt | 2 +- .../ui/message}/BotCommandSuggestions.kt | 2 +- .../ui/message}/BotCommandsSheet.kt | 2 +- .../ui/message}/ChatAlbumMessageBubble.kt | 4 +- .../ui/message}/CodeBlock.kt | 4 +- .../ui/message}/ContactMessageBubble.kt | 2 +- .../ui/message}/DocumentMessageBubble.kt | 6 +- .../ui/message}/ForwardContent.kt | 2 +- .../ui/message}/GifMessageBubble.kt | 8 +- .../ui/message}/LinkPreview.kt | 2 +- .../ui/message}/LocationMessageBubble.kt | 2 +- .../ui/message}/MediaLoadingComponents.kt | 2 +- .../ui/message}/MessageReactionsView.kt | 2 +- .../ui/message}/MessageSenderName.kt | 2 +- .../ui/message}/MessageText.kt | 4 +- .../ui/message}/MessageTextFormatter.kt | 2 +- .../ui/message}/MessageUtils.kt | 4 +- .../ui/message}/MessageViaBotAttribution.kt | 2 +- .../ui/message}/PhotoMessageBubble.kt | 4 +- .../ui/message}/PollMessageBubble.kt | 2 +- .../ui/message}/PollVotersSheet.kt | 2 +- .../ui/message}/QuoteBlock.kt | 2 +- .../ui/message}/ReplyContent.kt | 2 +- .../ui/message}/ReplyMarkupView.kt | 2 +- .../ui/message}/SpoilerShader.kt | 2 +- .../ui/message}/SpoilerShaderApi33.kt | 2 +- .../ui/message}/StickerMessageBubble.kt | 2 +- .../ui/message}/TextBlocks.kt | 6 +- .../ui/message}/TextMessageBubble.kt | 2 +- .../ui/message}/VideoMessageBubble.kt | 8 +- .../ui/message}/VideoNoteBubble.kt | 4 +- .../ui/message}/VoiceMessageBubble.kt | 4 +- .../ui/message}/code/CodeHighlighter.kt | 2 +- .../ui/message}/code/LanguagesConfigs.kt | 2 +- .../ui/message}/model/Mappers.kt | 2 +- .../ui}/pins/Mapper.kt | 2 +- .../ui}/pins/PinnedMessageBar.kt | 2 +- .../ui}/pins/PinnedMessagesListSheet.kt | 17 +- .../DefaultNewChatComponent.kt | 2 +- .../{newChat => creation}/NewChatComponent.kt | 2 +- .../{newChat => creation}/NewChatContent.kt | 10 +- .../{newChat => creation}/NewChatStore.kt | 2 +- .../NewChatStoreFactory.kt | 6 +- .../chats/{ => list}/ChatListComponent.kt | 2 +- .../{chatList => list}/ChatListContent.kt | 26 +-- .../chats/{ => list}/ChatListStore.kt | 2 +- .../chats/{ => list}/ChatListStoreFactory.kt | 8 +- .../DefaultChatListComponent.kt | 8 +- .../components/AccountMenu.kt | 2 +- .../components/ArchiveHeaderCard.kt | 2 +- .../{chatList => list}/components/Avatar.kt | 4 +- .../components/ChatCreationCommon.kt | 95 +---------- .../components/ChatListItem.kt | 8 +- .../components/ChatListShimmer.kt | 2 +- .../components/ChatListTopBar.kt | 2 +- .../components/EmptyStateView.kt | 2 +- .../components/FolderTabs.kt | 6 +- .../components/MessageSearchItem.kt | 2 +- .../components/NewChannelContent.kt | 4 +- .../components/NewGroupContent.kt | 4 +- .../components/PermissionRequestSheet.kt | 2 +- .../components/SelectionTopBar.kt | 2 +- .../gallery/components/PollComposerSheet.kt | 10 +- .../features/instantview/InstantViewer.kt | 2 +- .../components/InstantViewComponents.kt | 6 +- .../features/profile/ProfileContent.kt | 2 +- .../profile/admin/AdminManageContent.kt | 2 +- .../features/profile/admin/ChatEditContent.kt | 2 +- .../profile/components/ProfileMediaSection.kt | 4 +- .../profile/components/StatisticsViewer.kt | 2 +- .../profile/logs/ProfileLogsContent.kt | 4 +- .../profile/logs/components/MessagePreview.kt | 6 +- .../features/stickers/ui/menu/EmojisGrid.kt | 4 +- .../features/stickers/ui/menu/GifsGrid.kt | 4 +- .../stickers/ui/menu/MessageOptionsMenu.kt | 4 +- .../features/stickers/ui/menu/StickersGrid.kt | 2 +- .../webview/components/FindInPageBar.kt | 2 +- .../presentation/root/DefaultRootComponent.kt | 8 +- .../presentation/root/RootComponent.kt | 8 +- .../settings/adblock/AdBlockContent.kt | 2 +- .../chatSettings/ChatSettingsContent.kt | 2 +- .../chatSettings/ChatThemeEditorScreen.kt | 2 +- .../components/ChatSettingsPreview.kt | 2 +- .../settings/folders/FolderDialog.kt | 4 +- .../settings/profile/EditProfileContent.kt | 4 +- .../proxy/components/ProxyAddEditSheet.kt | 2 +- .../settings/storage/CacheController.kt | 2 +- .../photo/components/TransformControlsTest.kt | 2 +- .../editor/photo/crop/CropGeometryTest.kt | 2 +- 210 files changed, 740 insertions(+), 701 deletions(-) rename presentation/src/main/java/org/monogram/presentation/{features/chats/currentChat/components => core/media}/AlphaVideoPlayer.kt (99%) rename presentation/src/main/java/org/monogram/presentation/{features/chats/currentChat/components => core/media}/AvatarPlayer.kt (76%) create mode 100644 presentation/src/main/java/org/monogram/presentation/core/ui/SettingsTextField.kt rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat => conversation}/AutoDownloadSuppression.kt (87%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat => conversation}/ChatComponent.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat => conversation}/ChatContent.kt (95%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat => conversation}/ChatScrollModels.kt (92%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat => conversation}/ChatStore.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat => conversation}/ChatStoreFactory.kt (71%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat => conversation}/DefaultChatComponent.kt (97%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat => conversation}/editor/photo/PhotoEditorScreen.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat => conversation}/editor/photo/PhotoEditorUtils.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat => conversation}/editor/photo/components/ColorSelector.kt (95%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat => conversation}/editor/photo/components/DrawControls.kt (96%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat => conversation}/editor/photo/components/EditorTopBar.kt (97%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat => conversation}/editor/photo/components/FilterControls.kt (92%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat => conversation}/editor/photo/components/TextEntryDialog.kt (98%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat => conversation}/editor/photo/components/TransformControls.kt (98%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat => conversation}/editor/photo/crop/CropEditorState.kt (98%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat => conversation}/editor/photo/crop/CropGeometry.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat => conversation}/editor/photo/crop/CropOverlay.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat => conversation}/editor/video/VideoEditorScreen.kt (96%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat => conversation}/editor/video/VideoEditorUtils.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat => conversation}/editor/video/components/VideoCompressionControls.kt (94%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat => conversation}/editor/video/components/VideoFilterControls.kt (91%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat => conversation}/editor/video/components/VideoTextControls.kt (92%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat => conversation}/editor/video/components/VideoTrimControls.kt (91%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/impl => conversation/logic/bots}/BotOperations.kt (98%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/impl => conversation/logic/chat}/ChatInfo.kt (98%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/impl => conversation/logic/files}/FileOperations.kt (83%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/impl => conversation/logic/message-actions}/MessageActions.kt (97%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/impl => conversation/logic/message-actions}/MessageOperations.kt (97%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/impl => conversation/logic/message-loading}/MessageLoading.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/impl => conversation/logic/message-selection}/MessageSelection.kt (87%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/impl => conversation/logic/miniapp}/MiniAppOperations.kt (92%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/impl => conversation/logic/pinned}/PinnedMessages.kt (95%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/impl => conversation/logic/polls}/PollOperations.kt (87%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/impl => conversation/logic/preferences}/Preferences.kt (97%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/impl => conversation/logic/search}/SearchMessages.kt (97%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/impl => conversation/logic/stickers}/Stickers.kt (80%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/impl => conversation/logic/thread}/ThreadContext.kt (78%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/AdvancedCircularRecorderScreen.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/AlbumMessageBubbleContainer.kt (96%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/ChannelMessageBubbleContainer.kt (93%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/ChatInputBar.kt (95%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/ChatTopBar.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/CompactMediaMosaic.kt (98%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/DateSeparator.kt (94%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/FastReplyIndicator.kt (98%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/InlineVideoPlayer.kt (87%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/MessageBubbleContainer.kt (94%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/MessageListShimmer.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/SenderGrouping.kt (87%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/ServiceMessage.kt (98%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/StickerSetSheet.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/UnreadMessagesSeparator.kt (94%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/VoicePlayer.kt (97%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/channels => conversation/ui/channel}/ChannelAlbumMessageBubble.kt (97%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/channels => conversation/ui/channel}/ChannelCommentsButton.kt (97%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/channels => conversation/ui/channel}/ChannelGifMessageBubble.kt (95%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/channels => conversation/ui/channel}/ChannelMessageBubbleContainer.kt (96%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/channels => conversation/ui/channel}/ChannelMessageUtils.kt (93%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/channels => conversation/ui/channel}/ChannelPhotoMessageBubble.kt (93%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/channels => conversation/ui/channel}/ChannelPollMessageBubble.kt (93%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/channels => conversation/ui/channel}/ChannelTextMessageBubble.kt (91%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/channels => conversation/ui/channel}/ChannelVideoMessageBubble.kt (95%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/channels => conversation/ui/channel}/ChannelVoiceMessageBubble.kt (91%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/chatContent => conversation/ui/content}/ChatContentBackground.kt (93%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/chatContent => conversation/ui/content}/ChatContentDerivedState.kt (98%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/chatContent => conversation/ui/content}/ChatContentEffects.kt (97%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/chatContent => conversation/ui/content}/ChatContentInputConfiguration.kt (95%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/chatContent => conversation/ui/content}/ChatContentList.kt (98%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/chatContent => conversation/ui/content}/ChatContentMessageUtils.kt (95%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/chatContent => conversation/ui/content}/ChatContentOverlays.kt (90%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/chatContent => conversation/ui/content}/ChatContentScrollCoordinator.kt (98%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/chatContent => conversation/ui/content}/ChatContentSearchOverlay.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/chatContent => conversation/ui/content}/ChatContentTopBar.kt (98%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/chatContent => conversation/ui/content}/ChatContentUtils.kt (97%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/chatContent => conversation/ui/content}/ChatContentViewers.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/chatContent => conversation/ui/content}/ChatMessageOptionsMenu.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/chatContent => conversation/ui/content}/DeleteMessagesSheet.kt (98%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/chatContent => conversation/ui/content}/ReportChatDialog.kt (98%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/chatContent => conversation/ui/content}/RestrictUserSheet.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/inputbar/ChatInputBarComposerSection.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/inputbar/ChatInputBarContract.kt (97%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/inputbar/ChatInputBarHelpers.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/inputbar/ClosedTopicBar.kt (94%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/inputbar/EmojiInsertUtils.kt (94%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/inputbar/FullScreenEditorFindReplaceBar.kt (95%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/inputbar/FullScreenEditorMarkdownPreview.kt (98%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/inputbar/FullScreenEditorSheet.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/inputbar/FullScreenEditorStorageAndSearch.kt (95%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/inputbar/FullScreenEditorTemplatesSheet.kt (97%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/inputbar/InlineBotResults.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/inputbar/InputBarLeadingIcons.kt (96%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/inputbar/InputBarSendButton.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/inputbar/InputPreviewSection.kt (98%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/inputbar/InputTextField.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/inputbar/InputTextFieldContainer.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/inputbar/KeyboardMarkupView.kt (98%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/inputbar/MentionSuggestions.kt (97%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/inputbar/RecordingUI.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/inputbar/RestrictedInputBar.kt (98%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/inputbar/SchedulePickers.kt (97%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/inputbar/ScheduledMessagesSheet.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/inputbar/SendOptionsPopup.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/inputbar/SlowModeInputBar.kt (98%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/inputbar/VoiceRecorder.kt (98%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/chats => conversation/ui/message}/AudioMessageBubble.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/chats => conversation/ui/message}/BotCommandItem.kt (98%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/chats => conversation/ui/message}/BotCommandSuggestions.kt (96%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/chats => conversation/ui/message}/BotCommandsSheet.kt (98%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/chats => conversation/ui/message}/ChatAlbumMessageBubble.kt (98%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/chats => conversation/ui/message}/CodeBlock.kt (97%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/chats => conversation/ui/message}/ContactMessageBubble.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/chats => conversation/ui/message}/DocumentMessageBubble.kt (98%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/chats => conversation/ui/message}/ForwardContent.kt (98%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/chats => conversation/ui/message}/GifMessageBubble.kt (98%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/chats => conversation/ui/message}/LinkPreview.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/chats => conversation/ui/message}/LocationMessageBubble.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/chats => conversation/ui/message}/MediaLoadingComponents.kt (98%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/chats => conversation/ui/message}/MessageReactionsView.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/chats => conversation/ui/message}/MessageSenderName.kt (97%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/chats => conversation/ui/message}/MessageText.kt (98%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/chats => conversation/ui/message}/MessageTextFormatter.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/chats => conversation/ui/message}/MessageUtils.kt (98%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/chats => conversation/ui/message}/MessageViaBotAttribution.kt (95%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/chats => conversation/ui/message}/PhotoMessageBubble.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/chats => conversation/ui/message}/PollMessageBubble.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/chats => conversation/ui/message}/PollVotersSheet.kt (98%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/chats => conversation/ui/message}/QuoteBlock.kt (98%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/chats => conversation/ui/message}/ReplyContent.kt (98%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/chats => conversation/ui/message}/ReplyMarkupView.kt (98%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/chats => conversation/ui/message}/SpoilerShader.kt (97%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/chats => conversation/ui/message}/SpoilerShaderApi33.kt (96%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/chats => conversation/ui/message}/StickerMessageBubble.kt (98%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/chats => conversation/ui/message}/TextBlocks.kt (80%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/chats => conversation/ui/message}/TextMessageBubble.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/chats => conversation/ui/message}/VideoMessageBubble.kt (98%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/chats => conversation/ui/message}/VideoNoteBubble.kt (98%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/chats => conversation/ui/message}/VoiceMessageBubble.kt (98%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/chats => conversation/ui/message}/code/CodeHighlighter.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/chats => conversation/ui/message}/code/LanguagesConfigs.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components/chats => conversation/ui/message}/model/Mappers.kt (95%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/pins/Mapper.kt (93%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/pins/PinnedMessageBar.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{currentChat/components => conversation/ui}/pins/PinnedMessagesListSheet.kt (96%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{newChat => creation}/DefaultNewChatComponent.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{newChat => creation}/NewChatComponent.kt (96%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{newChat => creation}/NewChatContent.kt (98%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{newChat => creation}/NewChatStore.kt (95%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{newChat => creation}/NewChatStoreFactory.kt (92%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{ => list}/ChatListComponent.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{chatList => list}/ChatListContent.kt (98%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{ => list}/ChatListStore.kt (98%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{ => list}/ChatListStoreFactory.kt (94%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{chatList => list}/DefaultChatListComponent.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{chatList => list}/components/AccountMenu.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{chatList => list}/components/ArchiveHeaderCard.kt (98%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{chatList => list}/components/Avatar.kt (96%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{chatList => list}/components/ChatCreationCommon.kt (67%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{chatList => list}/components/ChatListItem.kt (98%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{chatList => list}/components/ChatListShimmer.kt (96%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{chatList => list}/components/ChatListTopBar.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{chatList => list}/components/EmptyStateView.kt (96%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{chatList => list}/components/FolderTabs.kt (97%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{chatList => list}/components/MessageSearchItem.kt (98%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{chatList => list}/components/NewChannelContent.kt (97%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{chatList => list}/components/NewGroupContent.kt (97%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{chatList => list}/components/PermissionRequestSheet.kt (99%) rename presentation/src/main/java/org/monogram/presentation/features/chats/{chatList => list}/components/SelectionTopBar.kt (98%) rename presentation/src/test/java/org/monogram/presentation/features/chats/{currentChat => conversation}/editor/photo/components/TransformControlsTest.kt (93%) rename presentation/src/test/java/org/monogram/presentation/features/chats/{currentChat => conversation}/editor/photo/crop/CropGeometryTest.kt (98%) diff --git a/app/src/main/java/org/monogram/app/MainActivity.kt b/app/src/main/java/org/monogram/app/MainActivity.kt index 163f28ef..3ddccd02 100644 --- a/app/src/main/java/org/monogram/app/MainActivity.kt +++ b/app/src/main/java/org/monogram/app/MainActivity.kt @@ -22,7 +22,7 @@ import org.monogram.domain.repository.PushProvider import org.monogram.presentation.core.util.AppPreferences import org.monogram.presentation.core.util.LocalVideoPlayerPool import org.monogram.presentation.core.util.NightMode -import org.monogram.presentation.features.chats.currentChat.components.chats.LocalLinkHandler +import org.monogram.presentation.features.chats.conversation.ui.message.LocalLinkHandler import org.monogram.presentation.root.DefaultAppComponentContext import org.monogram.presentation.root.DefaultRootComponent import org.monogram.presentation.root.RootComponent diff --git a/app/src/main/java/org/monogram/app/MainContent.kt b/app/src/main/java/org/monogram/app/MainContent.kt index 06f54562..88b1b12b 100644 --- a/app/src/main/java/org/monogram/app/MainContent.kt +++ b/app/src/main/java/org/monogram/app/MainContent.kt @@ -37,8 +37,8 @@ import org.monogram.app.components.MobileLayout import org.monogram.app.components.ProxyConfirmSheet import org.monogram.app.components.TabletLayout import org.monogram.presentation.core.util.LocalTabletInterfaceEnabled -import org.monogram.presentation.features.chats.currentChat.chatContent.ChatContentViewers -import org.monogram.presentation.features.chats.currentChat.components.StickerSetSheet +import org.monogram.presentation.features.chats.conversation.ui.StickerSetSheet +import org.monogram.presentation.features.chats.conversation.ui.content.ChatContentViewers import org.monogram.presentation.features.profile.ProfileViewers import org.monogram.presentation.features.stickers.core.toDomain import org.monogram.presentation.root.RootComponent diff --git a/app/src/main/java/org/monogram/app/components/ChatConfirmJoinSheet.kt b/app/src/main/java/org/monogram/app/components/ChatConfirmJoinSheet.kt index 7c9f6efe..0ffc9a42 100644 --- a/app/src/main/java/org/monogram/app/components/ChatConfirmJoinSheet.kt +++ b/app/src/main/java/org/monogram/app/components/ChatConfirmJoinSheet.kt @@ -15,7 +15,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import org.monogram.app.R -import org.monogram.presentation.features.chats.chatList.components.AvatarTopAppBar +import org.monogram.presentation.features.chats.list.components.AvatarTopAppBar import org.monogram.presentation.root.RootComponent @OptIn(ExperimentalMaterial3Api::class) diff --git a/app/src/main/java/org/monogram/app/components/RenderChild.kt b/app/src/main/java/org/monogram/app/components/RenderChild.kt index 593d3081..e464e73c 100644 --- a/app/src/main/java/org/monogram/app/components/RenderChild.kt +++ b/app/src/main/java/org/monogram/app/components/RenderChild.kt @@ -2,9 +2,9 @@ package org.monogram.app.components import androidx.compose.runtime.Composable import org.monogram.presentation.features.auth.AuthContent -import org.monogram.presentation.features.chats.chatList.ChatListContent -import org.monogram.presentation.features.chats.currentChat.ChatContent -import org.monogram.presentation.features.chats.newChat.NewChatContent +import org.monogram.presentation.features.chats.conversation.ChatContent +import org.monogram.presentation.features.chats.creation.NewChatContent +import org.monogram.presentation.features.chats.list.ChatListContent import org.monogram.presentation.features.profile.ProfileContent import org.monogram.presentation.features.profile.admin.AdminManageContent import org.monogram.presentation.features.profile.admin.ChatEditContent diff --git a/app/src/main/java/org/monogram/app/di/AppModule.kt b/app/src/main/java/org/monogram/app/di/AppModule.kt index cf75fc9b..be1265fe 100644 --- a/app/src/main/java/org/monogram/app/di/AppModule.kt +++ b/app/src/main/java/org/monogram/app/di/AppModule.kt @@ -23,6 +23,8 @@ import org.monogram.domain.repository.CacheProvider import org.monogram.domain.repository.EditorSnippetProvider import org.monogram.domain.repository.ExternalNavigator import org.monogram.domain.repository.MessageDisplayer +import org.monogram.presentation.core.media.ExoPlayerCache +import org.monogram.presentation.core.media.VideoPlayerPool import org.monogram.presentation.core.util.AppPreferences import org.monogram.presentation.core.util.BotPreferences import org.monogram.presentation.core.util.CachePreferences @@ -34,8 +36,6 @@ import org.monogram.presentation.core.util.ExternalNavigatorImpl import org.monogram.presentation.core.util.IDownloadUtils import org.monogram.presentation.core.util.ToastMessageDisplayer import org.monogram.presentation.di.uiModule -import org.monogram.presentation.features.chats.currentChat.components.ExoPlayerCache -import org.monogram.presentation.features.chats.currentChat.components.VideoPlayerPool import org.monogram.presentation.settings.storage.CacheController @SuppressLint("WrongConstant") diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/AlphaVideoPlayer.kt b/presentation/src/main/java/org/monogram/presentation/core/media/AlphaVideoPlayer.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/AlphaVideoPlayer.kt rename to presentation/src/main/java/org/monogram/presentation/core/media/AlphaVideoPlayer.kt index 5579251e..117281c8 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/AlphaVideoPlayer.kt +++ b/presentation/src/main/java/org/monogram/presentation/core/media/AlphaVideoPlayer.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components +package org.monogram.presentation.core.media import android.annotation.SuppressLint import android.app.Application diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/AvatarPlayer.kt b/presentation/src/main/java/org/monogram/presentation/core/media/AvatarPlayer.kt similarity index 76% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/AvatarPlayer.kt rename to presentation/src/main/java/org/monogram/presentation/core/media/AvatarPlayer.kt index 16ad5eef..48d970b5 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/AvatarPlayer.kt +++ b/presentation/src/main/java/org/monogram/presentation/core/media/AvatarPlayer.kt @@ -1,8 +1,9 @@ -package org.monogram.presentation.features.chats.currentChat.components +package org.monogram.presentation.core.media import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale +import org.monogram.presentation.features.chats.conversation.ui.InlineVideoPlayer @Composable fun AvatarPlayer( diff --git a/presentation/src/main/java/org/monogram/presentation/core/ui/Avatar.kt b/presentation/src/main/java/org/monogram/presentation/core/ui/Avatar.kt index 2e6b4b40..62cab58f 100644 --- a/presentation/src/main/java/org/monogram/presentation/core/ui/Avatar.kt +++ b/presentation/src/main/java/org/monogram/presentation/core/ui/Avatar.kt @@ -23,7 +23,7 @@ import coil3.compose.AsyncImage import coil3.request.ImageRequest import org.monogram.presentation.R import org.monogram.presentation.core.util.generateColorFromHash -import org.monogram.presentation.features.chats.currentChat.components.AvatarPlayer +import org.monogram.presentation.core.media.AvatarPlayer import java.io.File @Composable diff --git a/presentation/src/main/java/org/monogram/presentation/core/ui/AvatarHeader.kt b/presentation/src/main/java/org/monogram/presentation/core/ui/AvatarHeader.kt index 8a4ab8cf..85f960ff 100644 --- a/presentation/src/main/java/org/monogram/presentation/core/ui/AvatarHeader.kt +++ b/presentation/src/main/java/org/monogram/presentation/core/ui/AvatarHeader.kt @@ -22,7 +22,7 @@ import coil3.request.crossfade import coil3.size.Precision import coil3.size.Size import org.monogram.presentation.core.util.generateColorFromHash -import org.monogram.presentation.features.chats.currentChat.components.AvatarPlayer +import org.monogram.presentation.core.media.AvatarPlayer import java.io.File @Composable diff --git a/presentation/src/main/java/org/monogram/presentation/core/ui/SectionHeader.kt b/presentation/src/main/java/org/monogram/presentation/core/ui/SectionHeader.kt index 3c06ffde..f6aede16 100644 --- a/presentation/src/main/java/org/monogram/presentation/core/ui/SectionHeader.kt +++ b/presentation/src/main/java/org/monogram/presentation/core/ui/SectionHeader.kt @@ -9,10 +9,10 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @Composable -fun SectionHeader(text: String) { +fun SectionHeader(text: String, modifier: Modifier = Modifier) { Text( text = text, - modifier = Modifier.padding(start = 12.dp, bottom = 8.dp, top = 16.dp), + modifier = modifier.padding(start = 12.dp, bottom = 8.dp, top = 16.dp), style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold diff --git a/presentation/src/main/java/org/monogram/presentation/core/ui/SettingsTextField.kt b/presentation/src/main/java/org/monogram/presentation/core/ui/SettingsTextField.kt new file mode 100644 index 00000000..95577959 --- /dev/null +++ b/presentation/src/main/java/org/monogram/presentation/core/ui/SettingsTextField.kt @@ -0,0 +1,102 @@ +package org.monogram.presentation.core.ui + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import org.monogram.presentation.core.ui.ItemPosition + +@Composable +fun SettingsTextField( + value: String, + onValueChange: (String) -> Unit, + placeholder: String, + icon: ImageVector, + position: ItemPosition, + modifier: Modifier = Modifier, + enabled: Boolean = true, + singleLine: Boolean = false, + minLines: Int = 1, + maxLines: Int = Int.MAX_VALUE, + itemSpacing: Dp = 2.dp, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, + trailingIcon: @Composable (() -> Unit)? = null +) { + val cornerRadius = 24.dp + val shape = when (position) { + ItemPosition.TOP -> RoundedCornerShape( + topStart = cornerRadius, + topEnd = cornerRadius, + bottomStart = 4.dp, + bottomEnd = 4.dp + ) + + ItemPosition.MIDDLE -> RoundedCornerShape(4.dp) + ItemPosition.BOTTOM -> RoundedCornerShape( + bottomStart = cornerRadius, + bottomEnd = cornerRadius, + topStart = 4.dp, + topEnd = 4.dp + ) + + ItemPosition.STANDALONE -> RoundedCornerShape(cornerRadius) + } + + Surface( + color = MaterialTheme.colorScheme.surfaceContainer, + shape = shape, + modifier = modifier.fillMaxWidth() + ) { + TextField( + value = value, + onValueChange = onValueChange, + placeholder = { Text(placeholder) }, + modifier = Modifier.fillMaxWidth(), + enabled = enabled, + singleLine = singleLine, + minLines = minLines, + maxLines = maxLines, + leadingIcon = { + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + }, + trailingIcon = trailingIcon, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + colors = TextFieldDefaults.colors( + focusedContainerColor = Color.Transparent, + unfocusedContainerColor = Color.Transparent, + disabledContainerColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent, + disabledTextColor = MaterialTheme.colorScheme.onSurface, + disabledPlaceholderColor = MaterialTheme.colorScheme.onSurfaceVariant, + disabledLeadingIconColor = MaterialTheme.colorScheme.primary, + disabledTrailingIconColor = MaterialTheme.colorScheme.onSurfaceVariant + ) + ) + } + if (position != ItemPosition.BOTTOM && position != ItemPosition.STANDALONE && itemSpacing > 0.dp) { + Spacer(Modifier.height(itemSpacing)) + } +} diff --git a/presentation/src/main/java/org/monogram/presentation/core/util/CompositionLocal.kt b/presentation/src/main/java/org/monogram/presentation/core/util/CompositionLocal.kt index bc299cd4..0e56d73c 100644 --- a/presentation/src/main/java/org/monogram/presentation/core/util/CompositionLocal.kt +++ b/presentation/src/main/java/org/monogram/presentation/core/util/CompositionLocal.kt @@ -1,7 +1,7 @@ package org.monogram.presentation.core.util import androidx.compose.runtime.staticCompositionLocalOf -import org.monogram.presentation.features.chats.currentChat.components.VideoPlayerPool +import org.monogram.presentation.core.media.VideoPlayerPool val LocalVideoPlayerPool = staticCompositionLocalOf { error("VideoPlayerPool not provided") diff --git a/presentation/src/main/java/org/monogram/presentation/di/AppContainer.kt b/presentation/src/main/java/org/monogram/presentation/di/AppContainer.kt index 320e29ed..2bc8bb0e 100644 --- a/presentation/src/main/java/org/monogram/presentation/di/AppContainer.kt +++ b/presentation/src/main/java/org/monogram/presentation/di/AppContainer.kt @@ -57,8 +57,8 @@ import org.monogram.domain.repository.WallpaperRepository import org.monogram.domain.repository.WebAppRepository import org.monogram.presentation.core.util.AppPreferences import org.monogram.presentation.core.util.IDownloadUtils -import org.monogram.presentation.features.chats.currentChat.components.ExoPlayerCache -import org.monogram.presentation.features.chats.currentChat.components.VideoPlayerPool +import org.monogram.presentation.core.media.ExoPlayerCache +import org.monogram.presentation.core.media.VideoPlayerPool import org.monogram.presentation.settings.storage.CacheController interface AppContainer { diff --git a/presentation/src/main/java/org/monogram/presentation/di/KoinAppContainer.kt b/presentation/src/main/java/org/monogram/presentation/di/KoinAppContainer.kt index f9282e3a..ed731cb6 100644 --- a/presentation/src/main/java/org/monogram/presentation/di/KoinAppContainer.kt +++ b/presentation/src/main/java/org/monogram/presentation/di/KoinAppContainer.kt @@ -58,8 +58,8 @@ import org.monogram.domain.repository.WallpaperRepository import org.monogram.domain.repository.WebAppRepository import org.monogram.presentation.core.util.AppPreferences import org.monogram.presentation.core.util.IDownloadUtils -import org.monogram.presentation.features.chats.currentChat.components.ExoPlayerCache -import org.monogram.presentation.features.chats.currentChat.components.VideoPlayerPool +import org.monogram.presentation.core.media.ExoPlayerCache +import org.monogram.presentation.core.media.VideoPlayerPool import org.monogram.presentation.settings.storage.CacheController class KoinAppContainer(koin: Koin) : AppContainer { diff --git a/presentation/src/main/java/org/monogram/presentation/features/auth/components/PhoneInputScreen.kt b/presentation/src/main/java/org/monogram/presentation/features/auth/components/PhoneInputScreen.kt index 4a340af6..9e1cf0b3 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/auth/components/PhoneInputScreen.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/auth/components/PhoneInputScreen.kt @@ -102,7 +102,7 @@ import org.monogram.presentation.core.ui.ExpressiveDefaults import org.monogram.presentation.core.ui.ItemPosition import org.monogram.presentation.core.util.Country import org.monogram.presentation.core.util.CountryManager -import org.monogram.presentation.features.chats.chatList.components.SettingsTextField +import org.monogram.presentation.core.ui.SettingsTextField import java.util.Locale enum class ActiveField { diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/AutoDownloadSuppression.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/AutoDownloadSuppression.kt similarity index 87% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/AutoDownloadSuppression.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/AutoDownloadSuppression.kt index ae93ab7e..a09f06cf 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/AutoDownloadSuppression.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/AutoDownloadSuppression.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat +package org.monogram.presentation.features.chats.conversation import java.util.concurrent.ConcurrentHashMap 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/conversation/ChatComponent.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/ChatComponent.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ChatComponent.kt index ee85729b..3a3584c6 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/ChatComponent.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ChatComponent.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat +package org.monogram.presentation.features.chats.conversation import androidx.compose.runtime.Stable import androidx.compose.ui.platform.Clipboard 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/conversation/ChatContent.kt similarity index 95% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/ChatContent.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ChatContent.kt index 011604e7..c700c1a1 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/ChatContent.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ChatContent.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat +package org.monogram.presentation.features.chats.conversation import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest @@ -91,30 +91,30 @@ import org.monogram.domain.models.MessageModel import org.monogram.presentation.R 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.ChatContentEffects -import org.monogram.presentation.features.chats.currentChat.chatContent.ChatContentList -import org.monogram.presentation.features.chats.currentChat.chatContent.ChatContentOverlays -import org.monogram.presentation.features.chats.currentChat.chatContent.ChatContentSearchOverlay -import org.monogram.presentation.features.chats.currentChat.chatContent.ChatContentTopBar -import org.monogram.presentation.features.chats.currentChat.chatContent.GroupedMessageItem -import org.monogram.presentation.features.chats.currentChat.chatContent.chatContentLeadingItemsCount -import org.monogram.presentation.features.chats.currentChat.chatContent.extractTextContent -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.rememberChatChromeState -import org.monogram.presentation.features.chats.currentChat.chatContent.rememberChatContentPermissionState -import org.monogram.presentation.features.chats.currentChat.chatContent.rememberChatInputBarActions -import org.monogram.presentation.features.chats.currentChat.chatContent.rememberChatInputBarState -import org.monogram.presentation.features.chats.currentChat.chatContent.rememberChatMessageListState -import org.monogram.presentation.features.chats.currentChat.chatContent.rememberChatSearchUiState -import org.monogram.presentation.features.chats.currentChat.chatContent.rememberChatTopBarUiState -import org.monogram.presentation.features.chats.currentChat.chatContent.scrollToMessageIndex -import org.monogram.presentation.features.chats.currentChat.chatContent.withUpdatedTextContent -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.MessageListShimmer -import org.monogram.presentation.features.chats.currentChat.components.chats.LocalLinkHandler +import org.monogram.presentation.features.chats.conversation.ui.content.ChatContentBackground +import org.monogram.presentation.features.chats.conversation.ui.content.ChatContentEffects +import org.monogram.presentation.features.chats.conversation.ui.content.ChatContentList +import org.monogram.presentation.features.chats.conversation.ui.content.ChatContentOverlays +import org.monogram.presentation.features.chats.conversation.ui.content.ChatContentSearchOverlay +import org.monogram.presentation.features.chats.conversation.ui.content.ChatContentTopBar +import org.monogram.presentation.features.chats.conversation.ui.content.GroupedMessageItem +import org.monogram.presentation.features.chats.conversation.ui.content.chatContentLeadingItemsCount +import org.monogram.presentation.features.chats.conversation.ui.content.extractTextContent +import org.monogram.presentation.features.chats.conversation.ui.content.groupMessagesByAlbum +import org.monogram.presentation.features.chats.conversation.ui.content.groupedIndexToLazyIndex +import org.monogram.presentation.features.chats.conversation.ui.content.rememberChatChromeState +import org.monogram.presentation.features.chats.conversation.ui.content.rememberChatContentPermissionState +import org.monogram.presentation.features.chats.conversation.ui.content.rememberChatInputBarActions +import org.monogram.presentation.features.chats.conversation.ui.content.rememberChatInputBarState +import org.monogram.presentation.features.chats.conversation.ui.content.rememberChatMessageListState +import org.monogram.presentation.features.chats.conversation.ui.content.rememberChatSearchUiState +import org.monogram.presentation.features.chats.conversation.ui.content.rememberChatTopBarUiState +import org.monogram.presentation.features.chats.conversation.ui.content.scrollToMessageIndex +import org.monogram.presentation.features.chats.conversation.ui.content.withUpdatedTextContent +import org.monogram.presentation.features.chats.conversation.ui.AdvancedCircularRecorderScreen +import org.monogram.presentation.features.chats.conversation.ui.ChatInputBar +import org.monogram.presentation.features.chats.conversation.ui.MessageListShimmer +import org.monogram.presentation.features.chats.conversation.ui.message.LocalLinkHandler import java.io.File import java.io.FileOutputStream @@ -1010,3 +1010,4 @@ fun ChatContent( } } } + diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/ChatScrollModels.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ChatScrollModels.kt similarity index 92% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/ChatScrollModels.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ChatScrollModels.kt index b75e439e..49bc8850 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/ChatScrollModels.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ChatScrollModels.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat +package org.monogram.presentation.features.chats.conversation import androidx.compose.runtime.Immutable diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/ChatStore.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ChatStore.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/ChatStore.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ChatStore.kt index 13b2065d..8c3329f3 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/ChatStore.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ChatStore.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat +package org.monogram.presentation.features.chats.conversation import androidx.compose.ui.platform.Clipboard import com.arkivanov.mvikotlin.core.store.Store diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/ChatStoreFactory.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ChatStoreFactory.kt similarity index 71% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/ChatStoreFactory.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ChatStoreFactory.kt index 6e00e67c..15b64684 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/ChatStoreFactory.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ChatStoreFactory.kt @@ -1,87 +1,87 @@ -package org.monogram.presentation.features.chats.currentChat +package org.monogram.presentation.features.chats.conversation import com.arkivanov.mvikotlin.core.store.Reducer import com.arkivanov.mvikotlin.core.store.Store import com.arkivanov.mvikotlin.core.store.StoreFactory import com.arkivanov.mvikotlin.extensions.coroutines.CoroutineExecutor import kotlinx.coroutines.flow.update -import org.monogram.presentation.features.chats.currentChat.ChatStore.Intent -import org.monogram.presentation.features.chats.currentChat.ChatStore.Label -import org.monogram.presentation.features.chats.currentChat.impl.handleAcceptMiniAppTOS -import org.monogram.presentation.features.chats.currentChat.impl.handleAddToAdBlockWhitelist -import org.monogram.presentation.features.chats.currentChat.impl.handleAddToGifs -import org.monogram.presentation.features.chats.currentChat.impl.handleBlockUser -import org.monogram.presentation.features.chats.currentChat.impl.handleBotCommandClick -import org.monogram.presentation.features.chats.currentChat.impl.handleCancelDownloadFile -import org.monogram.presentation.features.chats.currentChat.impl.handleClearHistory -import org.monogram.presentation.features.chats.currentChat.impl.handleClearMessages -import org.monogram.presentation.features.chats.currentChat.impl.handleClearSelection -import org.monogram.presentation.features.chats.currentChat.impl.handleClosePoll -import org.monogram.presentation.features.chats.currentChat.impl.handleCommentsClick -import org.monogram.presentation.features.chats.currentChat.impl.handleConfirmRestrict -import org.monogram.presentation.features.chats.currentChat.impl.handleCopyLink -import org.monogram.presentation.features.chats.currentChat.impl.handleCopySelectedMessages -import org.monogram.presentation.features.chats.currentChat.impl.handleDeleteChat -import org.monogram.presentation.features.chats.currentChat.impl.handleDeleteMessage -import org.monogram.presentation.features.chats.currentChat.impl.handleDeleteSelectedMessages -import org.monogram.presentation.features.chats.currentChat.impl.handleDismissInvoice -import org.monogram.presentation.features.chats.currentChat.impl.handleDismissMiniAppTOS -import org.monogram.presentation.features.chats.currentChat.impl.handleDownloadFile -import org.monogram.presentation.features.chats.currentChat.impl.handleDownloadHighRes -import org.monogram.presentation.features.chats.currentChat.impl.handleDraftChange -import org.monogram.presentation.features.chats.currentChat.impl.handleInlineQueryChange -import org.monogram.presentation.features.chats.currentChat.impl.handleJoinChat -import org.monogram.presentation.features.chats.currentChat.impl.handleKeyboardButtonClick -import org.monogram.presentation.features.chats.currentChat.impl.handleLoadMoreInlineResults -import org.monogram.presentation.features.chats.currentChat.impl.handleMentionQueryChange -import org.monogram.presentation.features.chats.currentChat.impl.handleMessageVisible -import org.monogram.presentation.features.chats.currentChat.impl.handleOpenInvoice -import org.monogram.presentation.features.chats.currentChat.impl.handleOpenMiniApp -import org.monogram.presentation.features.chats.currentChat.impl.handlePinMessage -import org.monogram.presentation.features.chats.currentChat.impl.handlePinnedMessageClick -import org.monogram.presentation.features.chats.currentChat.impl.handlePollOptionClick -import org.monogram.presentation.features.chats.currentChat.impl.handleRemoveFromAdBlockWhitelist -import org.monogram.presentation.features.chats.currentChat.impl.handleRepeatMessage -import org.monogram.presentation.features.chats.currentChat.impl.handleReplyMarkupButtonClick -import org.monogram.presentation.features.chats.currentChat.impl.handleReportMessage -import org.monogram.presentation.features.chats.currentChat.impl.handleReportReasonSelected -import org.monogram.presentation.features.chats.currentChat.impl.handleRetractVote -import org.monogram.presentation.features.chats.currentChat.impl.handleSaveEditedMessage -import org.monogram.presentation.features.chats.currentChat.impl.handleSearchDateRangeChange -import org.monogram.presentation.features.chats.currentChat.impl.handleSearchNextResult -import org.monogram.presentation.features.chats.currentChat.impl.handleSearchPreviousResult -import org.monogram.presentation.features.chats.currentChat.impl.handleSearchQueryChange -import org.monogram.presentation.features.chats.currentChat.impl.handleSearchResultClick -import org.monogram.presentation.features.chats.currentChat.impl.handleSearchSenderChange -import org.monogram.presentation.features.chats.currentChat.impl.handleSearchToggle -import org.monogram.presentation.features.chats.currentChat.impl.handleSendAlbum -import org.monogram.presentation.features.chats.currentChat.impl.handleSendDocument -import org.monogram.presentation.features.chats.currentChat.impl.handleSendGif -import org.monogram.presentation.features.chats.currentChat.impl.handleSendGifFile -import org.monogram.presentation.features.chats.currentChat.impl.handleSendInlineResult -import org.monogram.presentation.features.chats.currentChat.impl.handleSendMessage -import org.monogram.presentation.features.chats.currentChat.impl.handleSendPhoto -import org.monogram.presentation.features.chats.currentChat.impl.handleSendPoll -import org.monogram.presentation.features.chats.currentChat.impl.handleSendReaction -import org.monogram.presentation.features.chats.currentChat.impl.handleSendScheduledNow -import org.monogram.presentation.features.chats.currentChat.impl.handleSendSticker -import org.monogram.presentation.features.chats.currentChat.impl.handleSendVideo -import org.monogram.presentation.features.chats.currentChat.impl.handleSendVoice -import org.monogram.presentation.features.chats.currentChat.impl.handleShowVoters -import org.monogram.presentation.features.chats.currentChat.impl.handleStickerClick -import org.monogram.presentation.features.chats.currentChat.impl.handleToggleMessageSelection -import org.monogram.presentation.features.chats.currentChat.impl.handleToggleMute -import org.monogram.presentation.features.chats.currentChat.impl.handleTopicClick -import org.monogram.presentation.features.chats.currentChat.impl.handleUnblockUser -import org.monogram.presentation.features.chats.currentChat.impl.handleUnpinMessage -import org.monogram.presentation.features.chats.currentChat.impl.handleVideoRecorded -import org.monogram.presentation.features.chats.currentChat.impl.loadAllPinnedMessages -import org.monogram.presentation.features.chats.currentChat.impl.loadMoreMessages -import org.monogram.presentation.features.chats.currentChat.impl.loadMoreSearchResults -import org.monogram.presentation.features.chats.currentChat.impl.loadNewerMessages -import org.monogram.presentation.features.chats.currentChat.impl.loadScheduledMessages -import org.monogram.presentation.features.chats.currentChat.impl.scrollToBottomInternal -import org.monogram.presentation.features.chats.currentChat.impl.scrollToMessageInternal +import org.monogram.presentation.features.chats.conversation.ChatStore.Intent +import org.monogram.presentation.features.chats.conversation.ChatStore.Label +import org.monogram.presentation.features.chats.conversation.logic.handleAcceptMiniAppTOS +import org.monogram.presentation.features.chats.conversation.logic.handleAddToAdBlockWhitelist +import org.monogram.presentation.features.chats.conversation.logic.handleAddToGifs +import org.monogram.presentation.features.chats.conversation.logic.handleBlockUser +import org.monogram.presentation.features.chats.conversation.logic.handleBotCommandClick +import org.monogram.presentation.features.chats.conversation.logic.handleCancelDownloadFile +import org.monogram.presentation.features.chats.conversation.logic.handleClearHistory +import org.monogram.presentation.features.chats.conversation.logic.handleClearMessages +import org.monogram.presentation.features.chats.conversation.logic.handleClearSelection +import org.monogram.presentation.features.chats.conversation.logic.handleClosePoll +import org.monogram.presentation.features.chats.conversation.logic.handleCommentsClick +import org.monogram.presentation.features.chats.conversation.logic.handleConfirmRestrict +import org.monogram.presentation.features.chats.conversation.logic.handleCopyLink +import org.monogram.presentation.features.chats.conversation.logic.handleCopySelectedMessages +import org.monogram.presentation.features.chats.conversation.logic.handleDeleteChat +import org.monogram.presentation.features.chats.conversation.logic.handleDeleteMessage +import org.monogram.presentation.features.chats.conversation.logic.handleDeleteSelectedMessages +import org.monogram.presentation.features.chats.conversation.logic.handleDismissInvoice +import org.monogram.presentation.features.chats.conversation.logic.handleDismissMiniAppTOS +import org.monogram.presentation.features.chats.conversation.logic.handleDownloadFile +import org.monogram.presentation.features.chats.conversation.logic.handleDownloadHighRes +import org.monogram.presentation.features.chats.conversation.logic.handleDraftChange +import org.monogram.presentation.features.chats.conversation.logic.handleInlineQueryChange +import org.monogram.presentation.features.chats.conversation.logic.handleJoinChat +import org.monogram.presentation.features.chats.conversation.logic.handleKeyboardButtonClick +import org.monogram.presentation.features.chats.conversation.logic.handleLoadMoreInlineResults +import org.monogram.presentation.features.chats.conversation.logic.handleMentionQueryChange +import org.monogram.presentation.features.chats.conversation.logic.handleMessageVisible +import org.monogram.presentation.features.chats.conversation.logic.handleOpenInvoice +import org.monogram.presentation.features.chats.conversation.logic.handleOpenMiniApp +import org.monogram.presentation.features.chats.conversation.logic.handlePinMessage +import org.monogram.presentation.features.chats.conversation.logic.handlePinnedMessageClick +import org.monogram.presentation.features.chats.conversation.logic.handlePollOptionClick +import org.monogram.presentation.features.chats.conversation.logic.handleRemoveFromAdBlockWhitelist +import org.monogram.presentation.features.chats.conversation.logic.handleRepeatMessage +import org.monogram.presentation.features.chats.conversation.logic.handleReplyMarkupButtonClick +import org.monogram.presentation.features.chats.conversation.logic.handleReportMessage +import org.monogram.presentation.features.chats.conversation.logic.handleReportReasonSelected +import org.monogram.presentation.features.chats.conversation.logic.handleRetractVote +import org.monogram.presentation.features.chats.conversation.logic.handleSaveEditedMessage +import org.monogram.presentation.features.chats.conversation.logic.handleSearchDateRangeChange +import org.monogram.presentation.features.chats.conversation.logic.handleSearchNextResult +import org.monogram.presentation.features.chats.conversation.logic.handleSearchPreviousResult +import org.monogram.presentation.features.chats.conversation.logic.handleSearchQueryChange +import org.monogram.presentation.features.chats.conversation.logic.handleSearchResultClick +import org.monogram.presentation.features.chats.conversation.logic.handleSearchSenderChange +import org.monogram.presentation.features.chats.conversation.logic.handleSearchToggle +import org.monogram.presentation.features.chats.conversation.logic.handleSendAlbum +import org.monogram.presentation.features.chats.conversation.logic.handleSendDocument +import org.monogram.presentation.features.chats.conversation.logic.handleSendGif +import org.monogram.presentation.features.chats.conversation.logic.handleSendGifFile +import org.monogram.presentation.features.chats.conversation.logic.handleSendInlineResult +import org.monogram.presentation.features.chats.conversation.logic.handleSendMessage +import org.monogram.presentation.features.chats.conversation.logic.handleSendPhoto +import org.monogram.presentation.features.chats.conversation.logic.handleSendPoll +import org.monogram.presentation.features.chats.conversation.logic.handleSendReaction +import org.monogram.presentation.features.chats.conversation.logic.handleSendScheduledNow +import org.monogram.presentation.features.chats.conversation.logic.handleSendSticker +import org.monogram.presentation.features.chats.conversation.logic.handleSendVideo +import org.monogram.presentation.features.chats.conversation.logic.handleSendVoice +import org.monogram.presentation.features.chats.conversation.logic.handleShowVoters +import org.monogram.presentation.features.chats.conversation.logic.handleStickerClick +import org.monogram.presentation.features.chats.conversation.logic.handleToggleMessageSelection +import org.monogram.presentation.features.chats.conversation.logic.handleToggleMute +import org.monogram.presentation.features.chats.conversation.logic.handleTopicClick +import org.monogram.presentation.features.chats.conversation.logic.handleUnblockUser +import org.monogram.presentation.features.chats.conversation.logic.handleUnpinMessage +import org.monogram.presentation.features.chats.conversation.logic.handleVideoRecorded +import org.monogram.presentation.features.chats.conversation.logic.loadAllPinnedMessages +import org.monogram.presentation.features.chats.conversation.logic.loadMoreMessages +import org.monogram.presentation.features.chats.conversation.logic.loadMoreSearchResults +import org.monogram.presentation.features.chats.conversation.logic.loadNewerMessages +import org.monogram.presentation.features.chats.conversation.logic.loadScheduledMessages +import org.monogram.presentation.features.chats.conversation.logic.scrollToBottomInternal +import org.monogram.presentation.features.chats.conversation.logic.scrollToMessageInternal class ChatStoreFactory( private val storeFactory: StoreFactory, 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/conversation/DefaultChatComponent.kt similarity index 97% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/DefaultChatComponent.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/DefaultChatComponent.kt index c9370455..67d25b8a 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/DefaultChatComponent.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/DefaultChatComponent.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat +package org.monogram.presentation.features.chats.conversation import android.util.Log import androidx.compose.ui.platform.Clipboard @@ -58,16 +58,16 @@ import org.monogram.domain.repository.WallpaperRepository import org.monogram.presentation.core.util.AppPreferences import org.monogram.presentation.core.util.IDownloadUtils import org.monogram.presentation.core.util.componentScope -import org.monogram.presentation.features.chats.currentChat.impl.loadChatInfo -import org.monogram.presentation.features.chats.currentChat.impl.loadDraft -import org.monogram.presentation.features.chats.currentChat.impl.loadMessages -import org.monogram.presentation.features.chats.currentChat.impl.loadPinnedMessage -import org.monogram.presentation.features.chats.currentChat.impl.loadScheduledMessages -import org.monogram.presentation.features.chats.currentChat.impl.loadWallpapers -import org.monogram.presentation.features.chats.currentChat.impl.observePreferences -import org.monogram.presentation.features.chats.currentChat.impl.observeUserUpdates -import org.monogram.presentation.features.chats.currentChat.impl.setupMessageCollectors -import org.monogram.presentation.features.chats.currentChat.impl.setupPinnedMessageCollector +import org.monogram.presentation.features.chats.conversation.logic.loadChatInfo +import org.monogram.presentation.features.chats.conversation.logic.loadDraft +import org.monogram.presentation.features.chats.conversation.logic.loadMessages +import org.monogram.presentation.features.chats.conversation.logic.loadPinnedMessage +import org.monogram.presentation.features.chats.conversation.logic.loadScheduledMessages +import org.monogram.presentation.features.chats.conversation.logic.loadWallpapers +import org.monogram.presentation.features.chats.conversation.logic.observePreferences +import org.monogram.presentation.features.chats.conversation.logic.observeUserUpdates +import org.monogram.presentation.features.chats.conversation.logic.setupMessageCollectors +import org.monogram.presentation.features.chats.conversation.logic.setupPinnedMessageCollector import org.monogram.presentation.root.AppComponentContext import org.monogram.presentation.settings.storage.CacheController import java.io.File diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/photo/PhotoEditorScreen.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/photo/PhotoEditorScreen.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/photo/PhotoEditorScreen.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/photo/PhotoEditorScreen.kt index 01c133b8..076a446b 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/photo/PhotoEditorScreen.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/photo/PhotoEditorScreen.kt @@ -1,6 +1,6 @@ @file:OptIn(ExperimentalMaterial3ExpressiveApi::class) -package org.monogram.presentation.features.chats.currentChat.editor.photo +package org.monogram.presentation.features.chats.conversation.editor.photo import android.graphics.BitmapFactory import androidx.activity.compose.BackHandler @@ -43,8 +43,8 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.monogram.presentation.R -import org.monogram.presentation.features.chats.currentChat.editor.photo.components.* -import org.monogram.presentation.features.chats.currentChat.editor.photo.crop.* +import org.monogram.presentation.features.chats.conversation.editor.photo.components.* +import org.monogram.presentation.features.chats.conversation.editor.photo.crop.* import java.io.File enum class EditorTool(val labelRes: Int, val icon: ImageVector) { diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/photo/PhotoEditorUtils.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/photo/PhotoEditorUtils.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/photo/PhotoEditorUtils.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/photo/PhotoEditorUtils.kt index 1b534410..9d2ba9f6 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/photo/PhotoEditorUtils.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/photo/PhotoEditorUtils.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.editor.photo +package org.monogram.presentation.features.chats.conversation.editor.photo import android.content.Context import android.graphics.* diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/photo/components/ColorSelector.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/photo/components/ColorSelector.kt similarity index 95% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/photo/components/ColorSelector.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/photo/components/ColorSelector.kt index 369d8c7a..9285ddc8 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/photo/components/ColorSelector.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/photo/components/ColorSelector.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.editor.photo.components +package org.monogram.presentation.features.chats.conversation.editor.photo.components import androidx.compose.foundation.background import androidx.compose.foundation.border diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/photo/components/DrawControls.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/photo/components/DrawControls.kt similarity index 96% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/photo/components/DrawControls.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/photo/components/DrawControls.kt index e5477ad1..31f51383 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/photo/components/DrawControls.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/photo/components/DrawControls.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.editor.photo.components +package org.monogram.presentation.features.chats.conversation.editor.photo.components import androidx.compose.foundation.background import androidx.compose.foundation.border diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/photo/components/EditorTopBar.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/photo/components/EditorTopBar.kt similarity index 97% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/photo/components/EditorTopBar.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/photo/components/EditorTopBar.kt index 8d02a6dc..934dca17 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/photo/components/EditorTopBar.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/photo/components/EditorTopBar.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.editor.photo.components +package org.monogram.presentation.features.chats.conversation.editor.photo.components import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/photo/components/FilterControls.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/photo/components/FilterControls.kt similarity index 92% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/photo/components/FilterControls.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/photo/components/FilterControls.kt index 7aeee1e5..9520b748 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/photo/components/FilterControls.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/photo/components/FilterControls.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.editor.photo.components +package org.monogram.presentation.features.chats.conversation.editor.photo.components import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -20,8 +20,8 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import org.monogram.presentation.R -import org.monogram.presentation.features.chats.currentChat.editor.photo.ImageFilter -import org.monogram.presentation.features.chats.currentChat.editor.photo.getPresetFilters +import org.monogram.presentation.features.chats.conversation.editor.photo.ImageFilter +import org.monogram.presentation.features.chats.conversation.editor.photo.getPresetFilters import java.io.File @Composable diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/photo/components/TextEntryDialog.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/photo/components/TextEntryDialog.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/photo/components/TextEntryDialog.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/photo/components/TextEntryDialog.kt index 54cfe1f3..ab0ef250 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/photo/components/TextEntryDialog.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/photo/components/TextEntryDialog.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.editor.photo.components +package org.monogram.presentation.features.chats.conversation.editor.photo.components import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/photo/components/TransformControls.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/photo/components/TransformControls.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/photo/components/TransformControls.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/photo/components/TransformControls.kt index c4fa9966..1233a05f 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/photo/components/TransformControls.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/photo/components/TransformControls.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.editor.photo.components +package org.monogram.presentation.features.chats.conversation.editor.photo.components import androidx.compose.foundation.Canvas import androidx.compose.foundation.background diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/photo/crop/CropEditorState.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/photo/crop/CropEditorState.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/photo/crop/CropEditorState.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/photo/crop/CropEditorState.kt index ad405297..d57daebc 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/photo/crop/CropEditorState.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/photo/crop/CropEditorState.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.editor.photo.crop +package org.monogram.presentation.features.chats.conversation.editor.photo.crop import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/photo/crop/CropGeometry.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/photo/crop/CropGeometry.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/photo/crop/CropGeometry.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/photo/crop/CropGeometry.kt index d742f860..aa03e6c4 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/photo/crop/CropGeometry.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/photo/crop/CropGeometry.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.editor.photo.crop +package org.monogram.presentation.features.chats.conversation.editor.photo.crop import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/photo/crop/CropOverlay.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/photo/crop/CropOverlay.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/photo/crop/CropOverlay.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/photo/crop/CropOverlay.kt index 1a884fd2..dc92bacd 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/photo/crop/CropOverlay.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/photo/crop/CropOverlay.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.editor.photo.crop +package org.monogram.presentation.features.chats.conversation.editor.photo.crop import androidx.compose.foundation.Canvas import androidx.compose.foundation.gestures.awaitEachGesture diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/video/VideoEditorScreen.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/video/VideoEditorScreen.kt similarity index 96% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/video/VideoEditorScreen.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/video/VideoEditorScreen.kt index e1716189..9580fcb2 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/video/VideoEditorScreen.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/video/VideoEditorScreen.kt @@ -1,6 +1,6 @@ @file:OptIn(androidx.compose.material3.ExperimentalMaterial3ExpressiveApi::class) -package org.monogram.presentation.features.chats.currentChat.editor.video +package org.monogram.presentation.features.chats.conversation.editor.video import android.widget.Toast import androidx.activity.compose.BackHandler @@ -41,13 +41,13 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.monogram.presentation.R import org.monogram.presentation.core.util.getMimeType -import org.monogram.presentation.features.chats.currentChat.components.VideoGLTextureView -import org.monogram.presentation.features.chats.currentChat.editor.photo.components.EditorTopBar -import org.monogram.presentation.features.chats.currentChat.editor.photo.components.TextEntryDialog -import org.monogram.presentation.features.chats.currentChat.editor.video.components.VideoCompressionControls -import org.monogram.presentation.features.chats.currentChat.editor.video.components.VideoFilterControls -import org.monogram.presentation.features.chats.currentChat.editor.video.components.VideoTextControls -import org.monogram.presentation.features.chats.currentChat.editor.video.components.VideoTrimControls +import org.monogram.presentation.core.media.VideoGLTextureView +import org.monogram.presentation.features.chats.conversation.editor.photo.components.EditorTopBar +import org.monogram.presentation.features.chats.conversation.editor.photo.components.TextEntryDialog +import org.monogram.presentation.features.chats.conversation.editor.video.components.VideoCompressionControls +import org.monogram.presentation.features.chats.conversation.editor.video.components.VideoFilterControls +import org.monogram.presentation.features.chats.conversation.editor.video.components.VideoTextControls +import org.monogram.presentation.features.chats.conversation.editor.video.components.VideoTrimControls import java.io.File import java.io.FileNotFoundException diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/video/VideoEditorUtils.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/video/VideoEditorUtils.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/video/VideoEditorUtils.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/video/VideoEditorUtils.kt index aa531d83..1a885c94 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/video/VideoEditorUtils.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/video/VideoEditorUtils.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.editor.video +package org.monogram.presentation.features.chats.conversation.editor.video import android.content.Context import android.graphics.SurfaceTexture diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/video/components/VideoCompressionControls.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/video/components/VideoCompressionControls.kt similarity index 94% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/video/components/VideoCompressionControls.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/video/components/VideoCompressionControls.kt index 7655832d..ec727710 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/video/components/VideoCompressionControls.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/video/components/VideoCompressionControls.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.editor.video.components +package org.monogram.presentation.features.chats.conversation.editor.video.components import androidx.compose.foundation.layout.* import androidx.compose.material3.MaterialTheme @@ -11,7 +11,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import org.monogram.presentation.R -import org.monogram.presentation.features.chats.currentChat.editor.video.VideoQuality +import org.monogram.presentation.features.chats.conversation.editor.video.VideoQuality @Composable fun VideoCompressionControls( diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/video/components/VideoFilterControls.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/video/components/VideoFilterControls.kt similarity index 91% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/video/components/VideoFilterControls.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/video/components/VideoFilterControls.kt index f1ff9428..5bd2f54a 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/video/components/VideoFilterControls.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/video/components/VideoFilterControls.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.editor.video.components +package org.monogram.presentation.features.chats.conversation.editor.video.components import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -16,8 +16,8 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import org.monogram.presentation.features.chats.currentChat.editor.video.VideoFilter -import org.monogram.presentation.features.chats.currentChat.editor.video.getPresetVideoFilters +import org.monogram.presentation.features.chats.conversation.editor.video.VideoFilter +import org.monogram.presentation.features.chats.conversation.editor.video.getPresetVideoFilters @Composable diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/video/components/VideoTextControls.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/video/components/VideoTextControls.kt similarity index 92% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/video/components/VideoTextControls.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/video/components/VideoTextControls.kt index 26de03af..d6cf1a5f 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/video/components/VideoTextControls.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/video/components/VideoTextControls.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.editor.video.components +package org.monogram.presentation.features.chats.conversation.editor.video.components import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/video/components/VideoTrimControls.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/video/components/VideoTrimControls.kt similarity index 91% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/video/components/VideoTrimControls.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/video/components/VideoTrimControls.kt index 474adae3..d0ec146c 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/editor/video/components/VideoTrimControls.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/editor/video/components/VideoTrimControls.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.editor.video.components +package org.monogram.presentation.features.chats.conversation.editor.video.components import androidx.compose.foundation.background import androidx.compose.foundation.layout.* @@ -9,8 +9,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp -import org.monogram.presentation.features.chats.currentChat.editor.video.VideoTrimRange -import org.monogram.presentation.features.chats.currentChat.editor.video.formatDuration +import org.monogram.presentation.features.chats.conversation.editor.video.VideoTrimRange +import org.monogram.presentation.features.chats.conversation.editor.video.formatDuration @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/BotOperations.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/bots/BotOperations.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/BotOperations.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/bots/BotOperations.kt index 198b787a..721cd6e9 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/BotOperations.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/bots/BotOperations.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.impl +package org.monogram.presentation.features.chats.conversation.logic import android.util.Log import kotlinx.coroutines.CancellationException @@ -14,7 +14,7 @@ import org.monogram.domain.models.KeyboardButtonType import org.monogram.domain.models.MessageContent import org.monogram.domain.models.UserModel import org.monogram.domain.repository.ChatMembersFilter -import org.monogram.presentation.features.chats.currentChat.DefaultChatComponent +import org.monogram.presentation.features.chats.conversation.DefaultChatComponent internal fun DefaultChatComponent.handleMentionQueryChange( query: String?, diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/ChatInfo.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/chat/ChatInfo.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/ChatInfo.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/chat/ChatInfo.kt index 6fc1b2d2..dd97cccf 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/ChatInfo.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/chat/ChatInfo.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.impl +package org.monogram.presentation.features.chats.conversation.logic import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged @@ -14,7 +14,7 @@ import org.monogram.domain.models.ChatType import org.monogram.domain.models.UserStatusType import org.monogram.domain.models.UserTypeEnum import org.monogram.domain.repository.ChatMemberStatus -import org.monogram.presentation.features.chats.currentChat.DefaultChatComponent +import org.monogram.presentation.features.chats.conversation.DefaultChatComponent internal fun DefaultChatComponent.loadChatInfo() { scope.launch { diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/FileOperations.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/files/FileOperations.kt similarity index 83% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/FileOperations.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/files/FileOperations.kt index 20a2a811..b666edea 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/FileOperations.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/files/FileOperations.kt @@ -1,8 +1,8 @@ -package org.monogram.presentation.features.chats.currentChat.impl +package org.monogram.presentation.features.chats.conversation.logic import android.util.Log import kotlinx.coroutines.launch -import org.monogram.presentation.features.chats.currentChat.DefaultChatComponent +import org.monogram.presentation.features.chats.conversation.DefaultChatComponent internal fun DefaultChatComponent.handleDownloadFile(fileId: Int) { repositoryMessage.downloadFile(fileId, priority = 32) diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/MessageActions.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/message-actions/MessageActions.kt similarity index 97% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/MessageActions.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/message-actions/MessageActions.kt index b24924ce..db633521 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/MessageActions.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/message-actions/MessageActions.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.impl +package org.monogram.presentation.features.chats.conversation.logic import android.content.ClipData import android.graphics.Bitmap @@ -16,10 +16,10 @@ import org.monogram.domain.models.MessageEntity import org.monogram.domain.models.MessageModel import org.monogram.domain.models.MessageSendOptions import org.monogram.domain.models.PollDraft -import org.monogram.presentation.features.chats.currentChat.DefaultChatComponent -import org.monogram.presentation.features.chats.currentChat.editor.video.VideoQuality -import org.monogram.presentation.features.chats.currentChat.editor.video.VideoTrimRange -import org.monogram.presentation.features.chats.currentChat.editor.video.processVideo +import org.monogram.presentation.features.chats.conversation.DefaultChatComponent +import org.monogram.presentation.features.chats.conversation.editor.video.VideoQuality +import org.monogram.presentation.features.chats.conversation.editor.video.VideoTrimRange +import org.monogram.presentation.features.chats.conversation.editor.video.processVideo import java.io.File import java.io.FileOutputStream diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/MessageOperations.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/message-actions/MessageOperations.kt similarity index 97% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/MessageOperations.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/message-actions/MessageOperations.kt index 1450a933..fe4922da 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/MessageOperations.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/message-actions/MessageOperations.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.impl +package org.monogram.presentation.features.chats.conversation.logic import kotlinx.coroutines.delay import kotlinx.coroutines.flow.update @@ -7,7 +7,7 @@ import org.monogram.domain.models.MessageContent import org.monogram.domain.models.MessageEntity import org.monogram.domain.models.MessageModel import org.monogram.domain.models.MessageReactionModel -import org.monogram.presentation.features.chats.currentChat.DefaultChatComponent +import org.monogram.presentation.features.chats.conversation.DefaultChatComponent private const val REACTION_UPDATE_SUPPRESSION_MS = 1500L diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/MessageLoading.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/message-loading/MessageLoading.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/MessageLoading.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/message-loading/MessageLoading.kt index 85515a63..d50fbb22 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/MessageLoading.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/message-loading/MessageLoading.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.impl +package org.monogram.presentation.features.chats.conversation.logic import android.util.Log import kotlinx.coroutines.CancellationException @@ -17,10 +17,10 @@ import org.monogram.domain.models.MessageReactionModel import org.monogram.domain.models.MessageSendingState import org.monogram.domain.models.UserModel import org.monogram.domain.repository.ReadUpdate -import org.monogram.presentation.features.chats.currentChat.AutoDownloadSuppression -import org.monogram.presentation.features.chats.currentChat.ChatScrollCommand -import org.monogram.presentation.features.chats.currentChat.DefaultChatComponent -import org.monogram.presentation.features.chats.currentChat.ScrollAlign +import org.monogram.presentation.features.chats.conversation.AutoDownloadSuppression +import org.monogram.presentation.features.chats.conversation.ChatScrollCommand +import org.monogram.presentation.features.chats.conversation.DefaultChatComponent +import org.monogram.presentation.features.chats.conversation.ScrollAlign import java.io.File import kotlin.math.abs diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/MessageSelection.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/message-selection/MessageSelection.kt similarity index 87% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/MessageSelection.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/message-selection/MessageSelection.kt index 1a439ff8..0102d24e 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/MessageSelection.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/message-selection/MessageSelection.kt @@ -1,8 +1,8 @@ -package org.monogram.presentation.features.chats.currentChat.impl +package org.monogram.presentation.features.chats.conversation.logic import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import org.monogram.presentation.features.chats.currentChat.DefaultChatComponent +import org.monogram.presentation.features.chats.conversation.DefaultChatComponent internal fun DefaultChatComponent.handleToggleMessageSelection(messageId: Long) { diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/MiniAppOperations.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/miniapp/MiniAppOperations.kt similarity index 92% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/MiniAppOperations.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/miniapp/MiniAppOperations.kt index 5d19239b..29c44855 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/MiniAppOperations.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/miniapp/MiniAppOperations.kt @@ -1,9 +1,9 @@ -package org.monogram.presentation.features.chats.currentChat.impl +package org.monogram.presentation.features.chats.conversation.logic import android.util.Log import kotlinx.coroutines.flow.update -import org.monogram.presentation.features.chats.currentChat.DefaultChatComponent +import org.monogram.presentation.features.chats.conversation.DefaultChatComponent internal fun DefaultChatComponent.handleOpenMiniApp(url: String, name: String, botUserId: Long) { if (botUserId != 0L && !botPreferences.getWebappPermission(botUserId, "tos_accepted")) { diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/PinnedMessages.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/pinned/PinnedMessages.kt similarity index 95% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/PinnedMessages.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/pinned/PinnedMessages.kt index 3bebe5c7..7cb13e7b 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/PinnedMessages.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/pinned/PinnedMessages.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.impl +package org.monogram.presentation.features.chats.conversation.logic import android.util.Log import kotlinx.coroutines.flow.launchIn @@ -6,9 +6,9 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.monogram.domain.models.MessageModel -import org.monogram.presentation.features.chats.currentChat.ChatScrollCommand -import org.monogram.presentation.features.chats.currentChat.DefaultChatComponent -import org.monogram.presentation.features.chats.currentChat.ScrollAlign +import org.monogram.presentation.features.chats.conversation.ChatScrollCommand +import org.monogram.presentation.features.chats.conversation.DefaultChatComponent +import org.monogram.presentation.features.chats.conversation.ScrollAlign internal fun DefaultChatComponent.loadPinnedMessage() { diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/PollOperations.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/polls/PollOperations.kt similarity index 87% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/PollOperations.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/polls/PollOperations.kt index ce85ce27..0a2d7e47 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/PollOperations.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/polls/PollOperations.kt @@ -1,8 +1,8 @@ -package org.monogram.presentation.features.chats.currentChat.impl +package org.monogram.presentation.features.chats.conversation.logic import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import org.monogram.presentation.features.chats.currentChat.DefaultChatComponent +import org.monogram.presentation.features.chats.conversation.DefaultChatComponent internal fun DefaultChatComponent.handlePollOptionClick(messageId: Long, optionId: Int) { scope.launch { diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/Preferences.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/preferences/Preferences.kt similarity index 97% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/Preferences.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/preferences/Preferences.kt index 89fc59bd..75a87641 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/Preferences.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/preferences/Preferences.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.impl +package org.monogram.presentation.features.chats.conversation.logic import kotlinx.coroutines.flow.combine @@ -6,7 +6,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import org.monogram.domain.models.WallpaperModel -import org.monogram.presentation.features.chats.currentChat.DefaultChatComponent +import org.monogram.presentation.features.chats.conversation.DefaultChatComponent internal fun DefaultChatComponent.observePreferences(availableWallpapers: List) { appPreferences.fontSize diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/SearchMessages.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/search/SearchMessages.kt similarity index 97% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/SearchMessages.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/search/SearchMessages.kt index 93b6b3f3..e9232b71 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/SearchMessages.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/search/SearchMessages.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.impl +package org.monogram.presentation.features.chats.conversation.logic import kotlinx.coroutines.CancellationException import kotlinx.coroutines.delay @@ -6,9 +6,9 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.monogram.domain.models.MessageModel import org.monogram.domain.models.UserModel -import org.monogram.presentation.features.chats.currentChat.ChatScrollCommand -import org.monogram.presentation.features.chats.currentChat.DefaultChatComponent -import org.monogram.presentation.features.chats.currentChat.ScrollAlign +import org.monogram.presentation.features.chats.conversation.ChatScrollCommand +import org.monogram.presentation.features.chats.conversation.DefaultChatComponent +import org.monogram.presentation.features.chats.conversation.ScrollAlign private const val SEARCH_DEBOUNCE_MS = 250L private const val SEARCH_PAGE_SIZE = 20 @@ -18,14 +18,14 @@ private fun hasDateFilter(fromEpochSeconds: Int?, toEpochSeconds: Int?): Boolean return fromEpochSeconds != null || toEpochSeconds != null } -private fun DefaultChatComponent.hasSearchCriteria(state: org.monogram.presentation.features.chats.currentChat.ChatComponent.State): Boolean { +private fun DefaultChatComponent.hasSearchCriteria(state: org.monogram.presentation.features.chats.conversation.ChatComponent.State): Boolean { return state.searchQuery.isNotBlank() || state.searchSender != null || state.searchDateFromEpochSeconds != null || state.searchDateToEpochSeconds != null } -private fun DefaultChatComponent.hasMoreSearchResults(state: org.monogram.presentation.features.chats.currentChat.ChatComponent.State): Boolean { +private fun DefaultChatComponent.hasMoreSearchResults(state: org.monogram.presentation.features.chats.conversation.ChatComponent.State): Boolean { return state.searchResults.size < state.searchResultsTotalCount || state.searchNextFromMessageId != 0L } diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/Stickers.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/stickers/Stickers.kt similarity index 80% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/Stickers.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/stickers/Stickers.kt index 5c2cc20f..79b8fbbf 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/Stickers.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/stickers/Stickers.kt @@ -1,9 +1,9 @@ -package org.monogram.presentation.features.chats.currentChat.impl +package org.monogram.presentation.features.chats.conversation.logic import android.util.Log import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import org.monogram.presentation.features.chats.currentChat.DefaultChatComponent +import org.monogram.presentation.features.chats.conversation.DefaultChatComponent internal fun DefaultChatComponent.handleStickerClick(setId: Long) { if (setId == 0L) return diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/ThreadContext.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/thread/ThreadContext.kt similarity index 78% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/ThreadContext.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/thread/ThreadContext.kt index 60dcb420..74294711 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/impl/ThreadContext.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/thread/ThreadContext.kt @@ -1,8 +1,8 @@ -package org.monogram.presentation.features.chats.currentChat.impl +package org.monogram.presentation.features.chats.conversation.logic import org.monogram.domain.models.MessageModel -import org.monogram.presentation.features.chats.currentChat.ChatComponent -import org.monogram.presentation.features.chats.currentChat.DefaultChatComponent +import org.monogram.presentation.features.chats.conversation.ChatComponent +import org.monogram.presentation.features.chats.conversation.DefaultChatComponent internal fun ChatComponent.State.effectiveThreadChatId(baseChatId: Long): Long { return currentThreadChatId ?: baseChatId diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/AdvancedCircularRecorderScreen.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/AdvancedCircularRecorderScreen.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/AdvancedCircularRecorderScreen.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/AdvancedCircularRecorderScreen.kt index a41e74c4..e1ef0a40 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/AdvancedCircularRecorderScreen.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/AdvancedCircularRecorderScreen.kt @@ -1,6 +1,6 @@ @file:OptIn(ExperimentalMaterial3ExpressiveApi::class) -package org.monogram.presentation.features.chats.currentChat.components +package org.monogram.presentation.features.chats.conversation.ui import android.Manifest import android.annotation.SuppressLint diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/AlbumMessageBubbleContainer.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/AlbumMessageBubbleContainer.kt similarity index 96% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/AlbumMessageBubbleContainer.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/AlbumMessageBubbleContainer.kt index 972ad59d..a7c5e792 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/AlbumMessageBubbleContainer.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/AlbumMessageBubbleContainer.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components +package org.monogram.presentation.features.chats.conversation.ui import android.content.res.Configuration import androidx.compose.animation.core.Animatable @@ -39,11 +39,11 @@ import org.monogram.domain.models.InlineKeyboardButtonModel import org.monogram.domain.models.MessageModel import org.monogram.presentation.core.ui.Avatar import org.monogram.presentation.core.util.IDownloadUtils -import org.monogram.presentation.features.chats.currentChat.chatContent.shouldShowDate -import org.monogram.presentation.features.chats.currentChat.components.channels.ChannelAlbumMessageBubble -import org.monogram.presentation.features.chats.currentChat.components.chats.ChatAlbumMessageBubble -import org.monogram.presentation.features.chats.currentChat.components.chats.MessageViaBotAttribution -import org.monogram.presentation.features.chats.currentChat.components.chats.ReplyMarkupView +import org.monogram.presentation.features.chats.conversation.ui.content.shouldShowDate +import org.monogram.presentation.features.chats.conversation.ui.channel.ChannelAlbumMessageBubble +import org.monogram.presentation.features.chats.conversation.ui.message.ChatAlbumMessageBubble +import org.monogram.presentation.features.chats.conversation.ui.message.MessageViaBotAttribution +import org.monogram.presentation.features.chats.conversation.ui.message.ReplyMarkupView @Composable fun AlbumMessageBubbleContainer( @@ -325,3 +325,4 @@ fun AlbumMessageBubbleContainer( } } } + diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/ChannelMessageBubbleContainer.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/ChannelMessageBubbleContainer.kt similarity index 93% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/ChannelMessageBubbleContainer.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/ChannelMessageBubbleContainer.kt index ebf40ea4..636a2acd 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/ChannelMessageBubbleContainer.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/ChannelMessageBubbleContainer.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components +package org.monogram.presentation.features.chats.conversation.ui import android.content.res.Configuration import androidx.compose.animation.Animatable @@ -37,15 +37,15 @@ import org.monogram.domain.models.InlineKeyboardButtonModel import org.monogram.domain.models.MessageContent import org.monogram.domain.models.MessageModel import org.monogram.presentation.core.util.IDownloadUtils -import org.monogram.presentation.features.chats.currentChat.chatContent.shouldShowDate -import org.monogram.presentation.features.chats.currentChat.components.channels.ChannelGifMessageBubble -import org.monogram.presentation.features.chats.currentChat.components.channels.ChannelPhotoMessageBubble -import org.monogram.presentation.features.chats.currentChat.components.channels.ChannelTextMessageBubble -import org.monogram.presentation.features.chats.currentChat.components.channels.ChannelVideoMessageBubble -import org.monogram.presentation.features.chats.currentChat.components.channels.ChannelVoiceMessageBubble -import org.monogram.presentation.features.chats.currentChat.components.chats.DocumentMessageBubble -import org.monogram.presentation.features.chats.currentChat.components.chats.MessageViaBotAttribution -import org.monogram.presentation.features.chats.currentChat.components.chats.ReplyMarkupView +import org.monogram.presentation.features.chats.conversation.ui.content.shouldShowDate +import org.monogram.presentation.features.chats.conversation.ui.channel.ChannelGifMessageBubble +import org.monogram.presentation.features.chats.conversation.ui.channel.ChannelPhotoMessageBubble +import org.monogram.presentation.features.chats.conversation.ui.channel.ChannelTextMessageBubble +import org.monogram.presentation.features.chats.conversation.ui.channel.ChannelVideoMessageBubble +import org.monogram.presentation.features.chats.conversation.ui.channel.ChannelVoiceMessageBubble +import org.monogram.presentation.features.chats.conversation.ui.message.DocumentMessageBubble +import org.monogram.presentation.features.chats.conversation.ui.message.MessageViaBotAttribution +import org.monogram.presentation.features.chats.conversation.ui.message.ReplyMarkupView @Composable fun ChannelMessageBubbleContainer( @@ -305,3 +305,4 @@ fun ChannelMessageBubbleContainer( } } } + diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/ChatInputBar.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/ChatInputBar.kt similarity index 95% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/ChatInputBar.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/ChatInputBar.kt index 9a2358c8..4fdc774e 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/ChatInputBar.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/ChatInputBar.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components +package org.monogram.presentation.features.chats.conversation.ui import android.Manifest import android.content.pm.PackageManager @@ -51,29 +51,29 @@ import org.monogram.domain.models.StickerModel import org.monogram.domain.repository.StickerRepository import org.monogram.presentation.core.util.AppPreferences import org.monogram.presentation.features.camera.CameraScreen -import org.monogram.presentation.features.chats.currentChat.components.chats.getEmojiFontFamily -import org.monogram.presentation.features.chats.currentChat.components.inputbar.ChatInputBarActions -import org.monogram.presentation.features.chats.currentChat.components.inputbar.ChatInputBarComposerSection -import org.monogram.presentation.features.chats.currentChat.components.inputbar.ChatInputBarState -import org.monogram.presentation.features.chats.currentChat.components.inputbar.ClosedTopicBar -import org.monogram.presentation.features.chats.currentChat.components.inputbar.FullScreenEditorSheet -import org.monogram.presentation.features.chats.currentChat.components.inputbar.InputBarMode -import org.monogram.presentation.features.chats.currentChat.components.inputbar.RestrictedInputBar -import org.monogram.presentation.features.chats.currentChat.components.inputbar.ScheduleDatePickerDialog -import org.monogram.presentation.features.chats.currentChat.components.inputbar.ScheduleTimePickerDialog -import org.monogram.presentation.features.chats.currentChat.components.inputbar.ScheduledMessagesSheet -import org.monogram.presentation.features.chats.currentChat.components.inputbar.SlowModeInputBar -import org.monogram.presentation.features.chats.currentChat.components.inputbar.applyMentionSuggestion -import org.monogram.presentation.features.chats.currentChat.components.inputbar.buildEditingMessageTextValue -import org.monogram.presentation.features.chats.currentChat.components.inputbar.buildScheduledDateEpochSeconds -import org.monogram.presentation.features.chats.currentChat.components.inputbar.copyUriToTempDocumentPath -import org.monogram.presentation.features.chats.currentChat.components.inputbar.copyUriToTempPath -import org.monogram.presentation.features.chats.currentChat.components.inputbar.declaredPermissions -import org.monogram.presentation.features.chats.currentChat.components.inputbar.extractEntities -import org.monogram.presentation.features.chats.currentChat.components.inputbar.hasAllPermissions -import org.monogram.presentation.features.chats.currentChat.components.inputbar.isInlineBotPrefillText -import org.monogram.presentation.features.chats.currentChat.components.inputbar.parseInlineQueryInput -import org.monogram.presentation.features.chats.currentChat.components.inputbar.rememberVoiceRecorder +import org.monogram.presentation.features.chats.conversation.ui.message.getEmojiFontFamily +import org.monogram.presentation.features.chats.conversation.ui.inputbar.ChatInputBarActions +import org.monogram.presentation.features.chats.conversation.ui.inputbar.ChatInputBarComposerSection +import org.monogram.presentation.features.chats.conversation.ui.inputbar.ChatInputBarState +import org.monogram.presentation.features.chats.conversation.ui.inputbar.ClosedTopicBar +import org.monogram.presentation.features.chats.conversation.ui.inputbar.FullScreenEditorSheet +import org.monogram.presentation.features.chats.conversation.ui.inputbar.InputBarMode +import org.monogram.presentation.features.chats.conversation.ui.inputbar.RestrictedInputBar +import org.monogram.presentation.features.chats.conversation.ui.inputbar.ScheduleDatePickerDialog +import org.monogram.presentation.features.chats.conversation.ui.inputbar.ScheduleTimePickerDialog +import org.monogram.presentation.features.chats.conversation.ui.inputbar.ScheduledMessagesSheet +import org.monogram.presentation.features.chats.conversation.ui.inputbar.SlowModeInputBar +import org.monogram.presentation.features.chats.conversation.ui.inputbar.applyMentionSuggestion +import org.monogram.presentation.features.chats.conversation.ui.inputbar.buildEditingMessageTextValue +import org.monogram.presentation.features.chats.conversation.ui.inputbar.buildScheduledDateEpochSeconds +import org.monogram.presentation.features.chats.conversation.ui.inputbar.copyUriToTempDocumentPath +import org.monogram.presentation.features.chats.conversation.ui.inputbar.copyUriToTempPath +import org.monogram.presentation.features.chats.conversation.ui.inputbar.declaredPermissions +import org.monogram.presentation.features.chats.conversation.ui.inputbar.extractEntities +import org.monogram.presentation.features.chats.conversation.ui.inputbar.hasAllPermissions +import org.monogram.presentation.features.chats.conversation.ui.inputbar.isInlineBotPrefillText +import org.monogram.presentation.features.chats.conversation.ui.inputbar.parseInlineQueryInput +import org.monogram.presentation.features.chats.conversation.ui.inputbar.rememberVoiceRecorder import org.monogram.presentation.features.gallery.GalleryScreen import org.monogram.presentation.features.gallery.components.PollComposerSheet import java.util.Calendar diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/ChatTopBar.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/ChatTopBar.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/ChatTopBar.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/ChatTopBar.kt index 111260e7..47864499 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/ChatTopBar.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/ChatTopBar.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components +package org.monogram.presentation.features.chats.conversation.ui import androidx.compose.animation.AnimatedContent import androidx.compose.animation.core.Spring diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/CompactMediaMosaic.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/CompactMediaMosaic.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/CompactMediaMosaic.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/CompactMediaMosaic.kt index 39ddfc70..e09287b0 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/CompactMediaMosaic.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/CompactMediaMosaic.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components +package org.monogram.presentation.features.chats.conversation.ui import androidx.compose.animation.AnimatedContent import androidx.compose.animation.Crossfade @@ -58,15 +58,17 @@ import org.monogram.domain.models.MessageContent import org.monogram.domain.models.MessageModel import org.monogram.domain.models.MessageSendingState import org.monogram.presentation.R +import org.monogram.presentation.core.media.VideoStickerPlayer +import org.monogram.presentation.core.media.VideoType import org.monogram.presentation.core.util.IDownloadUtils import org.monogram.presentation.core.util.namespacedCacheKey -import org.monogram.presentation.features.chats.currentChat.AutoDownloadSuppression -import org.monogram.presentation.features.chats.currentChat.components.channels.formatDuration -import org.monogram.presentation.features.chats.currentChat.components.channels.formatViews -import org.monogram.presentation.features.chats.currentChat.components.chats.ChatTimestampInfo -import org.monogram.presentation.features.chats.currentChat.components.chats.MediaLoadingAction -import org.monogram.presentation.features.chats.currentChat.components.chats.MediaLoadingBackground -import org.monogram.presentation.features.chats.currentChat.components.chats.SpoilerWrapper +import org.monogram.presentation.features.chats.conversation.AutoDownloadSuppression +import org.monogram.presentation.features.chats.conversation.ui.channel.formatDuration +import org.monogram.presentation.features.chats.conversation.ui.channel.formatViews +import org.monogram.presentation.features.chats.conversation.ui.message.ChatTimestampInfo +import org.monogram.presentation.features.chats.conversation.ui.message.MediaLoadingAction +import org.monogram.presentation.features.chats.conversation.ui.message.MediaLoadingBackground +import org.monogram.presentation.features.chats.conversation.ui.message.SpoilerWrapper @Composable fun CompactMediaMosaic( diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/DateSeparator.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/DateSeparator.kt similarity index 94% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/DateSeparator.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/DateSeparator.kt index f9e70476..4ed5ee68 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/DateSeparator.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/DateSeparator.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components +package org.monogram.presentation.features.chats.conversation.ui import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/FastReplyIndicator.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/FastReplyIndicator.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/FastReplyIndicator.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/FastReplyIndicator.kt index b11cee7e..288a97bb 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/FastReplyIndicator.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/FastReplyIndicator.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components +package org.monogram.presentation.features.chats.conversation.ui import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.Animatable diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/InlineVideoPlayer.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/InlineVideoPlayer.kt similarity index 87% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/InlineVideoPlayer.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/InlineVideoPlayer.kt index 7dba9edb..d18f153d 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/InlineVideoPlayer.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/InlineVideoPlayer.kt @@ -1,9 +1,11 @@ -package org.monogram.presentation.features.chats.currentChat.components +package org.monogram.presentation.features.chats.conversation.ui import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.media3.common.PlaybackException +import org.monogram.presentation.core.media.VideoStickerPlayer +import org.monogram.presentation.core.media.VideoType @Composable fun InlineVideoPlayer( diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/MessageBubbleContainer.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/MessageBubbleContainer.kt similarity index 94% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/MessageBubbleContainer.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/MessageBubbleContainer.kt index 596c8464..5c11003e 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/MessageBubbleContainer.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/MessageBubbleContainer.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components +package org.monogram.presentation.features.chats.conversation.ui import android.content.res.Configuration import androidx.compose.animation.Animatable @@ -47,22 +47,22 @@ import org.monogram.domain.models.MessageContent import org.monogram.domain.models.MessageModel import org.monogram.presentation.core.ui.Avatar import org.monogram.presentation.core.util.IDownloadUtils -import org.monogram.presentation.features.chats.currentChat.chatContent.shouldShowDate -import org.monogram.presentation.features.chats.currentChat.components.chats.AudioMessageBubble -import org.monogram.presentation.features.chats.currentChat.components.chats.ContactMessageBubble -import org.monogram.presentation.features.chats.currentChat.components.chats.DocumentMessageBubble -import org.monogram.presentation.features.chats.currentChat.components.chats.GifMessageBubble -import org.monogram.presentation.features.chats.currentChat.components.chats.LocationMessageBubble -import org.monogram.presentation.features.chats.currentChat.components.chats.MessageViaBotAttribution -import org.monogram.presentation.features.chats.currentChat.components.chats.PhotoMessageBubble -import org.monogram.presentation.features.chats.currentChat.components.chats.PollMessageBubble -import org.monogram.presentation.features.chats.currentChat.components.chats.ReplyMarkupView -import org.monogram.presentation.features.chats.currentChat.components.chats.StickerMessageBubble -import org.monogram.presentation.features.chats.currentChat.components.chats.TextMessageBubble -import org.monogram.presentation.features.chats.currentChat.components.chats.VenueMessageBubble -import org.monogram.presentation.features.chats.currentChat.components.chats.VideoMessageBubble -import org.monogram.presentation.features.chats.currentChat.components.chats.VideoNoteBubble -import org.monogram.presentation.features.chats.currentChat.components.chats.VoiceMessageBubble +import org.monogram.presentation.features.chats.conversation.ui.content.shouldShowDate +import org.monogram.presentation.features.chats.conversation.ui.message.AudioMessageBubble +import org.monogram.presentation.features.chats.conversation.ui.message.ContactMessageBubble +import org.monogram.presentation.features.chats.conversation.ui.message.DocumentMessageBubble +import org.monogram.presentation.features.chats.conversation.ui.message.GifMessageBubble +import org.monogram.presentation.features.chats.conversation.ui.message.LocationMessageBubble +import org.monogram.presentation.features.chats.conversation.ui.message.MessageViaBotAttribution +import org.monogram.presentation.features.chats.conversation.ui.message.PhotoMessageBubble +import org.monogram.presentation.features.chats.conversation.ui.message.PollMessageBubble +import org.monogram.presentation.features.chats.conversation.ui.message.ReplyMarkupView +import org.monogram.presentation.features.chats.conversation.ui.message.StickerMessageBubble +import org.monogram.presentation.features.chats.conversation.ui.message.TextMessageBubble +import org.monogram.presentation.features.chats.conversation.ui.message.VenueMessageBubble +import org.monogram.presentation.features.chats.conversation.ui.message.VideoMessageBubble +import org.monogram.presentation.features.chats.conversation.ui.message.VideoNoteBubble +import org.monogram.presentation.features.chats.conversation.ui.message.VoiceMessageBubble @Composable fun MessageBubbleContainer( @@ -756,3 +756,4 @@ private fun MessageReplyMarkup( } private fun androidx.compose.ui.geometry.Size.toOffset() = Offset(width, height) + diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/MessageListShimmer.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/MessageListShimmer.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/MessageListShimmer.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/MessageListShimmer.kt index 57632374..f7693248 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/MessageListShimmer.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/MessageListShimmer.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components +package org.monogram.presentation.features.chats.conversation.ui import androidx.compose.animation.core.* import androidx.compose.foundation.background diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/SenderGrouping.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/SenderGrouping.kt similarity index 87% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/SenderGrouping.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/SenderGrouping.kt index 56522ed8..4e3c8336 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/SenderGrouping.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/SenderGrouping.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components +package org.monogram.presentation.features.chats.conversation.ui import org.monogram.domain.models.MessageModel diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/ServiceMessage.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/ServiceMessage.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/ServiceMessage.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/ServiceMessage.kt index 08c8d211..ac334322 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/ServiceMessage.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/ServiceMessage.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components +package org.monogram.presentation.features.chats.conversation.ui import androidx.compose.foundation.background import androidx.compose.foundation.border diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/StickerSetSheet.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/StickerSetSheet.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/StickerSetSheet.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/StickerSetSheet.kt index 729ebe03..63d35666 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/StickerSetSheet.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/StickerSetSheet.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components +package org.monogram.presentation.features.chats.conversation.ui import android.content.ClipData import android.content.ClipboardManager diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/UnreadMessagesSeparator.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/UnreadMessagesSeparator.kt similarity index 94% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/UnreadMessagesSeparator.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/UnreadMessagesSeparator.kt index 557d9b7f..647a9072 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/UnreadMessagesSeparator.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/UnreadMessagesSeparator.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components +package org.monogram.presentation.features.chats.conversation.ui import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/VoicePlayer.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/VoicePlayer.kt similarity index 97% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/VoicePlayer.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/VoicePlayer.kt index 9061df72..92bad2bd 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/VoicePlayer.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/VoicePlayer.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components +package org.monogram.presentation.features.chats.conversation.ui import android.net.Uri import androidx.compose.runtime.* diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/channels/ChannelAlbumMessageBubble.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/channel/ChannelAlbumMessageBubble.kt similarity index 97% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/channels/ChannelAlbumMessageBubble.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/channel/ChannelAlbumMessageBubble.kt index 0129a411..69255851 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/channels/ChannelAlbumMessageBubble.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/channel/ChannelAlbumMessageBubble.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.channels +package org.monogram.presentation.features.chats.conversation.ui.channel import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -48,15 +48,15 @@ import org.monogram.domain.models.MessageModel import org.monogram.presentation.R import org.monogram.presentation.core.util.DateFormatManager import org.monogram.presentation.core.util.IDownloadUtils -import org.monogram.presentation.features.chats.currentChat.components.CompactMediaMosaic -import org.monogram.presentation.features.chats.currentChat.components.chats.ForwardContent -import org.monogram.presentation.features.chats.currentChat.components.chats.MessageMetadata -import org.monogram.presentation.features.chats.currentChat.components.chats.MessageReactionsView -import org.monogram.presentation.features.chats.currentChat.components.chats.MessageText -import org.monogram.presentation.features.chats.currentChat.components.chats.ReplyContent -import org.monogram.presentation.features.chats.currentChat.components.chats.buildAnnotatedMessageTextWithEmoji -import org.monogram.presentation.features.chats.currentChat.components.chats.formatFileSize -import org.monogram.presentation.features.chats.currentChat.components.chats.rememberMessageInlineContent +import org.monogram.presentation.features.chats.conversation.ui.CompactMediaMosaic +import org.monogram.presentation.features.chats.conversation.ui.message.ForwardContent +import org.monogram.presentation.features.chats.conversation.ui.message.MessageMetadata +import org.monogram.presentation.features.chats.conversation.ui.message.MessageReactionsView +import org.monogram.presentation.features.chats.conversation.ui.message.MessageText +import org.monogram.presentation.features.chats.conversation.ui.message.ReplyContent +import org.monogram.presentation.features.chats.conversation.ui.message.buildAnnotatedMessageTextWithEmoji +import org.monogram.presentation.features.chats.conversation.ui.message.formatFileSize +import org.monogram.presentation.features.chats.conversation.ui.message.rememberMessageInlineContent @Composable fun ChannelAlbumMessageBubble( diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/channels/ChannelCommentsButton.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/channel/ChannelCommentsButton.kt similarity index 97% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/channels/ChannelCommentsButton.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/channel/ChannelCommentsButton.kt index 0c5bb4f7..afc88398 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/channels/ChannelCommentsButton.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/channel/ChannelCommentsButton.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.channels +package org.monogram.presentation.features.chats.conversation.ui.channel import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/channels/ChannelGifMessageBubble.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/channel/ChannelGifMessageBubble.kt similarity index 95% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/channels/ChannelGifMessageBubble.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/channel/ChannelGifMessageBubble.kt index 4d840402..1096add4 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/channels/ChannelGifMessageBubble.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/channel/ChannelGifMessageBubble.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.channels +package org.monogram.presentation.features.chats.conversation.ui.channel import androidx.compose.animation.AnimatedContent import androidx.compose.animation.animateContentSize @@ -67,17 +67,17 @@ import org.monogram.presentation.R import org.monogram.presentation.core.util.DateFormatManager import org.monogram.presentation.core.util.IDownloadUtils import org.monogram.presentation.core.util.namespacedCacheKey -import org.monogram.presentation.features.chats.currentChat.AutoDownloadSuppression -import org.monogram.presentation.features.chats.currentChat.components.VideoStickerPlayer -import org.monogram.presentation.features.chats.currentChat.components.VideoType -import org.monogram.presentation.features.chats.currentChat.components.chats.BigEmojiContent -import org.monogram.presentation.features.chats.currentChat.components.chats.ForwardContent -import org.monogram.presentation.features.chats.currentChat.components.chats.MediaLoadingAction -import org.monogram.presentation.features.chats.currentChat.components.chats.MediaLoadingBackground -import org.monogram.presentation.features.chats.currentChat.components.chats.MessageReactionsView -import org.monogram.presentation.features.chats.currentChat.components.chats.MessageText -import org.monogram.presentation.features.chats.currentChat.components.chats.ReplyContent -import org.monogram.presentation.features.chats.currentChat.components.chats.rememberMessageTextRenderData +import org.monogram.presentation.features.chats.conversation.AutoDownloadSuppression +import org.monogram.presentation.core.media.VideoStickerPlayer +import org.monogram.presentation.core.media.VideoType +import org.monogram.presentation.features.chats.conversation.ui.message.BigEmojiContent +import org.monogram.presentation.features.chats.conversation.ui.message.ForwardContent +import org.monogram.presentation.features.chats.conversation.ui.message.MediaLoadingAction +import org.monogram.presentation.features.chats.conversation.ui.message.MediaLoadingBackground +import org.monogram.presentation.features.chats.conversation.ui.message.MessageReactionsView +import org.monogram.presentation.features.chats.conversation.ui.message.MessageText +import org.monogram.presentation.features.chats.conversation.ui.message.ReplyContent +import org.monogram.presentation.features.chats.conversation.ui.message.rememberMessageTextRenderData @Composable fun ChannelGifMessageBubble( diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/channels/ChannelMessageBubbleContainer.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/channel/ChannelMessageBubbleContainer.kt similarity index 96% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/channels/ChannelMessageBubbleContainer.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/channel/ChannelMessageBubbleContainer.kt index 5b6386b0..51ae9d0e 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/channels/ChannelMessageBubbleContainer.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/channel/ChannelMessageBubbleContainer.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.channels +package org.monogram.presentation.features.chats.conversation.ui.channel import android.content.res.Configuration import androidx.compose.animation.Animatable @@ -42,14 +42,14 @@ import org.monogram.domain.models.InlineKeyboardButtonModel import org.monogram.domain.models.MessageContent import org.monogram.domain.models.MessageModel import org.monogram.presentation.core.util.IDownloadUtils -import org.monogram.presentation.features.chats.currentChat.chatContent.shouldShowDate -import org.monogram.presentation.features.chats.currentChat.components.FastReplyIndicator -import org.monogram.presentation.features.chats.currentChat.components.chats.AudioMessageBubble -import org.monogram.presentation.features.chats.currentChat.components.chats.DocumentMessageBubble -import org.monogram.presentation.features.chats.currentChat.components.chats.MessageViaBotAttribution -import org.monogram.presentation.features.chats.currentChat.components.chats.ReplyMarkupView -import org.monogram.presentation.features.chats.currentChat.components.chats.StickerMessageBubble -import org.monogram.presentation.features.chats.currentChat.components.fastReplyPointer +import org.monogram.presentation.features.chats.conversation.ui.content.shouldShowDate +import org.monogram.presentation.features.chats.conversation.ui.FastReplyIndicator +import org.monogram.presentation.features.chats.conversation.ui.message.AudioMessageBubble +import org.monogram.presentation.features.chats.conversation.ui.message.DocumentMessageBubble +import org.monogram.presentation.features.chats.conversation.ui.message.MessageViaBotAttribution +import org.monogram.presentation.features.chats.conversation.ui.message.ReplyMarkupView +import org.monogram.presentation.features.chats.conversation.ui.message.StickerMessageBubble +import org.monogram.presentation.features.chats.conversation.ui.fastReplyPointer @Composable fun ChannelMessageBubbleContainer( @@ -458,3 +458,4 @@ fun ChannelMessageBubbleContainer( private fun IntSize.toSize() = androidx.compose.ui.geometry.Size(width.toFloat(), height.toFloat()) private fun androidx.compose.ui.geometry.Size.toOffset() = Offset(width, height) + diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/channels/ChannelMessageUtils.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/channel/ChannelMessageUtils.kt similarity index 93% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/channels/ChannelMessageUtils.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/channel/ChannelMessageUtils.kt index 42e9961a..dd9cbe7d 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/channels/ChannelMessageUtils.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/channel/ChannelMessageUtils.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.channels +package org.monogram.presentation.features.chats.conversation.ui.channel import android.content.Context import org.monogram.presentation.R diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/channels/ChannelPhotoMessageBubble.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/channel/ChannelPhotoMessageBubble.kt similarity index 93% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/channels/ChannelPhotoMessageBubble.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/channel/ChannelPhotoMessageBubble.kt index 4acf3161..4a990332 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/channels/ChannelPhotoMessageBubble.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/channel/ChannelPhotoMessageBubble.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.channels +package org.monogram.presentation.features.chats.conversation.ui.channel import androidx.compose.foundation.background import androidx.compose.foundation.gestures.detectTapGestures @@ -45,16 +45,16 @@ import org.monogram.domain.models.MessageModel import org.monogram.presentation.R import org.monogram.presentation.core.util.IDownloadUtils import org.monogram.presentation.core.util.namespacedCacheKey -import org.monogram.presentation.features.chats.currentChat.AutoDownloadSuppression -import org.monogram.presentation.features.chats.currentChat.components.chats.BigEmojiContent -import org.monogram.presentation.features.chats.currentChat.components.chats.ForwardContent -import org.monogram.presentation.features.chats.currentChat.components.chats.MediaLoadingAction -import org.monogram.presentation.features.chats.currentChat.components.chats.MediaLoadingBackground -import org.monogram.presentation.features.chats.currentChat.components.chats.MessageMetadata -import org.monogram.presentation.features.chats.currentChat.components.chats.MessageReactionsView -import org.monogram.presentation.features.chats.currentChat.components.chats.MessageText -import org.monogram.presentation.features.chats.currentChat.components.chats.ReplyContent -import org.monogram.presentation.features.chats.currentChat.components.chats.rememberMessageTextRenderData +import org.monogram.presentation.features.chats.conversation.AutoDownloadSuppression +import org.monogram.presentation.features.chats.conversation.ui.message.BigEmojiContent +import org.monogram.presentation.features.chats.conversation.ui.message.ForwardContent +import org.monogram.presentation.features.chats.conversation.ui.message.MediaLoadingAction +import org.monogram.presentation.features.chats.conversation.ui.message.MediaLoadingBackground +import org.monogram.presentation.features.chats.conversation.ui.message.MessageMetadata +import org.monogram.presentation.features.chats.conversation.ui.message.MessageReactionsView +import org.monogram.presentation.features.chats.conversation.ui.message.MessageText +import org.monogram.presentation.features.chats.conversation.ui.message.ReplyContent +import org.monogram.presentation.features.chats.conversation.ui.message.rememberMessageTextRenderData @Composable fun ChannelPhotoMessageBubble( diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/channels/ChannelPollMessageBubble.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/channel/ChannelPollMessageBubble.kt similarity index 93% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/channels/ChannelPollMessageBubble.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/channel/ChannelPollMessageBubble.kt index 29b53844..2f2f062f 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/channels/ChannelPollMessageBubble.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/channel/ChannelPollMessageBubble.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.channels +package org.monogram.presentation.features.chats.conversation.ui.channel import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth @@ -9,7 +9,7 @@ import androidx.compose.ui.geometry.Offset import org.monogram.domain.models.ForwardInfo import org.monogram.domain.models.MessageContent import org.monogram.domain.models.MessageModel -import org.monogram.presentation.features.chats.currentChat.components.chats.PollMessageBubble +import org.monogram.presentation.features.chats.conversation.ui.message.PollMessageBubble @Composable fun ChannelPollMessageBubble( diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/channels/ChannelTextMessageBubble.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/channel/ChannelTextMessageBubble.kt similarity index 91% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/channels/ChannelTextMessageBubble.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/channel/ChannelTextMessageBubble.kt index 96eb4b12..5af074b5 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/channels/ChannelTextMessageBubble.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/channel/ChannelTextMessageBubble.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.channels +package org.monogram.presentation.features.chats.conversation.ui.channel import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -29,14 +29,14 @@ import org.monogram.domain.models.ForwardInfo import org.monogram.domain.models.MessageContent import org.monogram.domain.models.MessageModel import org.monogram.presentation.core.util.DateFormatManager -import org.monogram.presentation.features.chats.currentChat.components.chats.BigEmojiContent -import org.monogram.presentation.features.chats.currentChat.components.chats.ForwardContent -import org.monogram.presentation.features.chats.currentChat.components.chats.LinkPreview -import org.monogram.presentation.features.chats.currentChat.components.chats.MessageReactionsView -import org.monogram.presentation.features.chats.currentChat.components.chats.MessageSendingStatusIcon -import org.monogram.presentation.features.chats.currentChat.components.chats.MessageText -import org.monogram.presentation.features.chats.currentChat.components.chats.ReplyContent -import org.monogram.presentation.features.chats.currentChat.components.chats.rememberMessageTextRenderData +import org.monogram.presentation.features.chats.conversation.ui.message.BigEmojiContent +import org.monogram.presentation.features.chats.conversation.ui.message.ForwardContent +import org.monogram.presentation.features.chats.conversation.ui.message.LinkPreview +import org.monogram.presentation.features.chats.conversation.ui.message.MessageReactionsView +import org.monogram.presentation.features.chats.conversation.ui.message.MessageSendingStatusIcon +import org.monogram.presentation.features.chats.conversation.ui.message.MessageText +import org.monogram.presentation.features.chats.conversation.ui.message.ReplyContent +import org.monogram.presentation.features.chats.conversation.ui.message.rememberMessageTextRenderData @Composable fun ChannelTextMessageBubble( diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/channels/ChannelVideoMessageBubble.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/channel/ChannelVideoMessageBubble.kt similarity index 95% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/channels/ChannelVideoMessageBubble.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/channel/ChannelVideoMessageBubble.kt index 49596631..4562341e 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/channels/ChannelVideoMessageBubble.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/channel/ChannelVideoMessageBubble.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.channels +package org.monogram.presentation.features.chats.conversation.ui.channel import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -58,18 +58,18 @@ import org.monogram.domain.models.MessageModel import org.monogram.presentation.R import org.monogram.presentation.core.util.IDownloadUtils import org.monogram.presentation.core.util.namespacedCacheKey -import org.monogram.presentation.features.chats.currentChat.AutoDownloadSuppression -import org.monogram.presentation.features.chats.currentChat.components.VideoStickerPlayer -import org.monogram.presentation.features.chats.currentChat.components.VideoType -import org.monogram.presentation.features.chats.currentChat.components.chats.BigEmojiContent -import org.monogram.presentation.features.chats.currentChat.components.chats.ForwardContent -import org.monogram.presentation.features.chats.currentChat.components.chats.MediaLoadingAction -import org.monogram.presentation.features.chats.currentChat.components.chats.MediaLoadingBackground -import org.monogram.presentation.features.chats.currentChat.components.chats.MessageMetadata -import org.monogram.presentation.features.chats.currentChat.components.chats.MessageReactionsView -import org.monogram.presentation.features.chats.currentChat.components.chats.MessageText -import org.monogram.presentation.features.chats.currentChat.components.chats.ReplyContent -import org.monogram.presentation.features.chats.currentChat.components.chats.rememberMessageTextRenderData +import org.monogram.presentation.features.chats.conversation.AutoDownloadSuppression +import org.monogram.presentation.core.media.VideoStickerPlayer +import org.monogram.presentation.core.media.VideoType +import org.monogram.presentation.features.chats.conversation.ui.message.BigEmojiContent +import org.monogram.presentation.features.chats.conversation.ui.message.ForwardContent +import org.monogram.presentation.features.chats.conversation.ui.message.MediaLoadingAction +import org.monogram.presentation.features.chats.conversation.ui.message.MediaLoadingBackground +import org.monogram.presentation.features.chats.conversation.ui.message.MessageMetadata +import org.monogram.presentation.features.chats.conversation.ui.message.MessageReactionsView +import org.monogram.presentation.features.chats.conversation.ui.message.MessageText +import org.monogram.presentation.features.chats.conversation.ui.message.ReplyContent +import org.monogram.presentation.features.chats.conversation.ui.message.rememberMessageTextRenderData @Composable fun ChannelVideoMessageBubble( diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/channels/ChannelVoiceMessageBubble.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/channel/ChannelVoiceMessageBubble.kt similarity index 91% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/channels/ChannelVoiceMessageBubble.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/channel/ChannelVoiceMessageBubble.kt index 044258d8..d845010f 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/channels/ChannelVoiceMessageBubble.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/channel/ChannelVoiceMessageBubble.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.channels +package org.monogram.presentation.features.chats.conversation.ui.channel import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -23,11 +23,11 @@ import org.monogram.domain.models.ForwardInfo import org.monogram.domain.models.MessageContent import org.monogram.domain.models.MessageModel import org.monogram.presentation.core.util.IDownloadUtils -import org.monogram.presentation.features.chats.currentChat.components.chats.ForwardContent -import org.monogram.presentation.features.chats.currentChat.components.chats.MessageMetadata -import org.monogram.presentation.features.chats.currentChat.components.chats.MessageReactionsView -import org.monogram.presentation.features.chats.currentChat.components.chats.ReplyContent -import org.monogram.presentation.features.chats.currentChat.components.chats.VoiceRow +import org.monogram.presentation.features.chats.conversation.ui.message.ForwardContent +import org.monogram.presentation.features.chats.conversation.ui.message.MessageMetadata +import org.monogram.presentation.features.chats.conversation.ui.message.MessageReactionsView +import org.monogram.presentation.features.chats.conversation.ui.message.ReplyContent +import org.monogram.presentation.features.chats.conversation.ui.message.VoiceRow @Composable fun ChannelVoiceMessageBubble( 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/conversation/ui/content/ChatContentBackground.kt similarity index 93% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentBackground.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentBackground.kt index d576b029..5c667b79 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/conversation/ui/content/ChatContentBackground.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.chatContent +package org.monogram.presentation.features.chats.conversation.ui.content import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -10,7 +10,7 @@ import androidx.compose.ui.draw.blur import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage -import org.monogram.presentation.features.chats.currentChat.ChatComponent +import org.monogram.presentation.features.chats.conversation.ChatComponent import org.monogram.presentation.settings.chatSettings.components.WallpaperBackground import java.io.File @@ -59,3 +59,4 @@ fun ChatContentBackground( ) } } + diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentDerivedState.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentDerivedState.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentDerivedState.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentDerivedState.kt index 10951782..5ba02b16 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentDerivedState.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentDerivedState.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.chatContent +package org.monogram.presentation.features.chats.conversation.ui.content import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable @@ -6,7 +6,7 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import org.monogram.domain.models.UserModel -import org.monogram.presentation.features.chats.currentChat.ChatComponent +import org.monogram.presentation.features.chats.conversation.ChatComponent @Immutable internal data class ChatContentPermissionState( @@ -413,3 +413,4 @@ internal fun rememberChatChromeState( ) } } + diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentEffects.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentEffects.kt similarity index 97% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentEffects.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentEffects.kt index 56534deb..a3fea6c6 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentEffects.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentEffects.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.chatContent +package org.monogram.presentation.features.chats.conversation.ui.content import androidx.compose.foundation.lazy.LazyListState import androidx.compose.runtime.Composable @@ -12,9 +12,9 @@ import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map -import org.monogram.presentation.features.chats.currentChat.ChatComponent -import org.monogram.presentation.features.chats.currentChat.ChatScrollCommand -import org.monogram.presentation.features.chats.currentChat.DefaultChatComponent +import org.monogram.presentation.features.chats.conversation.ChatComponent +import org.monogram.presentation.features.chats.conversation.ChatScrollCommand +import org.monogram.presentation.features.chats.conversation.DefaultChatComponent @Composable internal fun ChatContentEffects( @@ -372,3 +372,4 @@ internal fun ChatContentEffects( } } } + diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentInputConfiguration.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentInputConfiguration.kt similarity index 95% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentInputConfiguration.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentInputConfiguration.kt index 8bc14a3a..9c6da16b 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentInputConfiguration.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentInputConfiguration.kt @@ -1,11 +1,11 @@ -package org.monogram.presentation.features.chats.currentChat.chatContent +package org.monogram.presentation.features.chats.conversation.ui.content import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import org.monogram.domain.models.ReplyMarkupModel -import org.monogram.presentation.features.chats.currentChat.ChatComponent -import org.monogram.presentation.features.chats.currentChat.components.inputbar.ChatInputBarActions -import org.monogram.presentation.features.chats.currentChat.components.inputbar.ChatInputBarState +import org.monogram.presentation.features.chats.conversation.ChatComponent +import org.monogram.presentation.features.chats.conversation.ui.inputbar.ChatInputBarActions +import org.monogram.presentation.features.chats.conversation.ui.inputbar.ChatInputBarState import java.net.URLEncoder import java.nio.charset.StandardCharsets @@ -170,3 +170,4 @@ internal fun rememberChatInputBarActions( ) } } + 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/conversation/ui/content/ChatContentList.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentList.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentList.kt index b01026c8..435d9d79 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/conversation/ui/content/ChatContentList.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.chatContent +package org.monogram.presentation.features.chats.conversation.ui.content import android.os.SystemClock import androidx.compose.animation.AnimatedVisibility @@ -84,13 +84,13 @@ import org.monogram.domain.models.TopicModel import org.monogram.presentation.R import org.monogram.presentation.core.ui.Avatar import org.monogram.presentation.core.util.IDownloadUtils -import org.monogram.presentation.features.chats.currentChat.ChatComponent -import org.monogram.presentation.features.chats.currentChat.components.AlbumMessageBubbleContainer -import org.monogram.presentation.features.chats.currentChat.components.DateSeparator -import org.monogram.presentation.features.chats.currentChat.components.MessageBubbleContainer -import org.monogram.presentation.features.chats.currentChat.components.ServiceMessage -import org.monogram.presentation.features.chats.currentChat.components.UnreadMessagesSeparator -import org.monogram.presentation.features.chats.currentChat.components.channels.ChannelMessageBubbleContainer +import org.monogram.presentation.features.chats.conversation.ChatComponent +import org.monogram.presentation.features.chats.conversation.ui.AlbumMessageBubbleContainer +import org.monogram.presentation.features.chats.conversation.ui.DateSeparator +import org.monogram.presentation.features.chats.conversation.ui.MessageBubbleContainer +import org.monogram.presentation.features.chats.conversation.ui.ServiceMessage +import org.monogram.presentation.features.chats.conversation.ui.UnreadMessagesSeparator +import org.monogram.presentation.features.chats.conversation.ui.channel.ChannelMessageBubbleContainer import org.monogram.presentation.features.stickers.ui.view.StickerImage import java.io.File @@ -1489,3 +1489,4 @@ fun TopicItem( } } + diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentMessageUtils.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentMessageUtils.kt similarity index 95% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentMessageUtils.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentMessageUtils.kt index 55c80d4c..e6deb6f6 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentMessageUtils.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentMessageUtils.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.chatContent +package org.monogram.presentation.features.chats.conversation.ui.content import org.monogram.domain.models.MessageContent import org.monogram.domain.models.MessageModel @@ -33,3 +33,4 @@ internal fun MessageModel.withUpdatedTextContent(newText: String): MessageModel return copy(content = updatedContent) } + diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentOverlays.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentOverlays.kt similarity index 90% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentOverlays.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentOverlays.kt index 06de8aee..dc3f929e 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentOverlays.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentOverlays.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.chatContent +package org.monogram.presentation.features.chats.conversation.ui.content import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Box @@ -17,13 +17,13 @@ import org.monogram.domain.models.ChatPermissionsModel import org.monogram.domain.models.MessageModel import org.monogram.presentation.R import org.monogram.presentation.core.ui.ConfirmationSheet -import org.monogram.presentation.features.chats.currentChat.ChatComponent -import org.monogram.presentation.features.chats.currentChat.components.StickerSetSheet -import org.monogram.presentation.features.chats.currentChat.components.chats.BotCommandsSheet -import org.monogram.presentation.features.chats.currentChat.components.chats.PollVotersSheet -import org.monogram.presentation.features.chats.currentChat.components.pins.PinnedMessagesListSheet -import org.monogram.presentation.features.chats.currentChat.editor.photo.PhotoEditorScreen -import org.monogram.presentation.features.chats.currentChat.editor.video.VideoEditorScreen +import org.monogram.presentation.features.chats.conversation.ChatComponent +import org.monogram.presentation.features.chats.conversation.ui.StickerSetSheet +import org.monogram.presentation.features.chats.conversation.ui.message.BotCommandsSheet +import org.monogram.presentation.features.chats.conversation.ui.message.PollVotersSheet +import org.monogram.presentation.features.chats.conversation.ui.pins.PinnedMessagesListSheet +import org.monogram.presentation.features.chats.conversation.editor.photo.PhotoEditorScreen +import org.monogram.presentation.features.chats.conversation.editor.video.VideoEditorScreen @Composable internal fun ChatContentOverlays( @@ -202,3 +202,4 @@ internal fun ChatContentOverlays( onBack() } } + diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentScrollCoordinator.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentScrollCoordinator.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentScrollCoordinator.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentScrollCoordinator.kt index 2e349043..41c38a18 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentScrollCoordinator.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentScrollCoordinator.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.chatContent +package org.monogram.presentation.features.chats.conversation.ui.content import androidx.compose.foundation.gestures.animateScrollBy import androidx.compose.foundation.gestures.scrollBy @@ -9,7 +9,7 @@ import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.withTimeoutOrNull import org.monogram.domain.models.ChatViewportCacheEntry -import org.monogram.presentation.features.chats.currentChat.ScrollAlign +import org.monogram.presentation.features.chats.conversation.ScrollAlign import kotlin.math.abs @Immutable @@ -240,3 +240,4 @@ internal fun buildViewportSnapshot( atBottom = false ) } + diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentSearchOverlay.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentSearchOverlay.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentSearchOverlay.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentSearchOverlay.kt index 8e47f035..e0ada145 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentSearchOverlay.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentSearchOverlay.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.chatContent +package org.monogram.presentation.features.chats.conversation.ui.content import android.app.DatePickerDialog import android.content.Context @@ -881,3 +881,4 @@ private fun SearchResultsListOverlay( } } } + diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentTopBar.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentTopBar.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentTopBar.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentTopBar.kt index c0036798..6d92617d 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentTopBar.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentTopBar.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.chatContent +package org.monogram.presentation.features.chats.conversation.ui.content import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility @@ -64,9 +64,9 @@ 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.rememberUserStatusText -import org.monogram.presentation.features.chats.currentChat.ChatComponent -import org.monogram.presentation.features.chats.currentChat.components.ChatTopBar -import org.monogram.presentation.features.chats.currentChat.components.pins.PinnedMessageBar +import org.monogram.presentation.features.chats.conversation.ChatComponent +import org.monogram.presentation.features.chats.conversation.ui.ChatTopBar +import org.monogram.presentation.features.chats.conversation.ui.pins.PinnedMessageBar import org.monogram.presentation.features.stickers.ui.menu.MenuOptionRow import org.monogram.presentation.features.viewers.components.ViewerSettingsDropdown @@ -393,4 +393,4 @@ fun ChatContentTopBar( } } } -} \ No newline at end of file +} diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentUtils.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentUtils.kt similarity index 97% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentUtils.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentUtils.kt index 306e2638..65bfccd2 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentUtils.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentUtils.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.chatContent +package org.monogram.presentation.features.chats.conversation.ui.content import androidx.compose.runtime.Immutable import org.monogram.domain.models.MessageModel @@ -91,3 +91,4 @@ fun shouldShowDate(current: MessageModel, older: MessageModel?): Boolean { if (older == null) return true return !fmt.format(Date(msgTimestamp)).equals(fmt.format(Date(older.date.toLong() * 1000))) } + 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/conversation/ui/content/ChatContentViewers.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatContentViewers.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentViewers.kt index dcdbfb29..fe8a6beb 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/conversation/ui/content/ChatContentViewers.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.chatContent +package org.monogram.presentation.features.chats.conversation.ui.content import android.content.ClipData import android.util.Log @@ -8,7 +8,7 @@ import androidx.compose.ui.platform.Clipboard import androidx.compose.ui.text.AnnotatedString import org.monogram.domain.models.MessageContent import org.monogram.domain.models.MessageModel -import org.monogram.presentation.features.chats.currentChat.ChatComponent +import org.monogram.presentation.features.chats.conversation.ChatComponent import org.monogram.presentation.features.instantview.InstantViewer import org.monogram.presentation.features.viewers.ImageViewer import org.monogram.presentation.features.viewers.VideoViewer @@ -446,3 +446,4 @@ private fun MessageContent.matchesDisplayPath(path: String): Boolean { else -> false } } + 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/conversation/ui/content/ChatMessageOptionsMenu.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ChatMessageOptionsMenu.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatMessageOptionsMenu.kt index d8aad2f4..98a4f809 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/conversation/ui/content/ChatMessageOptionsMenu.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.chatContent +package org.monogram.presentation.features.chats.conversation.ui.content import android.content.ClipData import android.util.Log @@ -36,7 +36,7 @@ import org.monogram.domain.repository.StickerRepository import org.monogram.presentation.R import org.monogram.presentation.core.util.IDownloadUtils import org.monogram.presentation.core.util.coRunCatching -import org.monogram.presentation.features.chats.currentChat.ChatComponent +import org.monogram.presentation.features.chats.conversation.ChatComponent import org.monogram.presentation.features.stickers.ui.menu.MessageOptionsMenu import org.monogram.presentation.features.stickers.ui.menu.MessagePackMenuOption import java.util.Locale @@ -625,3 +625,4 @@ private fun extractDownloadPath(content: MessageContent): String? { else -> null } } + diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/DeleteMessagesSheet.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/DeleteMessagesSheet.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/DeleteMessagesSheet.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/DeleteMessagesSheet.kt index af60c207..74a1bd62 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/DeleteMessagesSheet.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/DeleteMessagesSheet.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.chatContent +package org.monogram.presentation.features.chats.conversation.ui.content import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape @@ -111,3 +111,4 @@ fun DeleteMessagesSheet( } } } + diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ReportChatDialog.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ReportChatDialog.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ReportChatDialog.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ReportChatDialog.kt index 5bddb4e0..a1ce1e02 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/ReportChatDialog.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ReportChatDialog.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.chatContent +package org.monogram.presentation.features.chats.conversation.ui.content import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -20,7 +20,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import org.monogram.presentation.R -import org.monogram.presentation.features.chats.chatList.components.SettingsTextField +import org.monogram.presentation.core.ui.SettingsTextField import org.monogram.presentation.core.ui.ItemPosition @OptIn(ExperimentalMaterial3Api::class) @@ -228,3 +228,4 @@ private data class ReportReason( val description: String, val icon: ImageVector ) + diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/RestrictUserSheet.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/RestrictUserSheet.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/RestrictUserSheet.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/RestrictUserSheet.kt index 9bc9dac0..28d3d594 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/chatContent/RestrictUserSheet.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/RestrictUserSheet.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.chatContent +package org.monogram.presentation.features.chats.conversation.ui.content import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -263,3 +263,4 @@ private fun PermissionToggle( ) } } + diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/ChatInputBarComposerSection.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/ChatInputBarComposerSection.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/ChatInputBarComposerSection.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/ChatInputBarComposerSection.kt index 0e60e88c..1fdbefd6 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/ChatInputBarComposerSection.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/ChatInputBarComposerSection.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.inputbar +package org.monogram.presentation.features.chats.conversation.ui.inputbar import android.net.Uri import androidx.compose.animation.AnimatedContent @@ -55,7 +55,7 @@ import org.monogram.domain.models.StickerModel import org.monogram.domain.models.UserModel import org.monogram.domain.repository.StickerRepository import org.monogram.presentation.R -import org.monogram.presentation.features.chats.currentChat.components.chats.BotCommandSuggestions +import org.monogram.presentation.features.chats.conversation.ui.message.BotCommandSuggestions import org.monogram.presentation.features.stickers.ui.menu.StickerEmojiMenu @Composable diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/ChatInputBarContract.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/ChatInputBarContract.kt similarity index 97% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/ChatInputBarContract.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/ChatInputBarContract.kt index ee229899..e38e6000 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/ChatInputBarContract.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/ChatInputBarContract.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.inputbar +package org.monogram.presentation.features.chats.conversation.ui.inputbar import androidx.compose.runtime.Immutable import org.monogram.domain.models.AttachMenuBotModel diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/ChatInputBarHelpers.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/ChatInputBarHelpers.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/ChatInputBarHelpers.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/ChatInputBarHelpers.kt index 62cc1141..3daff214 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/ChatInputBarHelpers.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/ChatInputBarHelpers.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.inputbar +package org.monogram.presentation.features.chats.conversation.ui.inputbar import android.content.Context import android.content.pm.PackageManager diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/ClosedTopicBar.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/ClosedTopicBar.kt similarity index 94% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/ClosedTopicBar.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/ClosedTopicBar.kt index af70db2a..011bdc8e 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/ClosedTopicBar.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/ClosedTopicBar.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.inputbar +package org.monogram.presentation.features.chats.conversation.ui.inputbar import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxWidth diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/EmojiInsertUtils.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/EmojiInsertUtils.kt similarity index 94% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/EmojiInsertUtils.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/EmojiInsertUtils.kt index 5c3d259d..43e56b11 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/EmojiInsertUtils.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/EmojiInsertUtils.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.inputbar +package org.monogram.presentation.features.chats.conversation.ui.inputbar import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextRange diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/FullScreenEditorFindReplaceBar.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/FullScreenEditorFindReplaceBar.kt similarity index 95% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/FullScreenEditorFindReplaceBar.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/FullScreenEditorFindReplaceBar.kt index b8072cb6..36c2596a 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/FullScreenEditorFindReplaceBar.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/FullScreenEditorFindReplaceBar.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.inputbar +package org.monogram.presentation.features.chats.conversation.ui.inputbar import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape @@ -11,7 +11,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import org.monogram.presentation.R import org.monogram.presentation.core.ui.ItemPosition -import org.monogram.presentation.features.chats.chatList.components.SettingsTextField +import org.monogram.presentation.core.ui.SettingsTextField @Composable fun FullScreenEditorFindReplaceBar( diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/FullScreenEditorMarkdownPreview.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/FullScreenEditorMarkdownPreview.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/FullScreenEditorMarkdownPreview.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/FullScreenEditorMarkdownPreview.kt index b588c8e3..972230e4 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/FullScreenEditorMarkdownPreview.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/FullScreenEditorMarkdownPreview.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.inputbar +package org.monogram.presentation.features.chats.conversation.ui.inputbar import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.AnnotatedString diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/FullScreenEditorSheet.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/FullScreenEditorSheet.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/FullScreenEditorSheet.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/FullScreenEditorSheet.kt index 780aa35e..fe75e973 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/FullScreenEditorSheet.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/FullScreenEditorSheet.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.inputbar +package org.monogram.presentation.features.chats.conversation.ui.inputbar import android.content.ClipData import android.widget.Toast @@ -129,8 +129,8 @@ import org.monogram.domain.repository.StickerRepository import org.monogram.domain.repository.TextCompositionStyleModel import org.monogram.presentation.R import org.monogram.presentation.core.ui.ItemPosition -import org.monogram.presentation.features.chats.chatList.components.SettingsTextField -import org.monogram.presentation.features.chats.currentChat.components.chats.addEmojiStyle +import org.monogram.presentation.core.ui.SettingsTextField +import org.monogram.presentation.features.chats.conversation.ui.message.addEmojiStyle import org.monogram.presentation.features.profile.logs.components.calculateDiff import org.monogram.presentation.features.stickers.ui.menu.StickerEmojiMenu import org.monogram.presentation.features.stickers.ui.view.StickerImage diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/FullScreenEditorStorageAndSearch.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/FullScreenEditorStorageAndSearch.kt similarity index 95% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/FullScreenEditorStorageAndSearch.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/FullScreenEditorStorageAndSearch.kt index 30ee1c95..423c03b1 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/FullScreenEditorStorageAndSearch.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/FullScreenEditorStorageAndSearch.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.inputbar +package org.monogram.presentation.features.chats.conversation.ui.inputbar import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/FullScreenEditorTemplatesSheet.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/FullScreenEditorTemplatesSheet.kt similarity index 97% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/FullScreenEditorTemplatesSheet.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/FullScreenEditorTemplatesSheet.kt index 25349fab..688a2a16 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/FullScreenEditorTemplatesSheet.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/FullScreenEditorTemplatesSheet.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.inputbar +package org.monogram.presentation.features.chats.conversation.ui.inputbar import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn @@ -16,7 +16,7 @@ import androidx.compose.ui.unit.dp import org.monogram.domain.repository.EditorSnippet import org.monogram.presentation.R import org.monogram.presentation.core.ui.ItemPosition -import org.monogram.presentation.features.chats.chatList.components.SettingsTextField +import org.monogram.presentation.core.ui.SettingsTextField @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/InlineBotResults.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/InlineBotResults.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/InlineBotResults.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/InlineBotResults.kt index 58535a8f..4056c197 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/InlineBotResults.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/InlineBotResults.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.inputbar +package org.monogram.presentation.features.chats.conversation.ui.inputbar import androidx.compose.animation.* import androidx.compose.animation.core.* diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/InputBarLeadingIcons.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/InputBarLeadingIcons.kt similarity index 96% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/InputBarLeadingIcons.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/InputBarLeadingIcons.kt index 98c7a4a1..d002902d 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/InputBarLeadingIcons.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/InputBarLeadingIcons.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.inputbar +package org.monogram.presentation.features.chats.conversation.ui.inputbar import androidx.compose.animation.AnimatedContent import androidx.compose.animation.SizeTransform diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/InputBarSendButton.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/InputBarSendButton.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/InputBarSendButton.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/InputBarSendButton.kt index 12d6ef17..ad6343f5 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/InputBarSendButton.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/InputBarSendButton.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.inputbar +package org.monogram.presentation.features.chats.conversation.ui.inputbar import androidx.compose.animation.AnimatedContent import androidx.compose.animation.SizeTransform diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/InputPreviewSection.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/InputPreviewSection.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/InputPreviewSection.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/InputPreviewSection.kt index 47c03a64..21cc3ec5 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/InputPreviewSection.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/InputPreviewSection.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.inputbar +package org.monogram.presentation.features.chats.conversation.ui.inputbar import androidx.compose.animation.AnimatedContent import androidx.compose.animation.core.animateFloatAsState @@ -65,8 +65,8 @@ import org.monogram.domain.models.MessageContent import org.monogram.domain.models.MessageEntity import org.monogram.domain.models.MessageModel import org.monogram.presentation.R -import org.monogram.presentation.features.chats.currentChat.components.chats.buildAnnotatedMessageTextWithEmoji -import org.monogram.presentation.features.chats.currentChat.components.chats.rememberMessageInlineContent +import org.monogram.presentation.features.chats.conversation.ui.message.buildAnnotatedMessageTextWithEmoji +import org.monogram.presentation.features.chats.conversation.ui.message.rememberMessageInlineContent import java.io.File import java.util.Collections diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/InputTextField.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/InputTextField.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/InputTextField.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/InputTextField.kt index c9134466..8f56381a 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/InputTextField.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/InputTextField.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.inputbar +package org.monogram.presentation.features.chats.conversation.ui.inputbar import android.content.ClipData import android.content.ClipboardManager @@ -74,7 +74,7 @@ import org.monogram.domain.models.MessageEntity import org.monogram.domain.models.MessageEntityType import org.monogram.domain.models.StickerModel import org.monogram.presentation.R -import org.monogram.presentation.features.chats.currentChat.components.chats.addEmojiStyle +import org.monogram.presentation.features.chats.conversation.ui.message.addEmojiStyle import org.monogram.presentation.features.stickers.ui.view.StickerImage internal const val CUSTOM_EMOJI_TAG = "custom_emoji" diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/InputTextFieldContainer.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/InputTextFieldContainer.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/InputTextFieldContainer.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/InputTextFieldContainer.kt index d40f3cd3..059e4206 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/InputTextFieldContainer.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/InputTextFieldContainer.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.inputbar +package org.monogram.presentation.features.chats.conversation.ui.inputbar import android.net.Uri import androidx.compose.animation.AnimatedContent diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/KeyboardMarkupView.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/KeyboardMarkupView.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/KeyboardMarkupView.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/KeyboardMarkupView.kt index 507b770e..148dcaf3 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/KeyboardMarkupView.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/KeyboardMarkupView.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.inputbar +package org.monogram.presentation.features.chats.conversation.ui.inputbar import android.content.ClipData import androidx.compose.foundation.ExperimentalFoundationApi diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/MentionSuggestions.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/MentionSuggestions.kt similarity index 97% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/MentionSuggestions.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/MentionSuggestions.kt index f4fe0961..7f83ed79 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/MentionSuggestions.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/MentionSuggestions.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.inputbar +package org.monogram.presentation.features.chats.conversation.ui.inputbar import androidx.compose.foundation.background import androidx.compose.foundation.clickable diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/RecordingUI.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/RecordingUI.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/RecordingUI.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/RecordingUI.kt index dcc71a55..6ba862a7 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/RecordingUI.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/RecordingUI.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.inputbar +package org.monogram.presentation.features.chats.conversation.ui.inputbar import androidx.compose.animation.* import androidx.compose.animation.core.* diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/RestrictedInputBar.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/RestrictedInputBar.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/RestrictedInputBar.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/RestrictedInputBar.kt index 7fb92bba..7479c73f 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/RestrictedInputBar.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/RestrictedInputBar.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.inputbar +package org.monogram.presentation.features.chats.conversation.ui.inputbar import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.tween diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/SchedulePickers.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/SchedulePickers.kt similarity index 97% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/SchedulePickers.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/SchedulePickers.kt index 1d13fd40..476a054e 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/SchedulePickers.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/SchedulePickers.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.inputbar +package org.monogram.presentation.features.chats.conversation.ui.inputbar import android.text.format.DateFormat import androidx.compose.material3.* diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/ScheduledMessagesSheet.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/ScheduledMessagesSheet.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/ScheduledMessagesSheet.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/ScheduledMessagesSheet.kt index dd8c0bf2..68165055 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/ScheduledMessagesSheet.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/ScheduledMessagesSheet.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.inputbar +package org.monogram.presentation.features.chats.conversation.ui.inputbar import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/SendOptionsPopup.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/SendOptionsPopup.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/SendOptionsPopup.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/SendOptionsPopup.kt index bc678a8c..9e3f7a28 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/SendOptionsPopup.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/SendOptionsPopup.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.inputbar +package org.monogram.presentation.features.chats.conversation.ui.inputbar import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/SlowModeInputBar.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/SlowModeInputBar.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/SlowModeInputBar.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/SlowModeInputBar.kt index f2e5bd08..b74ce3e0 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/SlowModeInputBar.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/SlowModeInputBar.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.inputbar +package org.monogram.presentation.features.chats.conversation.ui.inputbar import androidx.compose.animation.AnimatedContent import androidx.compose.animation.core.tween diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/VoiceRecorder.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/VoiceRecorder.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/VoiceRecorder.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/VoiceRecorder.kt index 8306d077..a5483332 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/inputbar/VoiceRecorder.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/VoiceRecorder.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.inputbar +package org.monogram.presentation.features.chats.conversation.ui.inputbar import android.Manifest import android.content.Context diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/AudioMessageBubble.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/AudioMessageBubble.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/AudioMessageBubble.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/AudioMessageBubble.kt index 00131469..eb712322 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/AudioMessageBubble.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/AudioMessageBubble.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.chats +package org.monogram.presentation.features.chats.conversation.ui.message import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -48,7 +48,7 @@ import org.monogram.domain.models.MessageContent import org.monogram.domain.models.MessageModel import org.monogram.presentation.R import org.monogram.presentation.core.util.IDownloadUtils -import org.monogram.presentation.features.chats.currentChat.components.channels.ChannelCommentsButton +import org.monogram.presentation.features.chats.conversation.ui.channel.ChannelCommentsButton @Composable fun AudioMessageBubble( diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/BotCommandItem.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/BotCommandItem.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/BotCommandItem.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/BotCommandItem.kt index 47a354e7..1d60034b 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/BotCommandItem.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/BotCommandItem.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.chats +package org.monogram.presentation.features.chats.conversation.ui.message import androidx.compose.foundation.background import androidx.compose.foundation.clickable diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/BotCommandSuggestions.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/BotCommandSuggestions.kt similarity index 96% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/BotCommandSuggestions.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/BotCommandSuggestions.kt index 89ca3186..12140c8f 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/BotCommandSuggestions.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/BotCommandSuggestions.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.chats +package org.monogram.presentation.features.chats.conversation.ui.message import androidx.compose.animation.* import androidx.compose.foundation.ExperimentalFoundationApi diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/BotCommandsSheet.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/BotCommandsSheet.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/BotCommandsSheet.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/BotCommandsSheet.kt index b5fac5fd..f929dfcc 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/BotCommandsSheet.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/BotCommandsSheet.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.chats +package org.monogram.presentation.features.chats.conversation.ui.message import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/ChatAlbumMessageBubble.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/ChatAlbumMessageBubble.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/ChatAlbumMessageBubble.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/ChatAlbumMessageBubble.kt index ec49fd5f..7fe971e9 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/ChatAlbumMessageBubble.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/ChatAlbumMessageBubble.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.chats +package org.monogram.presentation.features.chats.conversation.ui.message import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -33,7 +33,7 @@ import org.monogram.domain.models.MessageModel import org.monogram.domain.models.MessageSendingState import org.monogram.presentation.core.util.DateFormatManager import org.monogram.presentation.core.util.IDownloadUtils -import org.monogram.presentation.features.chats.currentChat.components.CompactMediaMosaic +import org.monogram.presentation.features.chats.conversation.ui.CompactMediaMosaic @Composable fun ChatAlbumMessageBubble( diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/CodeBlock.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/CodeBlock.kt similarity index 97% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/CodeBlock.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/CodeBlock.kt index 387c7472..b64acb73 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/CodeBlock.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/CodeBlock.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.chats +package org.monogram.presentation.features.chats.conversation.ui.message import android.content.ClipData import android.provider.Settings @@ -45,7 +45,7 @@ import org.koin.compose.koinInject import org.monogram.presentation.R import org.monogram.presentation.core.util.AppPreferences import org.monogram.presentation.core.util.NightMode -import org.monogram.presentation.features.chats.currentChat.components.chats.code.CodeHighlighter +import org.monogram.presentation.features.chats.conversation.ui.message.code.CodeHighlighter import java.util.Calendar @Composable diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/ContactMessageBubble.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/ContactMessageBubble.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/ContactMessageBubble.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/ContactMessageBubble.kt index 0785293c..ef3f216c 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/ContactMessageBubble.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/ContactMessageBubble.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.chats +package org.monogram.presentation.features.chats.conversation.ui.message import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/DocumentMessageBubble.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/DocumentMessageBubble.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/DocumentMessageBubble.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/DocumentMessageBubble.kt index 9f71cf0f..5d1978b7 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/DocumentMessageBubble.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/DocumentMessageBubble.kt @@ -1,6 +1,6 @@ @file:OptIn(ExperimentalMaterial3ExpressiveApi::class) -package org.monogram.presentation.features.chats.currentChat.components.chats +package org.monogram.presentation.features.chats.conversation.ui.message import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -50,8 +50,8 @@ import org.monogram.domain.models.MessageContent import org.monogram.domain.models.MessageModel import org.monogram.presentation.R import org.monogram.presentation.core.util.IDownloadUtils -import org.monogram.presentation.features.chats.currentChat.AutoDownloadSuppression -import org.monogram.presentation.features.chats.currentChat.components.channels.ChannelCommentsButton +import org.monogram.presentation.features.chats.conversation.AutoDownloadSuppression +import org.monogram.presentation.features.chats.conversation.ui.channel.ChannelCommentsButton @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/ForwardContent.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/ForwardContent.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/ForwardContent.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/ForwardContent.kt index f5ea7bcd..07363902 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/ForwardContent.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/ForwardContent.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.chats +package org.monogram.presentation.features.chats.conversation.ui.message import androidx.compose.foundation.background import androidx.compose.foundation.clickable diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/GifMessageBubble.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/GifMessageBubble.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/GifMessageBubble.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/GifMessageBubble.kt index b89815c8..2572b6cb 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/GifMessageBubble.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/GifMessageBubble.kt @@ -1,6 +1,6 @@ @file:OptIn(ExperimentalMaterial3ExpressiveApi::class) -package org.monogram.presentation.features.chats.currentChat.components.chats +package org.monogram.presentation.features.chats.conversation.ui.message import androidx.annotation.OptIn import androidx.compose.foundation.Image @@ -64,9 +64,9 @@ import org.monogram.presentation.R import org.monogram.presentation.core.util.DateFormatManager import org.monogram.presentation.core.util.IDownloadUtils import org.monogram.presentation.core.util.namespacedCacheKey -import org.monogram.presentation.features.chats.currentChat.AutoDownloadSuppression -import org.monogram.presentation.features.chats.currentChat.components.VideoStickerPlayer -import org.monogram.presentation.features.chats.currentChat.components.VideoType +import org.monogram.presentation.features.chats.conversation.AutoDownloadSuppression +import org.monogram.presentation.core.media.VideoStickerPlayer +import org.monogram.presentation.core.media.VideoType @OptIn(UnstableApi::class, ExperimentalMaterial3ExpressiveApi::class) @kotlin.OptIn(ExperimentalMaterial3ExpressiveApi::class) diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/LinkPreview.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/LinkPreview.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/LinkPreview.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/LinkPreview.kt index 71885b06..eb4d120e 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/LinkPreview.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/LinkPreview.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.chats +package org.monogram.presentation.features.chats.conversation.ui.message import androidx.compose.foundation.background import androidx.compose.foundation.clickable diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/LocationMessageBubble.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/LocationMessageBubble.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/LocationMessageBubble.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/LocationMessageBubble.kt index 73e825ac..247ed4c4 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/LocationMessageBubble.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/LocationMessageBubble.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.chats +package org.monogram.presentation.features.chats.conversation.ui.message import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/MediaLoadingComponents.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MediaLoadingComponents.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/MediaLoadingComponents.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MediaLoadingComponents.kt index 05b6c526..f88c469e 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/MediaLoadingComponents.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MediaLoadingComponents.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.chats +package org.monogram.presentation.features.chats.conversation.ui.message import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.RepeatMode diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/MessageReactionsView.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MessageReactionsView.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/MessageReactionsView.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MessageReactionsView.kt index 8a90cd05..eb24c675 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/MessageReactionsView.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MessageReactionsView.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.chats +package org.monogram.presentation.features.chats.conversation.ui.message import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/MessageSenderName.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MessageSenderName.kt similarity index 97% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/MessageSenderName.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MessageSenderName.kt index 9850114a..f8523401 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/MessageSenderName.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MessageSenderName.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.chats +package org.monogram.presentation.features.chats.conversation.ui.message import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Row diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/MessageText.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MessageText.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/MessageText.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MessageText.kt index da2f6ddc..0b57caab 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/MessageText.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MessageText.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.chats +package org.monogram.presentation.features.chats.conversation.ui.message import android.content.ClipData import android.os.Build @@ -29,7 +29,7 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextOverflow import org.monogram.domain.models.MessageEntity import org.monogram.domain.models.MessageEntityType -import org.monogram.presentation.features.chats.currentChat.components.chats.model.isBlockElement +import org.monogram.presentation.features.chats.conversation.ui.message.model.isBlockElement @Composable fun MessageText( diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/MessageTextFormatter.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MessageTextFormatter.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/MessageTextFormatter.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MessageTextFormatter.kt index 88c5a45f..95c9d448 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/MessageTextFormatter.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MessageTextFormatter.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.chats +package org.monogram.presentation.features.chats.conversation.ui.message import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/MessageUtils.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MessageUtils.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/MessageUtils.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MessageUtils.kt index fe6f893a..113730c3 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/MessageUtils.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MessageUtils.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.chats +package org.monogram.presentation.features.chats.conversation.ui.message import android.content.Context import androidx.compose.animation.AnimatedContent @@ -51,7 +51,7 @@ import org.monogram.domain.models.MessageSendingState import org.monogram.presentation.R import org.monogram.presentation.core.util.DateFormatManager import org.monogram.presentation.core.util.EmojiStyle -import org.monogram.presentation.features.chats.currentChat.components.channels.formatViews +import org.monogram.presentation.features.chats.conversation.ui.channel.formatViews import java.io.File import java.text.BreakIterator import java.text.SimpleDateFormat diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/MessageViaBotAttribution.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MessageViaBotAttribution.kt similarity index 95% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/MessageViaBotAttribution.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MessageViaBotAttribution.kt index 94efd224..b250e589 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/MessageViaBotAttribution.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MessageViaBotAttribution.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.chats +package org.monogram.presentation.features.chats.conversation.ui.message import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/PhotoMessageBubble.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/PhotoMessageBubble.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/PhotoMessageBubble.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/PhotoMessageBubble.kt index 7bd8cd17..174c98c4 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/PhotoMessageBubble.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/PhotoMessageBubble.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.chats +package org.monogram.presentation.features.chats.conversation.ui.message import androidx.compose.foundation.background import androidx.compose.foundation.gestures.detectTapGestures @@ -50,7 +50,7 @@ import org.monogram.domain.models.MessageModel import org.monogram.presentation.R import org.monogram.presentation.core.util.IDownloadUtils import org.monogram.presentation.core.util.namespacedCacheKey -import org.monogram.presentation.features.chats.currentChat.AutoDownloadSuppression +import org.monogram.presentation.features.chats.conversation.AutoDownloadSuppression @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/PollMessageBubble.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/PollMessageBubble.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/PollMessageBubble.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/PollMessageBubble.kt index da5dfab6..d0e4a904 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/PollMessageBubble.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/PollMessageBubble.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.chats +package org.monogram.presentation.features.chats.conversation.ui.message import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateContentSize diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/PollVotersSheet.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/PollVotersSheet.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/PollVotersSheet.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/PollVotersSheet.kt index 868f2b1e..eed48c27 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/PollVotersSheet.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/PollVotersSheet.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.chats +package org.monogram.presentation.features.chats.conversation.ui.message import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/QuoteBlock.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/QuoteBlock.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/QuoteBlock.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/QuoteBlock.kt index 157efecc..9f3d5cd3 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/QuoteBlock.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/QuoteBlock.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.chats +package org.monogram.presentation.features.chats.conversation.ui.message import androidx.compose.animation.animateContentSize import androidx.compose.foundation.background diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/ReplyContent.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/ReplyContent.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/ReplyContent.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/ReplyContent.kt index 8391ac5a..78bb2136 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/ReplyContent.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/ReplyContent.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.chats +package org.monogram.presentation.features.chats.conversation.ui.message import androidx.compose.foundation.background import androidx.compose.foundation.clickable diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/ReplyMarkupView.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/ReplyMarkupView.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/ReplyMarkupView.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/ReplyMarkupView.kt index ec2eafd9..fc3a7fd4 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/ReplyMarkupView.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/ReplyMarkupView.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.chats +package org.monogram.presentation.features.chats.conversation.ui.message import android.content.ClipData import androidx.compose.foundation.ExperimentalFoundationApi diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/SpoilerShader.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/SpoilerShader.kt similarity index 97% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/SpoilerShader.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/SpoilerShader.kt index 4305bf2c..364e2c67 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/SpoilerShader.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/SpoilerShader.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.chats +package org.monogram.presentation.features.chats.conversation.ui.message import android.os.Build import androidx.compose.foundation.background diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/SpoilerShaderApi33.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/SpoilerShaderApi33.kt similarity index 96% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/SpoilerShaderApi33.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/SpoilerShaderApi33.kt index eb70a536..7cb463b5 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/SpoilerShaderApi33.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/SpoilerShaderApi33.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.chats +package org.monogram.presentation.features.chats.conversation.ui.message import android.graphics.RuntimeShader import android.os.Build diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/StickerMessageBubble.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/StickerMessageBubble.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/StickerMessageBubble.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/StickerMessageBubble.kt index 6d79961f..d9ca636e 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/StickerMessageBubble.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/StickerMessageBubble.kt @@ -1,6 +1,6 @@ @file:OptIn(ExperimentalMaterial3ExpressiveApi::class) -package org.monogram.presentation.features.chats.currentChat.components.chats +package org.monogram.presentation.features.chats.conversation.ui.message import androidx.annotation.OptIn import androidx.compose.foundation.ExperimentalFoundationApi diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/TextBlocks.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/TextBlocks.kt similarity index 80% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/TextBlocks.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/TextBlocks.kt index c100aca3..d3362ea2 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/TextBlocks.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/TextBlocks.kt @@ -1,10 +1,10 @@ -package org.monogram.presentation.features.chats.currentChat.components.chats +package org.monogram.presentation.features.chats.conversation.ui.message import androidx.compose.runtime.Composable import org.monogram.domain.models.MessageEntity import org.monogram.domain.models.MessageEntityType -import org.monogram.presentation.features.chats.currentChat.components.chats.model.blockFor -import org.monogram.presentation.features.chats.currentChat.components.chats.model.inlineEntitiesForBlock +import org.monogram.presentation.features.chats.conversation.ui.message.model.blockFor +import org.monogram.presentation.features.chats.conversation.ui.message.model.inlineEntitiesForBlock @Composable internal fun TextBlocks( diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/TextMessageBubble.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/TextMessageBubble.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/TextMessageBubble.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/TextMessageBubble.kt index 8228b5cd..ff9c2c36 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/TextMessageBubble.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/TextMessageBubble.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.chats +package org.monogram.presentation.features.chats.conversation.ui.message import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/VideoMessageBubble.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/VideoMessageBubble.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/VideoMessageBubble.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/VideoMessageBubble.kt index 3f371304..cdcc6a28 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/VideoMessageBubble.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/VideoMessageBubble.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.chats +package org.monogram.presentation.features.chats.conversation.ui.message import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -61,9 +61,9 @@ import org.monogram.domain.models.MessageModel import org.monogram.presentation.R import org.monogram.presentation.core.util.IDownloadUtils import org.monogram.presentation.core.util.namespacedCacheKey -import org.monogram.presentation.features.chats.currentChat.AutoDownloadSuppression -import org.monogram.presentation.features.chats.currentChat.components.VideoStickerPlayer -import org.monogram.presentation.features.chats.currentChat.components.VideoType +import org.monogram.presentation.features.chats.conversation.AutoDownloadSuppression +import org.monogram.presentation.core.media.VideoStickerPlayer +import org.monogram.presentation.core.media.VideoType @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/VideoNoteBubble.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/VideoNoteBubble.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/VideoNoteBubble.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/VideoNoteBubble.kt index 9f060e9c..715cc616 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/VideoNoteBubble.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/VideoNoteBubble.kt @@ -1,6 +1,6 @@ @file:OptIn(ExperimentalMaterial3ExpressiveApi::class) -package org.monogram.presentation.features.chats.currentChat.components.chats +package org.monogram.presentation.features.chats.conversation.ui.message import androidx.annotation.OptIn import androidx.compose.foundation.Image @@ -59,7 +59,7 @@ import org.monogram.domain.models.MessageContent import org.monogram.domain.models.MessageModel import org.monogram.presentation.R import org.monogram.presentation.core.util.DateFormatManager -import org.monogram.presentation.features.chats.currentChat.components.InlineVideoPlayer +import org.monogram.presentation.features.chats.conversation.ui.InlineVideoPlayer import org.monogram.presentation.features.stickers.ui.view.shimmerEffect import java.io.File import java.io.FileNotFoundException diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/VoiceMessageBubble.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/VoiceMessageBubble.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/VoiceMessageBubble.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/VoiceMessageBubble.kt index 6e399ca6..b99eaf92 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/VoiceMessageBubble.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/VoiceMessageBubble.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.chats +package org.monogram.presentation.features.chats.conversation.ui.message import androidx.compose.foundation.Canvas import androidx.compose.foundation.background @@ -50,7 +50,7 @@ import org.monogram.domain.models.MessageContent import org.monogram.domain.models.MessageModel import org.monogram.presentation.R import org.monogram.presentation.core.util.IDownloadUtils -import org.monogram.presentation.features.chats.currentChat.components.rememberVoicePlayer +import org.monogram.presentation.features.chats.conversation.ui.rememberVoicePlayer @Composable fun VoiceMessageBubble( diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/code/CodeHighlighter.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/code/CodeHighlighter.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/code/CodeHighlighter.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/code/CodeHighlighter.kt index 8d0571e4..2513a28d 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/code/CodeHighlighter.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/code/CodeHighlighter.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.chats.code +package org.monogram.presentation.features.chats.conversation.ui.message.code import androidx.compose.material3.ColorScheme import androidx.compose.ui.graphics.Color diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/code/LanguagesConfigs.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/code/LanguagesConfigs.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/code/LanguagesConfigs.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/code/LanguagesConfigs.kt index a1d84bab..c80b9b3a 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/code/LanguagesConfigs.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/code/LanguagesConfigs.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.chats.code +package org.monogram.presentation.features.chats.conversation.ui.message.code data class LanguageConfig( val keywords: Set, diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/model/Mappers.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/model/Mappers.kt similarity index 95% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/model/Mappers.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/model/Mappers.kt index 706267fd..2ccccb72 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/chats/model/Mappers.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/model/Mappers.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.chats.model +package org.monogram.presentation.features.chats.conversation.ui.message.model import org.monogram.domain.models.MessageEntity import org.monogram.domain.models.MessageEntityType diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/pins/Mapper.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/pins/Mapper.kt similarity index 93% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/pins/Mapper.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/pins/Mapper.kt index 13b9dcfc..46dd36fe 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/pins/Mapper.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/pins/Mapper.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.pins +package org.monogram.presentation.features.chats.conversation.ui.pins import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/pins/PinnedMessageBar.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/pins/PinnedMessageBar.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/pins/PinnedMessageBar.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/pins/PinnedMessageBar.kt index 09222d5d..d913806a 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/pins/PinnedMessageBar.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/pins/PinnedMessageBar.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.pins +package org.monogram.presentation.features.chats.conversation.ui.pins import androidx.compose.animation.AnimatedContent import androidx.compose.animation.core.Spring diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/pins/PinnedMessagesListSheet.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/pins/PinnedMessagesListSheet.kt similarity index 96% rename from presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/pins/PinnedMessagesListSheet.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/pins/PinnedMessagesListSheet.kt index 759027df..56ba927e 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/components/pins/PinnedMessagesListSheet.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/pins/PinnedMessagesListSheet.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.components.pins +package org.monogram.presentation.features.chats.conversation.ui.pins import androidx.compose.animation.core.animate import androidx.compose.animation.core.spring @@ -32,13 +32,13 @@ import org.monogram.domain.models.MessageModel import org.monogram.presentation.R import org.monogram.presentation.core.ui.rememberShimmerBrush import org.monogram.presentation.core.util.IDownloadUtils -import org.monogram.presentation.features.chats.currentChat.chatContent.GroupedMessageItem -import org.monogram.presentation.features.chats.currentChat.chatContent.groupMessagesByAlbum -import org.monogram.presentation.features.chats.currentChat.chatContent.shouldShowDate -import org.monogram.presentation.features.chats.currentChat.components.AlbumMessageBubbleContainer -import org.monogram.presentation.features.chats.currentChat.components.ChannelMessageBubbleContainer -import org.monogram.presentation.features.chats.currentChat.components.DateSeparator -import org.monogram.presentation.features.chats.currentChat.components.MessageBubbleContainer +import org.monogram.presentation.features.chats.conversation.ui.content.GroupedMessageItem +import org.monogram.presentation.features.chats.conversation.ui.content.groupMessagesByAlbum +import org.monogram.presentation.features.chats.conversation.ui.content.shouldShowDate +import org.monogram.presentation.features.chats.conversation.ui.AlbumMessageBubbleContainer +import org.monogram.presentation.features.chats.conversation.ui.ChannelMessageBubbleContainer +import org.monogram.presentation.features.chats.conversation.ui.DateSeparator +import org.monogram.presentation.features.chats.conversation.ui.MessageBubbleContainer import kotlin.math.roundToInt @OptIn(ExperimentalMaterial3Api::class) @@ -451,3 +451,4 @@ private fun PinnedMessagesLoadingSkeleton( } } } + diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/newChat/DefaultNewChatComponent.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/creation/DefaultNewChatComponent.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/newChat/DefaultNewChatComponent.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/creation/DefaultNewChatComponent.kt index b221d8d0..455f023b 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/newChat/DefaultNewChatComponent.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/creation/DefaultNewChatComponent.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.newChat +package org.monogram.presentation.features.chats.creation import com.arkivanov.mvikotlin.core.instancekeeper.getStore import com.arkivanov.mvikotlin.extensions.coroutines.stateFlow diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/newChat/NewChatComponent.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/creation/NewChatComponent.kt similarity index 96% rename from presentation/src/main/java/org/monogram/presentation/features/chats/newChat/NewChatComponent.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/creation/NewChatComponent.kt index 77581abd..cb6cdc8f 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/newChat/NewChatComponent.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/creation/NewChatComponent.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.newChat +package org.monogram.presentation.features.chats.creation import kotlinx.coroutines.flow.StateFlow import org.monogram.domain.models.UserModel diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/newChat/NewChatContent.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/creation/NewChatContent.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/newChat/NewChatContent.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/creation/NewChatContent.kt index 2ab9333f..7aad77a4 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/newChat/NewChatContent.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/creation/NewChatContent.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.newChat +package org.monogram.presentation.features.chats.creation import androidx.activity.compose.BackHandler import androidx.activity.compose.rememberLauncherForActivityResult @@ -48,10 +48,10 @@ import org.monogram.presentation.core.ui.shimmerBackground import org.monogram.presentation.core.util.DateFormatManager import org.monogram.presentation.core.util.FileUtils import org.monogram.presentation.core.util.getUserStatusText -import org.monogram.presentation.features.chats.chatList.components.NewChannelContent -import org.monogram.presentation.features.chats.chatList.components.NewGroupContent -import org.monogram.presentation.features.chats.chatList.components.SectionHeader -import org.monogram.presentation.features.chats.chatList.components.SettingsTextField +import org.monogram.presentation.features.chats.list.components.NewChannelContent +import org.monogram.presentation.features.chats.list.components.NewGroupContent +import org.monogram.presentation.core.ui.SectionHeader +import org.monogram.presentation.core.ui.SettingsTextField @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/newChat/NewChatStore.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/creation/NewChatStore.kt similarity index 95% rename from presentation/src/main/java/org/monogram/presentation/features/chats/newChat/NewChatStore.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/creation/NewChatStore.kt index cdbecc41..64156048 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/newChat/NewChatStore.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/creation/NewChatStore.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.newChat +package org.monogram.presentation.features.chats.creation import com.arkivanov.mvikotlin.core.store.Store diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/newChat/NewChatStoreFactory.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/creation/NewChatStoreFactory.kt similarity index 92% rename from presentation/src/main/java/org/monogram/presentation/features/chats/newChat/NewChatStoreFactory.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/creation/NewChatStoreFactory.kt index 7de0bdf4..811e6c4c 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/newChat/NewChatStoreFactory.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/creation/NewChatStoreFactory.kt @@ -1,11 +1,11 @@ -package org.monogram.presentation.features.chats.newChat +package org.monogram.presentation.features.chats.creation import com.arkivanov.mvikotlin.core.store.Reducer import com.arkivanov.mvikotlin.core.store.Store import com.arkivanov.mvikotlin.core.store.StoreFactory import com.arkivanov.mvikotlin.extensions.coroutines.CoroutineExecutor -import org.monogram.presentation.features.chats.newChat.NewChatStore.Intent -import org.monogram.presentation.features.chats.newChat.NewChatStore.Label +import org.monogram.presentation.features.chats.creation.NewChatStore.Intent +import org.monogram.presentation.features.chats.creation.NewChatStore.Label class NewChatStoreFactory( private val storeFactory: StoreFactory, diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/ChatListComponent.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/list/ChatListComponent.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/ChatListComponent.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/list/ChatListComponent.kt index f76b04b2..0b9774c0 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/ChatListComponent.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/list/ChatListComponent.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats +package org.monogram.presentation.features.chats.list import androidx.compose.runtime.Immutable import kotlinx.coroutines.flow.StateFlow diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/chatList/ChatListContent.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/list/ChatListContent.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/chatList/ChatListContent.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/list/ChatListContent.kt index 5d18d0a4..f07e5db6 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/chatList/ChatListContent.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/list/ChatListContent.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.chatList +package org.monogram.presentation.features.chats.list import android.util.Log import androidx.activity.compose.BackHandler @@ -121,18 +121,18 @@ import org.monogram.presentation.R import org.monogram.presentation.core.ui.Avatar import org.monogram.presentation.core.ui.ConfirmationSheet import org.monogram.presentation.core.util.LocalTabletInterfaceEnabled -import org.monogram.presentation.features.chats.ChatListComponent -import org.monogram.presentation.features.chats.chatList.components.AccountMenu -import org.monogram.presentation.features.chats.chatList.components.ArchiveHeaderCard -import org.monogram.presentation.features.chats.chatList.components.ChatListItem -import org.monogram.presentation.features.chats.chatList.components.ChatListShimmer -import org.monogram.presentation.features.chats.chatList.components.ChatListTopBar -import org.monogram.presentation.features.chats.chatList.components.EmptyStateView -import org.monogram.presentation.features.chats.chatList.components.FolderTabs -import org.monogram.presentation.features.chats.chatList.components.MessageSearchItem -import org.monogram.presentation.features.chats.chatList.components.PermissionRequestSheet -import org.monogram.presentation.features.chats.chatList.components.SelectionTopBar -import org.monogram.presentation.features.chats.currentChat.components.chats.getEmojiFontFamily +import org.monogram.presentation.features.chats.list.ChatListComponent +import org.monogram.presentation.features.chats.list.components.AccountMenu +import org.monogram.presentation.features.chats.list.components.ArchiveHeaderCard +import org.monogram.presentation.features.chats.list.components.ChatListItem +import org.monogram.presentation.features.chats.list.components.ChatListShimmer +import org.monogram.presentation.features.chats.list.components.ChatListTopBar +import org.monogram.presentation.features.chats.list.components.EmptyStateView +import org.monogram.presentation.features.chats.list.components.FolderTabs +import org.monogram.presentation.features.chats.list.components.MessageSearchItem +import org.monogram.presentation.features.chats.list.components.PermissionRequestSheet +import org.monogram.presentation.features.chats.list.components.SelectionTopBar +import org.monogram.presentation.features.chats.conversation.ui.message.getEmojiFontFamily import org.monogram.presentation.features.instantview.InstantViewer import org.monogram.presentation.features.stickers.ui.menu.EmojisGrid import org.monogram.presentation.features.webapp.MiniAppViewer diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/ChatListStore.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/list/ChatListStore.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/ChatListStore.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/list/ChatListStore.kt index 7a15de0b..56af0733 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/ChatListStore.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/list/ChatListStore.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats +package org.monogram.presentation.features.chats.list import com.arkivanov.mvikotlin.core.store.Store diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/ChatListStoreFactory.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/list/ChatListStoreFactory.kt similarity index 94% rename from presentation/src/main/java/org/monogram/presentation/features/chats/ChatListStoreFactory.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/list/ChatListStoreFactory.kt index 746b892b..1f410f86 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/ChatListStoreFactory.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/list/ChatListStoreFactory.kt @@ -1,12 +1,12 @@ -package org.monogram.presentation.features.chats +package org.monogram.presentation.features.chats.list import com.arkivanov.mvikotlin.core.store.Reducer import com.arkivanov.mvikotlin.core.store.Store import com.arkivanov.mvikotlin.core.store.StoreFactory import com.arkivanov.mvikotlin.extensions.coroutines.CoroutineExecutor -import org.monogram.presentation.features.chats.ChatListStore.Intent -import org.monogram.presentation.features.chats.ChatListStore.Label -import org.monogram.presentation.features.chats.chatList.DefaultChatListComponent +import org.monogram.presentation.features.chats.list.ChatListStore.Intent +import org.monogram.presentation.features.chats.list.ChatListStore.Label +import org.monogram.presentation.features.chats.list.DefaultChatListComponent class ChatListStoreFactory( private val storeFactory: StoreFactory, diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/chatList/DefaultChatListComponent.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/list/DefaultChatListComponent.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/chatList/DefaultChatListComponent.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/list/DefaultChatListComponent.kt index 19f56e3a..eba4ef88 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/chatList/DefaultChatListComponent.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/list/DefaultChatListComponent.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.chatList +package org.monogram.presentation.features.chats.list import android.util.Log import com.arkivanov.decompose.value.Value @@ -35,9 +35,9 @@ import org.monogram.presentation.BuildConfig import org.monogram.presentation.core.util.AppPreferences import org.monogram.presentation.core.util.coRunCatching import org.monogram.presentation.core.util.componentScope -import org.monogram.presentation.features.chats.ChatListComponent -import org.monogram.presentation.features.chats.ChatListStore -import org.monogram.presentation.features.chats.ChatListStoreFactory +import org.monogram.presentation.features.chats.list.ChatListComponent +import org.monogram.presentation.features.chats.list.ChatListStore +import org.monogram.presentation.features.chats.list.ChatListStoreFactory import org.monogram.presentation.root.AppComponentContext class DefaultChatListComponent( diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/chatList/components/AccountMenu.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/list/components/AccountMenu.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/chatList/components/AccountMenu.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/list/components/AccountMenu.kt index af1c12f3..e1986294 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/chatList/components/AccountMenu.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/list/components/AccountMenu.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.chatList.components +package org.monogram.presentation.features.chats.list.components import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.Animatable diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/chatList/components/ArchiveHeaderCard.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/list/components/ArchiveHeaderCard.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/chatList/components/ArchiveHeaderCard.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/list/components/ArchiveHeaderCard.kt index 0a140f74..0ddb36b5 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/chatList/components/ArchiveHeaderCard.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/list/components/ArchiveHeaderCard.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.chatList.components +package org.monogram.presentation.features.chats.list.components import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/chatList/components/Avatar.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/list/components/Avatar.kt similarity index 96% rename from presentation/src/main/java/org/monogram/presentation/features/chats/chatList/components/Avatar.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/list/components/Avatar.kt index 9cd404c9..621ca694 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/chatList/components/Avatar.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/list/components/Avatar.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.chatList.components +package org.monogram.presentation.features.chats.list.components import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -22,7 +22,7 @@ import androidx.compose.ui.unit.sp import coil3.compose.rememberAsyncImagePainter import coil3.request.ImageRequest import org.monogram.presentation.core.util.generateColorFromHash -import org.monogram.presentation.features.chats.currentChat.components.AvatarPlayer +import org.monogram.presentation.core.media.AvatarPlayer import java.io.File @Composable diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/chatList/components/ChatCreationCommon.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/list/components/ChatCreationCommon.kt similarity index 67% rename from presentation/src/main/java/org/monogram/presentation/features/chats/chatList/components/ChatCreationCommon.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/list/components/ChatCreationCommon.kt index 48161788..ba81299b 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/chatList/components/ChatCreationCommon.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/list/components/ChatCreationCommon.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.chatList.components +package org.monogram.presentation.features.chats.list.components import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column @@ -39,99 +39,6 @@ import androidx.compose.ui.unit.sp import org.monogram.presentation.R import org.monogram.presentation.core.ui.ItemPosition -@Composable -fun SectionHeader(text: String, modifier: Modifier = Modifier) { - Text( - text = text, - modifier = modifier - .fillMaxWidth() - .padding(start = 12.dp, bottom = 8.dp, top = 16.dp), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.primary, - fontWeight = FontWeight.Bold - ) -} - -@Composable -fun SettingsTextField( - value: String, - onValueChange: (String) -> Unit, - placeholder: String, - icon: ImageVector, - position: ItemPosition, - modifier: Modifier = Modifier, - enabled: Boolean = true, - singleLine: Boolean = false, - minLines: Int = 1, - maxLines: Int = Int.MAX_VALUE, - itemSpacing: Dp = 2.dp, - keyboardOptions: KeyboardOptions = KeyboardOptions.Default, - keyboardActions: KeyboardActions = KeyboardActions.Default, - trailingIcon: @Composable (() -> Unit)? = null -) { - val cornerRadius = 24.dp - val shape = when (position) { - ItemPosition.TOP -> RoundedCornerShape( - topStart = cornerRadius, - topEnd = cornerRadius, - bottomStart = 4.dp, - bottomEnd = 4.dp - ) - - ItemPosition.MIDDLE -> RoundedCornerShape(4.dp) - ItemPosition.BOTTOM -> RoundedCornerShape( - bottomStart = cornerRadius, - bottomEnd = cornerRadius, - topStart = 4.dp, - topEnd = 4.dp - ) - - ItemPosition.STANDALONE -> RoundedCornerShape(cornerRadius) - } - - Surface( - color = MaterialTheme.colorScheme.surfaceContainer, - shape = shape, - modifier = modifier.fillMaxWidth() - ) { - TextField( - value = value, - onValueChange = onValueChange, - placeholder = { Text(placeholder) }, - modifier = Modifier.fillMaxWidth(), - enabled = enabled, - singleLine = singleLine, - minLines = minLines, - maxLines = maxLines, - leadingIcon = { - Icon( - imageVector = icon, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary - ) - }, - trailingIcon = trailingIcon, - keyboardOptions = keyboardOptions, - keyboardActions = keyboardActions, - colors = TextFieldDefaults.colors( - focusedContainerColor = Color.Transparent, - unfocusedContainerColor = Color.Transparent, - disabledContainerColor = Color.Transparent, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - disabledIndicatorColor = Color.Transparent, - disabledTextColor = MaterialTheme.colorScheme.onSurface, - disabledPlaceholderColor = MaterialTheme.colorScheme.onSurfaceVariant, - disabledLeadingIconColor = MaterialTheme.colorScheme.primary, - disabledTrailingIconColor = MaterialTheme.colorScheme.onSurfaceVariant - ) - ) - } - if (position != ItemPosition.BOTTOM && position != ItemPosition.STANDALONE && itemSpacing > 0.dp) { - Spacer(Modifier.height(itemSpacing)) - } -} - @OptIn(ExperimentalMaterial3Api::class) @Composable fun AutoDeleteSelectorSheet( diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/chatList/components/ChatListItem.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/list/components/ChatListItem.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/chatList/components/ChatListItem.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/list/components/ChatListItem.kt index 51e4baa5..60b2110c 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/chatList/components/ChatListItem.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/list/components/ChatListItem.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.chatList.components +package org.monogram.presentation.features.chats.list.components import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateColorAsState @@ -69,9 +69,9 @@ import org.monogram.presentation.core.ui.AvatarForChat import org.monogram.presentation.core.ui.TypingDots import org.monogram.presentation.core.util.DateFormatManager import org.monogram.presentation.core.util.toShortRelativeDate -import org.monogram.presentation.features.chats.currentChat.components.chats.addEmojiStyle -import org.monogram.presentation.features.chats.currentChat.components.chats.buildAnnotatedMessageTextWithEmoji -import org.monogram.presentation.features.chats.currentChat.components.chats.rememberMessageInlineContent +import org.monogram.presentation.features.chats.conversation.ui.message.addEmojiStyle +import org.monogram.presentation.features.chats.conversation.ui.message.buildAnnotatedMessageTextWithEmoji +import org.monogram.presentation.features.chats.conversation.ui.message.rememberMessageInlineContent import org.monogram.presentation.features.stickers.ui.view.StickerImage @OptIn(ExperimentalFoundationApi::class) diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/chatList/components/ChatListShimmer.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/list/components/ChatListShimmer.kt similarity index 96% rename from presentation/src/main/java/org/monogram/presentation/features/chats/chatList/components/ChatListShimmer.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/list/components/ChatListShimmer.kt index 01259c58..75e22b34 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/chatList/components/ChatListShimmer.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/list/components/ChatListShimmer.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.chatList.components +package org.monogram.presentation.features.chats.list.components import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/chatList/components/ChatListTopBar.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/list/components/ChatListTopBar.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/chatList/components/ChatListTopBar.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/list/components/ChatListTopBar.kt index 046d7f75..d57e4b8e 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/chatList/components/ChatListTopBar.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/list/components/ChatListTopBar.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.chatList.components +package org.monogram.presentation.features.chats.list.components import androidx.compose.animation.AnimatedContent import androidx.compose.animation.SizeTransform diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/chatList/components/EmptyStateView.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/list/components/EmptyStateView.kt similarity index 96% rename from presentation/src/main/java/org/monogram/presentation/features/chats/chatList/components/EmptyStateView.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/list/components/EmptyStateView.kt index e111248a..9c57bc97 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/chatList/components/EmptyStateView.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/list/components/EmptyStateView.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.chatList.components +package org.monogram.presentation.features.chats.list.components import androidx.compose.foundation.background import androidx.compose.foundation.layout.* diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/chatList/components/FolderTabs.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/list/components/FolderTabs.kt similarity index 97% rename from presentation/src/main/java/org/monogram/presentation/features/chats/chatList/components/FolderTabs.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/list/components/FolderTabs.kt index 9a128faa..1925cc4f 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/chatList/components/FolderTabs.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/list/components/FolderTabs.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.chatList.components +package org.monogram.presentation.features.chats.list.components import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.background @@ -30,8 +30,8 @@ import org.koin.compose.koinInject import org.monogram.domain.models.FolderModel import org.monogram.presentation.R import org.monogram.presentation.core.util.AppPreferences -import org.monogram.presentation.features.chats.currentChat.components.chats.addEmojiStyle -import org.monogram.presentation.features.chats.currentChat.components.chats.getEmojiFontFamily +import org.monogram.presentation.features.chats.conversation.ui.message.addEmojiStyle +import org.monogram.presentation.features.chats.conversation.ui.message.getEmojiFontFamily import org.monogram.presentation.settings.folders.getFolderIcon @Composable diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/chatList/components/MessageSearchItem.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/list/components/MessageSearchItem.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/chatList/components/MessageSearchItem.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/list/components/MessageSearchItem.kt index 78a4978f..e1af1f0d 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/chatList/components/MessageSearchItem.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/list/components/MessageSearchItem.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.chatList.components +package org.monogram.presentation.features.chats.list.components import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/chatList/components/NewChannelContent.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/list/components/NewChannelContent.kt similarity index 97% rename from presentation/src/main/java/org/monogram/presentation/features/chats/chatList/components/NewChannelContent.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/list/components/NewChannelContent.kt index 113bf1ea..abaa8da7 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/chatList/components/NewChannelContent.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/list/components/NewChannelContent.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.chatList.components +package org.monogram.presentation.features.chats.list.components import androidx.compose.foundation.* import androidx.compose.foundation.layout.* @@ -25,7 +25,9 @@ import androidx.compose.ui.unit.sp import org.monogram.presentation.R import org.monogram.presentation.core.ui.Avatar import org.monogram.presentation.core.ui.ItemPosition +import org.monogram.presentation.core.ui.SectionHeader import org.monogram.presentation.core.ui.SettingsItem +import org.monogram.presentation.core.ui.SettingsTextField @Composable fun NewChannelContent( diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/chatList/components/NewGroupContent.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/list/components/NewGroupContent.kt similarity index 97% rename from presentation/src/main/java/org/monogram/presentation/features/chats/chatList/components/NewGroupContent.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/list/components/NewGroupContent.kt index 5cac73fc..20292c99 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/chatList/components/NewGroupContent.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/list/components/NewGroupContent.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.chatList.components +package org.monogram.presentation.features.chats.list.components import androidx.compose.foundation.* import androidx.compose.foundation.layout.* @@ -25,7 +25,9 @@ import androidx.compose.ui.unit.sp import org.monogram.presentation.R import org.monogram.presentation.core.ui.Avatar import org.monogram.presentation.core.ui.ItemPosition +import org.monogram.presentation.core.ui.SectionHeader import org.monogram.presentation.core.ui.SettingsItem +import org.monogram.presentation.core.ui.SettingsTextField @Composable fun NewGroupContent( diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/chatList/components/PermissionRequestSheet.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/list/components/PermissionRequestSheet.kt similarity index 99% rename from presentation/src/main/java/org/monogram/presentation/features/chats/chatList/components/PermissionRequestSheet.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/list/components/PermissionRequestSheet.kt index 25fa78a8..46dfeca7 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/chatList/components/PermissionRequestSheet.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/list/components/PermissionRequestSheet.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.chatList.components +package org.monogram.presentation.features.chats.list.components import android.Manifest import android.content.Intent diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/chatList/components/SelectionTopBar.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/list/components/SelectionTopBar.kt similarity index 98% rename from presentation/src/main/java/org/monogram/presentation/features/chats/chatList/components/SelectionTopBar.kt rename to presentation/src/main/java/org/monogram/presentation/features/chats/list/components/SelectionTopBar.kt index f2f406dd..0a2101d6 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/chatList/components/SelectionTopBar.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/list/components/SelectionTopBar.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.chatList.components +package org.monogram.presentation.features.chats.list.components import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.VolumeOff diff --git a/presentation/src/main/java/org/monogram/presentation/features/gallery/components/PollComposerSheet.kt b/presentation/src/main/java/org/monogram/presentation/features/gallery/components/PollComposerSheet.kt index 206b6b09..ffcdf383 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/gallery/components/PollComposerSheet.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/gallery/components/PollComposerSheet.kt @@ -109,11 +109,11 @@ import org.monogram.presentation.R import org.monogram.presentation.core.ui.ItemPosition import org.monogram.presentation.core.ui.SettingsSwitchTile import org.monogram.presentation.core.ui.SettingsTile -import org.monogram.presentation.features.chats.chatList.components.SectionHeader -import org.monogram.presentation.features.chats.chatList.components.SettingsTextField -import org.monogram.presentation.features.chats.currentChat.components.inputbar.ScheduleDatePickerDialog -import org.monogram.presentation.features.chats.currentChat.components.inputbar.ScheduleTimePickerDialog -import org.monogram.presentation.features.chats.currentChat.components.inputbar.buildScheduledDateEpochSeconds +import org.monogram.presentation.core.ui.SectionHeader +import org.monogram.presentation.core.ui.SettingsTextField +import org.monogram.presentation.features.chats.conversation.ui.inputbar.ScheduleDatePickerDialog +import org.monogram.presentation.features.chats.conversation.ui.inputbar.ScheduleTimePickerDialog +import org.monogram.presentation.features.chats.conversation.ui.inputbar.buildScheduledDateEpochSeconds import java.text.DateFormat import java.util.Calendar import java.util.Date diff --git a/presentation/src/main/java/org/monogram/presentation/features/instantview/InstantViewer.kt b/presentation/src/main/java/org/monogram/presentation/features/instantview/InstantViewer.kt index 9fdec772..3f1f01b4 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/instantview/InstantViewer.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/instantview/InstantViewer.kt @@ -145,7 +145,7 @@ import org.monogram.domain.repository.FileRepository import org.monogram.domain.repository.MessageRepository import org.monogram.presentation.R import org.monogram.presentation.core.util.IDownloadUtils -import org.monogram.presentation.features.chats.currentChat.components.chats.normalizeUrl +import org.monogram.presentation.features.chats.conversation.ui.message.normalizeUrl import org.monogram.presentation.features.instantview.components.AsyncImageWithDownload import org.monogram.presentation.features.instantview.components.AsyncVideoWithDownload import org.monogram.presentation.features.instantview.components.LocalFileRepository diff --git a/presentation/src/main/java/org/monogram/presentation/features/instantview/components/InstantViewComponents.kt b/presentation/src/main/java/org/monogram/presentation/features/instantview/components/InstantViewComponents.kt index 2bae21e4..c412cbaa 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/instantview/components/InstantViewComponents.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/instantview/components/InstantViewComponents.kt @@ -46,9 +46,9 @@ import kotlinx.coroutines.withTimeoutOrNull import org.monogram.domain.models.FileDownloadEvent import org.monogram.domain.models.webapp.PageBlockCaption import org.monogram.domain.models.webapp.RichText -import org.monogram.presentation.features.chats.currentChat.components.VideoStickerPlayer -import org.monogram.presentation.features.chats.currentChat.components.VideoType -import org.monogram.presentation.features.chats.currentChat.components.chats.normalizeUrl +import org.monogram.presentation.core.media.VideoStickerPlayer +import org.monogram.presentation.core.media.VideoType +import org.monogram.presentation.features.chats.conversation.ui.message.normalizeUrl import org.monogram.presentation.features.stickers.ui.view.shimmerEffect @Composable diff --git a/presentation/src/main/java/org/monogram/presentation/features/profile/ProfileContent.kt b/presentation/src/main/java/org/monogram/presentation/features/profile/ProfileContent.kt index 64b1d87a..d1858d37 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/profile/ProfileContent.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/profile/ProfileContent.kt @@ -74,7 +74,7 @@ import org.monogram.presentation.core.util.DateFormatManager import org.monogram.presentation.core.util.LocalTabletInterfaceEnabled import org.monogram.presentation.core.util.ScrollStrategy import org.monogram.presentation.core.util.getUserStatusText -import org.monogram.presentation.features.chats.chatList.components.SettingsTextField +import org.monogram.presentation.core.ui.SettingsTextField import org.monogram.presentation.features.profile.components.LocationViewer import org.monogram.presentation.features.profile.components.ProfileHeaderTransformed import org.monogram.presentation.features.profile.components.ProfileInfoSection diff --git a/presentation/src/main/java/org/monogram/presentation/features/profile/admin/AdminManageContent.kt b/presentation/src/main/java/org/monogram/presentation/features/profile/admin/AdminManageContent.kt index 85115794..00d6eb24 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/profile/admin/AdminManageContent.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/profile/admin/AdminManageContent.kt @@ -22,7 +22,7 @@ import com.arkivanov.decompose.extensions.compose.subscribeAsState import org.monogram.domain.repository.ChatMemberStatus import org.monogram.presentation.R import org.monogram.presentation.core.ui.Avatar -import org.monogram.presentation.features.chats.chatList.components.SettingsTextField +import org.monogram.presentation.core.ui.SettingsTextField import org.monogram.presentation.core.ui.ItemPosition import org.monogram.presentation.core.ui.SettingsSwitchTile diff --git a/presentation/src/main/java/org/monogram/presentation/features/profile/admin/ChatEditContent.kt b/presentation/src/main/java/org/monogram/presentation/features/profile/admin/ChatEditContent.kt index 1614847b..8550158e 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/profile/admin/ChatEditContent.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/profile/admin/ChatEditContent.kt @@ -32,7 +32,7 @@ import org.monogram.presentation.R import org.monogram.presentation.core.ui.Avatar import org.monogram.presentation.core.ui.ConfirmationSheet import org.monogram.presentation.core.util.FileUtils -import org.monogram.presentation.features.chats.chatList.components.SettingsTextField +import org.monogram.presentation.core.ui.SettingsTextField import org.monogram.presentation.core.ui.ItemPosition import org.monogram.presentation.core.ui.SettingsSwitchTile import org.monogram.presentation.core.ui.SettingsTile diff --git a/presentation/src/main/java/org/monogram/presentation/features/profile/components/ProfileMediaSection.kt b/presentation/src/main/java/org/monogram/presentation/features/profile/components/ProfileMediaSection.kt index 6ded266c..5a37a84b 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/profile/components/ProfileMediaSection.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/profile/components/ProfileMediaSection.kt @@ -45,8 +45,8 @@ import org.monogram.presentation.core.ui.Avatar import org.monogram.presentation.core.ui.rememberShimmerBrush import org.monogram.presentation.core.util.DateFormatManager import org.monogram.presentation.core.util.getUserStatusText -import org.monogram.presentation.features.chats.currentChat.components.VideoStickerPlayer -import org.monogram.presentation.features.chats.currentChat.components.VideoType +import org.monogram.presentation.core.media.VideoStickerPlayer +import org.monogram.presentation.core.media.VideoType import org.monogram.presentation.features.profile.ProfileComponent import org.monogram.presentation.features.stickers.ui.view.StickerImage import java.io.File diff --git a/presentation/src/main/java/org/monogram/presentation/features/profile/components/StatisticsViewer.kt b/presentation/src/main/java/org/monogram/presentation/features/profile/components/StatisticsViewer.kt index 32eea554..e878146c 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/profile/components/StatisticsViewer.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/profile/components/StatisticsViewer.kt @@ -115,7 +115,7 @@ import org.monogram.domain.models.StatisticsType import org.monogram.presentation.R import org.monogram.presentation.core.util.DateFormatManager import org.monogram.presentation.core.util.coRunCatching -import org.monogram.presentation.features.chats.chatList.components.SectionHeader +import org.monogram.presentation.core.ui.SectionHeader import java.text.SimpleDateFormat import java.util.Date import java.util.Locale diff --git a/presentation/src/main/java/org/monogram/presentation/features/profile/logs/ProfileLogsContent.kt b/presentation/src/main/java/org/monogram/presentation/features/profile/logs/ProfileLogsContent.kt index 31e18932..fbbaa7dc 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/profile/logs/ProfileLogsContent.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/profile/logs/ProfileLogsContent.kt @@ -28,8 +28,8 @@ import org.monogram.domain.models.MessageSenderModel import org.monogram.presentation.R import org.monogram.presentation.core.ui.Avatar import org.monogram.presentation.core.ui.ItemPosition -import org.monogram.presentation.features.chats.chatList.components.SectionHeader -import org.monogram.presentation.features.chats.chatList.components.SettingsTextField +import org.monogram.presentation.core.ui.SectionHeader +import org.monogram.presentation.core.ui.SettingsTextField import org.monogram.presentation.features.profile.logs.components.DateHeader import org.monogram.presentation.features.profile.logs.components.FilterChipCompact import org.monogram.presentation.features.profile.logs.components.LogBubble diff --git a/presentation/src/main/java/org/monogram/presentation/features/profile/logs/components/MessagePreview.kt b/presentation/src/main/java/org/monogram/presentation/features/profile/logs/components/MessagePreview.kt index a9f23c03..3b9c6b01 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/profile/logs/components/MessagePreview.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/profile/logs/components/MessagePreview.kt @@ -30,9 +30,9 @@ import org.monogram.domain.models.MessageContent import org.monogram.domain.models.MessageEntity import org.monogram.domain.models.MessageModel import org.monogram.presentation.R -import org.monogram.presentation.features.chats.currentChat.components.chats.MessageText -import org.monogram.presentation.features.chats.currentChat.components.chats.buildAnnotatedMessageTextWithEmoji -import org.monogram.presentation.features.chats.currentChat.components.chats.rememberMessageInlineContent +import org.monogram.presentation.features.chats.conversation.ui.message.MessageText +import org.monogram.presentation.features.chats.conversation.ui.message.buildAnnotatedMessageTextWithEmoji +import org.monogram.presentation.features.chats.conversation.ui.message.rememberMessageInlineContent import org.monogram.presentation.features.profile.logs.ProfileLogsComponent import java.io.File diff --git a/presentation/src/main/java/org/monogram/presentation/features/stickers/ui/menu/EmojisGrid.kt b/presentation/src/main/java/org/monogram/presentation/features/stickers/ui/menu/EmojisGrid.kt index 8eb52de0..630202bb 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/stickers/ui/menu/EmojisGrid.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/stickers/ui/menu/EmojisGrid.kt @@ -78,8 +78,8 @@ import org.monogram.domain.repository.EmojiRepository import org.monogram.domain.repository.StickerRepository import org.monogram.presentation.R import org.monogram.presentation.core.util.AppPreferences -import org.monogram.presentation.features.chats.currentChat.components.StickerSetSheet -import org.monogram.presentation.features.chats.currentChat.components.chats.getEmojiFontFamily +import org.monogram.presentation.features.chats.conversation.ui.StickerSetSheet +import org.monogram.presentation.features.chats.conversation.ui.message.getEmojiFontFamily import org.monogram.presentation.features.stickers.ui.view.LocalIsScrolling import org.monogram.presentation.features.stickers.ui.view.StickerItem diff --git a/presentation/src/main/java/org/monogram/presentation/features/stickers/ui/menu/GifsGrid.kt b/presentation/src/main/java/org/monogram/presentation/features/stickers/ui/menu/GifsGrid.kt index 773452a4..f5284e0f 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/stickers/ui/menu/GifsGrid.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/stickers/ui/menu/GifsGrid.kt @@ -68,8 +68,8 @@ import org.koin.compose.koinInject import org.monogram.domain.models.GifModel import org.monogram.domain.repository.GifRepository import org.monogram.presentation.R -import org.monogram.presentation.features.chats.currentChat.components.VideoStickerPlayer -import org.monogram.presentation.features.chats.currentChat.components.VideoType +import org.monogram.presentation.core.media.VideoStickerPlayer +import org.monogram.presentation.core.media.VideoType import org.monogram.presentation.features.stickers.ui.view.shimmerEffect @androidx.annotation.OptIn(UnstableApi::class) diff --git a/presentation/src/main/java/org/monogram/presentation/features/stickers/ui/menu/MessageOptionsMenu.kt b/presentation/src/main/java/org/monogram/presentation/features/stickers/ui/menu/MessageOptionsMenu.kt index 1f9b58b2..6dcb4efd 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/stickers/ui/menu/MessageOptionsMenu.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/stickers/ui/menu/MessageOptionsMenu.kt @@ -136,8 +136,8 @@ import org.monogram.presentation.R import org.monogram.presentation.core.ui.Avatar import org.monogram.presentation.core.util.AppPreferences import org.monogram.presentation.core.util.DateFormatManager -import org.monogram.presentation.features.chats.currentChat.chatContent.DeleteMessagesSheet -import org.monogram.presentation.features.chats.currentChat.components.chats.getEmojiFontFamily +import org.monogram.presentation.features.chats.conversation.ui.content.DeleteMessagesSheet +import org.monogram.presentation.features.chats.conversation.ui.message.getEmojiFontFamily import org.monogram.presentation.features.stickers.ui.view.StickerImage import java.text.SimpleDateFormat import java.util.Date diff --git a/presentation/src/main/java/org/monogram/presentation/features/stickers/ui/menu/StickersGrid.kt b/presentation/src/main/java/org/monogram/presentation/features/stickers/ui/menu/StickersGrid.kt index 1e8ff98f..daddf992 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/stickers/ui/menu/StickersGrid.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/stickers/ui/menu/StickersGrid.kt @@ -70,7 +70,7 @@ import org.monogram.domain.models.StickerModel import org.monogram.domain.models.StickerSetModel import org.monogram.domain.repository.StickerRepository import org.monogram.presentation.R -import org.monogram.presentation.features.chats.currentChat.components.StickerSetSheet +import org.monogram.presentation.features.chats.conversation.ui.StickerSetSheet import org.monogram.presentation.features.stickers.ui.view.LocalIsScrolling import org.monogram.presentation.features.stickers.ui.view.StickerItem import org.monogram.presentation.features.stickers.ui.view.StickerSkeleton diff --git a/presentation/src/main/java/org/monogram/presentation/features/webview/components/FindInPageBar.kt b/presentation/src/main/java/org/monogram/presentation/features/webview/components/FindInPageBar.kt index 9136dfde..e7f42647 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/webview/components/FindInPageBar.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/webview/components/FindInPageBar.kt @@ -16,7 +16,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import org.monogram.presentation.R import org.monogram.presentation.core.ui.ItemPosition -import org.monogram.presentation.features.chats.chatList.components.SettingsTextField +import org.monogram.presentation.core.ui.SettingsTextField @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/presentation/src/main/java/org/monogram/presentation/root/DefaultRootComponent.kt b/presentation/src/main/java/org/monogram/presentation/root/DefaultRootComponent.kt index a29f85ac..6bfba3db 100644 --- a/presentation/src/main/java/org/monogram/presentation/root/DefaultRootComponent.kt +++ b/presentation/src/main/java/org/monogram/presentation/root/DefaultRootComponent.kt @@ -58,10 +58,10 @@ import org.monogram.presentation.core.util.IDownloadUtils import org.monogram.presentation.core.util.coRunCatching import org.monogram.presentation.core.util.componentScope import org.monogram.presentation.features.auth.DefaultAuthComponent -import org.monogram.presentation.features.chats.chatList.DefaultChatListComponent -import org.monogram.presentation.features.chats.currentChat.DefaultChatComponent -import org.monogram.presentation.features.chats.currentChat.components.VideoPlayerPool -import org.monogram.presentation.features.chats.newChat.DefaultNewChatComponent +import org.monogram.presentation.features.chats.list.DefaultChatListComponent +import org.monogram.presentation.features.chats.conversation.DefaultChatComponent +import org.monogram.presentation.core.media.VideoPlayerPool +import org.monogram.presentation.features.chats.creation.DefaultNewChatComponent import org.monogram.presentation.features.profile.DefaultProfileComponent import org.monogram.presentation.features.profile.admin.DefaultAdminManageComponent import org.monogram.presentation.features.profile.admin.DefaultChatEditComponent diff --git a/presentation/src/main/java/org/monogram/presentation/root/RootComponent.kt b/presentation/src/main/java/org/monogram/presentation/root/RootComponent.kt index 6b01b196..5bc92ad8 100644 --- a/presentation/src/main/java/org/monogram/presentation/root/RootComponent.kt +++ b/presentation/src/main/java/org/monogram/presentation/root/RootComponent.kt @@ -10,10 +10,10 @@ import org.monogram.domain.models.ChatModel import org.monogram.domain.models.ProxyTypeModel import org.monogram.presentation.core.util.AppPreferences import org.monogram.presentation.features.auth.AuthComponent -import org.monogram.presentation.features.chats.ChatListComponent -import org.monogram.presentation.features.chats.currentChat.ChatComponent -import org.monogram.presentation.features.chats.currentChat.components.VideoPlayerPool -import org.monogram.presentation.features.chats.newChat.NewChatComponent +import org.monogram.presentation.features.chats.list.ChatListComponent +import org.monogram.presentation.features.chats.conversation.ChatComponent +import org.monogram.presentation.core.media.VideoPlayerPool +import org.monogram.presentation.features.chats.creation.NewChatComponent import org.monogram.presentation.settings.folders.FoldersComponent import org.monogram.presentation.features.profile.ProfileComponent import org.monogram.presentation.features.profile.admin.AdminManageComponent diff --git a/presentation/src/main/java/org/monogram/presentation/settings/adblock/AdBlockContent.kt b/presentation/src/main/java/org/monogram/presentation/settings/adblock/AdBlockContent.kt index 80b72fd5..d6cf0143 100644 --- a/presentation/src/main/java/org/monogram/presentation/settings/adblock/AdBlockContent.kt +++ b/presentation/src/main/java/org/monogram/presentation/settings/adblock/AdBlockContent.kt @@ -28,7 +28,7 @@ import com.arkivanov.decompose.extensions.compose.subscribeAsState import org.monogram.domain.models.ChatModel import org.monogram.presentation.R import org.monogram.presentation.core.ui.Avatar -import org.monogram.presentation.features.chats.chatList.components.SettingsTextField +import org.monogram.presentation.core.ui.SettingsTextField import org.monogram.presentation.core.ui.ItemPosition import org.monogram.presentation.core.ui.SettingsSwitchTile import org.monogram.presentation.core.ui.SettingsTile diff --git a/presentation/src/main/java/org/monogram/presentation/settings/chatSettings/ChatSettingsContent.kt b/presentation/src/main/java/org/monogram/presentation/settings/chatSettings/ChatSettingsContent.kt index abcd9045..356c7c9f 100644 --- a/presentation/src/main/java/org/monogram/presentation/settings/chatSettings/ChatSettingsContent.kt +++ b/presentation/src/main/java/org/monogram/presentation/settings/chatSettings/ChatSettingsContent.kt @@ -139,7 +139,7 @@ import org.monogram.presentation.core.ui.SettingsSwitchTile import org.monogram.presentation.core.ui.SettingsTile import org.monogram.presentation.core.util.EmojiStyle import org.monogram.presentation.core.util.NightMode -import org.monogram.presentation.features.chats.currentChat.components.chats.getEmojiFontFamily +import org.monogram.presentation.features.chats.conversation.ui.message.getEmojiFontFamily import org.monogram.presentation.settings.chatSettings.components.ChatListPreview import org.monogram.presentation.settings.chatSettings.components.ChatSettingsPreview import org.monogram.presentation.settings.chatSettings.components.WallpaperItem diff --git a/presentation/src/main/java/org/monogram/presentation/settings/chatSettings/ChatThemeEditorScreen.kt b/presentation/src/main/java/org/monogram/presentation/settings/chatSettings/ChatThemeEditorScreen.kt index 0df8f764..578502c5 100644 --- a/presentation/src/main/java/org/monogram/presentation/settings/chatSettings/ChatThemeEditorScreen.kt +++ b/presentation/src/main/java/org/monogram/presentation/settings/chatSettings/ChatThemeEditorScreen.kt @@ -36,7 +36,7 @@ import org.monogram.presentation.R import org.monogram.presentation.core.ui.ItemPosition import org.monogram.presentation.core.util.NightMode import org.monogram.presentation.core.util.coRunCatching -import org.monogram.presentation.features.chats.chatList.components.SettingsTextField +import org.monogram.presentation.core.ui.SettingsTextField import java.util.* private enum class PaletteMode { LIGHT, DARK } diff --git a/presentation/src/main/java/org/monogram/presentation/settings/chatSettings/components/ChatSettingsPreview.kt b/presentation/src/main/java/org/monogram/presentation/settings/chatSettings/components/ChatSettingsPreview.kt index 4d15617e..87e30106 100644 --- a/presentation/src/main/java/org/monogram/presentation/settings/chatSettings/components/ChatSettingsPreview.kt +++ b/presentation/src/main/java/org/monogram/presentation/settings/chatSettings/components/ChatSettingsPreview.kt @@ -22,7 +22,7 @@ import coil3.compose.AsyncImage import org.monogram.domain.models.* import org.monogram.presentation.R import org.monogram.presentation.core.util.IDownloadUtils -import org.monogram.presentation.features.chats.currentChat.components.MessageBubbleContainer +import org.monogram.presentation.features.chats.conversation.ui.MessageBubbleContainer import java.io.File @Composable diff --git a/presentation/src/main/java/org/monogram/presentation/settings/folders/FolderDialog.kt b/presentation/src/main/java/org/monogram/presentation/settings/folders/FolderDialog.kt index 255f3bba..770826f6 100644 --- a/presentation/src/main/java/org/monogram/presentation/settings/folders/FolderDialog.kt +++ b/presentation/src/main/java/org/monogram/presentation/settings/folders/FolderDialog.kt @@ -26,8 +26,8 @@ import org.monogram.domain.models.ChatModel import org.monogram.presentation.R import org.monogram.presentation.core.ui.Avatar import org.monogram.presentation.core.ui.ItemPosition -import org.monogram.presentation.features.chats.chatList.components.SectionHeader -import org.monogram.presentation.features.chats.chatList.components.SettingsTextField +import org.monogram.presentation.core.ui.SectionHeader +import org.monogram.presentation.core.ui.SettingsTextField @OptIn(ExperimentalMaterial3Api::class) diff --git a/presentation/src/main/java/org/monogram/presentation/settings/profile/EditProfileContent.kt b/presentation/src/main/java/org/monogram/presentation/settings/profile/EditProfileContent.kt index f70807c8..5f244add 100644 --- a/presentation/src/main/java/org/monogram/presentation/settings/profile/EditProfileContent.kt +++ b/presentation/src/main/java/org/monogram/presentation/settings/profile/EditProfileContent.kt @@ -50,8 +50,8 @@ import org.monogram.presentation.R import org.monogram.presentation.core.ui.Avatar import org.monogram.presentation.core.ui.ItemPosition import org.monogram.presentation.core.util.FileUtils -import org.monogram.presentation.features.chats.chatList.components.SectionHeader -import org.monogram.presentation.features.chats.chatList.components.SettingsTextField +import org.monogram.presentation.core.ui.SectionHeader +import org.monogram.presentation.core.ui.SettingsTextField import java.util.* private const val MAP_STYLE = "https://tiles.openfreemap.org/styles/bright" diff --git a/presentation/src/main/java/org/monogram/presentation/settings/proxy/components/ProxyAddEditSheet.kt b/presentation/src/main/java/org/monogram/presentation/settings/proxy/components/ProxyAddEditSheet.kt index c0a32140..127db9a5 100644 --- a/presentation/src/main/java/org/monogram/presentation/settings/proxy/components/ProxyAddEditSheet.kt +++ b/presentation/src/main/java/org/monogram/presentation/settings/proxy/components/ProxyAddEditSheet.kt @@ -58,7 +58,7 @@ import org.monogram.domain.models.ProxyTypeModel import org.monogram.domain.proxy.MtprotoSecretNormalizer import org.monogram.presentation.R import org.monogram.presentation.core.ui.ItemPosition -import org.monogram.presentation.features.chats.chatList.components.SettingsTextField +import org.monogram.presentation.core.ui.SettingsTextField import org.monogram.presentation.features.stickers.ui.menu.MenuOptionRow @OptIn(ExperimentalMaterial3Api::class) diff --git a/presentation/src/main/java/org/monogram/presentation/settings/storage/CacheController.kt b/presentation/src/main/java/org/monogram/presentation/settings/storage/CacheController.kt index 4b62ab37..80d6450d 100644 --- a/presentation/src/main/java/org/monogram/presentation/settings/storage/CacheController.kt +++ b/presentation/src/main/java/org/monogram/presentation/settings/storage/CacheController.kt @@ -2,7 +2,7 @@ package org.monogram.presentation.settings.storage import android.content.Context import coil3.imageLoader -import org.monogram.presentation.features.chats.currentChat.components.ExoPlayerCache +import org.monogram.presentation.core.media.ExoPlayerCache import java.io.File class CacheController(val context: Context, val exoPlayerCache: ExoPlayerCache) { diff --git a/presentation/src/test/java/org/monogram/presentation/features/chats/currentChat/editor/photo/components/TransformControlsTest.kt b/presentation/src/test/java/org/monogram/presentation/features/chats/conversation/editor/photo/components/TransformControlsTest.kt similarity index 93% rename from presentation/src/test/java/org/monogram/presentation/features/chats/currentChat/editor/photo/components/TransformControlsTest.kt rename to presentation/src/test/java/org/monogram/presentation/features/chats/conversation/editor/photo/components/TransformControlsTest.kt index 04df4f1c..71479424 100644 --- a/presentation/src/test/java/org/monogram/presentation/features/chats/currentChat/editor/photo/components/TransformControlsTest.kt +++ b/presentation/src/test/java/org/monogram/presentation/features/chats/conversation/editor/photo/components/TransformControlsTest.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.editor.photo.components +package org.monogram.presentation.features.chats.conversation.editor.photo.components import org.junit.Assert.assertEquals import org.junit.Test diff --git a/presentation/src/test/java/org/monogram/presentation/features/chats/currentChat/editor/photo/crop/CropGeometryTest.kt b/presentation/src/test/java/org/monogram/presentation/features/chats/conversation/editor/photo/crop/CropGeometryTest.kt similarity index 98% rename from presentation/src/test/java/org/monogram/presentation/features/chats/currentChat/editor/photo/crop/CropGeometryTest.kt rename to presentation/src/test/java/org/monogram/presentation/features/chats/conversation/editor/photo/crop/CropGeometryTest.kt index 1aa60c9f..bf96b436 100644 --- a/presentation/src/test/java/org/monogram/presentation/features/chats/currentChat/editor/photo/crop/CropGeometryTest.kt +++ b/presentation/src/test/java/org/monogram/presentation/features/chats/conversation/editor/photo/crop/CropGeometryTest.kt @@ -1,4 +1,4 @@ -package org.monogram.presentation.features.chats.currentChat.editor.photo.crop +package org.monogram.presentation.features.chats.conversation.editor.photo.crop import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect From 48f83c63494a77a947da379747bb77c458c8a765 Mon Sep 17 00:00:00 2001 From: Artur Skubei <41114720+gdlbo@users.noreply.github.com> Date: Fri, 24 Apr 2026 20:35:38 +0300 Subject: [PATCH 4/8] refactor message rendering dependencies and centralize voice playback control - introduce `MessageRenderDependencies` and `LocalMessageRenderDependencies` to centralize emoji font and custom emoji path resolution across chat components - implement `VoicePlaybackController` and `ExoPlayerVoicePlaybackController` to manage voice message state globally within a conversation, replacing per-bubble player logic - refactor JNI function names in `native-lib.cpp` to match the relocation of `NativeVideoRenderer` and `VideoEditorUtils` to the core media package - enhance `AlphaVideoPlayer` with stricter surface validity checks and improved `ExoPlayer` surface cleanup to prevent crashes and leaks - optimize `MessageTextFormatter` and `MessageReactionsView` to consume render dependencies via `CompositionLocal` instead of manual repository injections - update `ChatContentEffects` with more precise `LaunchedEffect` keys to ensure scroll and visibility triggers react correctly to message list changes - integrate the new rendering and playback controllers into `ChatContent` using `CompositionLocalProvider` --- presentation/src/main/cpp/native-lib.cpp | 73 +++++++- .../core/media/AlphaVideoPlayer.kt | 32 +++- .../chats/conversation/ChatContent.kt | 25 ++- .../chats/conversation/ui/VoicePlayer.kt | 160 +++++++++++++----- .../ui/content/ChatContentEffects.kt | 20 ++- .../ui/message/MessageReactionsView.kt | 45 +---- .../ui/message/MessageRenderDependencies.kt | 143 ++++++++++++++++ .../ui/message/MessageTextFormatter.kt | 49 ++---- .../ui/message/VoiceMessageBubble.kt | 11 +- 9 files changed, 411 insertions(+), 147 deletions(-) create mode 100644 presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MessageRenderDependencies.kt diff --git a/presentation/src/main/cpp/native-lib.cpp b/presentation/src/main/cpp/native-lib.cpp index 97b29dbc..64fa8a70 100644 --- a/presentation/src/main/cpp/native-lib.cpp +++ b/presentation/src/main/cpp/native-lib.cpp @@ -517,7 +517,7 @@ bool processVideoNative(const char* inputPath, const char* outputPath, extern "C" { JNIEXPORT jlong JNICALL -Java_org_monogram_presentation_features_chats_currentChat_components_NativeVideoRenderer_create( +Java_org_monogram_presentation_core_media_NativeVideoRenderer_create( JNIEnv* env, jobject instance, jobject surface, jboolean useAlpha, jboolean removeBlackBg) { auto* renderer = new NativeVideoRenderer(env, instance, surface, useAlpha, removeBlackBg); renderer->start(); @@ -525,28 +525,28 @@ Java_org_monogram_presentation_features_chats_currentChat_components_NativeVideo } JNIEXPORT void JNICALL -Java_org_monogram_presentation_features_chats_currentChat_components_NativeVideoRenderer_destroy( +Java_org_monogram_presentation_core_media_NativeVideoRenderer_destroy( JNIEnv* env, jobject /* this */, jlong handle) { auto* renderer = reinterpret_cast(handle); delete renderer; } JNIEXPORT void JNICALL -Java_org_monogram_presentation_features_chats_currentChat_components_NativeVideoRenderer_updateSize( +Java_org_monogram_presentation_core_media_NativeVideoRenderer_updateSize( JNIEnv* env, jobject /* this */, jlong handle, jint width, jint height) { auto* renderer = reinterpret_cast(handle); renderer->updateSize(width, height); } JNIEXPORT void JNICALL -Java_org_monogram_presentation_features_chats_currentChat_components_NativeVideoRenderer_notifyFrameAvailable( +Java_org_monogram_presentation_core_media_NativeVideoRenderer_notifyFrameAvailable( JNIEnv* env, jobject /* this */, jlong handle) { auto* renderer = reinterpret_cast(handle); renderer->onFrameAvailable(); } JNIEXPORT void JNICALL -Java_org_monogram_presentation_features_chats_currentChat_components_NativeVideoRenderer_setFilter( +Java_org_monogram_presentation_core_media_NativeVideoRenderer_setFilter( JNIEnv* env, jobject /* this */, jlong handle, jfloatArray matrix) { auto* renderer = reinterpret_cast(handle); if (matrix == nullptr) { @@ -559,12 +559,58 @@ Java_org_monogram_presentation_features_chats_currentChat_components_NativeVideo } JNIEXPORT void JNICALL -Java_org_monogram_presentation_features_chats_currentChat_components_NativeVideoRenderer_setOverlayTexture( +Java_org_monogram_presentation_core_media_NativeVideoRenderer_setOverlayTexture( JNIEnv* env, jobject /* this */, jlong handle, jint textureId) { auto* renderer = reinterpret_cast(handle); renderer->setOverlayTexture(textureId); } +JNIEXPORT jlong JNICALL +Java_org_monogram_presentation_features_chats_currentChat_components_NativeVideoRenderer_create( + JNIEnv *env, jobject instance, jobject surface, jboolean useAlpha, jboolean removeBlackBg) { + return Java_org_monogram_presentation_core_media_NativeVideoRenderer_create( + env, instance, surface, useAlpha, removeBlackBg + ); +} + +JNIEXPORT void JNICALL +Java_org_monogram_presentation_features_chats_currentChat_components_NativeVideoRenderer_destroy( + JNIEnv *env, jobject instance, jlong handle) { + Java_org_monogram_presentation_core_media_NativeVideoRenderer_destroy(env, instance, handle); +} + +JNIEXPORT void JNICALL +Java_org_monogram_presentation_features_chats_currentChat_components_NativeVideoRenderer_updateSize( + JNIEnv *env, jobject instance, jlong handle, jint width, jint height) { + Java_org_monogram_presentation_core_media_NativeVideoRenderer_updateSize( + env, instance, handle, width, height + ); +} + +JNIEXPORT void JNICALL +Java_org_monogram_presentation_features_chats_currentChat_components_NativeVideoRenderer_notifyFrameAvailable( + JNIEnv *env, jobject instance, jlong handle) { + Java_org_monogram_presentation_core_media_NativeVideoRenderer_notifyFrameAvailable( + env, instance, handle + ); +} + +JNIEXPORT void JNICALL +Java_org_monogram_presentation_features_chats_currentChat_components_NativeVideoRenderer_setFilter( + JNIEnv *env, jobject instance, jlong handle, jfloatArray matrix) { + Java_org_monogram_presentation_core_media_NativeVideoRenderer_setFilter( + env, instance, handle, matrix + ); +} + +JNIEXPORT void JNICALL +Java_org_monogram_presentation_features_chats_currentChat_components_NativeVideoRenderer_setOverlayTexture( + JNIEnv *env, jobject instance, jlong handle, jint textureId) { + Java_org_monogram_presentation_core_media_NativeVideoRenderer_setOverlayTexture( + env, instance, handle, textureId + ); +} + JNIEXPORT jlong JNICALL Java_org_monogram_presentation_features_stickers_core_VpxWrapper_create(JNIEnv* env, jobject thiz) { return (jlong) new VpxDecoder(); @@ -603,7 +649,7 @@ Java_org_monogram_presentation_features_stickers_core_VpxWrapper_getHeight(JNIEn } JNIEXPORT jboolean JNICALL -Java_org_monogram_presentation_features_chats_currentChat_editor_video_VideoEditorUtils_processVideoNative( +Java_org_monogram_presentation_features_chats_conversation_editor_video_VideoEditorUtils_processVideoNative( JNIEnv* env, jclass clazz, jstring inputPath, jstring outputPath, jlong startMs, jlong endMs, @@ -629,4 +675,15 @@ Java_org_monogram_presentation_features_chats_currentChat_editor_video_VideoEdit return result; } -} \ No newline at end of file +JNIEXPORT jboolean JNICALL +Java_org_monogram_presentation_features_chats_currentChat_editor_video_VideoEditorUtils_processVideoNative( + JNIEnv *env, jclass clazz, + jstring inputPath, jstring outputPath, + jlong startMs, jlong endMs, + jint targetHeight, jint bitrate, jboolean muteAudio, jfloatArray filterMatrix) { + return Java_org_monogram_presentation_features_chats_conversation_editor_video_VideoEditorUtils_processVideoNative( + env, clazz, inputPath, outputPath, startMs, endMs, targetHeight, bitrate, muteAudio, filterMatrix + ); +} + +} diff --git a/presentation/src/main/java/org/monogram/presentation/core/media/AlphaVideoPlayer.kt b/presentation/src/main/java/org/monogram/presentation/core/media/AlphaVideoPlayer.kt index 117281c8..31ed7fc4 100644 --- a/presentation/src/main/java/org/monogram/presentation/core/media/AlphaVideoPlayer.kt +++ b/presentation/src/main/java/org/monogram/presentation/core/media/AlphaVideoPlayer.kt @@ -348,7 +348,7 @@ fun VideoStickerPlayer( } lifecycleOwner.lifecycle.removeObserver(lifecycleObserver) exoPlayer.removeListener(playerListener) - exoPlayer.setVideoSurface(null) + exoPlayer.clearVideoSurface() exoPlayer.stop() exoPlayer.clearMediaItems() videoPlayerPool.release(exoPlayer) @@ -367,8 +367,11 @@ fun VideoStickerPlayer( this.contentScale = contentScale this.configure(type.useAlphaChannel, type.removeBlackBackground) bindSurface { surface -> - if (!isDisposed.get()) { + if (isDisposed.get()) return@bindSurface + if (surface != null && surface.isValid) { exoPlayer.setVideoSurface(surface) + } else { + exoPlayer.clearVideoSurface() } } textureViewRef.value = this @@ -378,8 +381,11 @@ fun VideoStickerPlayer( view.contentScale = contentScale view.configure(type.useAlphaChannel, type.removeBlackBackground) view.bindSurface { surface -> - if (!isDisposed.get()) { + if (isDisposed.get()) return@bindSurface + if (surface != null && surface.isValid) { exoPlayer.setVideoSurface(surface) + } else { + exoPlayer.clearVideoSurface() } } textureViewRef.value = view @@ -687,10 +693,10 @@ class NativeVideoRenderer { class VideoGLTextureView(context: Context) : TextureView(context), TextureView.SurfaceTextureListener { var contentScale: ContentScale = ContentScale.Fit - var onSurfaceReady: ((Surface) -> Unit)? = null + var onSurfaceReady: ((Surface?) -> Unit)? = null set(value) { field = value - currentSurface?.let { surface -> value?.invoke(surface) } + dispatchCurrentSurfaceIfValid() } private var currentSurface: Surface? = null @@ -711,10 +717,20 @@ class VideoGLTextureView(context: Context) : TextureView(context), TextureView.S this.removeBlackBg = removeBlackBg } - fun bindSurface(onReady: (Surface) -> Unit) { + fun bindSurface(onReady: (Surface?) -> Unit) { onSurfaceReady = onReady } + private fun dispatchCurrentSurfaceIfValid() { + val surface = currentSurface ?: return + if (!surface.isValid) { + currentSurface = null + onSurfaceReady?.invoke(null) + return + } + onSurfaceReady?.invoke(surface) + } + fun setVideoSize(width: Int, height: Int) { this.videoWidth = width this.videoHeight = height @@ -765,7 +781,7 @@ class VideoGLTextureView(context: Context) : TextureView(context), TextureView.S nativeRenderer = NativeVideoRenderer() nativeRenderer?.onSurfaceReady = { surface -> currentSurface = surface - onSurfaceReady?.invoke(surface) + dispatchCurrentSurfaceIfValid() } nativeRenderer?.init(Surface(st), useAlpha, removeBlackBg) nativeRenderer?.setSize(width, height) @@ -776,9 +792,11 @@ class VideoGLTextureView(context: Context) : TextureView(context), TextureView.S } override fun onSurfaceTextureDestroyed(st: SurfaceTexture): Boolean { + onSurfaceReady?.invoke(null) currentSurface = null nativeRenderer?.release() nativeRenderer = null + onSurfaceReady = null return true } diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ChatContent.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ChatContent.kt index c700c1a1..086dd859 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ChatContent.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ChatContent.kt @@ -91,6 +91,10 @@ import org.monogram.domain.models.MessageModel import org.monogram.presentation.R import org.monogram.presentation.core.ui.ExpressiveDefaults import org.monogram.presentation.core.util.LocalTabletInterfaceEnabled +import org.monogram.presentation.features.chats.conversation.ui.AdvancedCircularRecorderScreen +import org.monogram.presentation.features.chats.conversation.ui.ChatInputBar +import org.monogram.presentation.features.chats.conversation.ui.LocalVoicePlaybackController +import org.monogram.presentation.features.chats.conversation.ui.MessageListShimmer import org.monogram.presentation.features.chats.conversation.ui.content.ChatContentBackground import org.monogram.presentation.features.chats.conversation.ui.content.ChatContentEffects import org.monogram.presentation.features.chats.conversation.ui.content.ChatContentList @@ -111,10 +115,10 @@ import org.monogram.presentation.features.chats.conversation.ui.content.remember import org.monogram.presentation.features.chats.conversation.ui.content.rememberChatTopBarUiState import org.monogram.presentation.features.chats.conversation.ui.content.scrollToMessageIndex import org.monogram.presentation.features.chats.conversation.ui.content.withUpdatedTextContent -import org.monogram.presentation.features.chats.conversation.ui.AdvancedCircularRecorderScreen -import org.monogram.presentation.features.chats.conversation.ui.ChatInputBar -import org.monogram.presentation.features.chats.conversation.ui.MessageListShimmer import org.monogram.presentation.features.chats.conversation.ui.message.LocalLinkHandler +import org.monogram.presentation.features.chats.conversation.ui.message.LocalMessageRenderDependencies +import org.monogram.presentation.features.chats.conversation.ui.message.rememberChatMessageRenderDependencies +import org.monogram.presentation.features.chats.conversation.ui.rememberVoicePlaybackController import java.io.File import java.io.FileOutputStream @@ -310,6 +314,15 @@ fun ChatContent( ) val permissionState = rememberChatContentPermissionState(state) + val messageRenderDependencies by rememberChatMessageRenderDependencies( + messages = remember(displayMessages, state.rootMessage) { + buildList { + addAll(displayMessages) + state.rootMessage?.let(::add) + } + } + ) + val voicePlaybackController = rememberVoicePlaybackController() val messageListState = rememberChatMessageListState( state = state, displayMessages = displayMessages, @@ -364,7 +377,11 @@ fun ChatContent( } val topBarUiState = rememberChatTopBarUiState(state) - CompositionLocalProvider(LocalLinkHandler provides { component.onLinkClick(it) }) { + CompositionLocalProvider( + LocalLinkHandler provides { component.onLinkClick(it) }, + LocalMessageRenderDependencies provides messageRenderDependencies, + LocalVoicePlaybackController provides voicePlaybackController + ) { val statusBarHeight = with(density) { WindowInsets.statusBars.getTop(this).toDp() } val headerOverlayHeight = statusBarHeight + 16.dp Box( diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/VoicePlayer.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/VoicePlayer.kt index 92bad2bd..fa3069de 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/VoicePlayer.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/VoicePlayer.kt @@ -1,7 +1,16 @@ package org.monogram.presentation.features.chats.conversation.ui import android.net.Uri -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.platform.LocalContext import androidx.media3.common.MediaItem import androidx.media3.common.Player @@ -9,82 +18,130 @@ import androidx.media3.exoplayer.ExoPlayer import kotlinx.coroutines.delay import kotlinx.coroutines.isActive +@Immutable +data class VoicePlaybackUiState( + val isPlaying: Boolean = false, + val progress: Float = 0f, + val currentPosition: Long = 0L, + val duration: Long = 0L +) + +interface VoicePlaybackController { + fun stateFor(messageId: Long, fallbackDurationSeconds: Int): VoicePlaybackUiState + fun togglePlayPause(messageId: Long, path: String?) + fun seekTo(messageId: Long, positionFraction: Float) +} + +private class EmptyVoicePlaybackController : VoicePlaybackController { + override fun stateFor(messageId: Long, fallbackDurationSeconds: Int): VoicePlaybackUiState { + return VoicePlaybackUiState(duration = fallbackDurationSeconds * 1000L) + } + + override fun togglePlayPause(messageId: Long, path: String?) = Unit + + override fun seekTo(messageId: Long, positionFraction: Float) = Unit +} + +val LocalVoicePlaybackController = staticCompositionLocalOf { + EmptyVoicePlaybackController() +} + @Composable -fun rememberVoicePlayer(path: String?): VoicePlayerState { +fun rememberVoicePlaybackController(): VoicePlaybackController { val context = LocalContext.current val player = remember { ExoPlayer.Builder(context).build().apply { repeatMode = Player.REPEAT_MODE_OFF } } + val controller = remember(player) { ExoPlayerVoicePlaybackController(player) } - val state = remember(path) { VoicePlayerState(player, path) } - - DisposableEffect(player) { - onDispose { - player.release() + LaunchedEffect(controller.activeMessageId, controller.isPlaying) { + while (isActive && controller.isPlaying) { + controller.updateProgress() + delay(50) } } - LaunchedEffect(path) { - if (path != null) { - player.setMediaItem(MediaItem.fromUri(Uri.parse(path))) - player.prepare() + androidx.compose.runtime.DisposableEffect(player) { + onDispose { + player.release() } } - return state + return controller } -class VoicePlayerState( - private val player: ExoPlayer, - private val path: String? -) { - var isPlaying by mutableStateOf(false) +private class ExoPlayerVoicePlaybackController( + private val player: ExoPlayer +) : VoicePlaybackController { + var activeMessageId by mutableStateOf(null) private set - var progress by mutableFloatStateOf(0f) - private set - var currentPosition by mutableLongStateOf(0L) - private set - var duration by mutableLongStateOf(0L) + private var activePath by mutableStateOf(null) + + var isPlaying by mutableStateOf(false) private set + private var progress by mutableFloatStateOf(0f) + private var currentPosition by mutableLongStateOf(0L) + private var duration by mutableLongStateOf(0L) init { player.addListener(object : Player.Listener { override fun onIsPlayingChanged(playing: Boolean) { isPlaying = playing + if (!playing) { + updateProgress() + } } override fun onPlaybackStateChanged(playbackState: Int) { - if (playbackState == Player.STATE_READY) { - duration = player.duration - } else if (playbackState == Player.STATE_ENDED) { - isPlaying = false - progress = 0f - currentPosition = 0 - player.seekTo(0) - player.pause() + when (playbackState) { + Player.STATE_READY -> { + duration = player.duration.coerceAtLeast(0L) + updateProgress() + } + + Player.STATE_ENDED -> { + isPlaying = false + progress = 0f + currentPosition = 0L + player.seekTo(0L) + player.pause() + } } } }) } - @Composable - fun ProgressUpdater() { - LaunchedEffect(isPlaying) { - while (isActive && isPlaying) { - currentPosition = player.currentPosition - val total = player.duration - if (total > 0) { - progress = currentPosition.toFloat() / total.toFloat() - } - delay(50) - } + override fun stateFor(messageId: Long, fallbackDurationSeconds: Int): VoicePlaybackUiState { + val isActiveMessage = activeMessageId == messageId + return if (isActiveMessage) { + VoicePlaybackUiState( + isPlaying = isPlaying, + progress = progress, + currentPosition = currentPosition, + duration = duration.takeIf { it > 0L } ?: fallbackDurationSeconds * 1000L + ) + } else { + VoicePlaybackUiState(duration = fallbackDurationSeconds * 1000L) } } - fun togglePlayPause() { + override fun togglePlayPause(messageId: Long, path: String?) { if (path == null) return + + if (activeMessageId != messageId || activePath != path) { + activeMessageId = messageId + activePath = path + progress = 0f + currentPosition = 0L + duration = 0L + player.setMediaItem(MediaItem.fromUri(Uri.parse(path))) + player.prepare() + player.play() + return + } + if (player.isPlaying) { player.pause() } else { @@ -92,10 +149,19 @@ class VoicePlayerState( } } - fun seekTo(pos: Float) { - val total = player.duration - if (total > 0) { - player.seekTo((pos * total).toLong()) - } + override fun seekTo(messageId: Long, positionFraction: Float) { + if (activeMessageId != messageId) return + val totalDuration = player.duration + if (totalDuration <= 0L) return + player.seekTo((positionFraction.coerceIn(0f, 1f) * totalDuration).toLong()) + updateProgress() + } + + fun updateProgress() { + if (activeMessageId == null) return + currentPosition = player.currentPosition.coerceAtLeast(0L) + val total = player.duration.coerceAtLeast(0L) + duration = total + progress = if (total > 0L) currentPosition.toFloat() / total.toFloat() else 0f } } diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentEffects.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentEffects.kt index a3fea6c6..6c3d7d8a 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentEffects.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentEffects.kt @@ -41,6 +41,8 @@ internal fun ChatContentEffects( onSearchSenderPickerChanged: (Boolean) -> Unit ) { val latestUiState = rememberUpdatedState(state) + val firstGroupedMessageId = groupedMessages.firstOrNull()?.firstMessageId + val lastGroupedMessageId = groupedMessages.lastOrNull()?.firstMessageId LaunchedEffect(Unit) { onVisible() @@ -83,7 +85,13 @@ internal fun ChatContentEffects( } } - LaunchedEffect(state.pendingScrollCommand, isComments) { + LaunchedEffect( + state.pendingScrollCommand, + isComments, + groupedMessages.size, + firstGroupedMessageId, + lastGroupedMessageId + ) { val command = state.pendingScrollCommand ?: return@LaunchedEffect val leadingItems = chatContentLeadingItemsCount( @@ -213,7 +221,9 @@ internal fun ChatContentEffects( LaunchedEffect( scrollState, - groupedMessages, + groupedMessages.size, + firstGroupedMessageId, + lastGroupedMessageId, isComments, state.isLatestLoaded, state.isLoadingOlder, @@ -242,7 +252,9 @@ internal fun ChatContentEffects( DisposableEffect( scrollState, - groupedMessages, + groupedMessages.size, + firstGroupedMessageId, + lastGroupedMessageId, isComments, state.currentTopicId, state.isLatestLoaded, @@ -267,7 +279,7 @@ internal fun ChatContentEffects( } } - LaunchedEffect(scrollState, groupedMessages) { + LaunchedEffect(scrollState, groupedMessages.size, firstGroupedMessageId, lastGroupedMessageId) { snapshotFlow { scrollState.layoutInfo.visibleItemsInfo } .map { visibleItems -> val currentState = latestUiState.value diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MessageReactionsView.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MessageReactionsView.kt index eb24c675..c20d824c 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MessageReactionsView.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MessageReactionsView.kt @@ -17,8 +17,6 @@ import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf @@ -27,17 +25,12 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import org.koin.compose.koinInject import org.monogram.domain.models.MessageReactionModel -import org.monogram.domain.repository.StickerRepository import org.monogram.presentation.core.ui.Avatar -import org.monogram.presentation.core.util.AppPreferences -import org.monogram.presentation.core.util.coRunCatching import org.monogram.presentation.features.stickers.ui.view.StickerImage @OptIn(ExperimentalLayoutApi::class) @@ -46,30 +39,9 @@ fun MessageReactionsView( reactions: List, onReactionClick: (String) -> Unit, modifier: Modifier = Modifier, - stickerRepository: StickerRepository = koinInject(), - appPreferences: AppPreferences = koinInject() + emojiFontFamily: FontFamily = LocalMessageRenderDependencies.current.emojiFontFamily, + customEmojiPathsById: Map = LocalMessageRenderDependencies.current.customEmojiPaths ) { - val context = LocalContext.current - val emojiStyle by appPreferences.emojiStyle.collectAsState() - val emojiFontFamily = remember(context, emojiStyle) { getEmojiFontFamily(context, emojiStyle) } - val customEmojiStickerSets by stickerRepository.customEmojiStickerSets.collectAsState() - - LaunchedEffect(Unit) { - coRunCatching { stickerRepository.loadCustomEmojiStickerSets() } - } - - val customEmojiFileIdsById = remember(customEmojiStickerSets) { - buildMap { - customEmojiStickerSets.forEach { set -> - set.stickers.forEach { sticker -> - val customEmojiId = sticker.customEmojiId - if (customEmojiId != null) { - put(customEmojiId, sticker.id) - } - } - } - } - } if (reactions.isEmpty()) return FlowRow( @@ -84,8 +56,7 @@ fun MessageReactionsView( reaction = reaction, onReactionClick = onReactionClick, emojiFontFamily = emojiFontFamily, - stickerRepository = stickerRepository, - customEmojiFileIdsById = customEmojiFileIdsById + customEmojiPathsById = customEmojiPathsById ) } } @@ -98,8 +69,7 @@ private fun MessageReactionItem( reaction: MessageReactionModel, onReactionClick: (String) -> Unit, emojiFontFamily: FontFamily, - stickerRepository: StickerRepository, - customEmojiFileIdsById: Map + customEmojiPathsById: Map ) { val customEmojiId = reaction.customEmojiId val emoji = reaction.emoji @@ -120,11 +90,8 @@ private fun MessageReactionItem( MaterialTheme.colorScheme.onSurfaceVariant } - val customEmojiFileId = customEmojiId?.let(customEmojiFileIdsById::get) - val customEmojiPath by if (customEmojiFileId != null && reaction.customEmojiPath == null) { - stickerRepository.getStickerFile(customEmojiFileId).collectAsState(initial = null) - } else { - remember { mutableStateOf(reaction.customEmojiPath) } + val customEmojiPath = remember(customEmojiId, reaction.customEmojiPath, customEmojiPathsById) { + reaction.customEmojiPath ?: customEmojiId?.let(customEmojiPathsById::get) } var showDropdown by remember { mutableStateOf(false) } diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MessageRenderDependencies.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MessageRenderDependencies.kt new file mode 100644 index 00000000..8caea365 --- /dev/null +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MessageRenderDependencies.kt @@ -0,0 +1,143 @@ +package org.monogram.presentation.features.chats.conversation.ui.message + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontFamily +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import org.koin.compose.koinInject +import org.monogram.domain.models.MessageContent +import org.monogram.domain.models.MessageEntity +import org.monogram.domain.models.MessageEntityType +import org.monogram.domain.models.MessageModel +import org.monogram.domain.repository.StickerRepository +import org.monogram.presentation.core.util.AppPreferences +import org.monogram.presentation.core.util.coRunCatching + +@Immutable +data class MessageRenderDependencies( + val emojiFontFamily: FontFamily = FontFamily.Default, + val customEmojiPaths: Map = emptyMap() +) + +internal val LocalMessageRenderDependencies = staticCompositionLocalOf { + MessageRenderDependencies() +} + +@Composable +internal fun rememberChatMessageRenderDependencies( + messages: List, + appPreferences: AppPreferences = koinInject(), + stickerRepository: StickerRepository = koinInject() +): State { + val context = LocalContext.current + val emojiStyle by appPreferences.emojiStyle.collectAsState() + val emojiFontFamily = remember(context, emojiStyle) { getEmojiFontFamily(context, emojiStyle) } + val customEmojiStickerSets by stickerRepository.customEmojiStickerSets.collectAsState() + + LaunchedEffect(stickerRepository) { + coRunCatching { stickerRepository.loadCustomEmojiStickerSets() } + } + + val customEmojiFileIdsById = remember(customEmojiStickerSets) { + buildMap { + customEmojiStickerSets.forEach { set -> + set.stickers.forEach { sticker -> + val customEmojiId = sticker.customEmojiId + if (customEmojiId != null) { + put(customEmojiId, sticker.id) + } + } + } + } + } + val requests = remember(messages) { collectCustomEmojiRequests(messages) } + return produceState( + initialValue = MessageRenderDependencies( + emojiFontFamily = emojiFontFamily, + customEmojiPaths = requests.explicitPaths + ), + emojiFontFamily, + requests, + customEmojiFileIdsById, + stickerRepository + ) { + value = MessageRenderDependencies( + emojiFontFamily = emojiFontFamily, + customEmojiPaths = requests.explicitPaths + ) + + coroutineScope { + requests.ids.forEach { customEmojiId -> + launch { + val resolvedFlow = + customEmojiFileIdsById[customEmojiId]?.let(stickerRepository::getStickerFile) + ?: stickerRepository.getCustomEmojiFile(customEmojiId) + resolvedFlow.collectLatest { resolvedPath -> + val nextPath = resolvedPath ?: requests.explicitPaths[customEmojiId] + if (value.customEmojiPaths[customEmojiId] != nextPath) { + value = value.copy( + customEmojiPaths = value.customEmojiPaths + (customEmojiId to nextPath) + ) + } + } + } + } + } + } +} + +@Immutable +private data class CustomEmojiRequests( + val ids: Set, + val explicitPaths: Map +) + +private fun collectCustomEmojiRequests(messages: List): CustomEmojiRequests { + val ids = LinkedHashSet() + val explicitPaths = LinkedHashMap() + + fun appendEntities(entities: List) { + entities.forEach { entity -> + val type = entity.type as? MessageEntityType.CustomEmoji ?: return@forEach + ids += type.emojiId + if (type.path != null) { + explicitPaths[type.emojiId] = type.path + } + } + } + + messages.forEach { message -> + when (val content = message.content) { + is MessageContent.Text -> appendEntities(content.entities) + is MessageContent.Photo -> appendEntities(content.entities) + is MessageContent.Video -> appendEntities(content.entities) + is MessageContent.Document -> appendEntities(content.entities) + is MessageContent.Audio -> appendEntities(content.entities) + is MessageContent.Gif -> appendEntities(content.entities) + else -> Unit + } + + message.reactions.forEach { reaction -> + val customEmojiId = reaction.customEmojiId ?: return@forEach + ids += customEmojiId + if (reaction.customEmojiPath != null) { + explicitPaths[customEmojiId] = reaction.customEmojiPath + } + } + } + + return CustomEmojiRequests( + ids = ids, + explicitPaths = explicitPaths + ) +} diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MessageTextFormatter.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MessageTextFormatter.kt index 95c9d448..5e15ba47 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MessageTextFormatter.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/MessageTextFormatter.kt @@ -8,14 +8,10 @@ import androidx.compose.foundation.text.appendInlineContent import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.key import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.Placeholder import androidx.compose.ui.text.PlaceholderVerticalAlign @@ -27,11 +23,8 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import org.koin.compose.koinInject import org.monogram.domain.models.MessageEntity import org.monogram.domain.models.MessageEntityType -import org.monogram.domain.repository.StickerRepository -import org.monogram.presentation.core.util.AppPreferences import org.monogram.presentation.features.stickers.ui.view.StickerImage @Immutable @@ -51,24 +44,16 @@ sealed interface BigEmojiItem { data class Custom(val path: String?) : BigEmojiItem } -@Composable private fun rememberResolvedCustomEmojiPaths( entities: List, - stickerRepository: StickerRepository = koinInject() + customEmojiPaths: Map ): List { val emojiEntities = entities.filter { it.type is MessageEntityType.CustomEmoji }.sortedBy { it.offset } return emojiEntities.map { entity -> val type = entity.type as MessageEntityType.CustomEmoji - key(entity.offset, entity.length, type.emojiId, type.path) { - val resolvedPath by if (type.path == null) { - stickerRepository.getCustomEmojiFile(type.emojiId).collectAsState(initial = null) - } else { - remember(type.path) { androidx.compose.runtime.mutableStateOf(type.path) } - } - resolvedPath - } + type.path ?: customEmojiPaths[type.emojiId] } } @@ -77,11 +62,13 @@ fun rememberMessageInlineContent( entities: List, fontSize: Float, isBigEmoji: Boolean = false, - stickerRepository: StickerRepository = koinInject() + customEmojiPaths: Map = LocalMessageRenderDependencies.current.customEmojiPaths ): Map { val emojiEntities = entities.filter { it.type is MessageEntityType.CustomEmoji }.sortedBy { it.offset } - val resolvedEmojiPaths = rememberResolvedCustomEmojiPaths(entities, stickerRepository) + val resolvedEmojiPaths = remember(entities, customEmojiPaths) { + rememberResolvedCustomEmojiPaths(entities, customEmojiPaths) + } return remember(emojiEntities, resolvedEmojiPaths, fontSize, isBigEmoji) { val map = mutableMapOf() @@ -109,25 +96,27 @@ fun rememberMessageTextRenderData( allowBigEmoji: Boolean = true, isOutgoing: Boolean = false, revealedSpoilers: List = emptyList(), - appPreferences: AppPreferences = koinInject(), - stickerRepository: StickerRepository = koinInject() + emojiFontFamily: FontFamily = LocalMessageRenderDependencies.current.emojiFontFamily, + customEmojiPaths: Map = LocalMessageRenderDependencies.current.customEmojiPaths ): MessageTextRenderData { val bigEmoji = remember(text, entities, allowBigEmoji) { allowBigEmoji && isBigEmoji(text, entities) } - val resolvedEmojiPaths = rememberResolvedCustomEmojiPaths(entities, stickerRepository) + val resolvedEmojiPaths = remember(entities, customEmojiPaths) { + rememberResolvedCustomEmojiPaths(entities, customEmojiPaths) + } val annotatedText = buildAnnotatedMessageTextWithEmoji( text = text, entities = entities, isOutgoing = isOutgoing, revealedSpoilers = revealedSpoilers, - appPreferences = appPreferences + emojiFontFamily = emojiFontFamily ) val inlineContent = rememberMessageInlineContent( entities = entities, fontSize = fontSize, isBigEmoji = bigEmoji, - stickerRepository = stickerRepository + customEmojiPaths = customEmojiPaths ) val bigEmojiItems = remember(text, entities, resolvedEmojiPaths, bigEmoji) { if (!bigEmoji) emptyList() else buildBigEmojiItems(text, entities, resolvedEmojiPaths) @@ -199,12 +188,8 @@ fun BigEmojiContent( items: List, sizeDp: Float, modifier: Modifier = Modifier, - appPreferences: AppPreferences = koinInject() + emojiFontFamily: FontFamily = LocalMessageRenderDependencies.current.emojiFontFamily ) { - val context = LocalContext.current - val emojiStyle by appPreferences.emojiStyle.collectAsState() - val emojiFontFamily = remember(context, emojiStyle) { getEmojiFontFamily(context, emojiStyle) } - Row( modifier = modifier, horizontalArrangement = Arrangement.spacedBy(4.dp), @@ -234,12 +219,8 @@ fun buildAnnotatedMessageTextWithEmoji( entities: List, isOutgoing: Boolean = false, revealedSpoilers: List = emptyList(), - appPreferences: AppPreferences = koinInject() + emojiFontFamily: FontFamily = LocalMessageRenderDependencies.current.emojiFontFamily ): AnnotatedString { - val context = LocalContext.current - val emojiStyle by appPreferences.emojiStyle.collectAsState() - val emojiFontFamily = remember(context, emojiStyle) { getEmojiFontFamily(context, emojiStyle) } - val linkColor = MaterialTheme.colorScheme.primary val codeBackgroundColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) val codeTextColor = MaterialTheme.colorScheme.primary diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/VoiceMessageBubble.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/VoiceMessageBubble.kt index b99eaf92..b81a4209 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/VoiceMessageBubble.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/VoiceMessageBubble.kt @@ -50,7 +50,7 @@ import org.monogram.domain.models.MessageContent import org.monogram.domain.models.MessageModel import org.monogram.presentation.R import org.monogram.presentation.core.util.IDownloadUtils -import org.monogram.presentation.features.chats.conversation.ui.rememberVoicePlayer +import org.monogram.presentation.features.chats.conversation.ui.LocalVoicePlaybackController @Composable fun VoiceMessageBubble( @@ -194,8 +194,11 @@ fun VoiceRow( onCancelDownload: (Int) -> Unit, isOutgoing: Boolean ) { - val playerState = rememberVoicePlayer(content.path) - playerState.ProgressUpdater() + val voicePlaybackController = LocalVoicePlaybackController.current + val playerState = voicePlaybackController.stateFor( + messageId = msg.id, + fallbackDurationSeconds = content.duration + ) Row( verticalAlignment = Alignment.CenterVertically, @@ -214,7 +217,7 @@ fun VoiceRow( } else if (content.path == null) { onVoiceClick(msg) } else { - playerState.togglePlayPause() + voicePlaybackController.togglePlayPause(msg.id, content.path) } }, contentAlignment = Alignment.Center From 71a6c52ec249f09428227d32ed24f10392a3c179 Mon Sep 17 00:00:00 2001 From: Artur Skubei <41114720+gdlbo@users.noreply.github.com> Date: Fri, 24 Apr 2026 21:50:36 +0300 Subject: [PATCH 5/8] refactor message and input UI components for improved state management and performance - introduce `MessageAppearanceConfig`, `MessageRowBehaviorConfig`, and `MessageRowUiFlags` to centralize message rendering and interaction logic - refactor `ChatInputBar` to use structured state objects (`ChatInputBarCapabilities`, `ComposerRowState`, `ComposerBotState`) for cleaner prop drilling - implement `derivedStateOf` and `rememberUpdatedState` in `InputTextField` to optimize composition and avoid unnecessary rebuilds during text transformation - decompose `VideoMessageBubble` into specialized sub-components (e.g., `VideoMuteToggle`, `VideoPlaybackBadge`, `VideoInteractionOverlay`) for better maintainability - extract message bubble layout tracking into `MessageBubbleLayoutTracker` to standardize position reporting for context menus and replies - modularize `ChatInputBarComposerSection` into `ComposerMainRow`, `ComposerInputSlot`, and `ComposerActionsSlot` - consolidate sender grouping logic into a unified `buildSenderGrouping` utility - update `MessageBubbleContainer` and `AlbumMessageBubbleContainer` to utilize the new configuration contracts for UI flags and behavior - simplify `InputBarSendButton` by passing a single `InputBarSendButtonState` object - refine `InputTextFieldContainer` logic to use `InputTextFieldUiState` for reactive visibility and action states --- .../ui/AlbumMessageBubbleContainer.kt | 183 ++--- .../chats/conversation/ui/ChatInputBar.kt | 100 ++- .../conversation/ui/MessageBubbleContainer.kt | 736 +++++++++--------- .../conversation/ui/MessageRowContracts.kt | 53 ++ .../chats/conversation/ui/SenderGrouping.kt | 30 + .../channel/ChannelMessageBubbleContainer.kt | 270 +++---- .../ui/content/ChatContentList.kt | 352 +++++---- .../inputbar/ChatInputBarComposerSection.kt | 497 ++++++++---- .../ui/inputbar/ChatInputBarContract.kt | 89 +++ .../ui/inputbar/InputBarSendButton.kt | 58 +- .../ui/inputbar/InputTextField.kt | 196 +++-- .../ui/inputbar/InputTextFieldContainer.kt | 63 +- .../ui/message/VideoMessageBubble.kt | 369 +++++---- .../ui/pins/PinnedMessagesListSheet.kt | 191 +++-- .../components/ChatSettingsPreview.kt | 57 +- 15 files changed, 1951 insertions(+), 1293 deletions(-) create mode 100644 presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/MessageRowContracts.kt diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/AlbumMessageBubbleContainer.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/AlbumMessageBubbleContainer.kt index a7c5e792..b418dada 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/AlbumMessageBubbleContainer.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/AlbumMessageBubbleContainer.kt @@ -18,10 +18,9 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset @@ -39,24 +38,18 @@ import org.monogram.domain.models.InlineKeyboardButtonModel import org.monogram.domain.models.MessageModel import org.monogram.presentation.core.ui.Avatar import org.monogram.presentation.core.util.IDownloadUtils -import org.monogram.presentation.features.chats.conversation.ui.content.shouldShowDate import org.monogram.presentation.features.chats.conversation.ui.channel.ChannelAlbumMessageBubble import org.monogram.presentation.features.chats.conversation.ui.message.ChatAlbumMessageBubble import org.monogram.presentation.features.chats.conversation.ui.message.MessageViaBotAttribution import org.monogram.presentation.features.chats.conversation.ui.message.ReplyMarkupView @Composable -fun AlbumMessageBubbleContainer( +internal fun AlbumMessageBubbleContainer( messages: List, - olderMsg: MessageModel? = null, - newerMsg: MessageModel? = null, - isGroup: Boolean, - isChannel: Boolean = false, - autoplayGifs: Boolean = true, - autoplayVideos: Boolean = true, - autoDownloadMobile: Boolean = false, - autoDownloadWifi: Boolean = false, - autoDownloadRoaming: Boolean = false, + appearance: MessageAppearanceConfig, + behavior: MessageRowBehaviorConfig, + uiFlags: MessageRowUiFlags = MessageRowUiFlags(), + senderGrouping: MessageSenderGrouping, onPhotoClick: (MessageModel) -> Unit, onDownloadPhoto: (Int) -> Unit = {}, onVideoClick: (MessageModel) -> Unit = {}, @@ -67,20 +60,14 @@ fun AlbumMessageBubbleContainer( onGoToReply: (MessageModel) -> Unit = {}, onReactionClick: (Long, String) -> Unit = { _, _ -> }, onReplyMarkupButtonClick: (Long, InlineKeyboardButtonModel) -> Unit = { _, _ -> }, - fontSize: Float = 16f, - bubbleRadius: Float = 16f, - shouldReportPosition: Boolean = false, onPositionChange: (Long, Offset, IntSize) -> Unit = { _, _, _ -> }, onCommentsClick: (Long) -> Unit = {}, showComments: Boolean = true, toProfile: (Long) -> Unit, onForwardOriginClick: (ForwardInfo) -> Unit = {}, onViaBotClick: (String) -> Unit = {}, - canReply: Boolean = false, onReplySwipe: (MessageModel) -> Unit = {}, - swipeEnabled: Boolean = true, - downloadUtils: IDownloadUtils, - isAnyViewerOpen: Boolean = false + downloadUtils: IDownloadUtils ) { if (messages.isEmpty()) return @@ -98,9 +85,9 @@ fun AlbumMessageBubbleContainer( val screenWidth = configuration.screenWidthDp.dp val isLandscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE - val maxWidth = remember(isChannel, isLandscape, screenWidth) { + val maxWidth = remember(behavior.isChannel, isLandscape, screenWidth) { when { - isChannel -> if (isLandscape) (screenWidth * 0.7f).coerceAtMost(600.dp) else (screenWidth * 0.94f).coerceAtMost( + behavior.isChannel -> if (isLandscape) (screenWidth * 0.7f).coerceAtMost(600.dp) else (screenWidth * 0.94f).coerceAtMost( 500.dp ) @@ -109,76 +96,50 @@ fun AlbumMessageBubbleContainer( } } - val isSameSenderAbove = remember( - olderMsg?.id, - olderMsg?.senderId, - olderMsg?.senderName, - olderMsg?.senderCustomTitle, - olderMsg?.date, - firstMsg.senderId, - firstMsg.senderName, - firstMsg.senderCustomTitle, - firstMsg.date - ) { - shouldGroupSenderBlock( - current = firstMsg, - neighbor = olderMsg, - dateBreak = olderMsg?.let { shouldShowDate(firstMsg, it) } ?: true - ) - } - val isSameSenderBelow = remember( - newerMsg?.id, - newerMsg?.senderId, - newerMsg?.senderName, - newerMsg?.senderCustomTitle, - newerMsg?.date, - lastMsg.senderId, - lastMsg.senderName, - lastMsg.senderCustomTitle, - lastMsg.date - ) { - shouldGroupSenderBlock( - current = lastMsg, - neighbor = newerMsg, - dateBreak = newerMsg?.let { shouldShowDate(it, lastMsg) } ?: true - ) - } - - val topSpacing = if (isChannel && !isSameSenderAbove) 12.dp else 2.dp - - var outerColumnPosition by remember { mutableStateOf(Offset.Zero) } - var bubblePosition by remember { mutableStateOf(Offset.Zero) } - var bubbleSize by remember { mutableStateOf(IntSize.Zero) } - + val topSpacing = if (behavior.isChannel && !senderGrouping.isSameSenderAbove) 12.dp else 2.dp val dragOffsetX = remember { Animatable(0f) } + val coroutineScope = rememberCoroutineScope() + val layoutTracker = remember { MessageBubbleLayoutTracker() } + val onReplyClickState by rememberUpdatedState(onReplyClick) + val onPositionChangeState by rememberUpdatedState(onPositionChange) Column( modifier = Modifier .fillMaxWidth() - .onGloballyPositioned { outerColumnPosition = it.positionInWindow() } + .onGloballyPositioned { layoutTracker.outerColumnPosition = it.positionInWindow() } .padding(top = topSpacing, bottom = 2.dp) .offset { IntOffset(dragOffsetX.value.toInt(), 0) } .fastReplyPointer( - canReply = canReply, + canReply = behavior.canReply && behavior.swipeEnabled, dragOffsetX = dragOffsetX, - scope = rememberCoroutineScope(), + scope = coroutineScope, onReplySwipe = { onReplySwipe(lastMsg) }, maxWidth = maxWidth.value ) .pointerInput(Unit) { detectTapGestures( onTap = { offset -> - val clickPos = outerColumnPosition + offset - val bubbleRect = Rect(bubblePosition, bubbleSize.toSize()) + val clickPos = layoutTracker.outerColumnPosition + offset + val bubbleRect = + Rect(layoutTracker.bubblePosition, layoutTracker.bubbleSize.toSize()) if (!bubbleRect.contains(clickPos)) { - onReplyClick(bubblePosition, bubbleSize, clickPos) + onReplyClickState( + layoutTracker.bubblePosition, + layoutTracker.bubbleSize, + clickPos + ) } }, onLongPress = { offset -> - val clickPos = outerColumnPosition + offset - val bubbleRect = Rect(bubblePosition, bubbleSize.toSize()) + val clickPos = layoutTracker.outerColumnPosition + offset + val bubbleRect = + Rect(layoutTracker.bubblePosition, layoutTracker.bubbleSize.toSize()) if (!bubbleRect.contains(clickPos)) { - onReplyClick(bubblePosition, bubbleSize, clickPos) + onReplyClickState( + layoutTracker.bubblePosition, + layoutTracker.bubbleSize, + clickPos + ) } } ) @@ -186,11 +147,11 @@ fun AlbumMessageBubbleContainer( ) { Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = if (isChannel) Arrangement.Center else if (isOutgoing) Arrangement.End else Arrangement.Start, + horizontalArrangement = if (behavior.isChannel) Arrangement.Center else if (isOutgoing) Arrangement.End else Arrangement.Start, verticalAlignment = Alignment.Bottom ) { - if (isGroup && !isOutgoing && !isChannel) { - if (!isSameSenderBelow) { + if (behavior.isGroup && !isOutgoing && !behavior.isChannel) { + if (!senderGrouping.isSameSenderBelow) { Avatar( path = firstMsg.senderAvatar, fallbackPath = firstMsg.senderPersonalAvatar, @@ -209,18 +170,22 @@ fun AlbumMessageBubbleContainer( ) { Column( modifier = Modifier - .then(if (isChannel) Modifier.padding(horizontal = 8.dp) else Modifier) + .then(if (behavior.isChannel) Modifier.padding(horizontal = 8.dp) else Modifier) .widthIn(max = maxWidth) - .then(if (isChannel) Modifier.fillMaxWidth() else Modifier) + .then(if (behavior.isChannel) Modifier.fillMaxWidth() else Modifier) .onGloballyPositioned { coordinates -> - bubblePosition = coordinates.positionInWindow() - bubbleSize = coordinates.size - if (shouldReportPosition) { - onPositionChange(lastMsg.id, bubblePosition, bubbleSize) + layoutTracker.bubblePosition = coordinates.positionInWindow() + layoutTracker.bubbleSize = coordinates.size + if (uiFlags.shouldReportPosition) { + onPositionChangeState( + lastMsg.id, + layoutTracker.bubblePosition, + layoutTracker.bubbleSize + ) } } ) { - if (isGroup && !isOutgoing && !isChannel && !isSameSenderAbove) { + if (behavior.isGroup && !isOutgoing && !behavior.isChannel && !senderGrouping.isSameSenderAbove) { Text( text = firstMsg.senderName, style = MaterialTheme.typography.labelSmall, @@ -229,16 +194,16 @@ fun AlbumMessageBubbleContainer( ) } - if (isChannel) { + if (behavior.isChannel) { ChannelAlbumMessageBubble( messages = orderedMessages, - isSameSenderAbove = isSameSenderAbove, - isSameSenderBelow = isSameSenderBelow, - autoplayGifs = autoplayGifs, - autoplayVideos = autoplayVideos, - autoDownloadMobile = autoDownloadMobile, - autoDownloadWifi = autoDownloadWifi, - autoDownloadRoaming = autoDownloadRoaming, + isSameSenderAbove = senderGrouping.isSameSenderAbove, + isSameSenderBelow = senderGrouping.isSameSenderBelow, + autoplayGifs = appearance.autoplayGifs, + autoplayVideos = appearance.autoplayVideos, + autoDownloadMobile = appearance.autoDownloadMobile, + autoDownloadWifi = appearance.autoDownloadWifi, + autoDownloadRoaming = appearance.autoDownloadRoaming, onPhotoClick = onPhotoClick, onDownloadPhoto = onDownloadPhoto, onVideoClick = onVideoClick, @@ -247,9 +212,9 @@ fun AlbumMessageBubbleContainer( onCancelDownload = onCancelDownload, onLongClick = { offset -> onReplyClick( - bubblePosition, - bubbleSize, - bubblePosition + offset + layoutTracker.bubblePosition, + layoutTracker.bubbleSize, + layoutTracker.bubblePosition + offset ) }, onReplyClick = onGoToReply, @@ -259,23 +224,23 @@ fun AlbumMessageBubbleContainer( toProfile = toProfile, onForwardOriginClick = onForwardOriginClick, modifier = Modifier.fillMaxWidth(), - fontSize = fontSize, - bubbleRadius = bubbleRadius, + fontSize = appearance.fontSize, + bubbleRadius = appearance.bubbleRadius, downloadUtils = downloadUtils, - isAnyViewerOpen = isAnyViewerOpen + isAnyViewerOpen = behavior.isAnyViewerOpen ) } else { ChatAlbumMessageBubble( messages = orderedMessages, isOutgoing = isOutgoing, - isGroup = isGroup, - isSameSenderAbove = isSameSenderAbove, - isSameSenderBelow = isSameSenderBelow, - autoplayGifs = autoplayGifs, - autoplayVideos = autoplayVideos, - autoDownloadMobile = autoDownloadMobile, - autoDownloadWifi = autoDownloadWifi, - autoDownloadRoaming = autoDownloadRoaming, + isGroup = behavior.isGroup, + isSameSenderAbove = senderGrouping.isSameSenderAbove, + isSameSenderBelow = senderGrouping.isSameSenderBelow, + autoplayGifs = appearance.autoplayGifs, + autoplayVideos = appearance.autoplayVideos, + autoDownloadMobile = appearance.autoDownloadMobile, + autoDownloadWifi = appearance.autoDownloadWifi, + autoDownloadRoaming = appearance.autoDownloadRoaming, onPhotoClick = onPhotoClick, onDownloadPhoto = onDownloadPhoto, onVideoClick = onVideoClick, @@ -284,9 +249,9 @@ fun AlbumMessageBubbleContainer( onCancelDownload = onCancelDownload, onLongClick = { offset -> onReplyClick( - bubblePosition, - bubbleSize, - bubblePosition + offset + layoutTracker.bubblePosition, + layoutTracker.bubbleSize, + layoutTracker.bubblePosition + offset ) }, onReplyClick = onGoToReply, @@ -294,9 +259,9 @@ fun AlbumMessageBubbleContainer( toProfile = toProfile, onForwardOriginClick = onForwardOriginClick, modifier = Modifier, - fontSize = fontSize, + fontSize = appearance.fontSize, downloadUtils = downloadUtils, - isAnyViewerOpen = isAnyViewerOpen + isAnyViewerOpen = behavior.isAnyViewerOpen ) } diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/ChatInputBar.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/ChatInputBar.kt index 4fdc774e..06365677 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/ChatInputBar.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/ChatInputBar.kt @@ -51,11 +51,15 @@ import org.monogram.domain.models.StickerModel import org.monogram.domain.repository.StickerRepository import org.monogram.presentation.core.util.AppPreferences import org.monogram.presentation.features.camera.CameraScreen -import org.monogram.presentation.features.chats.conversation.ui.message.getEmojiFontFamily import org.monogram.presentation.features.chats.conversation.ui.inputbar.ChatInputBarActions +import org.monogram.presentation.features.chats.conversation.ui.inputbar.ChatInputBarCapabilities import org.monogram.presentation.features.chats.conversation.ui.inputbar.ChatInputBarComposerSection import org.monogram.presentation.features.chats.conversation.ui.inputbar.ChatInputBarState import org.monogram.presentation.features.chats.conversation.ui.inputbar.ClosedTopicBar +import org.monogram.presentation.features.chats.conversation.ui.inputbar.ComposerAttachmentState +import org.monogram.presentation.features.chats.conversation.ui.inputbar.ComposerBotState +import org.monogram.presentation.features.chats.conversation.ui.inputbar.ComposerRowState +import org.monogram.presentation.features.chats.conversation.ui.inputbar.ComposerSuggestionState import org.monogram.presentation.features.chats.conversation.ui.inputbar.FullScreenEditorSheet import org.monogram.presentation.features.chats.conversation.ui.inputbar.InputBarMode import org.monogram.presentation.features.chats.conversation.ui.inputbar.RestrictedInputBar @@ -74,6 +78,7 @@ import org.monogram.presentation.features.chats.conversation.ui.inputbar.hasAllP import org.monogram.presentation.features.chats.conversation.ui.inputbar.isInlineBotPrefillText import org.monogram.presentation.features.chats.conversation.ui.inputbar.parseInlineQueryInput import org.monogram.presentation.features.chats.conversation.ui.inputbar.rememberVoiceRecorder +import org.monogram.presentation.features.chats.conversation.ui.message.getEmojiFontFamily import org.monogram.presentation.features.gallery.GalleryScreen import org.monogram.presentation.features.gallery.components.PollComposerSheet import java.util.Calendar @@ -150,6 +155,31 @@ fun ChatInputBar( canWriteText || canOpenAttachSheet || canSendStickers || canSendVoice || canSendVideoNotes || canSendPolls } } + val capabilities = remember( + canWriteText, + canSendPhotos, + canSendVideos, + canSendDocuments, + canSendAudios, + canOpenAttachSheet, + canSendStickers, + canSendVoice, + canSendVideoNotes, + canSendAnything + ) { + ChatInputBarCapabilities( + canWriteText = canWriteText, + canSendPhotos = canSendPhotos, + canSendVideos = canSendVideos, + canSendDocuments = canSendDocuments, + canSendAudios = canSendAudios, + canOpenAttachSheet = canOpenAttachSheet, + canSendStickers = canSendStickers, + canSendVoice = canSendVoice, + canSendVideoNotes = canSendVideoNotes, + canSendAnything = canSendAnything + ) + } val context = LocalContext.current val emojiStyle by appPreferences.emojiStyle.collectAsState() @@ -648,45 +678,49 @@ fun ChatInputBar( InputBarMode.Composer -> ChatInputBarComposerSection( editingMessage = state.editingMessage, replyMessage = state.replyMessage, - pendingMediaPaths = state.pendingMediaPaths, - pendingDocumentPaths = state.pendingDocumentPaths, - mentionSuggestions = state.mentionSuggestions, - filteredCommands = filteredCommands, - currentInlineBotUsername = state.currentInlineBotUsername.takeIf { canSendStickers }, - isInlineBotLoading = canSendStickers && state.isInlineBotLoading, - inlineBotResults = state.inlineBotResults.takeIf { canSendStickers }, - isBot = state.isBot, - botMenuButton = state.botMenuButton, - botCommands = state.botCommands, - scheduledMessagesCount = state.scheduledMessages.size, - textValue = textValue, + attachments = ComposerAttachmentState( + pendingMediaPaths = state.pendingMediaPaths, + pendingDocumentPaths = state.pendingDocumentPaths, + scheduledMessagesCount = state.scheduledMessages.size + ), + suggestions = ComposerSuggestionState( + mentionSuggestions = state.mentionSuggestions, + filteredCommands = filteredCommands, + currentInlineBotUsername = state.currentInlineBotUsername.takeIf { canSendStickers }, + isInlineBotLoading = canSendStickers && state.isInlineBotLoading, + inlineBotResults = state.inlineBotResults.takeIf { canSendStickers }, + replyMarkup = state.replyMarkup, + isGifSearchFocused = isGifSearchFocused + ), + botState = ComposerBotState( + isBot = state.isBot, + botMenuButton = state.botMenuButton, + botCommands = state.botCommands + ), + rowState = ComposerRowState( + textValue = textValue, + editingMessage = state.editingMessage, + isStickerMenuVisible = isStickerMenuVisible, + closeStickerMenuWithoutSlide = closeStickerMenuWithoutSlide, + isKeyboardVisible = isKeyboardVisible, + stickerMenuHeight = stickerMenuHeight, + showFullScreenEditor = showFullScreenEditor, + currentMessageLength = currentMessageLength, + maxMessageLength = maxMessageLength, + isOverMessageLimit = isOverMessageLimit, + showSendOptionsSheet = showSendOptionsSheet, + isVideoMessageMode = isVideoMessageMode, + isSlowModeActive = isSlowModeActive, + slowModeRemainingSeconds = slowModeRemainingSeconds + ), onTextValueChange = { textValue = it }, knownCustomEmojis = knownCustomEmojis, emojiFontFamily = emojiFontFamily, focusRequester = focusRequester, - canWriteText = canWriteText, - canOpenAttachSheet = canOpenAttachSheet, + capabilities = capabilities, canSendAttachments = canSendPendingAttachments, - canShowBotActions = canWriteText, canPasteMediaFromClipboard = canUseMediaPicker && state.editingMessage == null, - canSendStickers = canSendStickers, - canSendVoice = canSendVoice, - canSendVideoNotes = canSendVideoNotes, - isStickerMenuVisible = isStickerMenuVisible, - closeStickerMenuWithoutSlide = closeStickerMenuWithoutSlide, - isKeyboardVisible = isKeyboardVisible, - stickerMenuHeight = stickerMenuHeight, voiceRecorder = voiceRecorder, - isGifSearchFocused = isGifSearchFocused, - showFullScreenEditor = showFullScreenEditor, - currentMessageLength = currentMessageLength, - maxMessageLength = maxMessageLength, - isOverMessageLimit = isOverMessageLimit, - isVideoMessageMode = isVideoMessageMode, - isSlowModeActive = isSlowModeActive, - slowModeRemainingSeconds = slowModeRemainingSeconds, - replyMarkup = state.replyMarkup, - showSendOptionsSheet = showSendOptionsSheet, stickerRepository = stickerRepository, onCancelEdit = actions.onCancelEdit, onCancelReply = actions.onCancelReply, diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/MessageBubbleContainer.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/MessageBubbleContainer.kt index 5c11003e..649e6ec6 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/MessageBubbleContainer.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/MessageBubbleContainer.kt @@ -1,8 +1,8 @@ package org.monogram.presentation.features.chats.conversation.ui import android.content.res.Configuration -import androidx.compose.animation.Animatable import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.AnimationVector1D import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.gestures.detectTapGestures @@ -23,10 +23,9 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset @@ -47,7 +46,6 @@ import org.monogram.domain.models.MessageContent import org.monogram.domain.models.MessageModel import org.monogram.presentation.core.ui.Avatar import org.monogram.presentation.core.util.IDownloadUtils -import org.monogram.presentation.features.chats.conversation.ui.content.shouldShowDate import org.monogram.presentation.features.chats.conversation.ui.message.AudioMessageBubble import org.monogram.presentation.features.chats.conversation.ui.message.ContactMessageBubble import org.monogram.presentation.features.chats.conversation.ui.message.DocumentMessageBubble @@ -65,23 +63,13 @@ import org.monogram.presentation.features.chats.conversation.ui.message.VideoNot import org.monogram.presentation.features.chats.conversation.ui.message.VoiceMessageBubble @Composable -fun MessageBubbleContainer( +internal fun MessageBubbleContainer( msg: MessageModel, - olderMsg: MessageModel?, newerMsg: MessageModel?, - isGroup: Boolean, - fontSize: Float, - letterSpacing: Float, - bubbleRadius: Float = 12f, - stSize: Float = 200f, - autoDownloadMobile: Boolean, - autoDownloadWifi: Boolean, - autoDownloadRoaming: Boolean, - autoDownloadFiles: Boolean, - autoplayGifs: Boolean, - autoplayVideos: Boolean, - showLinkPreviews: Boolean = true, - highlighted: Boolean = false, + appearance: MessageAppearanceConfig, + behavior: MessageRowBehaviorConfig, + uiFlags: MessageRowUiFlags = MessageRowUiFlags(), + senderGrouping: MessageSenderGrouping, onHighlightConsumed: () -> Unit = {}, onPhotoClick: (MessageModel) -> Unit, onDownloadPhoto: (Int) -> Unit = {}, @@ -100,16 +88,12 @@ fun MessageBubbleContainer( onInstantViewClick: ((String) -> Unit)? = null, onYouTubeClick: ((String) -> Unit)? = null, onReplyMarkupButtonClick: (Long, InlineKeyboardButtonModel) -> Unit = { _, _ -> }, - shouldReportPosition: Boolean = false, onPositionChange: (Long, Offset, IntSize) -> Unit = { _, _, _ -> }, toProfile: (Long) -> Unit, onForwardOriginClick: (ForwardInfo) -> Unit = {}, onViaBotClick: (String) -> Unit = {}, - canReply: Boolean = true, onReplySwipe: (MessageModel) -> Unit = {}, - swipeEnabled: Boolean = true, - downloadUtils: IDownloadUtils, - isAnyViewerOpen: Boolean = false + downloadUtils: IDownloadUtils ) { val configuration = LocalConfiguration.current val screenWidth = configuration.screenWidthDp.dp @@ -124,45 +108,139 @@ fun MessageBubbleContainer( } val isOutgoing = msg.isOutgoing - val isSameSenderAbove = remember( - olderMsg?.id, - olderMsg?.senderId, - olderMsg?.senderName, - olderMsg?.senderCustomTitle, - olderMsg?.date, - msg.senderId, - msg.senderName, - msg.senderCustomTitle, - msg.date - ) { - shouldGroupSenderBlock( - current = msg, - neighbor = olderMsg, - dateBreak = olderMsg?.let { shouldShowDate(msg, it) } ?: true - ) + val topSpacing = if (!senderGrouping.isSameSenderAbove) 8.dp else 2.dp + val dragOffsetX = remember { Animatable(0f) } + val coroutineScope = rememberCoroutineScope() + val layoutTracker = remember { MessageBubbleLayoutTracker() } + val onReplyClickState by rememberUpdatedState(onReplyClick) + val onPositionChangeState by rememberUpdatedState(onPositionChange) + val onReplySwipeState by rememberUpdatedState(onReplySwipe) + + val onBubbleClick: (Offset) -> Unit = remember(msg.id) { + { offset -> + onReplyClickState( + layoutTracker.bubblePosition, + layoutTracker.bubbleSize, + layoutTracker.bubblePosition + offset + ) + } } - val isSameSenderBelow = remember( - newerMsg?.id, - newerMsg?.senderId, - newerMsg?.senderName, - newerMsg?.senderCustomTitle, - newerMsg?.date, - msg.senderId, - msg.senderName, - msg.senderCustomTitle, - msg.date - ) { - shouldGroupSenderBlock( - current = msg, - neighbor = newerMsg, - dateBreak = newerMsg?.let { shouldShowDate(it, msg) } ?: true - ) + val onBubbleCenterClick: () -> Unit = remember(msg.id) { + { + onReplyClickState( + layoutTracker.bubblePosition, + layoutTracker.bubbleSize, + layoutTracker.bubblePosition + (layoutTracker.bubbleSize.toSize() / 2f).toOffset() + ) + } } - val topSpacing = if (!isSameSenderAbove) 8.dp else 2.dp + MessageHighlightLayer( + highlighted = uiFlags.isHighlighted, + onHighlightConsumed = onHighlightConsumed + ) { + MessageBubbleGestureLayer( + modifier = Modifier.padding(top = topSpacing), + canReply = behavior.canReply, + swipeEnabled = behavior.swipeEnabled, + dragOffsetX = dragOffsetX, + scope = coroutineScope, + maxWidth = maxWidth.value, + onReplySwipe = { onReplySwipeState(msg) }, + layoutTracker = layoutTracker, + onOutsideBubblePress = { clickPosition -> + onReplyClickState( + layoutTracker.bubblePosition, + layoutTracker.bubbleSize, + clickPosition + ) + } + ) { + MessageBubbleShell( + isOutgoing = isOutgoing, + avatar = { + MessageAvatar( + avatarPath = msg.senderAvatar, + fallbackPath = msg.senderPersonalAvatar, + senderName = msg.senderName, + senderId = msg.senderId, + isVisible = behavior.isGroup && !isOutgoing && !senderGrouping.isSameSenderBelow, + toProfile = toProfile + ) + if (behavior.isGroup && !isOutgoing) { + Spacer(modifier = Modifier.width(8.dp)) + } + }, + content = { + Box(modifier = Modifier.wrapContentSize()) { + MessageBubbleContentHost( + modifier = Modifier + .width(IntrinsicSize.Max) + .widthIn(max = maxWidth) + .onGloballyPositioned { coordinates -> + layoutTracker.bubblePosition = coordinates.positionInWindow() + layoutTracker.bubbleSize = coordinates.size + if (uiFlags.shouldReportPosition) { + onPositionChangeState( + msg.id, + layoutTracker.bubblePosition, + layoutTracker.bubbleSize + ) + } + }, + msg = msg, + newerMsg = newerMsg, + isOutgoing = isOutgoing, + senderGrouping = senderGrouping, + isGroup = behavior.isGroup, + appearance = appearance, + onPhotoClick = onPhotoClick, + onDownloadPhoto = onDownloadPhoto, + onVideoClick = onVideoClick, + onDocumentClick = onDocumentClick, + onAudioClick = onAudioClick, + onCancelDownload = onCancelDownload, + onBubbleClick = onBubbleClick, + onBubbleLongClick = onBubbleClick, + onBubbleCenterLongClick = onBubbleCenterClick, + onGoToReply = onGoToReply, + onReactionClick = onReactionClick, + onStickerClick = onStickerClick, + onPollOptionClick = onPollOptionClick, + onRetractVote = onRetractVote, + onShowVoters = onShowVoters, + onClosePoll = onClosePoll, + onInstantViewClick = onInstantViewClick, + onYouTubeClick = onYouTubeClick, + onReplyMarkupButtonClick = onReplyMarkupButtonClick, + toProfile = toProfile, + onForwardOriginClick = onForwardOriginClick, + onViaBotClick = onViaBotClick, + downloadUtils = downloadUtils, + isAnyViewerOpen = behavior.isAnyViewerOpen + ) + + FastReplyIndicator( + modifier = Modifier.align(Alignment.CenterEnd), + dragOffsetX = dragOffsetX, + isOutgoing = isOutgoing, + maxWidth = maxWidth + ) + } + } + ) + } + } +} +@Composable +private fun MessageHighlightLayer( + highlighted: Boolean, + onHighlightConsumed: () -> Unit, + content: @Composable () -> Unit +) { val highlightColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f) - val animatedColor = remember { Animatable(Color.Transparent) } + val animatedColor = remember { androidx.compose.animation.Animatable(Color.Transparent) } LaunchedEffect(highlighted) { if (highlighted) { @@ -173,161 +251,184 @@ fun MessageBubbleContainer( } } - var outerColumnPosition by remember { mutableStateOf(Offset.Zero) } - var bubblePosition by remember { mutableStateOf(Offset.Zero) } - var bubbleSize by remember { mutableStateOf(IntSize.Zero) } + Box( + modifier = Modifier.background(animatedColor.value, RoundedCornerShape(12.dp)) + ) { + content() + } +} - val dragOffsetX = remember { Animatable(0f) } +@Composable +private fun MessageBubbleGestureLayer( + modifier: Modifier = Modifier, + canReply: Boolean, + swipeEnabled: Boolean, + dragOffsetX: Animatable, + scope: kotlinx.coroutines.CoroutineScope, + maxWidth: Float, + onReplySwipe: () -> Unit, + layoutTracker: MessageBubbleLayoutTracker, + onOutsideBubblePress: (Offset) -> Unit, + content: @Composable () -> Unit +) { + val onOutsideBubblePressState by rememberUpdatedState(onOutsideBubblePress) Column( - modifier = Modifier + modifier = modifier .fillMaxWidth() - .background(animatedColor.value, RoundedCornerShape(12.dp)) - .onGloballyPositioned { outerColumnPosition = it.positionInWindow() } - .padding(top = topSpacing) + .onGloballyPositioned { layoutTracker.outerColumnPosition = it.positionInWindow() } .offset { IntOffset(dragOffsetX.value.toInt(), 0) } .fastReplyPointer( - canReply = canReply, + canReply = canReply && swipeEnabled, dragOffsetX = dragOffsetX, - scope = rememberCoroutineScope(), - onReplySwipe = { onReplySwipe(msg) }, - maxWidth = maxWidth.value + scope = scope, + onReplySwipe = onReplySwipe, + maxWidth = maxWidth ) .pointerInput(Unit) { detectTapGestures( onTap = { offset -> - val clickPos = outerColumnPosition + offset - val bubbleRect = Rect(bubblePosition, bubbleSize.toSize()) + val clickPos = layoutTracker.outerColumnPosition + offset + val bubbleRect = + Rect(layoutTracker.bubblePosition, layoutTracker.bubbleSize.toSize()) if (!bubbleRect.contains(clickPos)) { - onReplyClick(bubblePosition, bubbleSize, clickPos) + onOutsideBubblePressState(clickPos) } }, onLongPress = { offset -> - val clickPos = outerColumnPosition + offset - val bubbleRect = Rect(bubblePosition, bubbleSize.toSize()) + val clickPos = layoutTracker.outerColumnPosition + offset + val bubbleRect = + Rect(layoutTracker.bubblePosition, layoutTracker.bubbleSize.toSize()) if (!bubbleRect.contains(clickPos)) { - onReplyClick(bubblePosition, bubbleSize, clickPos) + onOutsideBubblePressState(clickPos) } } ) } ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = if (isOutgoing) Arrangement.End else Arrangement.Start, - verticalAlignment = Alignment.Bottom - ) { - MessageAvatar( - msg = msg, - isGroup = isGroup, - isOutgoing = isOutgoing, - isSameSenderBelow = isSameSenderBelow, - toProfile = toProfile - ) + content() + } +} - Box( - modifier = Modifier.wrapContentSize() - ) { - Column( - modifier = Modifier - .width(IntrinsicSize.Max) - .widthIn(max = maxWidth) - .onGloballyPositioned { coordinates -> - bubblePosition = coordinates.positionInWindow() - bubbleSize = coordinates.size - if (shouldReportPosition) { - onPositionChange(msg.id, bubblePosition, bubbleSize) - } - }, - horizontalAlignment = if (isOutgoing) Alignment.End else Alignment.Start - ) { - MessageContentSelector( - msg = msg, - newerMsg = newerMsg, - isOutgoing = isOutgoing, - isSameSenderAbove = isSameSenderAbove, - isSameSenderBelow = isSameSenderBelow, - isGroup = isGroup, - fontSize = fontSize, - letterSpacing = letterSpacing, - bubbleRadius = bubbleRadius, - stSize = stSize, - autoDownloadMobile = autoDownloadMobile, - autoDownloadWifi = autoDownloadWifi, - autoDownloadRoaming = autoDownloadRoaming, - autoDownloadFiles = autoDownloadFiles, - autoplayGifs = autoplayGifs, - autoplayVideos = autoplayVideos, - showLinkPreviews = showLinkPreviews, - onPhotoClick = onPhotoClick, - onDownloadPhoto = onDownloadPhoto, - onVideoClick = onVideoClick, - onDocumentClick = onDocumentClick, - onAudioClick = onAudioClick, - onCancelDownload = onCancelDownload, - onReplyClick = onReplyClick, - onGoToReply = onGoToReply, - onReactionClick = onReactionClick, - onStickerClick = onStickerClick, - onPollOptionClick = onPollOptionClick, - onRetractVote = onRetractVote, - onShowVoters = onShowVoters, - onClosePoll = onClosePoll, - onInstantViewClick = onInstantViewClick, - onYouTubeClick = onYouTubeClick, - toProfile = toProfile, - onForwardOriginClick = onForwardOriginClick, - bubblePosition = bubblePosition, - bubbleSize = bubbleSize, - downloadUtils = downloadUtils, - isAnyViewerOpen = isAnyViewerOpen - ) +@Composable +private fun MessageBubbleShell( + isOutgoing: Boolean, + avatar: @Composable () -> Unit, + content: @Composable () -> Unit +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = if (isOutgoing) Arrangement.End else Arrangement.Start, + verticalAlignment = Alignment.Bottom + ) { + avatar() + content() + } +} - MessageReplyMarkup( - msg = msg, - onReplyMarkupButtonClick = onReplyMarkupButtonClick - ) +@Composable +private fun MessageBubbleContentHost( + modifier: Modifier = Modifier, + msg: MessageModel, + newerMsg: MessageModel?, + isOutgoing: Boolean, + senderGrouping: MessageSenderGrouping, + isGroup: Boolean, + appearance: MessageAppearanceConfig, + onPhotoClick: (MessageModel) -> Unit, + onDownloadPhoto: (Int) -> Unit, + onVideoClick: (MessageModel) -> Unit, + onDocumentClick: (MessageModel) -> Unit, + onAudioClick: (MessageModel) -> Unit, + onCancelDownload: (Int) -> Unit, + onBubbleClick: (Offset) -> Unit, + onBubbleLongClick: (Offset) -> Unit, + onBubbleCenterLongClick: () -> Unit, + onGoToReply: (MessageModel) -> Unit, + onReactionClick: (Long, String) -> Unit, + onStickerClick: (Long) -> Unit, + onPollOptionClick: (Long, Int) -> Unit, + onRetractVote: (Long) -> Unit, + onShowVoters: (Long, Int) -> Unit, + onClosePoll: (Long) -> Unit, + onInstantViewClick: ((String) -> Unit)?, + onYouTubeClick: ((String) -> Unit)?, + onReplyMarkupButtonClick: (Long, InlineKeyboardButtonModel) -> Unit, + toProfile: (Long) -> Unit, + onForwardOriginClick: (ForwardInfo) -> Unit, + onViaBotClick: (String) -> Unit, + downloadUtils: IDownloadUtils, + isAnyViewerOpen: Boolean +) { + Column( + modifier = modifier, + horizontalAlignment = if (isOutgoing) Alignment.End else Alignment.Start + ) { + MessageContentSelector( + msg = msg, + newerMsg = newerMsg, + isOutgoing = isOutgoing, + senderGrouping = senderGrouping, + isGroup = isGroup, + appearance = appearance, + onPhotoClick = onPhotoClick, + onDownloadPhoto = onDownloadPhoto, + onVideoClick = onVideoClick, + onDocumentClick = onDocumentClick, + onAudioClick = onAudioClick, + onCancelDownload = onCancelDownload, + onBubbleClick = onBubbleClick, + onBubbleLongClick = onBubbleLongClick, + onBubbleCenterLongClick = onBubbleCenterLongClick, + onGoToReply = onGoToReply, + onReactionClick = onReactionClick, + onStickerClick = onStickerClick, + onPollOptionClick = onPollOptionClick, + onRetractVote = onRetractVote, + onShowVoters = onShowVoters, + onClosePoll = onClosePoll, + onInstantViewClick = onInstantViewClick, + onYouTubeClick = onYouTubeClick, + toProfile = toProfile, + onForwardOriginClick = onForwardOriginClick, + downloadUtils = downloadUtils, + isAnyViewerOpen = isAnyViewerOpen + ) - MessageViaBotAttribution( - msg = msg, - isOutgoing = isOutgoing, - onViaBotClick = onViaBotClick, - modifier = Modifier.align(if (isOutgoing) Alignment.End else Alignment.Start) - ) - } + MessageReplyMarkup( + msg = msg, + onReplyMarkupButtonClick = onReplyMarkupButtonClick + ) - FastReplyIndicator( - modifier = Modifier.align(Alignment.CenterEnd), - dragOffsetX = dragOffsetX, - isOutgoing = isOutgoing, - maxWidth = maxWidth - ) - } - } + MessageViaBotAttribution( + msg = msg, + isOutgoing = isOutgoing, + onViaBotClick = onViaBotClick, + modifier = Modifier.align(if (isOutgoing) Alignment.End else Alignment.Start) + ) } } @Composable private fun MessageAvatar( - msg: MessageModel, - isGroup: Boolean, - isOutgoing: Boolean, - isSameSenderBelow: Boolean, + avatarPath: String?, + fallbackPath: String?, + senderName: String, + senderId: Long, + isVisible: Boolean, toProfile: (Long) -> Unit ) { - if (isGroup && !isOutgoing) { - if (!isSameSenderBelow) { - Avatar( - path = msg.senderAvatar, - fallbackPath = msg.senderPersonalAvatar, - name = msg.senderName, - size = 40.dp, - isLocal = msg.senderAvatar?.contains("local") ?: false, - onClick = { toProfile(msg.senderId) }) - } else { - Spacer(modifier = Modifier.width(40.dp)) - } - Spacer(modifier = Modifier.width(8.dp)) + if (isVisible) { + Avatar( + path = avatarPath, + fallbackPath = fallbackPath, + name = senderName, + size = 40.dp, + isLocal = avatarPath?.contains("local") ?: false, + onClick = { toProfile(senderId) } + ) + } else { + Spacer(modifier = Modifier.width(40.dp)) } } @@ -336,27 +437,18 @@ private fun MessageContentSelector( msg: MessageModel, newerMsg: MessageModel?, isOutgoing: Boolean, - isSameSenderAbove: Boolean, - isSameSenderBelow: Boolean, + senderGrouping: MessageSenderGrouping, isGroup: Boolean, - fontSize: Float, - letterSpacing: Float, - bubbleRadius: Float, - stSize: Float, - autoDownloadMobile: Boolean, - autoDownloadWifi: Boolean, - autoDownloadRoaming: Boolean, - autoDownloadFiles: Boolean, - autoplayGifs: Boolean, - autoplayVideos: Boolean, - showLinkPreviews: Boolean, + appearance: MessageAppearanceConfig, onPhotoClick: (MessageModel) -> Unit, onDownloadPhoto: (Int) -> Unit, onVideoClick: (MessageModel) -> Unit, onDocumentClick: (MessageModel) -> Unit, onAudioClick: (MessageModel) -> Unit, onCancelDownload: (Int) -> Unit, - onReplyClick: (Offset, IntSize, Offset) -> Unit, + onBubbleClick: (Offset) -> Unit, + onBubbleLongClick: (Offset) -> Unit, + onBubbleCenterLongClick: () -> Unit, onGoToReply: (MessageModel) -> Unit, onReactionClick: (Long, String) -> Unit, onStickerClick: (Long) -> Unit, @@ -368,8 +460,6 @@ private fun MessageContentSelector( onYouTubeClick: ((String) -> Unit)?, toProfile: (Long) -> Unit, onForwardOriginClick: (ForwardInfo) -> Unit, - bubblePosition: Offset, - bubbleSize: IntSize, downloadUtils: IDownloadUtils, isAnyViewerOpen: Boolean = false ) { @@ -383,23 +473,19 @@ private fun MessageContentSelector( content = content, msg = msg, isOutgoing = isOutgoing, - isSameSenderAbove = isSameSenderAbove, - isSameSenderBelow = isSameSenderBelow, - fontSize = fontSize, - letterSpacing = letterSpacing, - bubbleRadius = bubbleRadius, + isSameSenderAbove = senderGrouping.isSameSenderAbove, + isSameSenderBelow = senderGrouping.isSameSenderBelow, + fontSize = appearance.fontSize, + letterSpacing = appearance.letterSpacing, + bubbleRadius = appearance.bubbleRadius, isGroup = isGroup, - showLinkPreviews = showLinkPreviews, + showLinkPreviews = appearance.showLinkPreviews, onReplyClick = onGoToReply, onReactionClick = { onReactionClick(msg.id, it) }, onInstantViewClick = onInstantViewClick, onYouTubeClick = onYouTubeClick, - onClick = { offset -> - onReplyClick(bubblePosition, bubbleSize, bubblePosition + offset) - }, - onLongClick = { offset -> - onReplyClick(bubblePosition, bubbleSize, bubblePosition + offset) - }, + onClick = onBubbleClick, + onLongClick = onBubbleLongClick, toProfile = toProfile, onForwardOriginClick = onForwardOriginClick ) @@ -410,17 +496,11 @@ private fun MessageContentSelector( content = content, msg = msg, isOutgoing = isOutgoing, - stickerSize = stSize, + stickerSize = appearance.stickerSize, onReplyClick = onGoToReply, onReactionClick = { onReactionClick(msg.id, it) }, onStickerClick = { onStickerClick(it) }, - onLongClick = { - onReplyClick( - bubblePosition, - bubbleSize, - bubblePosition + (bubbleSize.toSize() / 2f).toOffset() - ) - } + onLongClick = onBubbleCenterLongClick ) } @@ -429,24 +509,18 @@ private fun MessageContentSelector( content = content, msg = msg, isOutgoing = isOutgoing, - isSameSenderAbove = isSameSenderAbove, - isSameSenderBelow = isSameSenderBelow, - fontSize = fontSize, - letterSpacing = letterSpacing, + isSameSenderAbove = senderGrouping.isSameSenderAbove, + isSameSenderBelow = senderGrouping.isSameSenderBelow, + fontSize = appearance.fontSize, + letterSpacing = appearance.letterSpacing, isGroup = isGroup, - autoDownloadMobile = autoDownloadMobile, - autoDownloadWifi = autoDownloadWifi, - autoDownloadRoaming = autoDownloadRoaming, + autoDownloadMobile = appearance.autoDownloadMobile, + autoDownloadWifi = appearance.autoDownloadWifi, + autoDownloadRoaming = appearance.autoDownloadRoaming, onPhotoClick = onPhotoClick, onDownloadPhoto = onDownloadPhoto, onCancelDownload = onCancelDownload, - onLongClick = { offset -> - onReplyClick( - bubblePosition, - bubbleSize, - bubblePosition + offset - ) - }, + onLongClick = onBubbleLongClick, onReplyClick = onGoToReply, onReactionClick = { onReactionClick(msg.id, it) }, toProfile = toProfile, @@ -461,23 +535,17 @@ private fun MessageContentSelector( content = content, msg = msg, isOutgoing = isOutgoing, - isSameSenderAbove = isSameSenderAbove, - isSameSenderBelow = isSameSenderBelow, - fontSize = fontSize, - letterSpacing = letterSpacing, - autoDownloadMobile = autoDownloadMobile, - autoDownloadWifi = autoDownloadWifi, - autoDownloadRoaming = autoDownloadRoaming, - autoplayVideos = autoplayVideos, + isSameSenderAbove = senderGrouping.isSameSenderAbove, + isSameSenderBelow = senderGrouping.isSameSenderBelow, + fontSize = appearance.fontSize, + letterSpacing = appearance.letterSpacing, + autoDownloadMobile = appearance.autoDownloadMobile, + autoDownloadWifi = appearance.autoDownloadWifi, + autoDownloadRoaming = appearance.autoDownloadRoaming, + autoplayVideos = appearance.autoplayVideos, onVideoClick = onVideoClick, onCancelDownload = onCancelDownload, - onLongClick = { offset -> - onReplyClick( - bubblePosition, - bubbleSize, - bubblePosition + offset - ) - }, + onLongClick = onBubbleLongClick, onReplyClick = onGoToReply, onReactionClick = { onReactionClick(msg.id, it) }, toProfile = toProfile, @@ -495,13 +563,7 @@ private fun MessageContentSelector( isOutgoing = isOutgoing, onVideoClick = onVideoClick, onCancelDownload = onCancelDownload, - onLongClick = { offset -> - onReplyClick( - bubblePosition, - bubbleSize, - bubblePosition + offset - ) - }, + onLongClick = onBubbleLongClick, onReplyClick = onGoToReply, onReactionClick = { onReactionClick(msg.id, it) } ) @@ -512,24 +574,18 @@ private fun MessageContentSelector( content = content, msg = msg, isOutgoing = isOutgoing, - isSameSenderAbove = isSameSenderAbove, - isSameSenderBelow = isSameSenderBelow, - fontSize = fontSize, - letterSpacing = letterSpacing, + isSameSenderAbove = senderGrouping.isSameSenderAbove, + isSameSenderBelow = senderGrouping.isSameSenderBelow, + fontSize = appearance.fontSize, + letterSpacing = appearance.letterSpacing, isGroup = isGroup, - autoDownloadFiles = autoDownloadFiles, - autoDownloadMobile = autoDownloadMobile, - autoDownloadWifi = autoDownloadWifi, - autoDownloadRoaming = autoDownloadRoaming, + autoDownloadFiles = appearance.autoDownloadFiles, + autoDownloadMobile = appearance.autoDownloadMobile, + autoDownloadWifi = appearance.autoDownloadWifi, + autoDownloadRoaming = appearance.autoDownloadRoaming, onVoiceClick = onAudioClick, onCancelDownload = onCancelDownload, - onLongClick = { offset -> - onReplyClick( - bubblePosition, - bubbleSize, - bubblePosition + offset - ) - }, + onLongClick = onBubbleLongClick, onReplyClick = onGoToReply, onReactionClick = { onReactionClick(msg.id, it) }, toProfile = toProfile, @@ -543,23 +599,17 @@ private fun MessageContentSelector( content = content, msg = msg, isOutgoing = isOutgoing, - isSameSenderAbove = isSameSenderAbove, - isSameSenderBelow = isSameSenderBelow, - fontSize = fontSize, - letterSpacing = letterSpacing, - autoDownloadMobile = autoDownloadMobile, - autoDownloadWifi = autoDownloadWifi, - autoDownloadRoaming = autoDownloadRoaming, - autoplayGifs = autoplayGifs, + isSameSenderAbove = senderGrouping.isSameSenderAbove, + isSameSenderBelow = senderGrouping.isSameSenderBelow, + fontSize = appearance.fontSize, + letterSpacing = appearance.letterSpacing, + autoDownloadMobile = appearance.autoDownloadMobile, + autoDownloadWifi = appearance.autoDownloadWifi, + autoDownloadRoaming = appearance.autoDownloadRoaming, + autoplayGifs = appearance.autoplayGifs, onGifClick = onVideoClick, onCancelDownload = onCancelDownload, - onLongClick = { offset -> - onReplyClick( - bubblePosition, - bubbleSize, - bubblePosition + offset - ) - }, + onLongClick = onBubbleLongClick, onReplyClick = onGoToReply, onReactionClick = { onReactionClick(msg.id, it) }, toProfile = toProfile, @@ -574,24 +624,18 @@ private fun MessageContentSelector( content = content, msg = msg, isOutgoing = isOutgoing, - isSameSenderAbove = isSameSenderAbove, - isSameSenderBelow = isSameSenderBelow, - fontSize = fontSize, - letterSpacing = letterSpacing, + isSameSenderAbove = senderGrouping.isSameSenderAbove, + isSameSenderBelow = senderGrouping.isSameSenderBelow, + fontSize = appearance.fontSize, + letterSpacing = appearance.letterSpacing, isGroup = isGroup, - autoDownloadFiles = autoDownloadFiles, - autoDownloadMobile = autoDownloadMobile, - autoDownloadWifi = autoDownloadWifi, - autoDownloadRoaming = autoDownloadRoaming, + autoDownloadFiles = appearance.autoDownloadFiles, + autoDownloadMobile = appearance.autoDownloadMobile, + autoDownloadWifi = appearance.autoDownloadWifi, + autoDownloadRoaming = appearance.autoDownloadRoaming, onDocumentClick = onDocumentClick, onCancelDownload = onCancelDownload, - onLongClick = { offset -> - onReplyClick( - bubblePosition, - bubbleSize, - bubblePosition + offset - ) - }, + onLongClick = onBubbleLongClick, onReplyClick = onGoToReply, onReactionClick = { onReactionClick(msg.id, it) }, onForwardOriginClick = onForwardOriginClick, @@ -604,24 +648,18 @@ private fun MessageContentSelector( content = content, msg = msg, isOutgoing = isOutgoing, - isSameSenderAbove = isSameSenderAbove, - isSameSenderBelow = isSameSenderBelow, - fontSize = fontSize, - letterSpacing = letterSpacing, + isSameSenderAbove = senderGrouping.isSameSenderAbove, + isSameSenderBelow = senderGrouping.isSameSenderBelow, + fontSize = appearance.fontSize, + letterSpacing = appearance.letterSpacing, isGroup = isGroup, - autoDownloadFiles = autoDownloadFiles, - autoDownloadMobile = autoDownloadMobile, - autoDownloadWifi = autoDownloadWifi, - autoDownloadRoaming = autoDownloadRoaming, + autoDownloadFiles = appearance.autoDownloadFiles, + autoDownloadMobile = appearance.autoDownloadMobile, + autoDownloadWifi = appearance.autoDownloadWifi, + autoDownloadRoaming = appearance.autoDownloadRoaming, onAudioClick = onAudioClick, onCancelDownload = onCancelDownload, - onLongClick = { offset -> - onReplyClick( - bubblePosition, - bubbleSize, - bubblePosition + offset - ) - }, + onLongClick = onBubbleLongClick, onReplyClick = onGoToReply, onReactionClick = { onReactionClick(msg.id, it) }, downloadUtils = downloadUtils @@ -633,20 +671,14 @@ private fun MessageContentSelector( content = content, msg = msg, isOutgoing = isOutgoing, - isSameSenderAbove = isSameSenderAbove, - isSameSenderBelow = isSameSenderBelow, - fontSize = fontSize, - letterSpacing = letterSpacing, - bubbleRadius = bubbleRadius, + isSameSenderAbove = senderGrouping.isSameSenderAbove, + isSameSenderBelow = senderGrouping.isSameSenderBelow, + fontSize = appearance.fontSize, + letterSpacing = appearance.letterSpacing, + bubbleRadius = appearance.bubbleRadius, isGroup = isGroup, onClick = { onGoToReply(msg) }, - onLongClick = { - onReplyClick( - bubblePosition, - bubbleSize, - bubblePosition + (bubbleSize.toSize() / 2f).toOffset() - ) - }, + onLongClick = onBubbleCenterLongClick, onReplyClick = onGoToReply, onReactionClick = { onReactionClick(msg.id, it) }, toProfile = toProfile, @@ -660,20 +692,14 @@ private fun MessageContentSelector( content = content, msg = msg, isOutgoing = isOutgoing, - isSameSenderAbove = isSameSenderAbove, - isSameSenderBelow = isSameSenderBelow, - fontSize = fontSize, - letterSpacing = letterSpacing, - bubbleRadius = bubbleRadius, + isSameSenderAbove = senderGrouping.isSameSenderAbove, + isSameSenderBelow = senderGrouping.isSameSenderBelow, + fontSize = appearance.fontSize, + letterSpacing = appearance.letterSpacing, + bubbleRadius = appearance.bubbleRadius, onOptionClick = { onPollOptionClick(msg.id, it) }, onRetractVote = { onRetractVote(msg.id) }, - onLongClick = { offset -> - onReplyClick( - bubblePosition, - bubbleSize, - bubblePosition + offset - ) - }, + onLongClick = onBubbleLongClick, onReplyClick = onGoToReply, onReactionClick = { onReactionClick(msg.id, it) }, onShowVoters = { onShowVoters(msg.id, it) }, @@ -688,20 +714,14 @@ private fun MessageContentSelector( content = content, msg = msg, isOutgoing = isOutgoing, - isSameSenderAbove = isSameSenderAbove, - isSameSenderBelow = isSameSenderBelow, - fontSize = fontSize, - letterSpacing = letterSpacing, + isSameSenderAbove = senderGrouping.isSameSenderAbove, + isSameSenderBelow = senderGrouping.isSameSenderBelow, + fontSize = appearance.fontSize, + letterSpacing = appearance.letterSpacing, isGroup = isGroup, - bubbleRadius = bubbleRadius, + bubbleRadius = appearance.bubbleRadius, onClick = { onGoToReply(msg) }, - onLongClick = { - onReplyClick( - bubblePosition, - bubbleSize, - bubblePosition + (bubbleSize.toSize() / 2f).toOffset() - ) - }, + onLongClick = onBubbleCenterLongClick, onReplyClick = onGoToReply, onReactionClick = { onReactionClick(msg.id, it) }, toProfile = toProfile, @@ -714,20 +734,14 @@ private fun MessageContentSelector( content = content, msg = msg, isOutgoing = isOutgoing, - isSameSenderAbove = isSameSenderAbove, - isSameSenderBelow = isSameSenderBelow, - fontSize = fontSize, - letterSpacing = letterSpacing, + isSameSenderAbove = senderGrouping.isSameSenderAbove, + isSameSenderBelow = senderGrouping.isSameSenderBelow, + fontSize = appearance.fontSize, + letterSpacing = appearance.letterSpacing, isGroup = isGroup, - bubbleRadius = bubbleRadius, + bubbleRadius = appearance.bubbleRadius, onClick = { onGoToReply(msg) }, - onLongClick = { - onReplyClick( - bubblePosition, - bubbleSize, - bubblePosition + (bubbleSize.toSize() / 2f).toOffset() - ) - }, + onLongClick = onBubbleCenterLongClick, onReplyClick = onGoToReply, onReactionClick = { onReactionClick(msg.id, it) }, toProfile = toProfile, diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/MessageRowContracts.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/MessageRowContracts.kt new file mode 100644 index 00000000..7b49c6a2 --- /dev/null +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/MessageRowContracts.kt @@ -0,0 +1,53 @@ +package org.monogram.presentation.features.chats.conversation.ui + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.unit.IntSize + +@Immutable +internal data class MessageAppearanceConfig( + val fontSize: Float, + val letterSpacing: Float, + val bubbleRadius: Float, + val stickerSize: Float, + val showLinkPreviews: Boolean = true, + val autoplayGifs: Boolean = true, + val autoplayVideos: Boolean = true, + val autoDownloadMobile: Boolean = false, + val autoDownloadWifi: Boolean = false, + val autoDownloadRoaming: Boolean = false, + val autoDownloadFiles: Boolean = false +) + +@Immutable +internal data class MessageRowBehaviorConfig( + val isGroup: Boolean, + val isChannel: Boolean, + val isTopicClosed: Boolean, + val canReply: Boolean, + val swipeEnabled: Boolean, + val isSelectionMode: Boolean, + val isAnyViewerOpen: Boolean +) + +@Immutable +internal data class MessageRowUiFlags( + val isSelected: Boolean = false, + val isHighlighted: Boolean = false, + val showUnreadSeparator: Boolean = false, + val unreadCount: Int = 0, + val shouldReportPosition: Boolean = false +) + +@Immutable +internal data class MessageSenderGrouping( + val isSameSenderAbove: Boolean, + val isSameSenderBelow: Boolean +) + +internal class MessageBubbleLayoutTracker { + var outerColumnPosition: Offset = Offset.Zero + var bubblePosition: Offset = Offset.Zero + var bubbleSize: IntSize = IntSize.Zero +} + diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/SenderGrouping.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/SenderGrouping.kt index 4e3c8336..9b0ff2a8 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/SenderGrouping.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/SenderGrouping.kt @@ -1,6 +1,8 @@ package org.monogram.presentation.features.chats.conversation.ui import org.monogram.domain.models.MessageModel +import org.monogram.presentation.features.chats.conversation.ui.content.GroupedMessageItem +import org.monogram.presentation.features.chats.conversation.ui.content.shouldShowDate internal fun shouldGroupSenderBlock( current: MessageModel, @@ -14,3 +16,31 @@ internal fun shouldGroupSenderBlock( if (current.senderCustomTitle != neighbor.senderCustomTitle) return false return !dateBreak } + +internal fun buildSenderGrouping( + item: GroupedMessageItem, + olderMsg: MessageModel?, + newerMsg: MessageModel? +): MessageSenderGrouping { + val firstMsg = when (item) { + is GroupedMessageItem.Single -> item.message + is GroupedMessageItem.Album -> item.messages.first() + } + val lastMsg = when (item) { + is GroupedMessageItem.Single -> item.message + is GroupedMessageItem.Album -> item.messages.last() + } + + return MessageSenderGrouping( + isSameSenderAbove = shouldGroupSenderBlock( + current = firstMsg, + neighbor = olderMsg, + dateBreak = olderMsg?.let { shouldShowDate(firstMsg, it) } ?: true + ), + isSameSenderBelow = shouldGroupSenderBlock( + current = lastMsg, + neighbor = newerMsg, + dateBreak = newerMsg?.let { shouldShowDate(it, lastMsg) } ?: true + ) + ) +} diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/channel/ChannelMessageBubbleContainer.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/channel/ChannelMessageBubbleContainer.kt index 51ae9d0e..dbc27430 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/channel/ChannelMessageBubbleContainer.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/channel/ChannelMessageBubbleContainer.kt @@ -19,10 +19,9 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset @@ -42,25 +41,27 @@ import org.monogram.domain.models.InlineKeyboardButtonModel import org.monogram.domain.models.MessageContent import org.monogram.domain.models.MessageModel import org.monogram.presentation.core.util.IDownloadUtils -import org.monogram.presentation.features.chats.conversation.ui.content.shouldShowDate import org.monogram.presentation.features.chats.conversation.ui.FastReplyIndicator +import org.monogram.presentation.features.chats.conversation.ui.MessageAppearanceConfig +import org.monogram.presentation.features.chats.conversation.ui.MessageBubbleLayoutTracker +import org.monogram.presentation.features.chats.conversation.ui.MessageRowBehaviorConfig +import org.monogram.presentation.features.chats.conversation.ui.MessageRowUiFlags +import org.monogram.presentation.features.chats.conversation.ui.MessageSenderGrouping +import org.monogram.presentation.features.chats.conversation.ui.fastReplyPointer import org.monogram.presentation.features.chats.conversation.ui.message.AudioMessageBubble import org.monogram.presentation.features.chats.conversation.ui.message.DocumentMessageBubble import org.monogram.presentation.features.chats.conversation.ui.message.MessageViaBotAttribution import org.monogram.presentation.features.chats.conversation.ui.message.ReplyMarkupView import org.monogram.presentation.features.chats.conversation.ui.message.StickerMessageBubble -import org.monogram.presentation.features.chats.conversation.ui.fastReplyPointer @Composable -fun ChannelMessageBubbleContainer( +internal fun ChannelMessageBubbleContainer( msg: MessageModel, - olderMsg: MessageModel?, newerMsg: MessageModel?, - autoplayGifs: Boolean = true, - autoplayVideos: Boolean = true, - autoDownloadFiles: Boolean = false, - showLinkPreviews: Boolean = true, - highlighted: Boolean = false, + appearance: MessageAppearanceConfig, + behavior: MessageRowBehaviorConfig, + uiFlags: MessageRowUiFlags = MessageRowUiFlags(), + senderGrouping: MessageSenderGrouping, onHighlightConsumed: () -> Unit = {}, onPhotoClick: (MessageModel) -> Unit, onDownloadPhoto: (Int) -> Unit = {}, @@ -70,9 +71,6 @@ fun ChannelMessageBubbleContainer( onCancelDownload: (Int) -> Unit = {}, onReplyClick: (Offset, IntSize, Offset) -> Unit, onGoToReply: (MessageModel) -> Unit = {}, - autoDownloadMobile: Boolean = false, - autoDownloadWifi: Boolean = false, - autoDownloadRoaming: Boolean = false, onReactionClick: (Long, String) -> Unit = { _, _ -> }, onReplyMarkupButtonClick: (Long, InlineKeyboardButtonModel) -> Unit = { _, _ -> }, onStickerClick: (Long) -> Unit = {}, @@ -82,21 +80,14 @@ fun ChannelMessageBubbleContainer( onClosePoll: (Long) -> Unit = {}, onInstantViewClick: ((String) -> Unit)? = null, onYouTubeClick: ((String) -> Unit)? = null, - fontSize: Float, - letterSpacing: Float, - bubbleRadius: Float, - stickerSize: Float = 200f, - shouldReportPosition: Boolean = false, onPositionChange: (Long, Offset, IntSize) -> Unit = { _, _, _ -> }, onCommentsClick: (Long) -> Unit = {}, showComments: Boolean = true, toProfile: (Long) -> Unit = {}, onForwardOriginClick: (ForwardInfo) -> Unit = {}, onViaBotClick: (String) -> Unit = {}, - canReply: Boolean = true, onReplySwipe: (MessageModel) -> Unit = {}, downloadUtils: IDownloadUtils, - isAnyViewerOpen: Boolean = false, ) { val configuration = LocalConfiguration.current val screenWidth = configuration.screenWidthDp.dp @@ -108,16 +99,13 @@ fun ChannelMessageBubbleContainer( (screenWidth * 0.94f).coerceAtMost(500.dp) } - val isSameSenderAbove = olderMsg?.senderId == msg.senderId && !shouldShowDate(msg, olderMsg) - val isSameSenderBelow = newerMsg != null && newerMsg.senderId == msg.senderId && !shouldShowDate(newerMsg, msg) - - val topSpacing = if (!isSameSenderAbove) 12.dp else 2.dp + val topSpacing = if (!senderGrouping.isSameSenderAbove) 12.dp else 2.dp val highlightColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f) val animatedColor = remember { Animatable(Color.Transparent) } - LaunchedEffect(highlighted) { - if (highlighted) { + LaunchedEffect(uiFlags.isHighlighted) { + if (uiFlags.isHighlighted) { animatedColor.animateTo(highlightColor, animationSpec = tween(300)) delay(450) animatedColor.animateTo(Color.Transparent, animationSpec = tween(1800)) @@ -125,40 +113,50 @@ fun ChannelMessageBubbleContainer( } } - var outerColumnPosition by remember { mutableStateOf(Offset.Zero) } - var bubblePosition by remember { mutableStateOf(Offset.Zero) } - var bubbleSize by remember { mutableStateOf(IntSize.Zero) } - val dragOffsetX = remember { androidx.compose.animation.core.Animatable(0f) } + val coroutineScope = rememberCoroutineScope() + val layoutTracker = remember { MessageBubbleLayoutTracker() } + val onReplyClickState by rememberUpdatedState(onReplyClick) + val onPositionChangeState by rememberUpdatedState(onPositionChange) Column( modifier = Modifier .fillMaxWidth() .background(animatedColor.value, RoundedCornerShape(12.dp)) - .onGloballyPositioned { outerColumnPosition = it.positionInWindow() } + .onGloballyPositioned { layoutTracker.outerColumnPosition = it.positionInWindow() } .padding(top = topSpacing) .offset { IntOffset(dragOffsetX.value.toInt(), 0) } .fastReplyPointer( - canReply = canReply, + canReply = behavior.canReply && behavior.swipeEnabled, dragOffsetX = dragOffsetX, - scope = rememberCoroutineScope(), + scope = coroutineScope, onReplySwipe = { onReplySwipe(msg) }, maxWidth = maxWidth.value ) .pointerInput(Unit) { detectTapGestures( onTap = { offset -> - val clickPos = outerColumnPosition + offset - val bubbleRect = Rect(bubblePosition, bubbleSize.toSize()) + val clickPos = layoutTracker.outerColumnPosition + offset + val bubbleRect = + Rect(layoutTracker.bubblePosition, layoutTracker.bubbleSize.toSize()) if (!bubbleRect.contains(clickPos)) { - onReplyClick(bubblePosition, bubbleSize, clickPos) + onReplyClickState( + layoutTracker.bubblePosition, + layoutTracker.bubbleSize, + clickPos + ) } }, onLongPress = { offset -> - val clickPos = outerColumnPosition + offset - val bubbleRect = Rect(bubblePosition, bubbleSize.toSize()) + val clickPos = layoutTracker.outerColumnPosition + offset + val bubbleRect = + Rect(layoutTracker.bubblePosition, layoutTracker.bubbleSize.toSize()) if (!bubbleRect.contains(clickPos)) { - onReplyClick(bubblePosition, bubbleSize, clickPos) + onReplyClickState( + layoutTracker.bubblePosition, + layoutTracker.bubbleSize, + clickPos + ) } } ) @@ -178,10 +176,14 @@ fun ChannelMessageBubbleContainer( .widthIn(max = maxWidth) .fillMaxWidth() .onGloballyPositioned { coordinates -> - bubblePosition = coordinates.positionInWindow() - bubbleSize = coordinates.size - if (shouldReportPosition) { - onPositionChange(msg.id, bubblePosition, bubbleSize) + layoutTracker.bubblePosition = coordinates.positionInWindow() + layoutTracker.bubbleSize = coordinates.size + if (uiFlags.shouldReportPosition) { + onPositionChangeState( + msg.id, + layoutTracker.bubblePosition, + layoutTracker.bubbleSize + ) } } ) { @@ -190,21 +192,29 @@ fun ChannelMessageBubbleContainer( ChannelTextMessageBubble( content = content, msg = msg, - isSameSenderAbove = isSameSenderAbove, - isSameSenderBelow = isSameSenderBelow, - fontSize = fontSize, - letterSpacing = letterSpacing, - bubbleRadius = bubbleRadius, - showLinkPreviews = showLinkPreviews, + isSameSenderAbove = senderGrouping.isSameSenderAbove, + isSameSenderBelow = senderGrouping.isSameSenderBelow, + fontSize = appearance.fontSize, + letterSpacing = appearance.letterSpacing, + bubbleRadius = appearance.bubbleRadius, + showLinkPreviews = appearance.showLinkPreviews, onReplyClick = onGoToReply, onReactionClick = { onReactionClick(msg.id, it) }, onInstantViewClick = onInstantViewClick, onYouTubeClick = onYouTubeClick, onClick = { offset -> - onReplyClick(bubblePosition, bubbleSize, bubblePosition + offset) + onReplyClickState( + layoutTracker.bubblePosition, + layoutTracker.bubbleSize, + layoutTracker.bubblePosition + offset + ) }, onLongClick = { offset -> - onReplyClick(bubblePosition, bubbleSize, bubblePosition + offset) + onReplyClickState( + layoutTracker.bubblePosition, + layoutTracker.bubbleSize, + layoutTracker.bubblePosition + offset + ) }, onCommentsClick = onCommentsClick, showComments = showComments, @@ -218,22 +228,22 @@ fun ChannelMessageBubbleContainer( ChannelPhotoMessageBubble( content = content, msg = msg, - isSameSenderAbove = isSameSenderAbove, - isSameSenderBelow = isSameSenderBelow, - fontSize = fontSize, - letterSpacing = letterSpacing, - bubbleRadius = bubbleRadius, - autoDownloadMobile = autoDownloadMobile, - autoDownloadWifi = autoDownloadWifi, - autoDownloadRoaming = autoDownloadRoaming, + isSameSenderAbove = senderGrouping.isSameSenderAbove, + isSameSenderBelow = senderGrouping.isSameSenderBelow, + fontSize = appearance.fontSize, + letterSpacing = appearance.letterSpacing, + bubbleRadius = appearance.bubbleRadius, + autoDownloadMobile = appearance.autoDownloadMobile, + autoDownloadWifi = appearance.autoDownloadWifi, + autoDownloadRoaming = appearance.autoDownloadRoaming, onPhotoClick = onPhotoClick, onDownloadPhoto = onDownloadPhoto, onCancelDownload = onCancelDownload, onLongClick = { offset -> - onReplyClick( - bubblePosition, - bubbleSize, - bubblePosition + offset + onReplyClickState( + layoutTracker.bubblePosition, + layoutTracker.bubbleSize, + layoutTracker.bubblePosition + offset ) }, onReplyClick = onGoToReply, @@ -251,22 +261,22 @@ fun ChannelMessageBubbleContainer( ChannelVideoMessageBubble( content = content, msg = msg, - isSameSenderAbove = isSameSenderAbove, - isSameSenderBelow = isSameSenderBelow, - fontSize = fontSize, - letterSpacing = letterSpacing, - bubbleRadius = bubbleRadius, - autoDownloadMobile = autoDownloadMobile, - autoDownloadWifi = autoDownloadWifi, - autoDownloadRoaming = autoDownloadRoaming, - autoplayVideos = autoplayVideos, + isSameSenderAbove = senderGrouping.isSameSenderAbove, + isSameSenderBelow = senderGrouping.isSameSenderBelow, + fontSize = appearance.fontSize, + letterSpacing = appearance.letterSpacing, + bubbleRadius = appearance.bubbleRadius, + autoDownloadMobile = appearance.autoDownloadMobile, + autoDownloadWifi = appearance.autoDownloadWifi, + autoDownloadRoaming = appearance.autoDownloadRoaming, + autoplayVideos = appearance.autoplayVideos, onVideoClick = onVideoClick, onCancelDownload = onCancelDownload, onLongClick = { offset -> - onReplyClick( - bubblePosition, - bubbleSize, - bubblePosition + offset + onReplyClickState( + layoutTracker.bubblePosition, + layoutTracker.bubbleSize, + layoutTracker.bubblePosition + offset ) }, onReplyClick = onGoToReply, @@ -277,7 +287,7 @@ fun ChannelMessageBubbleContainer( onForwardOriginClick = onForwardOriginClick, modifier = Modifier.fillMaxWidth(), downloadUtils = downloadUtils, - isAnyViewerOpen = isAnyViewerOpen + isAnyViewerOpen = behavior.isAnyViewerOpen ) } @@ -286,21 +296,21 @@ fun ChannelMessageBubbleContainer( content = content, msg = msg, isOutgoing = false, - isSameSenderAbove = isSameSenderAbove, - isSameSenderBelow = isSameSenderBelow, - fontSize = fontSize, - letterSpacing = letterSpacing, - autoDownloadFiles = autoDownloadFiles, - autoDownloadMobile = autoDownloadMobile, - autoDownloadWifi = autoDownloadWifi, - autoDownloadRoaming = autoDownloadRoaming, + isSameSenderAbove = senderGrouping.isSameSenderAbove, + isSameSenderBelow = senderGrouping.isSameSenderBelow, + fontSize = appearance.fontSize, + letterSpacing = appearance.letterSpacing, + autoDownloadFiles = appearance.autoDownloadFiles, + autoDownloadMobile = appearance.autoDownloadMobile, + autoDownloadWifi = appearance.autoDownloadWifi, + autoDownloadRoaming = appearance.autoDownloadRoaming, onDocumentClick = onDocumentClick, onCancelDownload = onCancelDownload, onLongClick = { offset -> - onReplyClick( - bubblePosition, - bubbleSize, - bubblePosition + offset + onReplyClickState( + layoutTracker.bubblePosition, + layoutTracker.bubbleSize, + layoutTracker.bubblePosition + offset ) }, toProfile = toProfile, @@ -317,21 +327,21 @@ fun ChannelMessageBubbleContainer( content = content, msg = msg, isOutgoing = false, - isSameSenderAbove = isSameSenderAbove, - isSameSenderBelow = isSameSenderBelow, - fontSize = fontSize, - letterSpacing = letterSpacing, - autoDownloadFiles = autoDownloadFiles, - autoDownloadMobile = autoDownloadMobile, - autoDownloadWifi = autoDownloadWifi, - autoDownloadRoaming = autoDownloadRoaming, + isSameSenderAbove = senderGrouping.isSameSenderAbove, + isSameSenderBelow = senderGrouping.isSameSenderBelow, + fontSize = appearance.fontSize, + letterSpacing = appearance.letterSpacing, + autoDownloadFiles = appearance.autoDownloadFiles, + autoDownloadMobile = appearance.autoDownloadMobile, + autoDownloadWifi = appearance.autoDownloadWifi, + autoDownloadRoaming = appearance.autoDownloadRoaming, onAudioClick = onAudioClick, onCancelDownload = onCancelDownload, onLongClick = { offset -> - onReplyClick( - bubblePosition, - bubbleSize, - bubblePosition + offset + onReplyClickState( + layoutTracker.bubblePosition, + layoutTracker.bubbleSize, + layoutTracker.bubblePosition + offset ) }, toProfile = toProfile, @@ -347,22 +357,22 @@ fun ChannelMessageBubbleContainer( ChannelGifMessageBubble( content = content, msg = msg, - isSameSenderAbove = isSameSenderAbove, - isSameSenderBelow = isSameSenderBelow, - fontSize = fontSize, - letterSpacing = letterSpacing, - bubbleRadius = bubbleRadius, - autoDownloadMobile = autoDownloadMobile, - autoDownloadWifi = autoDownloadWifi, - autoDownloadRoaming = autoDownloadRoaming, - autoplayGifs = autoplayGifs, + isSameSenderAbove = senderGrouping.isSameSenderAbove, + isSameSenderBelow = senderGrouping.isSameSenderBelow, + fontSize = appearance.fontSize, + letterSpacing = appearance.letterSpacing, + bubbleRadius = appearance.bubbleRadius, + autoDownloadMobile = appearance.autoDownloadMobile, + autoDownloadWifi = appearance.autoDownloadWifi, + autoDownloadRoaming = appearance.autoDownloadRoaming, + autoplayGifs = appearance.autoplayGifs, onGifClick = onVideoClick, onCancelDownload = onCancelDownload, onLongClick = { offset -> - onReplyClick( - bubblePosition, - bubbleSize, - bubblePosition + offset + onReplyClickState( + layoutTracker.bubblePosition, + layoutTracker.bubbleSize, + layoutTracker.bubblePosition + offset ) }, onReplyClick = onGoToReply, @@ -373,7 +383,7 @@ fun ChannelMessageBubbleContainer( onForwardOriginClick = onForwardOriginClick, modifier = Modifier.fillMaxWidth(), downloadUtils = downloadUtils, - isAnyViewerOpen = isAnyViewerOpen + isAnyViewerOpen = behavior.isAnyViewerOpen ) } @@ -382,15 +392,15 @@ fun ChannelMessageBubbleContainer( content = content, msg = msg, isOutgoing = false, - stickerSize = stickerSize, + stickerSize = appearance.stickerSize, onReplyClick = onGoToReply, onReactionClick = { onReactionClick(msg.id, it) }, onStickerClick = { onStickerClick(content.setId) }, onLongClick = { - onReplyClick( - bubblePosition, - bubbleSize, - bubblePosition + (bubbleSize.toSize() / 2f).toOffset() + onReplyClickState( + layoutTracker.bubblePosition, + layoutTracker.bubbleSize, + layoutTracker.bubblePosition + (layoutTracker.bubbleSize.toSize() / 2f).toOffset() ) }, toProfile = toProfile, @@ -402,11 +412,11 @@ fun ChannelMessageBubbleContainer( ChannelPollMessageBubble( content = content, msg = msg, - isSameSenderAbove = isSameSenderAbove, - isSameSenderBelow = isSameSenderBelow, - fontSize = fontSize, - letterSpacing = letterSpacing, - bubbleRadius = bubbleRadius, + isSameSenderAbove = senderGrouping.isSameSenderAbove, + isSameSenderBelow = senderGrouping.isSameSenderBelow, + fontSize = appearance.fontSize, + letterSpacing = appearance.letterSpacing, + bubbleRadius = appearance.bubbleRadius, onOptionClick = { onPollOptionClick(msg.id, it) }, onRetractVote = { onRetractVote(msg.id) }, onShowVoters = { onShowVoters(msg.id, it) }, @@ -414,10 +424,10 @@ fun ChannelMessageBubbleContainer( onReplyClick = onGoToReply, onReactionClick = { onReactionClick(msg.id, it) }, onLongClick = { offset -> - onReplyClick( - bubblePosition, - bubbleSize, - bubblePosition + offset + onReplyClickState( + layoutTracker.bubblePosition, + layoutTracker.bubbleSize, + layoutTracker.bubblePosition + offset ) }, onCommentsClick = onCommentsClick, diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentList.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentList.kt index 435d9d79..1ae2928f 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentList.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentList.kt @@ -87,9 +87,14 @@ import org.monogram.presentation.core.util.IDownloadUtils import org.monogram.presentation.features.chats.conversation.ChatComponent import org.monogram.presentation.features.chats.conversation.ui.AlbumMessageBubbleContainer import org.monogram.presentation.features.chats.conversation.ui.DateSeparator +import org.monogram.presentation.features.chats.conversation.ui.MessageAppearanceConfig import org.monogram.presentation.features.chats.conversation.ui.MessageBubbleContainer +import org.monogram.presentation.features.chats.conversation.ui.MessageRowBehaviorConfig +import org.monogram.presentation.features.chats.conversation.ui.MessageRowUiFlags +import org.monogram.presentation.features.chats.conversation.ui.MessageSenderGrouping import org.monogram.presentation.features.chats.conversation.ui.ServiceMessage import org.monogram.presentation.features.chats.conversation.ui.UnreadMessagesSeparator +import org.monogram.presentation.features.chats.conversation.ui.buildSenderGrouping import org.monogram.presentation.features.chats.conversation.ui.channel.ChannelMessageBubbleContainer import org.monogram.presentation.features.stickers.ui.view.StickerImage import java.io.File @@ -171,6 +176,7 @@ fun ChatContentList( bottomContentPadding: Dp = 8.dp ) { val isComments = state.isComments + val appearance = state.toAppearanceConfig() val isScrolling by remember(scrollState) { derivedStateOf { scrollState.isScrollInProgress } } val latestState by rememberUpdatedState(state) var lastOlderLoadTriggerUptimeMs by remember { mutableLongStateOf(0L) } @@ -417,13 +423,22 @@ fun ChatContentList( MessageRowItem( item = item, - state = state, + appearance = appearance, component = component, olderMsg = olderMsg, newerMsg = newerMsg, - isSelected = isItemSelected(item, state.selectedMessageIds), - isSelectionMode = state.selectedMessageIds.isNotEmpty(), - selectedMessageId = selectedMessageId, + behavior = state.toBehaviorConfig( + isSelectionMode = state.selectedMessageIds.isNotEmpty(), + isAnyViewerOpen = isAnyViewerOpen + ), + uiFlags = MessageRowUiFlags( + isSelected = isItemSelected(item, state.selectedMessageIds), + isHighlighted = isItemHighlighted(item, state.highlightedMessageId), + showUnreadSeparator = index == unreadBoundaryIndex && !hasUnreadSeparatorDismissed, + unreadCount = state.unreadSeparatorCount, + shouldReportPosition = item.lastMessageId == selectedMessageId + ), + rootMessageId = state.rootMessage?.id, onPhotoClick = onPhotoClick, onPhotoDownload = onPhotoDownload, onVideoClick = onVideoClick, @@ -435,13 +450,11 @@ fun ChatContentList( onViaBotClick = onViaBotClick, toProfile = toProfile, onForwardOriginClick = onForwardOriginClick, + isChatAnimationsEnabled = state.isChatAnimationsEnabled, isScrolling = isScrolling, isEntryAnimationPending = pendingEntryAnimationIds.containsKey(item.firstMessageId), onEntryAnimationConsumed = { pendingEntryAnimationIds.remove(it) }, - downloadUtils = downloadUtils, - isAnyViewerOpen = isAnyViewerOpen, - showUnreadSeparator = index == unreadBoundaryIndex && !hasUnreadSeparatorDismissed, - unreadCount = state.unreadSeparatorCount + downloadUtils = downloadUtils ) } } else { @@ -476,13 +489,22 @@ fun ChatContentList( MessageRowItem( item = item, - state = state, + appearance = appearance, component = component, olderMsg = olderMsg, newerMsg = newerMsg, - isSelected = isItemSelected(item, state.selectedMessageIds), - isSelectionMode = state.selectedMessageIds.isNotEmpty(), - selectedMessageId = selectedMessageId, + behavior = state.toBehaviorConfig( + isSelectionMode = state.selectedMessageIds.isNotEmpty(), + isAnyViewerOpen = isAnyViewerOpen + ), + uiFlags = MessageRowUiFlags( + isSelected = isItemSelected(item, state.selectedMessageIds), + isHighlighted = isItemHighlighted(item, state.highlightedMessageId), + showUnreadSeparator = index == unreadBoundaryIndex && !hasUnreadSeparatorDismissed, + unreadCount = state.unreadSeparatorCount, + shouldReportPosition = item.lastMessageId == selectedMessageId + ), + rootMessageId = state.rootMessage?.id, onPhotoClick = onPhotoClick, onPhotoDownload = onPhotoDownload, onVideoClick = onVideoClick, @@ -494,13 +516,11 @@ fun ChatContentList( onViaBotClick = onViaBotClick, toProfile = toProfile, onForwardOriginClick = onForwardOriginClick, + isChatAnimationsEnabled = state.isChatAnimationsEnabled, isScrolling = isScrolling, isEntryAnimationPending = pendingEntryAnimationIds.containsKey(item.firstMessageId), onEntryAnimationConsumed = { pendingEntryAnimationIds.remove(it) }, - downloadUtils = downloadUtils, - isAnyViewerOpen = isAnyViewerOpen, - showUnreadSeparator = index == unreadBoundaryIndex && !hasUnreadSeparatorDismissed, - unreadCount = state.unreadSeparatorCount + downloadUtils = downloadUtils ) } } @@ -560,13 +580,13 @@ private fun PagingLoadingIndicator() { @Composable private fun MessageRowItem( item: GroupedMessageItem, - state: ChatMessageListUiState, + appearance: MessageAppearanceConfig, component: ChatComponent, olderMsg: MessageModel?, newerMsg: MessageModel?, - isSelected: Boolean, - isSelectionMode: Boolean, - selectedMessageId: Long?, + behavior: MessageRowBehaviorConfig, + uiFlags: MessageRowUiFlags, + rootMessageId: Long?, onPhotoClick: (MessageModel, List, List, List, Int) -> Unit, onPhotoDownload: (Int) -> Unit, onVideoClick: (MessageModel, String?, String?) -> Unit, @@ -578,20 +598,18 @@ private fun MessageRowItem( onViaBotClick: (String) -> Unit, toProfile: (Long) -> Unit, onForwardOriginClick: (ForwardInfo) -> Unit, + isChatAnimationsEnabled: Boolean, isScrolling: Boolean, isEntryAnimationPending: Boolean, onEntryAnimationConsumed: (Long) -> Unit, - downloadUtils: IDownloadUtils, - isAnyViewerOpen: Boolean = false, - showUnreadSeparator: Boolean, - unreadCount: Int + downloadUtils: IDownloadUtils ) { val mainMsg = remember(item) { if (item is GroupedMessageItem.Single) item.message else (item as GroupedMessageItem.Album).messages.last() } val shouldAnimateEntry = - state.isChatAnimationsEnabled && isEntryAnimationPending && !isScrolling + isChatAnimationsEnabled && isEntryAnimationPending && !isScrolling val scale = remember(mainMsg.id) { Animatable( @@ -631,10 +649,13 @@ private fun MessageRowItem( } val backgroundColor by animateColorAsState( - targetValue = if (isSelected) MaterialTheme.colorScheme.primary.copy(alpha = 0.15f) else Color.Transparent, + targetValue = if (uiFlags.isSelected) MaterialTheme.colorScheme.primary.copy(alpha = 0.15f) else Color.Transparent, label = "bg" ) - val horizontalPadding by animateDpAsState(if (isSelectionMode) 16.dp else 8.dp, label = "padding") + val horizontalPadding by animateDpAsState( + if (behavior.isSelectionMode) 16.dp else 8.dp, + label = "padding" + ) Box( modifier = Modifier @@ -649,7 +670,7 @@ private fun MessageRowItem( .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null, - enabled = isSelectionMode, + enabled = behavior.isSelectionMode, onClick = { component.onToggleMessageSelection(mainMsg.id) } ) ) { @@ -659,8 +680,15 @@ private fun MessageRowItem( .padding(horizontal = horizontalPadding, vertical = 1.dp), verticalAlignment = Alignment.Bottom ) { - AnimatedVisibility(visible = isSelectionMode, enter = expandHorizontally(), exit = shrinkHorizontally()) { - SelectionIndicator(isSelected = isSelected, modifier = Modifier.padding(end = 12.dp, bottom = 4.dp)) + AnimatedVisibility( + visible = behavior.isSelectionMode, + enter = expandHorizontally(), + exit = shrinkHorizontally() + ) { + SelectionIndicator( + isSelected = uiFlags.isSelected, + modifier = Modifier.padding(end = 12.dp, bottom = 4.dp) + ) } Column(modifier = Modifier.weight(1f)) { @@ -669,21 +697,22 @@ private fun MessageRowItem( Spacer(modifier = Modifier.height(16.dp)) } - AnimatedVisibility(visible = showUnreadSeparator && !isScrolling) { + AnimatedVisibility(visible = uiFlags.showUnreadSeparator && !isScrolling) { Column { - UnreadMessagesSeparator(unreadCount = unreadCount) + UnreadMessagesSeparator(unreadCount = uiFlags.unreadCount) Spacer(modifier = Modifier.height(16.dp)) } } MessageBubbleSwitcher( item = item, - state = state, + appearance = appearance, component = component, olderMsg = olderMsg, newerMsg = newerMsg, - isSelectionMode = isSelectionMode, - selectedMessageId = selectedMessageId, + behavior = behavior, + uiFlags = uiFlags, + rootMessageId = rootMessageId, onPhotoClick = onPhotoClick, onPhotoDownload = onPhotoDownload, onVideoClick = onVideoClick, @@ -695,8 +724,7 @@ private fun MessageRowItem( onViaBotClick = onViaBotClick, toProfile = toProfile, onForwardOriginClick = onForwardOriginClick, - downloadUtils = downloadUtils, - isAnyViewerOpen = isAnyViewerOpen + downloadUtils = downloadUtils ) } } @@ -706,12 +734,13 @@ private fun MessageRowItem( @Composable private fun MessageBubbleSwitcher( item: GroupedMessageItem, - state: ChatMessageListUiState, + appearance: MessageAppearanceConfig, component: ChatComponent, olderMsg: MessageModel?, newerMsg: MessageModel?, - isSelectionMode: Boolean, - selectedMessageId: Long?, + behavior: MessageRowBehaviorConfig, + uiFlags: MessageRowUiFlags, + rootMessageId: Long?, onPhotoClick: (MessageModel, List, List, List, Int) -> Unit, onPhotoDownload: (Int) -> Unit, onVideoClick: (MessageModel, String?, String?) -> Unit, @@ -723,61 +752,66 @@ private fun MessageBubbleSwitcher( onViaBotClick: (String) -> Unit, toProfile: (Long) -> Unit, onForwardOriginClick: (ForwardInfo) -> Unit, - downloadUtils: IDownloadUtils, - isAnyViewerOpen: Boolean = false + downloadUtils: IDownloadUtils ) { - val isChannel = state.isChannelFeed - val isTopicClosed = state.isCurrentTopicClosed - val sanitizedItem = remember(item, state.rootMessage) { - item.withSuppressedRootReply(state.rootMessage?.id) + val sanitizedItem = remember(item, rootMessageId) { + item.withSuppressedRootReply(rootMessageId) } - val sanitizedOlderMsg = remember(olderMsg, state.rootMessage) { - olderMsg?.suppressRootReply(state.rootMessage?.id) + val sanitizedOlderMsg = remember(olderMsg, rootMessageId) { + olderMsg?.suppressRootReply(rootMessageId) } - val sanitizedNewerMsg = remember(newerMsg, state.rootMessage) { - newerMsg?.suppressRootReply(state.rootMessage?.id) + val sanitizedNewerMsg = remember(newerMsg, rootMessageId) { + newerMsg?.suppressRootReply(rootMessageId) + } + val senderGrouping = remember(sanitizedItem, sanitizedOlderMsg, sanitizedNewerMsg) { + buildSenderGrouping( + item = sanitizedItem, + olderMsg = sanitizedOlderMsg, + newerMsg = sanitizedNewerMsg + ) } when (sanitizedItem) { is GroupedMessageItem.Single -> { if (sanitizedItem.message.content is MessageContent.Service) { ServiceMessage(service = sanitizedItem.message.content as MessageContent.Service) - } else if (isChannel) { + } else if (behavior.isChannel) { ChannelMessageBubbleContainer( msg = sanitizedItem.message, - olderMsg = sanitizedOlderMsg, newerMsg = sanitizedNewerMsg, - autoplayGifs = state.autoplayGifs, - autoplayVideos = state.autoplayVideos, - autoDownloadFiles = state.autoDownloadFiles, - highlighted = state.highlightedMessageId == sanitizedItem.message.id, + appearance = appearance, + behavior = behavior, + uiFlags = uiFlags, + senderGrouping = senderGrouping, onHighlightConsumed = { component.onHighlightConsumed() }, onPhotoClick = { - if (isSelectionMode) component.onToggleMessageSelection(it.id) else handlePhotoClick( + if (behavior.isSelectionMode) component.onToggleMessageSelection(it.id) else handlePhotoClick( it, onPhotoClick ) }, onDownloadPhoto = onPhotoDownload, onVideoClick = { - if (isSelectionMode) component.onToggleMessageSelection(it.id) else handleVideoClick( + if (behavior.isSelectionMode) component.onToggleMessageSelection(it.id) else handleVideoClick( it, onVideoClick ) }, onDocumentClick = { - if (isSelectionMode) component.onToggleMessageSelection(it.id) else onDocumentClick( + if (behavior.isSelectionMode) component.onToggleMessageSelection(it.id) else onDocumentClick( it ) }, onAudioClick = { - if (isSelectionMode) component.onToggleMessageSelection(it.id) else onAudioClick( + if (behavior.isSelectionMode) component.onToggleMessageSelection(it.id) else onAudioClick( it ) }, onCancelDownload = { component.onCancelDownloadFile(it) }, onReplyClick = { pos, size, click -> - if (isSelectionMode) component.onToggleMessageSelection(sanitizedItem.message.id) else onMessageOptionsClick( + if (behavior.isSelectionMode) component.onToggleMessageSelection( + sanitizedItem.message.id + ) else onMessageOptionsClick( sanitizedItem.message, pos, size, @@ -786,7 +820,7 @@ private fun MessageBubbleSwitcher( }, onGoToReply = onGoToReply, onReactionClick = { id, r -> - if (isSelectionMode) component.onToggleMessageSelection(id) else component.onSendReaction( + if (behavior.isSelectionMode) component.onToggleMessageSelection(id) else component.onSendReaction( id, r ) @@ -799,94 +833,81 @@ private fun MessageBubbleSwitcher( ) }, onStickerClick = { - if (isSelectionMode) component.onToggleMessageSelection(sanitizedItem.message.id) else component.onStickerClick( + if (behavior.isSelectionMode) component.onToggleMessageSelection( + sanitizedItem.message.id + ) else component.onStickerClick( it ) }, onPollOptionClick = { id, opt -> - if (isSelectionMode) component.onToggleMessageSelection(id) else component.onPollOptionClick( + if (behavior.isSelectionMode) component.onToggleMessageSelection(id) else component.onPollOptionClick( id, opt ) }, onRetractVote = { - if (isSelectionMode) component.onToggleMessageSelection(it) else component.onRetractVote( + if (behavior.isSelectionMode) component.onToggleMessageSelection(it) else component.onRetractVote( it ) }, onShowVoters = { id, opt -> - if (isSelectionMode) component.onToggleMessageSelection(id) else component.onShowVoters( + if (behavior.isSelectionMode) component.onToggleMessageSelection(id) else component.onShowVoters( id, opt ) }, onClosePoll = { - if (isSelectionMode) component.onToggleMessageSelection(it) else component.onClosePoll( + if (behavior.isSelectionMode) component.onToggleMessageSelection(it) else component.onClosePoll( it ) }, - fontSize = state.fontSize, - letterSpacing = state.letterSpacing, - bubbleRadius = state.bubbleRadius, - stickerSize = state.stickerSize, - shouldReportPosition = sanitizedItem.message.id == selectedMessageId, onPositionChange = { _, pos, size -> onMessagePositionChange(pos, size) }, onCommentsClick = { component.onCommentsClick(it) }, toProfile = toProfile, onForwardOriginClick = onForwardOriginClick, onViaBotClick = onViaBotClick, - canReply = state.canWrite && !isSelectionMode && state.canSendAnything, onReplySwipe = { component.onReplyMessage(it) }, onYouTubeClick = { component.onOpenYouTube(it) }, onInstantViewClick = { component.onOpenInstantView(it) }, - downloadUtils = downloadUtils, - isAnyViewerOpen = isAnyViewerOpen + downloadUtils = downloadUtils ) } else { MessageBubbleContainer( msg = sanitizedItem.message, - olderMsg = sanitizedOlderMsg, newerMsg = sanitizedNewerMsg, - 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 == sanitizedItem.message.id, + appearance = appearance, + behavior = behavior, + uiFlags = uiFlags, + senderGrouping = senderGrouping, onHighlightConsumed = { component.onHighlightConsumed() }, onPhotoClick = { - if (isSelectionMode) component.onToggleMessageSelection(it.id) else handlePhotoClick( + if (behavior.isSelectionMode) component.onToggleMessageSelection(it.id) else handlePhotoClick( it, onPhotoClick ) }, onDownloadPhoto = onPhotoDownload, onVideoClick = { - if (isSelectionMode) component.onToggleMessageSelection(it.id) else handleVideoClick( + if (behavior.isSelectionMode) component.onToggleMessageSelection(it.id) else handleVideoClick( it, onVideoClick ) }, onDocumentClick = { - if (isSelectionMode) component.onToggleMessageSelection(it.id) else onDocumentClick( + if (behavior.isSelectionMode) component.onToggleMessageSelection(it.id) else onDocumentClick( it ) }, onAudioClick = { - if (isSelectionMode) component.onToggleMessageSelection(it.id) else onAudioClick( + if (behavior.isSelectionMode) component.onToggleMessageSelection(it.id) else onAudioClick( it ) }, onCancelDownload = { component.onCancelDownloadFile(it) }, onReplyClick = { pos, size, click -> - if (isSelectionMode) component.onToggleMessageSelection(sanitizedItem.message.id) else onMessageOptionsClick( + if (behavior.isSelectionMode) component.onToggleMessageSelection( + sanitizedItem.message.id + ) else onMessageOptionsClick( sanitizedItem.message, pos, size, @@ -895,7 +916,7 @@ private fun MessageBubbleSwitcher( }, onGoToReply = onGoToReply, onReactionClick = { id, r -> - if (isSelectionMode) component.onToggleMessageSelection(id) else component.onSendReaction( + if (behavior.isSelectionMode) component.onToggleMessageSelection(id) else component.onSendReaction( id, r ) @@ -908,44 +929,42 @@ private fun MessageBubbleSwitcher( ) }, onStickerClick = { - if (isSelectionMode) component.onToggleMessageSelection(sanitizedItem.message.id) else component.onStickerClick( + if (behavior.isSelectionMode) component.onToggleMessageSelection( + sanitizedItem.message.id + ) else component.onStickerClick( it ) }, onPollOptionClick = { id, opt -> - if (isSelectionMode) component.onToggleMessageSelection(id) else component.onPollOptionClick( + if (behavior.isSelectionMode) component.onToggleMessageSelection(id) else component.onPollOptionClick( id, opt ) }, onRetractVote = { - if (isSelectionMode) component.onToggleMessageSelection(it) else component.onRetractVote( + if (behavior.isSelectionMode) component.onToggleMessageSelection(it) else component.onRetractVote( it ) }, onShowVoters = { id, opt -> - if (isSelectionMode) component.onToggleMessageSelection(id) else component.onShowVoters( + if (behavior.isSelectionMode) component.onToggleMessageSelection(id) else component.onShowVoters( id, opt ) }, onClosePoll = { - if (isSelectionMode) component.onToggleMessageSelection(it) else component.onClosePoll( + if (behavior.isSelectionMode) component.onToggleMessageSelection(it) else component.onClosePoll( it ) }, onInstantViewClick = { component.onOpenInstantView(it) }, onYouTubeClick = { component.onOpenYouTube(it) }, - shouldReportPosition = sanitizedItem.message.id == selectedMessageId, onPositionChange = { _, pos, size -> onMessagePositionChange(pos, size) }, toProfile = toProfile, onForwardOriginClick = onForwardOriginClick, onViaBotClick = onViaBotClick, - canReply = state.canWrite && !isSelectionMode && (!isTopicClosed || state.isAdmin) && state.canSendAnything, onReplySwipe = { component.onReplyMessage(it) }, - swipeEnabled = !isSelectionMode, - downloadUtils = downloadUtils, - isAnyViewerOpen = isAnyViewerOpen + downloadUtils = downloadUtils ) } } @@ -953,17 +972,12 @@ private fun MessageBubbleSwitcher( is GroupedMessageItem.Album -> { AlbumMessageBubbleContainer( messages = sanitizedItem.messages, - olderMsg = sanitizedOlderMsg, - newerMsg = sanitizedNewerMsg, - isGroup = state.isGroup || state.currentTopicId != null, - isChannel = isChannel, - autoplayGifs = state.autoplayGifs, - autoplayVideos = state.autoplayVideos, - autoDownloadMobile = state.autoDownloadMobile, - autoDownloadWifi = state.autoDownloadWifi, - autoDownloadRoaming = state.autoDownloadRoaming, + behavior = behavior, + appearance = appearance, + uiFlags = uiFlags, + senderGrouping = senderGrouping, onPhotoClick = { - if (isSelectionMode) component.onToggleMessageSelection(it.id) else handleAlbumPhotoClick( + if (behavior.isSelectionMode) component.onToggleMessageSelection(it.id) else handleAlbumPhotoClick( it, sanitizedItem.messages, onPhotoClick @@ -971,7 +985,7 @@ private fun MessageBubbleSwitcher( }, onDownloadPhoto = onPhotoDownload, onVideoClick = { - if (isSelectionMode) component.onToggleMessageSelection(it.id) else handleAlbumVideoClick( + if (behavior.isSelectionMode) component.onToggleMessageSelection(it.id) else handleAlbumVideoClick( it, sanitizedItem.messages, onPhotoClick, @@ -979,18 +993,18 @@ private fun MessageBubbleSwitcher( ) }, onDocumentClick = { - if (isSelectionMode) component.onToggleMessageSelection(it.id) else onDocumentClick( + if (behavior.isSelectionMode) component.onToggleMessageSelection(it.id) else onDocumentClick( it ) }, onAudioClick = { - if (isSelectionMode) component.onToggleMessageSelection(it.id) else onAudioClick( + if (behavior.isSelectionMode) component.onToggleMessageSelection(it.id) else onAudioClick( it ) }, onCancelDownload = { component.onCancelDownloadFile(it) }, onReplyClick = { pos, size, click -> - if (isSelectionMode) component.onToggleMessageSelection(sanitizedItem.messages.last().id) else onMessageOptionsClick( + if (behavior.isSelectionMode) component.onToggleMessageSelection(sanitizedItem.messages.last().id) else onMessageOptionsClick( sanitizedItem.messages.last(), pos, size, @@ -999,22 +1013,18 @@ private fun MessageBubbleSwitcher( }, onGoToReply = onGoToReply, onReactionClick = { id, r -> - if (isSelectionMode) component.onToggleMessageSelection(id) else component.onSendReaction( + if (behavior.isSelectionMode) component.onToggleMessageSelection(id) else component.onSendReaction( id, r ) }, - shouldReportPosition = sanitizedItem.messages.last().id == selectedMessageId, onPositionChange = { _, pos, size -> onMessagePositionChange(pos, size) }, onCommentsClick = { component.onCommentsClick(it) }, toProfile = toProfile, onForwardOriginClick = onForwardOriginClick, onViaBotClick = onViaBotClick, - canReply = state.canWrite && !isSelectionMode && (!isTopicClosed || state.isAdmin) && state.canSendAnything, onReplySwipe = { component.onReplyMessage(it) }, - swipeEnabled = !isSelectionMode, - downloadUtils = downloadUtils, - isAnyViewerOpen = isAnyViewerOpen + downloadUtils = downloadUtils ) } } @@ -1061,6 +1071,21 @@ private fun RootMessageSection( isAnyViewerOpen: Boolean = false ) { val root = state.rootMessage ?: return + val appearance = state.toAppearanceConfig() + val behavior = MessageRowBehaviorConfig( + isGroup = state.isGroup, + isChannel = state.isChannel, + isTopicClosed = state.isCurrentTopicClosed, + canReply = false, + swipeEnabled = false, + isSelectionMode = false, + isAnyViewerOpen = isAnyViewerOpen + ) + val rootUiFlags = MessageRowUiFlags() + val senderGrouping = MessageSenderGrouping( + isSameSenderAbove = false, + isSameSenderBelow = false + ) Column( modifier = Modifier .fillMaxWidth() @@ -1068,9 +1093,12 @@ private fun RootMessageSection( ) { if (state.isChannel) { ChannelMessageBubbleContainer( - msg = root, olderMsg = null, newerMsg = null, - autoplayGifs = state.autoplayGifs, autoplayVideos = state.autoplayVideos, - autoDownloadFiles = state.autoDownloadFiles, + msg = root, + newerMsg = null, + appearance = appearance, + behavior = behavior, + uiFlags = rootUiFlags, + senderGrouping = senderGrouping, onPhotoClick = { handlePhotoClick(it, onPhotoClick) }, onDownloadPhoto = onPhotoDownload, onVideoClick = { handleVideoClick(it, onVideoClick) }, @@ -1086,29 +1114,22 @@ 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, onCommentsClick = {}, showComments = false, toProfile = toProfile, onForwardOriginClick = onForwardOriginClick, onViaBotClick = onViaBotClick, onYouTubeClick = { component.onOpenYouTube(it) }, onInstantViewClick = { component.onOpenInstantView(it) }, - downloadUtils = downloadUtils, - isAnyViewerOpen = isAnyViewerOpen + downloadUtils = downloadUtils ) } 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, + newerMsg = null, + appearance = appearance, + behavior = behavior, + uiFlags = rootUiFlags, + senderGrouping = senderGrouping, onPhotoClick = { handlePhotoClick(it, onPhotoClick) }, onDownloadPhoto = onPhotoDownload, onVideoClick = { handleVideoClick(it, onVideoClick) }, @@ -1126,12 +1147,10 @@ private fun RootMessageSection( onClosePoll = { component.onClosePoll(it) }, toProfile = toProfile, onForwardOriginClick = onForwardOriginClick, - swipeEnabled = false, onViaBotClick = onViaBotClick, onInstantViewClick = { component.onOpenInstantView(it) }, onYouTubeClick = { component.onOpenYouTube(it) }, - downloadUtils = downloadUtils, - isAnyViewerOpen = isAnyViewerOpen + downloadUtils = downloadUtils ) } @@ -1158,6 +1177,49 @@ private fun isItemSelected(item: GroupedMessageItem, selectedIds: Set): Bo } } +private val GroupedMessageItem.lastMessageId: Long + get() = when (this) { + is GroupedMessageItem.Single -> message.id + is GroupedMessageItem.Album -> messages.last().id + } + +private fun isItemHighlighted(item: GroupedMessageItem, highlightedMessageId: Long?): Boolean { + if (highlightedMessageId == null) return false + return when (item) { + is GroupedMessageItem.Single -> item.message.id == highlightedMessageId + is GroupedMessageItem.Album -> item.messages.any { it.id == highlightedMessageId } + } +} + +private fun ChatMessageListUiState.toAppearanceConfig(): MessageAppearanceConfig = + MessageAppearanceConfig( + fontSize = fontSize, + letterSpacing = letterSpacing, + bubbleRadius = bubbleRadius, + stickerSize = stickerSize, + showLinkPreviews = showLinkPreviews, + autoplayGifs = autoplayGifs, + autoplayVideos = autoplayVideos, + autoDownloadMobile = autoDownloadMobile, + autoDownloadWifi = autoDownloadWifi, + autoDownloadRoaming = autoDownloadRoaming, + autoDownloadFiles = autoDownloadFiles + ) + +private fun ChatMessageListUiState.toBehaviorConfig( + isSelectionMode: Boolean, + isAnyViewerOpen: Boolean +): MessageRowBehaviorConfig = + MessageRowBehaviorConfig( + isGroup = isGroup || currentTopicId != null, + isChannel = isChannelFeed, + isTopicClosed = isCurrentTopicClosed, + canReply = canWrite && !isSelectionMode && (!isCurrentTopicClosed || isAdmin) && canSendAnything, + swipeEnabled = !isSelectionMode, + isSelectionMode = isSelectionMode, + isAnyViewerOpen = isAnyViewerOpen + ) + private fun GroupedMessageItem.withSuppressedRootReply(rootMessageId: Long?): GroupedMessageItem { return when (this) { is GroupedMessageItem.Single -> copy(message = message.suppressRootReply(rootMessageId)) diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/ChatInputBarComposerSection.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/ChatInputBarComposerSection.kt index 1fdbefd6..449a74b3 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/ChatInputBarComposerSection.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/ChatInputBarComposerSection.kt @@ -22,7 +22,6 @@ import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.windowInsetsPadding @@ -35,6 +34,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester @@ -42,10 +42,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import org.monogram.domain.models.BotCommandModel -import org.monogram.domain.models.BotMenuButtonModel import org.monogram.domain.models.GifModel import org.monogram.domain.models.KeyboardButtonModel import org.monogram.domain.models.MessageModel @@ -59,49 +56,22 @@ import org.monogram.presentation.features.chats.conversation.ui.message.BotComma import org.monogram.presentation.features.stickers.ui.menu.StickerEmojiMenu @Composable -fun ChatInputBarComposerSection( +internal fun ChatInputBarComposerSection( modifier: Modifier = Modifier, editingMessage: MessageModel?, replyMessage: MessageModel?, - pendingMediaPaths: List, - pendingDocumentPaths: List, - mentionSuggestions: List, - filteredCommands: List, - currentInlineBotUsername: String?, - isInlineBotLoading: Boolean, - inlineBotResults: org.monogram.domain.repository.InlineBotResultsModel?, - isBot: Boolean, - botMenuButton: BotMenuButtonModel, - botCommands: List, - scheduledMessagesCount: Int, - textValue: TextFieldValue, + attachments: ComposerAttachmentState, + suggestions: ComposerSuggestionState, + botState: ComposerBotState, + rowState: ComposerRowState, onTextValueChange: (TextFieldValue) -> Unit, knownCustomEmojis: MutableMap, emojiFontFamily: FontFamily, focusRequester: FocusRequester, - canWriteText: Boolean, - canOpenAttachSheet: Boolean, + capabilities: ChatInputBarCapabilities, canSendAttachments: Boolean, - canShowBotActions: Boolean, canPasteMediaFromClipboard: Boolean, - canSendStickers: Boolean, - canSendVoice: Boolean, - canSendVideoNotes: Boolean, - isStickerMenuVisible: Boolean, - closeStickerMenuWithoutSlide: Boolean, - isKeyboardVisible: Boolean, - stickerMenuHeight: Dp, voiceRecorder: VoiceRecorderState, - isGifSearchFocused: Boolean, - showFullScreenEditor: Boolean, - currentMessageLength: Int, - maxMessageLength: Int, - isOverMessageLimit: Boolean, - isVideoMessageMode: Boolean, - isSlowModeActive: Boolean, - slowModeRemainingSeconds: Int, - replyMarkup: ReplyMarkupModel?, - showSendOptionsSheet: Boolean, stickerRepository: StickerRepository, isTablet: Boolean = false, onCancelEdit: () -> Unit, @@ -144,6 +114,66 @@ fun ChatInputBarComposerSection( onGifSearchFocusedChange: (Boolean) -> Unit, onReplyMarkupButtonClick: (KeyboardButtonModel) -> Unit ) { + val inputUiState = remember( + attachments.pendingMediaPaths, + attachments.pendingDocumentPaths, + botState, + rowState.textValue, + rowState.editingMessage, + rowState.isStickerMenuVisible, + capabilities.canSendStickers, + capabilities.canWriteText, + capabilities.canOpenAttachSheet, + canPasteMediaFromClipboard + ) { + InputTextFieldUiState( + textValue = rowState.textValue, + isBot = botState.isBot, + botMenuButton = botState.botMenuButton, + botCommands = botState.botCommands, + canSendStickers = capabilities.canSendStickers, + canWriteText = capabilities.canWriteText, + canShowBotActions = capabilities.canWriteText, + isStickerMenuVisible = rowState.isStickerMenuVisible, + canAttachMedia = rowState.editingMessage == null && + attachments.pendingMediaPaths.isEmpty() && + attachments.pendingDocumentPaths.isEmpty() && + capabilities.canOpenAttachSheet, + canPasteMediaFromClipboard = canPasteMediaFromClipboard, + pendingMediaPaths = attachments.pendingMediaPaths, + pendingDocumentPaths = attachments.pendingDocumentPaths, + showExpandEditorAction = rowState.textValue.text.contains('\n') || rowState.textValue.text.length > 150 + ) + } + val sendButtonState = remember( + rowState.textValue.text, + rowState.editingMessage, + attachments.pendingMediaPaths, + attachments.pendingDocumentPaths, + rowState.isOverMessageLimit, + capabilities.canWriteText, + canSendAttachments, + capabilities.canSendVoice, + capabilities.canSendVideoNotes, + rowState.isVideoMessageMode, + rowState.isSlowModeActive, + rowState.slowModeRemainingSeconds + ) { + InputBarSendButtonState( + isTextEmpty = rowState.textValue.text.isBlank(), + isEditing = rowState.editingMessage != null, + hasPendingAttachments = attachments.pendingMediaPaths.isNotEmpty() || attachments.pendingDocumentPaths.isNotEmpty(), + isOverCharLimit = rowState.isOverMessageLimit, + canWriteText = capabilities.canWriteText, + canSendAttachments = canSendAttachments, + canSendVoice = capabilities.canSendVoice, + canSendVideoNotes = capabilities.canSendVideoNotes, + isVideoMessageMode = rowState.isVideoMessageMode, + isSlowModeActive = rowState.isSlowModeActive, + slowModeRemainingSeconds = rowState.slowModeRemainingSeconds + ) + } + Surface( modifier = modifier, color = MaterialTheme.colorScheme.surface, @@ -159,7 +189,7 @@ fun ChatInputBarComposerSection( .fillMaxWidth() .imePadding() .windowInsetsPadding( - if (isStickerMenuVisible) WindowInsets(0, 0, 0, 0) + if (rowState.isStickerMenuVisible) WindowInsets(0, 0, 0, 0) else WindowInsets.navigationBars ) .animateContentSize() @@ -167,8 +197,8 @@ fun ChatInputBarComposerSection( InputPreviewSection( editingMessage = editingMessage, replyMessage = replyMessage, - pendingMediaPaths = pendingMediaPaths, - pendingDocumentPaths = pendingDocumentPaths, + pendingMediaPaths = attachments.pendingMediaPaths, + pendingDocumentPaths = attachments.pendingDocumentPaths, onCancelEdit = onCancelEdit, onCancelReply = onCancelReply, onCancelMedia = onCancelMedia, @@ -181,12 +211,12 @@ fun ChatInputBarComposerSection( ) AnimatedVisibility( - visible = mentionSuggestions.isNotEmpty(), + visible = suggestions.mentionSuggestions.isNotEmpty(), enter = expandVertically(expandFrom = Alignment.Bottom) + fadeIn(), exit = shrinkVertically(shrinkTowards = Alignment.Bottom) + fadeOut() ) { MentionSuggestions( - suggestions = mentionSuggestions, + suggestions = suggestions.mentionSuggestions, onMentionClick = { onMentionClick(it) onMentionQueryClear() @@ -195,26 +225,26 @@ fun ChatInputBarComposerSection( } AnimatedVisibility( - visible = filteredCommands.isNotEmpty(), + visible = suggestions.filteredCommands.isNotEmpty(), enter = expandVertically() + fadeIn(), exit = shrinkVertically() + fadeOut() ) { BotCommandSuggestions( - commands = filteredCommands, + commands = suggestions.filteredCommands, onCommandClick = onCommandClick, modifier = Modifier.fillMaxWidth() ) } AnimatedVisibility( - visible = currentInlineBotUsername != null || isInlineBotLoading, + visible = suggestions.currentInlineBotUsername != null || suggestions.isInlineBotLoading, enter = expandVertically() + fadeIn(), exit = shrinkVertically() + fadeOut() ) { InlineBotResults( - inlineBotResults = inlineBotResults, - isInlineMode = currentInlineBotUsername != null, - isLoading = isInlineBotLoading, + inlineBotResults = suggestions.inlineBotResults, + isInlineMode = suggestions.currentInlineBotUsername != null, + isLoading = suggestions.isInlineBotLoading, onResultClick = onInlineResultClick, onSwitchPmClick = onInlineSwitchPmClick, onLoadMore = onLoadMoreInlineResults @@ -222,146 +252,75 @@ fun ChatInputBarComposerSection( } AnimatedVisibility( - visible = !isGifSearchFocused, + visible = !suggestions.isGifSearchFocused, enter = expandVertically(animationSpec = tween(200)), exit = shrinkVertically(animationSpec = tween(200)) ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp, vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Box( - modifier = Modifier - .weight(1f) - .padding(start = if (voiceRecorder.isRecording) 0.dp else 4.dp) - ) { - AnimatedContent( - targetState = voiceRecorder.isRecording, - transitionSpec = { (fadeIn() + scaleIn()).togetherWith(fadeOut() + scaleOut()) }, - label = "InputContent" - ) { isRecording -> - if (isRecording) { - RecordingUI( - voiceRecorderState = voiceRecorder, - onStop = { onVoiceStop(false) }, - onCancel = { onVoiceStop(true) }, - modifier = Modifier.fillMaxWidth() - ) - } else { - InputTextFieldContainer( - textValue = textValue, - onValueChange = { - onTextValueChange( - mergeInputTextValuePreservingAnnotations( - textValue, - it - ) - ) - }, - onRichTextValueChange = onTextValueChange, - isBot = isBot, - botMenuButton = botMenuButton, - botCommands = botCommands, - canSendStickers = canSendStickers, - canWriteText = canWriteText, - canShowBotActions = canShowBotActions, - isStickerMenuVisible = isStickerMenuVisible, - editingMessage = editingMessage, - canOpenAttachSheet = canOpenAttachSheet, - onStickerMenuToggle = onStickerMenuToggle, - onAttachClick = onAttachClick, - onShowBotCommands = onShowBotCommands, - onOpenMiniApp = onOpenMiniApp, - knownCustomEmojis = knownCustomEmojis, - emojiFontFamily = emojiFontFamily, - focusRequester = focusRequester, - pendingMediaPaths = pendingMediaPaths, - pendingDocumentPaths = pendingDocumentPaths, - canPasteMediaFromClipboard = canPasteMediaFromClipboard, - onPasteImages = onPasteImages, - onFocus = onInputFocus, - onOpenFullScreenEditor = onOpenFullScreenEditor, - modifier = Modifier.fillMaxWidth() - ) - } - } - } - - if (!voiceRecorder.isLocked) { - if (scheduledMessagesCount > 0) { - IconButton(onClick = onOpenScheduledMessages) { - Icon( - imageVector = Icons.Default.Schedule, - contentDescription = stringResource(R.string.action_scheduled_messages), - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - - Spacer(modifier = Modifier.width(8.dp)) - - Box(contentAlignment = Alignment.CenterEnd) { - InputBarSendButton( - textValue = textValue, - editingMessage = editingMessage, - pendingMediaPaths = pendingMediaPaths, - pendingDocumentPaths = pendingDocumentPaths, - isOverCharLimit = isOverMessageLimit, - canWriteText = canWriteText, - canSendAttachments = canSendAttachments, - canSendVoice = canSendVoice, - canSendVideoNotes = canSendVideoNotes, - isVideoMessageMode = isVideoMessageMode, - isSlowModeActive = isSlowModeActive, - slowModeRemainingSeconds = slowModeRemainingSeconds, - onSendWithOptions = onSendWithOptions, - onShowSendOptionsMenu = onShowSendOptionsMenu, - onCameraClick = onCameraClick, - onVideoModeToggle = onVideoModeToggle, - onVoiceStart = onVoiceStart, - onVoiceStop = onVoiceStop, - onVoiceLock = onVoiceLock - ) + ComposerMainRow( + inputUiState = inputUiState, + sendButtonState = sendButtonState, + attachments = attachments, + voiceRecorder = voiceRecorder, + knownCustomEmojis = knownCustomEmojis, + emojiFontFamily = emojiFontFamily, + focusRequester = focusRequester, + onTextValueChange = onTextValueChange, + onStickerMenuToggle = onStickerMenuToggle, + onAttachClick = onAttachClick, + onShowBotCommands = onShowBotCommands, + onOpenMiniApp = onOpenMiniApp, + onPasteImages = onPasteImages, + onInputFocus = onInputFocus, + onOpenFullScreenEditor = onOpenFullScreenEditor, + onOpenScheduledMessages = onOpenScheduledMessages, + onSendWithOptions = onSendWithOptions, + onShowSendOptionsMenu = onShowSendOptionsMenu, + onCameraClick = onCameraClick, + onVideoModeToggle = onVideoModeToggle, + onVoiceStart = onVoiceStart, + onVoiceStop = onVoiceStop, + onVoiceLock = onVoiceLock + ) - SendOptionsPopup( - expanded = showSendOptionsSheet, - scheduledMessagesCount = scheduledMessagesCount, - showSendAsDocument = pendingMediaPaths.isNotEmpty(), - onDismiss = onDismissSendOptions, - onSendAsDocument = onSendAsDocument, - onSendSilent = onSendSilent, - onScheduleMessage = onScheduleMessage, - onOpenScheduledMessages = onOpenScheduledMessagesFromPopup - ) - } - } - } + ComposerSendOptionsPopup( + expanded = rowState.showSendOptionsSheet, + scheduledMessagesCount = attachments.scheduledMessagesCount, + showSendAsDocument = attachments.pendingMediaPaths.isNotEmpty(), + onDismiss = onDismissSendOptions, + onSendAsDocument = onSendAsDocument, + onSendSilent = onSendSilent, + onScheduleMessage = onScheduleMessage, + onOpenScheduledMessages = onOpenScheduledMessagesFromPopup + ) } AnimatedVisibility( - visible = !voiceRecorder.isRecording && !showFullScreenEditor && currentMessageLength > 1000, + visible = !voiceRecorder.isRecording && !rowState.showFullScreenEditor && rowState.currentMessageLength > 1000, enter = fadeIn() + expandVertically(), exit = fadeOut() + shrinkVertically() ) { Text( - text = stringResource(R.string.message_length_counter, currentMessageLength, maxMessageLength), + text = stringResource( + R.string.message_length_counter, + rowState.currentMessageLength, + rowState.maxMessageLength + ), modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp), textAlign = TextAlign.End, style = MaterialTheme.typography.labelSmall, - color = if (isOverMessageLimit) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurfaceVariant + color = if (rowState.isOverMessageLimit) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurfaceVariant ) } AnimatedVisibility( - visible = replyMarkup is ReplyMarkupModel.ShowKeyboard && textValue.text.isEmpty() && !isStickerMenuVisible && !isKeyboardVisible, + visible = suggestions.replyMarkup is ReplyMarkupModel.ShowKeyboard && rowState.textValue.text.isEmpty() && !rowState.isStickerMenuVisible && !rowState.isKeyboardVisible, enter = expandVertically() + fadeIn(), exit = shrinkVertically() + fadeOut() ) { - val markup = replyMarkup as? ReplyMarkupModel.ShowKeyboard ?: return@AnimatedVisibility + val markup = suggestions.replyMarkup as? ReplyMarkupModel.ShowKeyboard + ?: return@AnimatedVisibility KeyboardMarkupView( markup = markup, onButtonClick = onReplyMarkupButtonClick, @@ -370,11 +329,11 @@ fun ChatInputBarComposerSection( } AnimatedVisibility( - visible = isStickerMenuVisible, + visible = rowState.isStickerMenuVisible, enter = slideInVertically( animationSpec = tween(220), initialOffsetY = { it }) + fadeIn(animationSpec = tween(170)), - exit = if (closeStickerMenuWithoutSlide) { + exit = if (rowState.closeStickerMenuWithoutSlide) { fadeOut(animationSpec = tween(90)) } else { slideOutVertically( @@ -387,7 +346,7 @@ fun ChatInputBarComposerSection( onEmojiSelected = { emoji, sticker -> onTextValueChange( insertEmojiAtSelection( - value = textValue, + value = rowState.textValue, emoji = emoji, sticker = sticker, knownCustomEmojis = knownCustomEmojis @@ -396,11 +355,209 @@ fun ChatInputBarComposerSection( }, onGifSelected = onGifClick, onSearchFocused = onGifSearchFocusedChange, - panelHeight = stickerMenuHeight, - canSendStickers = canSendStickers, + panelHeight = rowState.stickerMenuHeight, + canSendStickers = capabilities.canSendStickers, stickerRepository = stickerRepository ) } } } } + +@Composable +private fun ComposerMainRow( + inputUiState: InputTextFieldUiState, + sendButtonState: InputBarSendButtonState, + attachments: ComposerAttachmentState, + voiceRecorder: VoiceRecorderState, + knownCustomEmojis: MutableMap, + emojiFontFamily: FontFamily, + focusRequester: FocusRequester, + onTextValueChange: (TextFieldValue) -> Unit, + onStickerMenuToggle: () -> Unit, + onAttachClick: () -> Unit, + onShowBotCommands: () -> Unit, + onOpenMiniApp: (String, String) -> Unit, + onPasteImages: (List) -> Unit, + onInputFocus: () -> Unit, + onOpenFullScreenEditor: () -> Unit, + onOpenScheduledMessages: () -> Unit, + onSendWithOptions: (MessageSendOptions) -> Unit, + onShowSendOptionsMenu: () -> Unit, + onCameraClick: () -> Unit, + onVideoModeToggle: () -> Unit, + onVoiceStart: () -> Unit, + onVoiceStop: (Boolean) -> Unit, + onVoiceLock: () -> Unit, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + ComposerInputSlot( + modifier = Modifier.weight(1f), + uiState = inputUiState, + attachments = attachments, + voiceRecorder = voiceRecorder, + knownCustomEmojis = knownCustomEmojis, + emojiFontFamily = emojiFontFamily, + focusRequester = focusRequester, + onTextValueChange = onTextValueChange, + onStickerMenuToggle = onStickerMenuToggle, + onAttachClick = onAttachClick, + onShowBotCommands = onShowBotCommands, + onOpenMiniApp = onOpenMiniApp, + onPasteImages = onPasteImages, + onInputFocus = onInputFocus, + onOpenFullScreenEditor = onOpenFullScreenEditor, + onVoiceStop = onVoiceStop + ) + + ComposerActionsSlot( + attachments = attachments, + sendButtonState = sendButtonState, + voiceRecorder = voiceRecorder, + onOpenScheduledMessages = onOpenScheduledMessages, + onSendWithOptions = onSendWithOptions, + onShowSendOptionsMenu = onShowSendOptionsMenu, + onCameraClick = onCameraClick, + onVideoModeToggle = onVideoModeToggle, + onVoiceStart = onVoiceStart, + onVoiceStop = onVoiceStop, + onVoiceLock = onVoiceLock + ) + } +} + +@Composable +private fun ComposerInputSlot( + modifier: Modifier = Modifier, + uiState: InputTextFieldUiState, + attachments: ComposerAttachmentState, + voiceRecorder: VoiceRecorderState, + knownCustomEmojis: MutableMap, + emojiFontFamily: FontFamily, + focusRequester: FocusRequester, + onTextValueChange: (TextFieldValue) -> Unit, + onStickerMenuToggle: () -> Unit, + onAttachClick: () -> Unit, + onShowBotCommands: () -> Unit, + onOpenMiniApp: (String, String) -> Unit, + onPasteImages: (List) -> Unit, + onInputFocus: () -> Unit, + onOpenFullScreenEditor: () -> Unit, + onVoiceStop: (Boolean) -> Unit, +) { + Box( + modifier = modifier + .padding(start = if (voiceRecorder.isRecording) 0.dp else 4.dp) + ) { + AnimatedContent( + targetState = voiceRecorder.isRecording, + transitionSpec = { (fadeIn() + scaleIn()).togetherWith(fadeOut() + scaleOut()) }, + label = "InputContent" + ) { isRecording -> + if (isRecording) { + RecordingUI( + voiceRecorderState = voiceRecorder, + onStop = { onVoiceStop(false) }, + onCancel = { onVoiceStop(true) }, + modifier = Modifier.fillMaxWidth() + ) + } else { + InputTextFieldContainer( + uiState = uiState, + onValueChange = { + onTextValueChange( + mergeInputTextValuePreservingAnnotations( + uiState.textValue, + it + ) + ) + }, + onRichTextValueChange = onTextValueChange, + onStickerMenuToggle = onStickerMenuToggle, + onAttachClick = onAttachClick, + onShowBotCommands = onShowBotCommands, + onOpenMiniApp = onOpenMiniApp, + knownCustomEmojis = knownCustomEmojis, + emojiFontFamily = emojiFontFamily, + focusRequester = focusRequester, + pendingMediaPaths = attachments.pendingMediaPaths, + pendingDocumentPaths = attachments.pendingDocumentPaths, + onPasteImages = onPasteImages, + onFocus = onInputFocus, + onOpenFullScreenEditor = onOpenFullScreenEditor, + modifier = Modifier.fillMaxWidth() + ) + } + } + } +} + +@Composable +private fun ComposerActionsSlot( + attachments: ComposerAttachmentState, + sendButtonState: InputBarSendButtonState, + voiceRecorder: VoiceRecorderState, + onOpenScheduledMessages: () -> Unit, + onSendWithOptions: (MessageSendOptions) -> Unit, + onShowSendOptionsMenu: () -> Unit, + onCameraClick: () -> Unit, + onVideoModeToggle: () -> Unit, + onVoiceStart: () -> Unit, + onVoiceStop: (Boolean) -> Unit, + onVoiceLock: () -> Unit +) { + if (!voiceRecorder.isLocked) { + if (attachments.scheduledMessagesCount > 0) { + IconButton(onClick = onOpenScheduledMessages) { + Icon( + imageVector = Icons.Default.Schedule, + contentDescription = stringResource(R.string.action_scheduled_messages), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + Spacer(modifier = Modifier.width(8.dp)) + + Box(contentAlignment = Alignment.CenterEnd) { + InputBarSendButton( + state = sendButtonState, + onSendWithOptions = onSendWithOptions, + onShowSendOptionsMenu = onShowSendOptionsMenu, + onCameraClick = onCameraClick, + onVideoModeToggle = onVideoModeToggle, + onVoiceStart = onVoiceStart, + onVoiceStop = onVoiceStop, + onVoiceLock = onVoiceLock + ) + } + } +} + +@Composable +private fun ComposerSendOptionsPopup( + expanded: Boolean, + scheduledMessagesCount: Int, + showSendAsDocument: Boolean, + onDismiss: () -> Unit, + onSendAsDocument: () -> Unit, + onSendSilent: () -> Unit, + onScheduleMessage: () -> Unit, + onOpenScheduledMessages: () -> Unit +) { + SendOptionsPopup( + expanded = expanded, + scheduledMessagesCount = scheduledMessagesCount, + showSendAsDocument = showSendAsDocument, + onDismiss = onDismiss, + onSendAsDocument = onSendAsDocument, + onSendSilent = onSendSilent, + onScheduleMessage = onScheduleMessage, + onOpenScheduledMessages = onOpenScheduledMessages + ) +} diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/ChatInputBarContract.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/ChatInputBarContract.kt index e38e6000..9456233b 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/ChatInputBarContract.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/ChatInputBarContract.kt @@ -81,6 +81,95 @@ data class ChatInputBarActions( val onSendScheduledNow: (MessageModel) -> Unit = {}, ) +@Immutable +internal data class ChatInputBarCapabilities( + val canWriteText: Boolean, + val canSendPhotos: Boolean, + val canSendVideos: Boolean, + val canSendDocuments: Boolean, + val canSendAudios: Boolean, + val canOpenAttachSheet: Boolean, + val canSendStickers: Boolean, + val canSendVoice: Boolean, + val canSendVideoNotes: Boolean, + val canSendAnything: Boolean +) + +@Immutable +internal data class ComposerAttachmentState( + val pendingMediaPaths: List = emptyList(), + val pendingDocumentPaths: List = emptyList(), + val scheduledMessagesCount: Int = 0 +) + +@Immutable +internal data class ComposerSuggestionState( + val mentionSuggestions: List = emptyList(), + val filteredCommands: List = emptyList(), + val currentInlineBotUsername: String? = null, + val isInlineBotLoading: Boolean = false, + val inlineBotResults: InlineBotResultsModel? = null, + val replyMarkup: ReplyMarkupModel? = null, + val isGifSearchFocused: Boolean = false +) + +@Immutable +internal data class ComposerBotState( + val isBot: Boolean, + val botMenuButton: BotMenuButtonModel, + val botCommands: List +) + +@Immutable +internal data class ComposerRowState( + val textValue: androidx.compose.ui.text.input.TextFieldValue, + val editingMessage: MessageModel? = null, + val isStickerMenuVisible: Boolean = false, + val closeStickerMenuWithoutSlide: Boolean = false, + val isKeyboardVisible: Boolean = false, + val stickerMenuHeight: androidx.compose.ui.unit.Dp, + val showFullScreenEditor: Boolean = false, + val currentMessageLength: Int = 0, + val maxMessageLength: Int = 4096, + val isOverMessageLimit: Boolean = false, + val showSendOptionsSheet: Boolean = false, + val isVideoMessageMode: Boolean = false, + val isSlowModeActive: Boolean = false, + val slowModeRemainingSeconds: Int = 0, +) + +@Immutable +internal data class InputTextFieldUiState( + val textValue: androidx.compose.ui.text.input.TextFieldValue, + val isBot: Boolean, + val botMenuButton: BotMenuButtonModel, + val botCommands: List, + val canSendStickers: Boolean, + val canWriteText: Boolean, + val canShowBotActions: Boolean, + val isStickerMenuVisible: Boolean, + val canAttachMedia: Boolean, + val canPasteMediaFromClipboard: Boolean, + val pendingMediaPaths: List, + val pendingDocumentPaths: List, + val showExpandEditorAction: Boolean, +) + +@Immutable +internal data class InputBarSendButtonState( + val isTextEmpty: Boolean, + val isEditing: Boolean, + val hasPendingAttachments: Boolean, + val isOverCharLimit: Boolean, + val canWriteText: Boolean, + val canSendAttachments: Boolean, + val canSendVoice: Boolean, + val canSendVideoNotes: Boolean, + val isVideoMessageMode: Boolean, + val isSlowModeActive: Boolean, + val slowModeRemainingSeconds: Int, +) + internal enum class InputBarMode { Composer, SlowMode, diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/InputBarSendButton.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/InputBarSendButton.kt index ad6343f5..e53b5a0d 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/InputBarSendButton.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/InputBarSendButton.kt @@ -42,26 +42,13 @@ import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.positionChange import androidx.compose.ui.platform.LocalHapticFeedback -import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp -import org.monogram.domain.models.MessageModel import org.monogram.domain.models.MessageSendOptions @OptIn(ExperimentalFoundationApi::class) @Composable -fun InputBarSendButton( - textValue: TextFieldValue, - editingMessage: MessageModel?, - pendingMediaPaths: List, - pendingDocumentPaths: List, - isOverCharLimit: Boolean, - canWriteText: Boolean, - canSendAttachments: Boolean, - canSendVoice: Boolean, - canSendVideoNotes: Boolean, - isVideoMessageMode: Boolean, - isSlowModeActive: Boolean, - slowModeRemainingSeconds: Int, +internal fun InputBarSendButton( + state: InputBarSendButtonState, onSendWithOptions: (MessageSendOptions) -> Unit, onShowSendOptionsMenu: () -> Unit, onCameraClick: () -> Unit, @@ -71,26 +58,25 @@ fun InputBarSendButton( onVoiceLock: () -> Unit = {} ) { val haptic = LocalHapticFeedback.current - val isTextEmpty = textValue.text.isBlank() - val hasPendingAttachments = pendingMediaPaths.isNotEmpty() || pendingDocumentPaths.isNotEmpty() - val canSendContent = canWriteText || (hasPendingAttachments && canSendAttachments) - val isSlowModeBlocked = isSlowModeActive && editingMessage == null + val canSendContent = + state.canWriteText || (state.hasPendingAttachments && state.canSendAttachments) + val isSlowModeBlocked = state.isSlowModeActive && !state.isEditing val isSendEnabled = - (!isTextEmpty || editingMessage != null || hasPendingAttachments) && + (!state.isTextEmpty || state.isEditing || state.hasPendingAttachments) && canSendContent && - !isOverCharLimit && + !state.isOverCharLimit && !isSlowModeBlocked var isVoiceRecordingActive by remember { mutableStateOf(false) } val effectiveVideoMode = when { - !canSendVideoNotes -> false - !canSendVoice -> true - else -> isVideoMessageMode + !state.canSendVideoNotes -> false + !state.canSendVoice -> true + else -> state.isVideoMessageMode } - val canUseRecording = canSendVoice || canSendVideoNotes - val canToggleRecordingMode = canSendVoice && canSendVideoNotes + val canUseRecording = state.canSendVoice || state.canSendVideoNotes + val canToggleRecordingMode = state.canSendVoice && state.canSendVideoNotes val isRecordingMode = - isTextEmpty && editingMessage == null && !hasPendingAttachments && canUseRecording && !isSlowModeBlocked + state.isTextEmpty && !state.isEditing && !state.hasPendingAttachments && canUseRecording && !isSlowModeBlocked val backgroundColor by animateColorAsState( targetValue = if (isSendEnabled || isVoiceRecordingActive || isSlowModeBlocked) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant, @@ -103,18 +89,18 @@ fun InputBarSendButton( label = "ContentColor" ) - if (canWriteText || canSendVoice || canSendVideoNotes || (hasPendingAttachments && canSendAttachments)) { + if (state.canWriteText || state.canSendVoice || state.canSendVideoNotes || (state.hasPendingAttachments && state.canSendAttachments)) { val sendIcon = when { - hasPendingAttachments -> Icons.AutoMirrored.Filled.Send - editingMessage != null -> Icons.Default.Check - !isTextEmpty -> Icons.AutoMirrored.Filled.Send + state.hasPendingAttachments -> Icons.AutoMirrored.Filled.Send + state.isEditing -> Icons.Default.Check + !state.isTextEmpty -> Icons.AutoMirrored.Filled.Send effectiveVideoMode -> Icons.Default.Videocam else -> Icons.Outlined.Mic } - val canShowOptions = editingMessage == null && - (!isTextEmpty || (hasPendingAttachments && canSendAttachments)) && - (canWriteText || (hasPendingAttachments && canSendAttachments)) && - !isOverCharLimit && + val canShowOptions = !state.isEditing && + (!state.isTextEmpty || (state.hasPendingAttachments && state.canSendAttachments)) && + (state.canWriteText || (state.hasPendingAttachments && state.canSendAttachments)) && + !state.isOverCharLimit && !isSlowModeBlocked Box( @@ -210,7 +196,7 @@ fun InputBarSendButton( ) { if (isSlowModeBlocked) { Text( - text = formatSlowModeCountdown(slowModeRemainingSeconds), + text = formatSlowModeCountdown(state.slowModeRemainingSeconds), style = MaterialTheme.typography.labelSmall, color = contentColor ) diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/InputTextField.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/InputTextField.kt index 8f56381a..58d4cfb0 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/InputTextField.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/InputTextField.kt @@ -7,7 +7,6 @@ import android.net.Uri import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.content.ReceiveContentListener import androidx.compose.foundation.content.contentReceiver -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -28,13 +27,11 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.AlertDialog import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.OutlinedTextFieldDefaults -import androidx.compose.material3.OutlinedTextFieldDefaults.FocusedBorderThickness -import androidx.compose.material3.OutlinedTextFieldDefaults.UnfocusedBorderThickness import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -64,7 +61,6 @@ import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.OffsetMapping import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TransformedText -import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.LineHeightStyle import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.Dp @@ -129,9 +125,12 @@ fun InputTextField( var showPreLanguageDialog by rememberSaveable { mutableStateOf(false) } var preLanguageValue by rememberSaveable { mutableStateOf("") } val context = LocalContext.current + val currentOnPasteImages by rememberUpdatedState(onPasteImages) + val currentOnFocus by rememberUpdatedState(onFocus) + val currentOnRichTextValueChange by rememberUpdatedState(onRichTextValueChange) val emojiSize = 20.sp - val inlineContentMap = remember(knownCustomEmojis.size, knownCustomEmojis.hashCode()) { + val inlineContentMap = remember(knownCustomEmojis) { knownCustomEmojis.map { (id, sticker) -> id.toString() to InlineTextContent( Placeholder(emojiSize, emojiSize, PlaceholderVerticalAlign.Center) @@ -145,68 +144,100 @@ fun InputTextField( } val primaryColor = MaterialTheme.colorScheme.primary - val transformedTextState = remember(textValue.annotatedString, knownCustomEmojis, emojiFontFamily, primaryColor) { - val text = textValue.annotatedString - val emojiAnnotations = text.getStringAnnotations(CUSTOM_EMOJI_TAG, 0, text.length) - val mentionAnnotations = text.getStringAnnotations(MENTION_TAG, 0, text.length) - - val builder = AnnotatedString.Builder() - var lastIndex = 0 - val sortedEmojiAnnotations = emojiAnnotations.sortedBy { it.start } - - for (annotation in sortedEmojiAnnotations) { - if (annotation.start < lastIndex) continue - if (annotation.start > text.length || annotation.end > text.length) continue - builder.append(text.subSequence(lastIndex, annotation.start)) - val stickerId = annotation.item.toLongOrNull() - val originalEmoji = text.substring(annotation.start, annotation.end) - if (stickerId != null && knownCustomEmojis.containsKey(stickerId)) { - builder.appendInlineContent(stickerId.toString(), originalEmoji) - } else { - builder.append(originalEmoji) + val transformedTextState by remember( + textValue.annotatedString, + knownCustomEmojis, + emojiFontFamily, + primaryColor + ) { + derivedStateOf { + val text = textValue.annotatedString + val emojiAnnotations = text.getStringAnnotations(CUSTOM_EMOJI_TAG, 0, text.length) + val mentionAnnotations = text.getStringAnnotations(MENTION_TAG, 0, text.length) + + val builder = AnnotatedString.Builder() + var lastIndex = 0 + val sortedEmojiAnnotations = emojiAnnotations.sortedBy { it.start } + + for (annotation in sortedEmojiAnnotations) { + if (annotation.start < lastIndex) continue + if (annotation.start > text.length || annotation.end > text.length) continue + builder.append(text.subSequence(lastIndex, annotation.start)) + val stickerId = annotation.item.toLongOrNull() + val originalEmoji = text.substring(annotation.start, annotation.end) + if (stickerId != null && knownCustomEmojis.containsKey(stickerId)) { + builder.appendInlineContent(stickerId.toString(), originalEmoji) + } else { + builder.append(originalEmoji) + } + lastIndex = annotation.end } - lastIndex = annotation.end - } - if (lastIndex < text.length) builder.append(text.subSequence(lastIndex, text.length)) + if (lastIndex < text.length) builder.append(text.subSequence(lastIndex, text.length)) - val result = builder.toAnnotatedString() - val finalBuilder = AnnotatedString.Builder(result) + val result = builder.toAnnotatedString() + val finalBuilder = AnnotatedString.Builder(result) - // Add emoji style - finalBuilder.addEmojiStyle(result.text, emojiFontFamily) + finalBuilder.addEmojiStyle(result.text, emojiFontFamily) - // Add mention highlighting - mentionAnnotations.forEach { annotation -> - if (annotation.start < annotation.end && annotation.start >= 0 && annotation.end <= result.length) { - finalBuilder.addStyle(SpanStyle(color = primaryColor), annotation.start, annotation.end) + mentionAnnotations.forEach { annotation -> + if (annotation.start < annotation.end && annotation.start >= 0 && annotation.end <= result.length) { + finalBuilder.addStyle( + SpanStyle(color = primaryColor), + annotation.start, + annotation.end + ) + } } - } - text.getStringAnnotations(RICH_ENTITY_TAG, 0, text.length).forEach { annotation -> - val style = decodeRichEntity(annotation.item)?.toEditorStyle(primaryColor) - if (style != null && annotation.start < annotation.end && annotation.end <= result.length) { - finalBuilder.addStyle(style, annotation.start, annotation.end) + text.getStringAnnotations(RICH_ENTITY_TAG, 0, text.length).forEach { annotation -> + val style = decodeRichEntity(annotation.item)?.toEditorStyle(primaryColor) + if (style != null && annotation.start < annotation.end && annotation.end <= result.length) { + finalBuilder.addStyle(style, annotation.start, annotation.end) + } } - } - // Highlight @username style mentions that are not yet annotated - val mentionRegex = Regex("@(\\w+)") - mentionRegex.findAll(result.text).forEach { match -> - if (mentionAnnotations.none { it.start <= match.range.first && it.end >= match.range.last + 1 }) { - finalBuilder.addStyle(SpanStyle(color = primaryColor), match.range.first, match.range.last + 1) + val mentionRegex = Regex("@(\\w+)") + mentionRegex.findAll(result.text).forEach { match -> + if (mentionAnnotations.none { it.start <= match.range.first && it.end >= match.range.last + 1 }) { + finalBuilder.addStyle( + SpanStyle(color = primaryColor), + match.range.first, + match.range.last + 1 + ) + } } - } - TransformedText(finalBuilder.toAnnotatedString(), OffsetMapping.Identity) + TransformedText(finalBuilder.toAnnotatedString(), OffsetMapping.Identity) + } } - val hasCustomEmojis = knownCustomEmojis.isNotEmpty() && - textValue.annotatedString.getStringAnnotations(CUSTOM_EMOJI_TAG, 0, textValue.text.length).isNotEmpty() - val hasRichFormatting = textValue.annotatedString - .getStringAnnotations(RICH_ENTITY_TAG, 0, textValue.text.length) - .isNotEmpty() - val shouldUseOverlayText = - hasCustomEmojis || emojiFontFamily != FontFamily.Default || textValue.text.contains('@') || hasRichFormatting + val hasCustomEmojis by remember(textValue.annotatedString, knownCustomEmojis) { + derivedStateOf { + knownCustomEmojis.isNotEmpty() && + textValue.annotatedString.getStringAnnotations( + CUSTOM_EMOJI_TAG, + 0, + textValue.text.length + ).isNotEmpty() + } + } + val hasRichFormatting by remember(textValue.annotatedString) { + derivedStateOf { + textValue.annotatedString + .getStringAnnotations(RICH_ENTITY_TAG, 0, textValue.text.length) + .isNotEmpty() + } + } + val shouldUseOverlayText by remember( + textValue.text, + hasCustomEmojis, + hasRichFormatting, + emojiFontFamily + ) { + derivedStateOf { + hasCustomEmojis || emojiFontFamily != FontFamily.Default || textValue.text.contains('@') || hasRichFormatting + } + } val scrollState = rememberScrollState() val editorState = rememberTextFieldState(initialText = textValue.text) @@ -241,6 +272,13 @@ fun InputTextField( LaunchedEffect(textValue.text) { scrollState.scrollTo(scrollState.maxValue) } + val placeholderText = remember(pendingMediaPaths, pendingDocumentPaths, context) { + if (pendingMediaPaths.isNotEmpty() || pendingDocumentPaths.isNotEmpty()) { + context.getString(R.string.input_placeholder_caption) + } else { + context.getString(R.string.input_placeholder_message) + } + } val richTextBold = stringResource(R.string.rich_text_bold) val richTextItalic = stringResource(R.string.rich_text_italic) @@ -252,7 +290,10 @@ fun InputTextField( val richTextLink = stringResource(R.string.rich_text_link) val richTextClear = stringResource(R.string.rich_text_clear) val actionPasteImage = stringResource(R.string.action_paste_image) - val receiveContentListener = remember(context, canPasteMediaFromClipboard, onPasteImages) { + val clipboardImageUris = remember(context, canPasteMediaFromClipboard) { + if (canPasteMediaFromClipboard) extractImageUrisFromClipboard(context) else emptyList() + } + val receiveContentListener = remember(context, canPasteMediaFromClipboard) { ReceiveContentListener { transferableContent -> if (!canPasteMediaFromClipboard) return@ReceiveContentListener transferableContent @@ -261,7 +302,7 @@ fun InputTextField( if (imageUris.isEmpty()) { transferableContent } else { - onPasteImages(imageUris) + currentOnPasteImages(imageUris) null } } @@ -279,7 +320,7 @@ fun InputTextField( val fieldModifier = Modifier .fillMaxWidth() .focusRequester(focusRequester) - .onFocusChanged { if (it.isFocused) onFocus() } + .onFocusChanged { if (it.isFocused) currentOnFocus() } .contentReceiver(receiveContentListener) .let { base -> when { @@ -308,11 +349,10 @@ fun InputTextField( } .appendTextContextMenuComponents { if (canPasteMediaFromClipboard) { - val imageUris = extractImageUrisFromClipboard(context) - if (imageUris.isNotEmpty()) { + if (clipboardImageUris.isNotEmpty()) { item(RichMenuActionPasteImage, actionPasteImage) { close() - onPasteImages(imageUris) + currentOnPasteImages(clipboardImageUris) } } } @@ -320,7 +360,7 @@ fun InputTextField( if (hasFormattableSelection(textValue)) { separator() item(RichMenuActionBold, richTextBold) { - onRichTextValueChange( + currentOnRichTextValueChange( toggleRichEntity( textValue, MessageEntityType.Bold @@ -329,7 +369,7 @@ fun InputTextField( close() } item(RichMenuActionItalic, richTextItalic) { - onRichTextValueChange( + currentOnRichTextValueChange( toggleRichEntity( textValue, MessageEntityType.Italic @@ -338,7 +378,7 @@ fun InputTextField( close() } item(RichMenuActionUnderline, richTextUnderline) { - onRichTextValueChange( + currentOnRichTextValueChange( toggleRichEntity( textValue, MessageEntityType.Underline @@ -347,7 +387,7 @@ fun InputTextField( close() } item(RichMenuActionStrike, richTextStrike) { - onRichTextValueChange( + currentOnRichTextValueChange( toggleRichEntity( textValue, MessageEntityType.Strikethrough @@ -356,7 +396,7 @@ fun InputTextField( close() } item(RichMenuActionSpoiler, richTextSpoiler) { - onRichTextValueChange( + currentOnRichTextValueChange( toggleRichEntity( textValue, MessageEntityType.Spoiler @@ -365,7 +405,7 @@ fun InputTextField( close() } item(RichMenuActionCode, richTextCode) { - onRichTextValueChange( + currentOnRichTextValueChange( toggleRichEntity( textValue, MessageEntityType.Code @@ -403,7 +443,11 @@ fun InputTextField( close() } item(RichMenuActionClear, richTextClear) { - onRichTextValueChange(clearRichFormatting(textValue)) + currentOnRichTextValueChange( + clearRichFormatting( + textValue + ) + ) close() } } @@ -446,10 +490,7 @@ fun InputTextField( ) { if (textValue.text.isEmpty()) { Text( - text = if (pendingMediaPaths.isNotEmpty() || pendingDocumentPaths.isNotEmpty()) - stringResource(R.string.input_placeholder_caption) - else - stringResource(R.string.input_placeholder_message), + text = placeholderText, style = textStyle, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.fillMaxWidth() @@ -500,7 +541,7 @@ fun InputTextField( onClick = { val normalizedUrl = normalizeUrl(linkValue) if (normalizedUrl != null && !textValue.selection.collapsed) { - onRichTextValueChange( + currentOnRichTextValueChange( applyRichEntity( textValue, MessageEntityType.TextUrl(normalizedUrl), @@ -538,7 +579,12 @@ fun InputTextField( confirmButton = { TextButton( onClick = { - onRichTextValueChange(applyPreEntity(textValue, preLanguageValue)) + currentOnRichTextValueChange( + applyPreEntity( + textValue, + preLanguageValue + ) + ) showPreLanguageDialog = false } ) { diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/InputTextFieldContainer.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/InputTextFieldContainer.kt index 059e4206..28898aed 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/InputTextFieldContainer.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/inputbar/InputTextFieldContainer.kt @@ -29,7 +29,8 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester @@ -40,25 +41,15 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import org.monogram.domain.models.BotCommandModel import org.monogram.domain.models.BotMenuButtonModel -import org.monogram.domain.models.MessageModel import org.monogram.domain.models.StickerModel import org.monogram.presentation.R import org.monogram.presentation.core.util.LocalTabletInterfaceEnabled @Composable -fun InputTextFieldContainer( - textValue: TextFieldValue, +internal fun InputTextFieldContainer( + uiState: InputTextFieldUiState, onValueChange: (TextFieldValue) -> Unit, onRichTextValueChange: (TextFieldValue) -> Unit = onValueChange, - isBot: Boolean, - botMenuButton: BotMenuButtonModel, - botCommands: List, - canSendStickers: Boolean, - canWriteText: Boolean, - canShowBotActions: Boolean, - isStickerMenuVisible: Boolean, - editingMessage: MessageModel?, - canOpenAttachSheet: Boolean, onStickerMenuToggle: () -> Unit, onAttachClick: () -> Unit, onShowBotCommands: () -> Unit, @@ -68,12 +59,15 @@ fun InputTextFieldContainer( focusRequester: FocusRequester, pendingMediaPaths: List, pendingDocumentPaths: List, - canPasteMediaFromClipboard: Boolean = false, onPasteImages: (List) -> Unit = {}, onFocus: () -> Unit = {}, onOpenFullScreenEditor: () -> Unit = {}, modifier: Modifier = Modifier ) { + val onAttachClickState by rememberUpdatedState(onAttachClick) + val onStickerMenuToggleState by rememberUpdatedState(onStickerMenuToggle) + val onOpenFullScreenEditorState by rememberUpdatedState(onOpenFullScreenEditor) + Surface( modifier = modifier, shape = RoundedCornerShape(24.dp), @@ -82,21 +76,12 @@ fun InputTextFieldContainer( val isTablet = LocalConfiguration.current.screenWidthDp >= 600 && LocalTabletInterfaceEnabled.current - val canAttachMedia = remember( - editingMessage, - pendingMediaPaths, - pendingDocumentPaths, - canOpenAttachSheet - ) { - editingMessage == null && pendingMediaPaths.isEmpty() && pendingDocumentPaths.isEmpty() && canOpenAttachSheet - } - Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(start = 4.dp, end = 12.dp, top = 4.dp, bottom = 4.dp) ) { AnimatedContent( - targetState = canAttachMedia, + targetState = uiState.canAttachMedia, transitionSpec = { (fadeIn() + scaleIn(initialScale = 0.85f)).togetherWith( fadeOut() + scaleOut(targetScale = 0.85f) @@ -106,7 +91,7 @@ fun InputTextFieldContainer( ) { showAttach -> if (showAttach) { IconButton( - onClick = onAttachClick, + onClick = { onAttachClickState() }, modifier = Modifier.size(40.dp) ) { Icon( @@ -120,47 +105,43 @@ fun InputTextFieldContainer( } } - val showBotActions = remember(isBot, textValue.text, canShowBotActions) { - isBot && canShowBotActions && textValue.text.isEmpty() - } - InputTextField( - textValue = textValue, + textValue = uiState.textValue, onValueChange = onValueChange, onRichTextValueChange = onRichTextValueChange, - canWriteText = canWriteText, + canWriteText = uiState.canWriteText, knownCustomEmojis = knownCustomEmojis, emojiFontFamily = emojiFontFamily, focusRequester = focusRequester, pendingMediaPaths = pendingMediaPaths, pendingDocumentPaths = pendingDocumentPaths, - canPasteMediaFromClipboard = canPasteMediaFromClipboard, + canPasteMediaFromClipboard = uiState.canPasteMediaFromClipboard, onPasteImages = onPasteImages, onFocus = onFocus, modifier = Modifier.weight(1f) ) AnimatedVisibility( - visible = showBotActions, + visible = uiState.isBot && uiState.canShowBotActions && uiState.textValue.text.isEmpty(), enter = fadeIn() + expandHorizontally(expandFrom = Alignment.End), exit = fadeOut() + shrinkHorizontally(shrinkTowards = Alignment.End) ) { BotInputActions( - botMenuButton = botMenuButton, - botCommands = botCommands, + botMenuButton = uiState.botMenuButton, + botCommands = uiState.botCommands, onShowBotCommands = onShowBotCommands, onOpenMiniApp = onOpenMiniApp ) } - if (canWriteText) { + if (uiState.canWriteText) { AnimatedVisibility( - visible = isTablet || textValue.text.contains('\n') || textValue.text.length > 150, + visible = uiState.showExpandEditorAction || isTablet, enter = fadeIn() + expandHorizontally(expandFrom = Alignment.End), exit = fadeOut() + shrinkHorizontally(shrinkTowards = Alignment.End) ) { IconButton( - onClick = onOpenFullScreenEditor, + onClick = { onOpenFullScreenEditorState() }, modifier = Modifier .align(Alignment.CenterVertically) .size(36.dp) @@ -175,16 +156,16 @@ fun InputTextFieldContainer( } AnimatedVisibility( - visible = canSendStickers, + visible = uiState.canSendStickers, enter = fadeIn() + expandHorizontally(expandFrom = Alignment.End), exit = fadeOut() + shrinkHorizontally(shrinkTowards = Alignment.End) ) { IconButton( - onClick = onStickerMenuToggle, + onClick = { onStickerMenuToggleState() }, modifier = Modifier.size(40.dp) ) { Icon( - imageVector = if (isStickerMenuVisible) Icons.Default.Keyboard else Icons.Outlined.EmojiEmotions, + imageVector = if (uiState.isStickerMenuVisible) Icons.Default.Keyboard else Icons.Outlined.EmojiEmotions, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant ) diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/VideoMessageBubble.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/VideoMessageBubble.kt index cdcc6a28..41539851 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/VideoMessageBubble.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/message/VideoMessageBubble.kt @@ -35,6 +35,7 @@ import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -59,11 +60,15 @@ import org.monogram.domain.models.ForwardInfo import org.monogram.domain.models.MessageContent import org.monogram.domain.models.MessageModel import org.monogram.presentation.R +import org.monogram.presentation.core.media.VideoStickerPlayer +import org.monogram.presentation.core.media.VideoType import org.monogram.presentation.core.util.IDownloadUtils import org.monogram.presentation.core.util.namespacedCacheKey import org.monogram.presentation.features.chats.conversation.AutoDownloadSuppression -import org.monogram.presentation.core.media.VideoStickerPlayer -import org.monogram.presentation.core.media.VideoType + +private class VideoBubbleLayoutTracker { + var videoPosition: Offset = Offset.Zero +} @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable @@ -147,14 +152,18 @@ fun VideoMessageBubble( bottomStart = bottomStart ) - var videoPosition by remember { mutableStateOf(Offset.Zero) } + val layoutTracker = remember { VideoBubbleLayoutTracker() } var isMuted by remember { mutableStateOf(true) } - var currentPositionSeconds by remember { mutableIntStateOf(0) } var isVisible by remember { mutableStateOf(false) } val resources = LocalResources.current val screenHeightPx = remember { resources.displayMetrics.heightPixels } val revealedSpoilers = remember { mutableStateListOf() } var isMediaSpoilerRevealed by remember { mutableStateOf(!content.hasSpoiler) } + val currentPositionSecondsState = remember(msg.id, content.fileId) { mutableIntStateOf(0) } + val currentPositionSeconds = currentPositionSecondsState.intValue + val onLongClickState by rememberUpdatedState(onLongClick) + val onVideoClickState by rememberUpdatedState(onVideoClick) + val onCancelDownloadState by rememberUpdatedState(onCancelDownload) Column( modifier = modifier.onGloballyPositioned { @@ -208,7 +217,9 @@ fun VideoMessageBubble( .heightIn(min = 160.dp, max = 360.dp) .aspectRatio(ratio) .clipToBounds() - .onGloballyPositioned { videoPosition = it.positionInWindow() } + .onGloballyPositioned { + layoutTracker.videoPosition = it.positionInWindow() + } ) { if (hasPath || content.supportsStreaming) { @@ -224,30 +235,21 @@ fun VideoMessageBubble( reportProgress = true, onProgressUpdate = { pos -> val seconds = (pos / 1000).toInt() - if (seconds != currentPositionSeconds) { - currentPositionSeconds = seconds + if (seconds != currentPositionSecondsState.intValue) { + currentPositionSecondsState.intValue = seconds } }, fileId = content.fileId, thumbnailData = content.minithumbnail ) - Box( + VideoMuteToggle( modifier = Modifier .align(Alignment.TopEnd) - .padding(8.dp) - .size(30.dp) - .background(Color.Black.copy(alpha = 0.45f), CircleShape) - .clickable { isMuted = !isMuted }, - contentAlignment = Alignment.Center - ) { - Icon( - imageVector = if (isMuted) Icons.AutoMirrored.Filled.VolumeOff else Icons.AutoMirrored.Filled.VolumeUp, - contentDescription = stringResource(R.string.cd_toggle_sound), - tint = Color.White, - modifier = Modifier.size(16.dp) - ) - } + .padding(8.dp), + isMuted = isMuted, + onToggle = { isMuted = !isMuted } + ) } else { if (hasPath) { Image( @@ -304,119 +306,60 @@ fun VideoMessageBubble( } } } else { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - MediaLoadingBackground( - previewData = content.minithumbnail, - contentScale = ContentScale.Crop, - previewBlur = 14.dp - ) - - Box( - modifier = Modifier - .fillMaxSize() - .background(Color.Black.copy(alpha = 0.25f)) - ) - - MediaLoadingAction( - isDownloading = content.isDownloading, - progress = content.downloadProgress, - idleIcon = if (content.supportsStreaming) Icons.Rounded.Stream else Icons.Default.Download, - idleContentDescription = if (content.supportsStreaming) { - stringResource(R.string.cd_stream) - } else { - stringResource(R.string.cd_download) - }, - onCancelClick = { - isAutoDownloadSuppressed = true - AutoDownloadSuppression.suppress(content.fileId) - onCancelDownload(content.fileId) - }, - onIdleClick = { - isAutoDownloadSuppressed = false - AutoDownloadSuppression.clear(content.fileId) - onVideoClick(msg) - } - ) - } + VideoLoadingLayer( + content = content, + onCancelDownload = { + isAutoDownloadSuppressed = true + AutoDownloadSuppression.suppress(content.fileId) + onCancelDownloadState(content.fileId) + }, + onStartDownload = { + isAutoDownloadSuppressed = false + AutoDownloadSuppression.clear(content.fileId) + onVideoClickState(msg) + } + ) } - Box( - modifier = Modifier - .matchParentSize() - .pointerInput( - content.isDownloading, - content.fileId, - isMediaSpoilerRevealed, - stablePath, - content.supportsStreaming - ) { - detectTapGestures( - onTap = { - if (!isMediaSpoilerRevealed) { - isMediaSpoilerRevealed = true - } else if (content.isDownloading) { - isAutoDownloadSuppressed = true - AutoDownloadSuppression.suppress(content.fileId) - onCancelDownload(content.fileId) - } else { - isAutoDownloadSuppressed = false - AutoDownloadSuppression.clear(content.fileId) - onVideoClick(msg) - } - }, - onLongPress = { offset -> onLongClick(videoPosition + offset) } - ) - } + VideoInteractionOverlay( + modifier = Modifier.matchParentSize(), + content = content, + isMediaSpoilerRevealed = isMediaSpoilerRevealed, + videoPosition = { layoutTracker.videoPosition }, + onRevealSpoiler = { isMediaSpoilerRevealed = true }, + onCancelDownload = { + isAutoDownloadSuppressed = true + AutoDownloadSuppression.suppress(content.fileId) + onCancelDownloadState(content.fileId) + }, + onOpenVideo = { + isAutoDownloadSuppressed = false + AutoDownloadSuppression.clear(content.fileId) + onVideoClickState(msg) + }, + onLongClick = { anchor -> onLongClickState(anchor) } ) - Box( + VideoPlaybackBadge( modifier = Modifier .align(Alignment.TopStart) - .padding(8.dp) - .background(Color.Black.copy(alpha = 0.45f), RoundedCornerShape(6.dp)) - .padding(horizontal = 6.dp, vertical = 2.dp) - ) { - Text( - text = if ((hasPath || content.supportsStreaming) && autoplayVideos) { - "${formatDuration(currentPositionSeconds)} / ${formatDuration(content.duration)}" - } else { - formatDuration(content.duration) - }, - style = MaterialTheme.typography.labelSmall, - color = Color.White - ) - } + .padding(8.dp), + durationSeconds = content.duration, + currentPositionSeconds = currentPositionSeconds, + showCurrentProgress = (hasPath || content.supportsStreaming) && autoplayVideos + ) - if (content.isUploading) { - Box( - modifier = Modifier - .matchParentSize() - .background(Color.Black.copy(alpha = 0.5f)), - contentAlignment = Alignment.Center - ) { - if (content.uploadProgress > 0f) { - CircularWavyProgressIndicator( - progress = { content.uploadProgress }, - color = Color.White, - trackColor = Color.White.copy(alpha = 0.3f) - ) - } else { - LoadingIndicator( - color = Color.White - ) - } - } - } + VideoUploadOverlay( + isUploading = content.isUploading, + uploadProgress = content.uploadProgress + ) - SpoilerWrapper(isRevealed = isMediaSpoilerRevealed) { - Box(modifier = Modifier.fillMaxSize()) - } + VideoSpoilerOverlay( + isRevealed = isMediaSpoilerRevealed + ) if (content.caption.isEmpty() && showMetadata) { - Box( + VideoMetadataBadge( modifier = Modifier .align(Alignment.BottomEnd) .padding(6.dp) @@ -424,10 +367,10 @@ fun VideoMessageBubble( Color.Black.copy(alpha = 0.45f), RoundedCornerShape(12.dp) ) - .padding(horizontal = 6.dp, vertical = 2.dp) - ) { - MessageMetadata(msg, isOutgoing, Color.White) - } + .padding(horizontal = 6.dp, vertical = 2.dp), + msg = msg, + isOutgoing = isOutgoing + ) } } @@ -477,8 +420,8 @@ fun VideoMessageBubble( revealedSpoilers.add(index) } }, - onClick = { offset -> onLongClick(videoPosition + offset) }, - onLongClick = { offset -> onLongClick(videoPosition + offset) } + onClick = { offset -> onLongClickState(layoutTracker.videoPosition + offset) }, + onLongClick = { offset -> onLongClickState(layoutTracker.videoPosition + offset) } ) } if (showMetadata) { @@ -500,3 +443,169 @@ fun VideoMessageBubble( } } } + +@Composable +private fun VideoMuteToggle( + modifier: Modifier = Modifier, + isMuted: Boolean, + onToggle: () -> Unit +) { + Box( + modifier = modifier + .size(30.dp) + .background(Color.Black.copy(alpha = 0.45f), CircleShape) + .clickable(onClick = onToggle), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = if (isMuted) Icons.AutoMirrored.Filled.VolumeOff else Icons.AutoMirrored.Filled.VolumeUp, + contentDescription = stringResource(R.string.cd_toggle_sound), + tint = Color.White, + modifier = Modifier.size(16.dp) + ) + } +} + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +private fun VideoLoadingLayer( + content: MessageContent.Video, + onCancelDownload: () -> Unit, + onStartDownload: () -> Unit +) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + MediaLoadingBackground( + previewData = content.minithumbnail, + contentScale = ContentScale.Crop, + previewBlur = 14.dp + ) + + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.25f)) + ) + + MediaLoadingAction( + isDownloading = content.isDownloading, + progress = content.downloadProgress, + idleIcon = if (content.supportsStreaming) Icons.Rounded.Stream else Icons.Default.Download, + idleContentDescription = if (content.supportsStreaming) { + stringResource(R.string.cd_stream) + } else { + stringResource(R.string.cd_download) + }, + onCancelClick = onCancelDownload, + onIdleClick = onStartDownload + ) + } +} + +@Composable +private fun VideoInteractionOverlay( + modifier: Modifier = Modifier, + content: MessageContent.Video, + isMediaSpoilerRevealed: Boolean, + videoPosition: () -> Offset, + onRevealSpoiler: () -> Unit, + onCancelDownload: () -> Unit, + onOpenVideo: () -> Unit, + onLongClick: (Offset) -> Unit +) { + Box( + modifier = modifier.pointerInput( + content.isDownloading, + content.fileId, + isMediaSpoilerRevealed, + content.supportsStreaming + ) { + detectTapGestures( + onTap = { + if (!isMediaSpoilerRevealed) { + onRevealSpoiler() + } else if (content.isDownloading) { + onCancelDownload() + } else { + onOpenVideo() + } + }, + onLongPress = { offset -> onLongClick(videoPosition() + offset) } + ) + } + ) +} + +@Composable +private fun VideoPlaybackBadge( + modifier: Modifier = Modifier, + durationSeconds: Int, + currentPositionSeconds: Int, + showCurrentProgress: Boolean +) { + Box( + modifier = modifier + .background(Color.Black.copy(alpha = 0.45f), RoundedCornerShape(6.dp)) + .padding(horizontal = 6.dp, vertical = 2.dp) + ) { + Text( + text = if (showCurrentProgress) { + "${formatDuration(currentPositionSeconds)} / ${formatDuration(durationSeconds)}" + } else { + formatDuration(durationSeconds) + }, + style = MaterialTheme.typography.labelSmall, + color = Color.White + ) + } +} + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +private fun VideoUploadOverlay( + isUploading: Boolean, + uploadProgress: Float +) { + if (!isUploading) return + + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.5f)), + contentAlignment = Alignment.Center + ) { + if (uploadProgress > 0f) { + CircularWavyProgressIndicator( + progress = { uploadProgress }, + color = Color.White, + trackColor = Color.White.copy(alpha = 0.3f) + ) + } else { + LoadingIndicator( + color = Color.White + ) + } + } +} + +@Composable +private fun VideoSpoilerOverlay( + isRevealed: Boolean +) { + SpoilerWrapper(isRevealed = isRevealed) { + Box(modifier = Modifier.fillMaxSize()) + } +} + +@Composable +private fun VideoMetadataBadge( + modifier: Modifier = Modifier, + msg: MessageModel, + isOutgoing: Boolean +) { + Box(modifier = modifier) { + MessageMetadata(msg, isOutgoing, Color.White) + } +} diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/pins/PinnedMessagesListSheet.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/pins/PinnedMessagesListSheet.kt index 56ba927e..239c4ea8 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/pins/PinnedMessagesListSheet.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/pins/PinnedMessagesListSheet.kt @@ -9,12 +9,40 @@ import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.draggable import androidx.compose.foundation.gestures.rememberDraggableState import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.BottomSheetDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush @@ -32,13 +60,16 @@ import org.monogram.domain.models.MessageModel import org.monogram.presentation.R import org.monogram.presentation.core.ui.rememberShimmerBrush import org.monogram.presentation.core.util.IDownloadUtils -import org.monogram.presentation.features.chats.conversation.ui.content.GroupedMessageItem -import org.monogram.presentation.features.chats.conversation.ui.content.groupMessagesByAlbum -import org.monogram.presentation.features.chats.conversation.ui.content.shouldShowDate import org.monogram.presentation.features.chats.conversation.ui.AlbumMessageBubbleContainer -import org.monogram.presentation.features.chats.conversation.ui.ChannelMessageBubbleContainer import org.monogram.presentation.features.chats.conversation.ui.DateSeparator +import org.monogram.presentation.features.chats.conversation.ui.MessageAppearanceConfig import org.monogram.presentation.features.chats.conversation.ui.MessageBubbleContainer +import org.monogram.presentation.features.chats.conversation.ui.MessageRowBehaviorConfig +import org.monogram.presentation.features.chats.conversation.ui.buildSenderGrouping +import org.monogram.presentation.features.chats.conversation.ui.channel.ChannelMessageBubbleContainer +import org.monogram.presentation.features.chats.conversation.ui.content.GroupedMessageItem +import org.monogram.presentation.features.chats.conversation.ui.content.groupMessagesByAlbum +import org.monogram.presentation.features.chats.conversation.ui.content.shouldShowDate import kotlin.math.roundToInt @OptIn(ExperimentalMaterial3Api::class) @@ -162,15 +193,15 @@ fun PinnedMessagesListSheet( return@draggable } - val shouldDismiss = - dismissOffsetY > dismissDistanceThresholdPx || - velocity > dismissVelocityThresholdPx + val shouldDismiss = + dismissOffsetY > dismissDistanceThresholdPx || + velocity > dismissVelocityThresholdPx - if (shouldDismiss) { - onDismissRequest() - } else { - scope.launch { - animate( + if (shouldDismiss) { + onDismissRequest() + } else { + scope.launch { + animate( initialValue = dismissOffsetY, targetValue = 0f, animationSpec = spring() @@ -273,41 +304,73 @@ fun PinnedMessagesListSheet( Box(modifier = Modifier.animateItem()) { if (isChannel) { if (item is GroupedMessageItem.Single) { + val behavior = MessageRowBehaviorConfig( + isGroup = false, + isChannel = true, + isTopicClosed = false, + canReply = false, + swipeEnabled = false, + isSelectionMode = false, + isAnyViewerOpen = false + ) + val appearance = MessageAppearanceConfig( + fontSize = fontSize, + letterSpacing = letterSpacing, + bubbleRadius = bubbleRadius, + stickerSize = stickerSize, + autoplayGifs = autoplayGifs, + autoplayVideos = autoplayVideos, + autoDownloadFiles = autoDownloadFiles + ) ChannelMessageBubbleContainer( msg = item.message, - olderMsg = olderMsg, newerMsg = newerMsg, - autoplayGifs = autoplayGifs, - autoplayVideos = autoplayVideos, - autoDownloadFiles = autoDownloadFiles, + appearance = appearance, + behavior = behavior, + senderGrouping = buildSenderGrouping( + item, + olderMsg, + newerMsg + ), onPhotoClick = { onMessageClick(it) }, onVideoClick = { onMessageClick(it) }, onDocumentClick = { onMessageClick(it) }, onReplyClick = { _, _, _ -> onMessageClick(item.message) }, onGoToReply = { onReplyClick(it) }, onReactionClick = onReactionClick, - fontSize = fontSize, - letterSpacing = letterSpacing, - bubbleRadius = bubbleRadius, - stickerSize = stickerSize, downloadUtils = downloadUtils ) } else if (item is GroupedMessageItem.Album) { AlbumMessageBubbleContainer( messages = item.messages, - olderMsg = olderMsg, - newerMsg = newerMsg, - isGroup = false, - isChannel = true, - autoplayGifs = autoplayGifs, - autoplayVideos = autoplayVideos, + appearance = MessageAppearanceConfig( + fontSize = fontSize, + letterSpacing = letterSpacing, + bubbleRadius = bubbleRadius, + stickerSize = stickerSize, + autoplayGifs = autoplayGifs, + autoplayVideos = autoplayVideos + ), + behavior = MessageRowBehaviorConfig( + isGroup = false, + isChannel = true, + isTopicClosed = false, + canReply = false, + swipeEnabled = false, + isSelectionMode = false, + isAnyViewerOpen = false + ), + senderGrouping = buildSenderGrouping( + item, + olderMsg, + newerMsg + ), onPhotoClick = { onMessageClick(it) }, onVideoClick = { onMessageClick(it) }, onReplyClick = { _, _, _ -> onMessageClick(item.messages.last()) }, onGoToReply = { onReplyClick(it) }, onReactionClick = onReactionClick, toProfile = {}, - canReply = false, downloadUtils = downloadUtils ) } @@ -315,19 +378,33 @@ fun PinnedMessagesListSheet( if (item is GroupedMessageItem.Single) { MessageBubbleContainer( msg = item.message, - olderMsg = olderMsg, newerMsg = newerMsg, - isGroup = isGroup, - fontSize = fontSize, - letterSpacing = letterSpacing, - bubbleRadius = bubbleRadius, - stSize = stickerSize, - autoDownloadMobile = autoDownloadMobile, - autoDownloadWifi = autoDownloadWifi, - autoDownloadRoaming = autoDownloadRoaming, - autoDownloadFiles = autoDownloadFiles, - autoplayGifs = autoplayGifs, - autoplayVideos = autoplayVideos, + appearance = MessageAppearanceConfig( + fontSize = fontSize, + letterSpacing = letterSpacing, + bubbleRadius = bubbleRadius, + stickerSize = stickerSize, + autoplayGifs = autoplayGifs, + autoplayVideos = autoplayVideos, + autoDownloadMobile = autoDownloadMobile, + autoDownloadWifi = autoDownloadWifi, + autoDownloadRoaming = autoDownloadRoaming, + autoDownloadFiles = autoDownloadFiles + ), + behavior = MessageRowBehaviorConfig( + isGroup = isGroup, + isChannel = false, + isTopicClosed = false, + canReply = false, + swipeEnabled = false, + isSelectionMode = false, + isAnyViewerOpen = false + ), + senderGrouping = buildSenderGrouping( + item, + olderMsg, + newerMsg + ), onPhotoClick = { onMessageClick(it) }, onVideoClick = { onMessageClick(it) }, onDocumentClick = { onMessageClick(it) }, @@ -335,25 +412,39 @@ fun PinnedMessagesListSheet( onGoToReply = { onReplyClick(it) }, onReactionClick = onReactionClick, toProfile = {}, - canReply = false, downloadUtils = downloadUtils ) } else if (item is GroupedMessageItem.Album) { AlbumMessageBubbleContainer( messages = item.messages, - olderMsg = olderMsg, - newerMsg = newerMsg, - isGroup = isGroup, - isChannel = false, - autoplayGifs = autoplayGifs, - autoplayVideos = autoplayVideos, + appearance = MessageAppearanceConfig( + fontSize = fontSize, + letterSpacing = letterSpacing, + bubbleRadius = bubbleRadius, + stickerSize = stickerSize, + autoplayGifs = autoplayGifs, + autoplayVideos = autoplayVideos + ), + behavior = MessageRowBehaviorConfig( + isGroup = isGroup, + isChannel = false, + isTopicClosed = false, + canReply = false, + swipeEnabled = false, + isSelectionMode = false, + isAnyViewerOpen = false + ), + senderGrouping = buildSenderGrouping( + item, + olderMsg, + newerMsg + ), onPhotoClick = { onMessageClick(it) }, onVideoClick = { onMessageClick(it) }, onReplyClick = { _, _, _ -> onMessageClick(item.messages.last()) }, onGoToReply = { onReplyClick(it) }, onReactionClick = onReactionClick, toProfile = {}, - canReply = false, downloadUtils = downloadUtils ) } diff --git a/presentation/src/main/java/org/monogram/presentation/settings/chatSettings/components/ChatSettingsPreview.kt b/presentation/src/main/java/org/monogram/presentation/settings/chatSettings/components/ChatSettingsPreview.kt index 87e30106..e6d95eeb 100644 --- a/presentation/src/main/java/org/monogram/presentation/settings/chatSettings/components/ChatSettingsPreview.kt +++ b/presentation/src/main/java/org/monogram/presentation/settings/chatSettings/components/ChatSettingsPreview.kt @@ -1,6 +1,13 @@ package org.monogram.presentation.settings.chatSettings.components -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.CircleShape @@ -19,10 +26,19 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage -import org.monogram.domain.models.* +import org.monogram.domain.models.MessageContent +import org.monogram.domain.models.MessageEntity +import org.monogram.domain.models.MessageEntityType +import org.monogram.domain.models.MessageModel +import org.monogram.domain.models.MessageReactionModel +import org.monogram.domain.models.WallpaperModel import org.monogram.presentation.R import org.monogram.presentation.core.util.IDownloadUtils +import org.monogram.presentation.features.chats.conversation.ui.MessageAppearanceConfig import org.monogram.presentation.features.chats.conversation.ui.MessageBubbleContainer +import org.monogram.presentation.features.chats.conversation.ui.MessageRowBehaviorConfig +import org.monogram.presentation.features.chats.conversation.ui.buildSenderGrouping +import org.monogram.presentation.features.chats.conversation.ui.content.GroupedMessageItem import java.io.File @Composable @@ -217,18 +233,33 @@ fun ChatSettingsPreview( MessageBubbleContainer( msg = msg, - olderMsg = olderMsg, newerMsg = newerMsg, - isGroup = true, - fontSize = fontSize, - letterSpacing = letterSpacing, - bubbleRadius = bubbleRadius, - autoDownloadMobile = true, - autoDownloadWifi = true, - autoDownloadRoaming = false, - autoDownloadFiles = false, - autoplayGifs = true, - autoplayVideos = true, + appearance = MessageAppearanceConfig( + fontSize = fontSize, + letterSpacing = letterSpacing, + bubbleRadius = bubbleRadius, + stickerSize = 200f, + autoDownloadMobile = true, + autoDownloadWifi = true, + autoDownloadRoaming = false, + autoDownloadFiles = false, + autoplayGifs = true, + autoplayVideos = true + ), + behavior = MessageRowBehaviorConfig( + isGroup = true, + isChannel = false, + isTopicClosed = false, + canReply = false, + swipeEnabled = false, + isSelectionMode = false, + isAnyViewerOpen = false + ), + senderGrouping = buildSenderGrouping( + item = GroupedMessageItem.Single(msg), + olderMsg = olderMsg, + newerMsg = newerMsg + ), onPhotoClick = onPhotoClick, onReplyClick = onReplyClick, toProfile = toProfile, From a8f61992ed0fa6e39b717add0bb9f0f44b63f773 Mon Sep 17 00:00:00 2001 From: Artur Skubei <41114720+gdlbo@users.noreply.github.com> Date: Sat, 25 Apr 2026 11:40:32 +0300 Subject: [PATCH 6/8] refactor media viewer architecture and enhance tablet layout support - decouple `MediaViewer` into specialized `ImageViewer` and `VideoViewer` components to reduce complexity and improve state management - introduce `FullscreenViewerHost` and `FullscreenViewerHostState` to centralize common fullscreen UI logic, including system bar visibility, gesture-based dismissal, and PiP handling - update `VideoViewer` to use `VideoPage` directly within the new host, improving lifecycle management of the `ExoPlayer` instance - reimplement `ImageViewer` using a `HorizontalPager` and a dedicated `ImageOverlay` for better navigation and caption handling - refactor `ChatContentOverlays` and `MainContent` to handle media viewers differently based on window size class and tablet interface settings - optimize `MediaViewer` entry point to bypass pager logic when rendering a single video item - remove redundant logging and standardize internal visibility for chat overlay components - ensure consistent reset of zoom and scroll states when navigating between media items in a pager --- .../main/java/org/monogram/app/MainContent.kt | 14 +- .../ui/content/ChatContentOverlays.kt | 27 +- .../ui/content/ChatContentViewers.kt | 460 +++++++++--------- .../features/viewers/ImageViewer.kt | 112 ++++- .../features/viewers/MediaViewer.kt | 304 ++++++------ .../features/viewers/VideoViewer.kt | 80 ++- .../components/ImageViewerComponents.kt | 1 + .../components/VideoViewerComponents.kt | 1 - 8 files changed, 554 insertions(+), 445 deletions(-) diff --git a/app/src/main/java/org/monogram/app/MainContent.kt b/app/src/main/java/org/monogram/app/MainContent.kt index 88b1b12b..8a926d9e 100644 --- a/app/src/main/java/org/monogram/app/MainContent.kt +++ b/app/src/main/java/org/monogram/app/MainContent.kt @@ -172,12 +172,14 @@ fun MainContent( when (activeChild) { is RootComponent.Child.ChatDetailChild -> { - val chatState by activeChild.component.state.collectAsState() - ChatContentViewers( - state = chatState, - component = activeChild.component, - localClipboard = localClipboard - ) + if (isExpanded && isTabletInterfaceEnabled) { + val chatState by activeChild.component.state.collectAsState() + ChatContentViewers( + state = chatState, + component = activeChild.component, + localClipboard = localClipboard + ) + } } is RootComponent.Child.ProfileChild -> { val profileState by activeChild.component.state.subscribeAsState() diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentOverlays.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentOverlays.kt index dc3f929e..c41b6591 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentOverlays.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentOverlays.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Block +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset @@ -13,17 +14,19 @@ import androidx.compose.ui.platform.Clipboard import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.IntSize import androidx.compose.ui.zIndex +import androidx.window.core.layout.WindowWidthSizeClass import org.monogram.domain.models.ChatPermissionsModel import org.monogram.domain.models.MessageModel import org.monogram.presentation.R import org.monogram.presentation.core.ui.ConfirmationSheet +import org.monogram.presentation.core.util.LocalTabletInterfaceEnabled import org.monogram.presentation.features.chats.conversation.ChatComponent +import org.monogram.presentation.features.chats.conversation.editor.photo.PhotoEditorScreen +import org.monogram.presentation.features.chats.conversation.editor.video.VideoEditorScreen import org.monogram.presentation.features.chats.conversation.ui.StickerSetSheet import org.monogram.presentation.features.chats.conversation.ui.message.BotCommandsSheet import org.monogram.presentation.features.chats.conversation.ui.message.PollVotersSheet import org.monogram.presentation.features.chats.conversation.ui.pins.PinnedMessagesListSheet -import org.monogram.presentation.features.chats.conversation.editor.photo.PhotoEditorScreen -import org.monogram.presentation.features.chats.conversation.editor.video.VideoEditorScreen @Composable internal fun ChatContentOverlays( @@ -58,6 +61,11 @@ internal fun ChatContentOverlays( isCustomBackHandlingEnabled: Boolean, onBack: () -> Unit ) { + val adaptiveInfo = currentWindowAdaptiveInfo() + val isTabletInterfaceEnabled = LocalTabletInterfaceEnabled.current + val isTablet = + adaptiveInfo.windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.EXPANDED && + isTabletInterfaceEnabled if (renderPinnedMessagesList) { PinnedMessagesListSheet( isVisible = state.showPinnedMessagesList, @@ -117,11 +125,16 @@ internal fun ChatContentOverlays( ) } - ChatContentViewers( - state = state, - component = component, - localClipboard = localClipboard - ) + if (!isTablet) { + 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) + } selectedMessage?.let { msg -> ChatMessageOptionsMenu( diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentViewers.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentViewers.kt index fe8a6beb..589344a6 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentViewers.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentViewers.kt @@ -1,9 +1,17 @@ package org.monogram.presentation.features.chats.conversation.ui.content import android.content.ClipData -import android.util.Log -import androidx.compose.animation.* -import androidx.compose.runtime.* +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.platform.Clipboard import androidx.compose.ui.text.AnnotatedString import org.monogram.domain.models.MessageContent @@ -35,7 +43,7 @@ fun ChatContentViewers( } @Composable -private fun InstantViewOverlay(state: ChatComponent.State, component: ChatComponent) { +internal fun InstantViewOverlay(state: ChatComponent.State, component: ChatComponent) { AnimatedVisibility( visible = state.instantViewUrl != null, enter = slideInVertically(initialOffsetY = { it }) + fadeIn(), @@ -54,7 +62,7 @@ private fun InstantViewOverlay(state: ChatComponent.State, component: ChatCompon } @Composable -private fun YouTubeOverlay( +internal fun YouTubeOverlay( state: ChatComponent.State, component: ChatComponent, localClipboard: Clipboard @@ -92,7 +100,7 @@ private fun YouTubeOverlay( } @Composable -private fun MiniAppOverlay(state: ChatComponent.State, component: ChatComponent) { +internal fun MiniAppOverlay(state: ChatComponent.State, component: ChatComponent) { AnimatedVisibility( visible = state.miniAppUrl != null, enter = slideInVertically(initialOffsetY = { it }) + fadeIn(), @@ -113,7 +121,7 @@ private fun MiniAppOverlay(state: ChatComponent.State, component: ChatComponent) } @Composable -private fun WebViewOverlay(state: ChatComponent.State, component: ChatComponent) { +internal fun WebViewOverlay(state: ChatComponent.State, component: ChatComponent) { AnimatedVisibility( visible = state.webViewUrl != null, enter = slideInVertically(initialOffsetY = { it }) + fadeIn(), @@ -129,18 +137,14 @@ private fun WebViewOverlay(state: ChatComponent.State, component: ChatComponent) } @Composable -private fun ImagesOverlay( +internal fun ImagesOverlay( state: ChatComponent.State, component: ChatComponent, localClipboard: Clipboard ) { - AnimatedVisibility( - visible = state.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) { + state.fullScreenImages?.let { images -> + val autoDownload = + remember(state.autoDownloadWifi, state.autoDownloadRoaming, state.autoDownloadMobile) { when { component.downloadUtils.isWifiConnected() -> state.autoDownloadWifi component.downloadUtils.isRoaming() -> state.autoDownloadRoaming @@ -148,157 +152,168 @@ private fun ImagesOverlay( } } - val viewerItems = remember(images, state.fullScreenImageMessageIds, state.messages) { - val messageMap = state.messages.associateBy { it.id } - val items = if (state.fullScreenImageMessageIds.size == images.size) { - state.fullScreenImageMessageIds.mapIndexed { index, messageId -> - val message = messageMap[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) } - ViewerMediaItem( - messageId = message?.id ?: 0L, - path = message?.displayMediaPathForViewer() ?: path - ) - } + val viewerItems = remember(images, state.fullScreenImageMessageIds, state.messages) { + val messageMap = state.messages.associateBy { it.id } + val items = if (state.fullScreenImageMessageIds.size == images.size) { + state.fullScreenImageMessageIds.mapIndexed { index, messageId -> + val message = messageMap[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) } + ViewerMediaItem( + messageId = message?.id ?: 0L, + path = message?.displayMediaPathForViewer() ?: path + ) } - items.sortedBy { it.messageId } } + items.sortedBy { it.messageId } + } - val viewerImages = remember(viewerItems) { viewerItems.map { it.path } } - val imageMessageIds = remember(viewerItems) { viewerItems.map { it.messageId } } - val startMessageId = state.fullScreenImageMessageIds.getOrNull(state.fullScreenStartIndex) + val viewerImages = remember(viewerItems) { viewerItems.map { it.path } } + val imageMessageIds = remember(viewerItems) { viewerItems.map { it.messageId } } + val startMessageId = state.fullScreenImageMessageIds.getOrNull(state.fullScreenStartIndex) - val startIndex = remember(viewerItems, startMessageId) { - val index = viewerItems.indexOfFirst { it.messageId == startMessageId } - index - .takeIf { it != -1 } - ?.coerceIn(0, viewerItems.lastIndex.coerceAtLeast(0)) - ?: 0 - } + val startIndex = remember(viewerItems, startMessageId) { + val index = viewerItems.indexOfFirst { it.messageId == startMessageId } + index + .takeIf { it != -1 } + ?.coerceIn(0, viewerItems.lastIndex.coerceAtLeast(0)) + ?: 0 + } - var currentImageIndex by remember(viewerItems, startIndex) { - mutableIntStateOf( - startIndex.coerceIn(0, viewerItems.lastIndex.coerceAtLeast(0)) - ) - } + var currentImageIndex by remember(viewerItems, startIndex) { + mutableIntStateOf( + startIndex.coerceIn(0, viewerItems.lastIndex.coerceAtLeast(0)) + ) + } - val currentViewerMessage = remember(currentImageIndex, imageMessageIds, state.messages) { - imageMessageIds.getOrNull(currentImageIndex) - ?.takeIf { it != 0L } - ?.let { id -> state.messages.firstOrNull { it.id == id } } - } + val currentViewerMessage = remember(currentImageIndex, imageMessageIds, state.messages) { + imageMessageIds.getOrNull(currentImageIndex) + ?.takeIf { it != 0L } + ?.let { id -> state.messages.firstOrNull { it.id == id } } + } - val imageDownloadingStates = remember(imageMessageIds, state.messages) { - imageMessageIds.map { id -> - val content = state.messages.firstOrNull { it.id == id }?.content - when (content) { - is MessageContent.Photo -> content.isDownloading - else -> false - } + val imageDownloadingStates = remember(imageMessageIds, state.messages) { + imageMessageIds.map { id -> + val content = state.messages.firstOrNull { it.id == id }?.content + when (content) { + is MessageContent.Photo -> content.isDownloading + else -> false } } + } - val imageDownloadProgressStates = remember(imageMessageIds, state.messages) { - imageMessageIds.map { id -> - val content = state.messages.firstOrNull { it.id == id }?.content - when (content) { - is MessageContent.Photo -> content.downloadProgress - else -> 0f - } + val imageDownloadProgressStates = remember(imageMessageIds, state.messages) { + imageMessageIds.map { id -> + val content = state.messages.firstOrNull { it.id == id }?.content + when (content) { + is MessageContent.Photo -> content.downloadProgress + else -> 0f } } + } - if (viewerImages.isNotEmpty()) { - ImageViewer( - images = viewerImages, - startIndex = startIndex.coerceIn(0, viewerImages.lastIndex.coerceAtLeast(0)), - onDismiss = component::onDismissImages, - autoDownload = autoDownload, - onPageChanged = { index -> - currentImageIndex = index - imageMessageIds.getOrNull(index)?.takeIf { it != 0L }?.let(component::onDownloadHighRes) - imageMessageIds.getOrNull(index + 1)?.takeIf { it != 0L }?.let(component::onDownloadHighRes) - }, - onForward = { path -> - val msg = currentViewerMessage ?: state.messages.find { it.content.matchesDisplayPath(path) } - msg?.let { component.onForwardMessage(it) } - }, - onDelete = { path -> - val msg = currentViewerMessage ?: state.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 link = if (msg != null) { - if (!state.isGroup && !state.isChannel) { - "tg://openmessage?user_id=${state.chatId}&message_id=${msg.id shr 20}" - } else { - "https://t.me/c/${state.chatId.toString().removePrefix("-100")}/${msg.id shr 20}" - } + if (viewerImages.isNotEmpty()) { + ImageViewer( + images = viewerImages, + startIndex = startIndex.coerceIn(0, viewerImages.lastIndex.coerceAtLeast(0)), + onDismiss = component::onDismissImages, + autoDownload = autoDownload, + onPageChanged = { index -> + currentImageIndex = index + imageMessageIds.getOrNull(index)?.takeIf { it != 0L } + ?.let(component::onDownloadHighRes) + imageMessageIds.getOrNull(index + 1)?.takeIf { it != 0L } + ?.let(component::onDownloadHighRes) + }, + onForward = { path -> + val msg = currentViewerMessage ?: state.messages.find { + it.content.matchesDisplayPath(path) + } + msg?.let { component.onForwardMessage(it) } + }, + onDelete = { path -> + val msg = currentViewerMessage ?: state.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 link = if (msg != null) { + if (!state.isGroup && !state.isChannel) { + "tg://openmessage?user_id=${state.chatId}&message_id=${msg.id shr 20}" } else { - path + "https://t.me/c/${ + state.chatId.toString().removePrefix("-100") + }/${msg.id shr 20}" + } + } else { + path + } + localClipboard.nativeClipboard.setPrimaryClip( + ClipData.newPlainText("", AnnotatedString(link)) + ) + }, + onCopyText = { path -> + val msg = state.messages.find { + when (val content = it.content) { + is MessageContent.Photo -> content.path == path + is MessageContent.Video -> content.path == path + is MessageContent.Gif -> content.path == path + else -> false } + } + val textToCopy = when (val content = msg?.content) { + is MessageContent.Photo -> content.caption + is MessageContent.Video -> content.caption + is MessageContent.Gif -> content.caption + else -> "" + } + if (textToCopy.isNotEmpty()) { localClipboard.nativeClipboard.setPrimaryClip( - ClipData.newPlainText("", AnnotatedString(link)) + ClipData.newPlainText("", AnnotatedString(textToCopy)) ) - }, - onCopyText = { path -> - val msg = state.messages.find { - when (val content = it.content) { - is MessageContent.Photo -> content.path == path - is MessageContent.Video -> content.path == path - is MessageContent.Gif -> content.path == path - else -> false + } + }, + onVideoClick = { path -> + val msg = currentViewerMessage ?: state.messages.find { + it.content.matchesDisplayPath(path) + } + if (msg != null) { + val mediaPath = msg.displayMediaPathForViewer() ?: path + component.onOpenVideo( + path = mediaPath, + messageId = msg.id, + caption = when (val content = msg.content) { + is MessageContent.Video -> content.caption + is MessageContent.Gif -> content.caption + else -> null } - } - val textToCopy = when (val content = msg?.content) { - is MessageContent.Photo -> content.caption - is MessageContent.Video -> content.caption - is MessageContent.Gif -> content.caption - else -> "" - } - if (textToCopy.isNotEmpty()) { - localClipboard.nativeClipboard.setPrimaryClip( - ClipData.newPlainText("", AnnotatedString(textToCopy)) - ) - } - }, - onVideoClick = { path -> - val msg = currentViewerMessage ?: state.messages.find { it.content.matchesDisplayPath(path) } - if (msg != null) { - val mediaPath = msg.displayMediaPathForViewer() ?: path - component.onOpenVideo( - path = mediaPath, - messageId = msg.id, - caption = when (val content = msg.content) { - is MessageContent.Video -> content.caption - is MessageContent.Gif -> content.caption - else -> null - } - ) - } else { - component.onOpenVideo(path = path, messageId = null, caption = null) - } - }, - captions = state.fullScreenCaptions, - imageDownloadingStates = imageDownloadingStates, - imageDownloadProgressStates = imageDownloadProgressStates, - downloadUtils = component.downloadUtils - ) - } + ) + } else { + component.onOpenVideo(path = path, messageId = null, caption = null) + } + }, + captions = state.fullScreenCaptions, + imageDownloadingStates = imageDownloadingStates, + imageDownloadProgressStates = imageDownloadProgressStates, + downloadUtils = component.downloadUtils + ) } } } @Composable -private fun VideoOverlay( +internal fun VideoOverlay( state: ChatComponent.State, component: ChatComponent, localClipboard: Clipboard @@ -306,103 +321,96 @@ private fun VideoOverlay( val videoVisible = (state.fullScreenVideoPath != null || state.fullScreenVideoMessageId != null) && state.fullScreenImages == null - AnimatedVisibility( - visible = videoVisible, - enter = fadeIn() + scaleIn(initialScale = 0.9f), - exit = fadeOut() + scaleOut(targetScale = 0.9f) - ) { - if (videoVisible) { - val messageId = state.fullScreenVideoMessageId - val path = state.fullScreenVideoPath + if (videoVisible) { + val messageId = state.fullScreenVideoMessageId + val path = state.fullScreenVideoPath - val msg = remember(messageId, path, state.messages) { - state.messages.find { it.id == messageId } ?: state.messages.find { - it.content.matchesDisplayPath(path ?: "") - } + val msg = remember(messageId, path, state.messages) { + state.messages.find { it.id == messageId } ?: state.messages.find { + it.content.matchesDisplayPath(path ?: "") } + } - val videoContent = msg?.content as? MessageContent.Video - val gifContent = msg?.content as? MessageContent.Gif + val videoContent = msg?.content as? MessageContent.Video + val gifContent = msg?.content as? MessageContent.Gif - val fileId = videoContent?.fileId ?: gifContent?.fileId ?: 0 - val supportsStreaming = videoContent?.supportsStreaming ?: false - val finalPath = path ?: videoContent?.path ?: gifContent?.path ?: "" + val fileId = videoContent?.fileId ?: gifContent?.fileId ?: 0 + val supportsStreaming = videoContent?.supportsStreaming ?: false + val finalPath = path ?: videoContent?.path ?: gifContent?.path ?: "" - if (finalPath.isNotBlank() || (supportsStreaming && fileId != 0)) { - key(finalPath, fileId) { - Log.d("ChatContentViewers", "Rendering VideoViewer for $finalPath") - VideoViewer( - path = finalPath, - onDismiss = component::onDismissVideo, - isGesturesEnabled = state.isPlayerGesturesEnabled, - isDoubleTapSeekEnabled = state.isPlayerDoubleTapSeekEnabled, - seekDuration = state.playerSeekDuration, - isZoomEnabled = state.isPlayerZoomEnabled, - onForward = { videoPath -> - val forwardMsg = state.messages.find { - it.content.matchesDisplayPath(videoPath) - } - forwardMsg?.let { component.onForwardMessage(it) } - }, - onDelete = { videoPath -> - val deleteMsg = state.messages.find { - it.content.matchesDisplayPath(videoPath) - } - if (deleteMsg?.isOutgoing == true) { - component.onDeleteMessage(deleteMsg, true) - component.onDismissVideo() - } - }, - onCopyLink = { videoPath -> - val linkMsg = state.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}" - } else { - "https://t.me/c/${ - state.chatId.toString().removePrefix("-100") - }/${linkMsg.id shr 20}" - } + if (finalPath.isNotBlank() || (supportsStreaming && fileId != 0)) { + key(finalPath, fileId) { + VideoViewer( + path = finalPath, + onDismiss = component::onDismissVideo, + isGesturesEnabled = state.isPlayerGesturesEnabled, + isDoubleTapSeekEnabled = state.isPlayerDoubleTapSeekEnabled, + seekDuration = state.playerSeekDuration, + isZoomEnabled = state.isPlayerZoomEnabled, + onForward = { videoPath -> + val forwardMsg = state.messages.find { + it.content.matchesDisplayPath(videoPath) + } + forwardMsg?.let { component.onForwardMessage(it) } + }, + onDelete = { videoPath -> + val deleteMsg = state.messages.find { + it.content.matchesDisplayPath(videoPath) + } + if (deleteMsg?.isOutgoing == true) { + component.onDeleteMessage(deleteMsg, true) + component.onDismissVideo() + } + }, + onCopyLink = { videoPath -> + val linkMsg = state.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}" } else { - videoPath + "https://t.me/c/${ + state.chatId.toString().removePrefix("-100") + }/${linkMsg.id shr 20}" } + } else { + videoPath + } + localClipboard.nativeClipboard.setPrimaryClip( + ClipData.newPlainText("", AnnotatedString(link)) + ) + }, + onCopyText = { videoPath -> + val textMsg = state.messages.find { + it.content.matchesDisplayPath(videoPath) + } + val textToCopy = when (val content = textMsg?.content) { + is MessageContent.Video -> content.caption + is MessageContent.Gif -> content.caption + else -> "" + } + if (textToCopy.isNotEmpty()) { localClipboard.nativeClipboard.setPrimaryClip( - ClipData.newPlainText("", AnnotatedString(link)) + ClipData.newPlainText("", AnnotatedString(textToCopy)) ) - }, - onCopyText = { videoPath -> - val textMsg = state.messages.find { - it.content.matchesDisplayPath(videoPath) - } - val textToCopy = when (val content = textMsg?.content) { - is MessageContent.Video -> content.caption - is MessageContent.Gif -> content.caption - else -> "" - } - if (textToCopy.isNotEmpty()) { - localClipboard.nativeClipboard.setPrimaryClip( - ClipData.newPlainText("", AnnotatedString(textToCopy)) - ) - } - }, - onSaveGif = if (state.messages.any { (it.content as? MessageContent.Gif)?.path == finalPath }) { - { videoPath -> component.onAddToGifs(videoPath) } - } else null, - caption = state.fullScreenVideoCaption, - fileId = fileId, - supportsStreaming = supportsStreaming, - downloadUtils = component.downloadUtils - ) - } + } + }, + onSaveGif = if (state.messages.any { (it.content as? MessageContent.Gif)?.path == finalPath }) { + { videoPath -> component.onAddToGifs(videoPath) } + } else null, + caption = state.fullScreenVideoCaption, + fileId = fileId, + supportsStreaming = supportsStreaming, + downloadUtils = component.downloadUtils + ) } } } } @Composable -private fun InvoiceOverlay(state: ChatComponent.State, component: ChatComponent) { +internal fun InvoiceOverlay(state: ChatComponent.State, component: ChatComponent) { if (state.invoiceSlug != null || state.invoiceMessageId != null) { InvoiceDialog( slug = state.invoiceSlug, @@ -416,7 +424,7 @@ private fun InvoiceOverlay(state: ChatComponent.State, component: ChatComponent) } @Composable -private fun MiniAppTOSOverlay(state: ChatComponent.State, component: ChatComponent) { +internal fun MiniAppTOSOverlay(state: ChatComponent.State, component: ChatComponent) { MiniAppTOSBottomSheet( isVisible = state.showMiniAppTOS, onDismiss = { component.onDismissMiniAppTOS() }, diff --git a/presentation/src/main/java/org/monogram/presentation/features/viewers/ImageViewer.kt b/presentation/src/main/java/org/monogram/presentation/features/viewers/ImageViewer.kt index 9e102cba..7d18da30 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/viewers/ImageViewer.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/viewers/ImageViewer.kt @@ -1,8 +1,25 @@ package org.monogram.presentation.features.viewers +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PageSize +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds import org.monogram.presentation.core.util.IDownloadUtils +import org.monogram.presentation.features.viewers.components.ImageOverlay +import org.monogram.presentation.features.viewers.components.ImagePage +@OptIn(ExperimentalFoundationApi::class) @Composable fun ImageViewer( images: List, @@ -21,20 +38,85 @@ fun ImageViewer( downloadUtils: IDownloadUtils, showImageNumber: Boolean = true ) { - MediaViewer( - mediaItems = images, - startIndex = startIndex, - onDismiss = onDismiss, - autoDownload = autoDownload, - onPageChanged = onPageChanged, - onForward = onForward, - onDelete = onDelete, - onCopyLink = onCopyLink, - onCopyText = onCopyText, - captions = captions, - imageDownloadingStates = imageDownloadingStates, - imageDownloadProgressStates = imageDownloadProgressStates, - downloadUtils = downloadUtils, - showImageNumber = showImageNumber + require(images.isNotEmpty()) { "images can't be empty" } + + val resolvedIndex = startIndex.coerceIn(0, images.lastIndex.coerceAtLeast(0)) + val pagerState = rememberPagerState( + initialPage = resolvedIndex, + pageCount = { images.size } ) + val scope = rememberCoroutineScope() + val hostState = rememberFullscreenViewerHostState() + + var showControls by remember { mutableStateOf(true) } + var showSettingsMenu by remember { mutableStateOf(false) } + + LaunchedEffect(pagerState.currentPage) { + onPageChanged?.invoke(pagerState.currentPage) + hostState.zoomState.resetInstant(scope) + hostState.rootState.resetInstant(scope) + showSettingsMenu = false + } + + FullscreenViewerHost( + onDismiss = onDismiss, + showControls = showControls, + showSettingsMenu = showSettingsMenu, + onCloseSettingsMenu = { showSettingsMenu = false }, + hostState = hostState + ) { + Box(modifier = Modifier.fillMaxSize()) { + HorizontalPager( + state = pagerState, + key = { page -> + val path = images.getOrNull(page).orEmpty() + "image_page_${path}_$page" + }, + pageSize = PageSize.Fill, + beyondViewportPageCount = 0, + userScrollEnabled = zoomState.scale.value == 1f && rootState.offsetY.value == 0f + ) { page -> + val path = images.getOrNull(page) ?: return@HorizontalPager + + Box( + modifier = Modifier + .fillMaxSize() + .clipToBounds() + ) { + ImagePage( + path = path, + isDownloading = imageDownloadingStates.getOrNull(page) == true, + downloadProgress = imageDownloadProgressStates.getOrNull(page) ?: 0f, + zoomState = zoomState, + rootState = rootState, + screenHeightPx = screenHeightPx, + dismissDistancePx = dismissDistancePx, + dismissVelocityThreshold = dismissVelocityThreshold, + onDismiss = onDismiss, + showControls = showControls, + onToggleControls = { showControls = !showControls }, + pageIndex = page, + pagerIndex = pagerState.currentPage + ) + } + } + + ImageOverlay( + showControls = showControls, + rootState = rootState, + pagerState = pagerState, + mediaItems = images, + captions = captions, + showImageNumber = showImageNumber, + onDismiss = onDismiss, + showSettingsMenu = showSettingsMenu, + onToggleSettings = { showSettingsMenu = !showSettingsMenu }, + downloadUtils = downloadUtils, + onForward = onForward, + onDelete = onDelete, + onCopyLink = onCopyLink, + onCopyText = onCopyText + ) + } + } } diff --git a/presentation/src/main/java/org/monogram/presentation/features/viewers/MediaViewer.kt b/presentation/src/main/java/org/monogram/presentation/features/viewers/MediaViewer.kt index b9d7a048..715e9245 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/viewers/MediaViewer.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/viewers/MediaViewer.kt @@ -1,19 +1,16 @@ package org.monogram.presentation.features.viewers -import android.util.Log import androidx.activity.compose.BackHandler -import androidx.annotation.OptIn import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.foundation.pager.PageSize -import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer @@ -28,89 +25,74 @@ import androidx.media3.common.util.UnstableApi import kotlinx.coroutines.launch import org.monogram.presentation.core.util.IDownloadUtils import org.monogram.presentation.core.util.getMimeType -import org.monogram.presentation.features.viewers.components.* +import org.monogram.presentation.features.viewers.components.DismissRootState +import org.monogram.presentation.features.viewers.components.ZoomState +import org.monogram.presentation.features.viewers.components.findActivity +import org.monogram.presentation.features.viewers.components.rememberDismissRootState +import org.monogram.presentation.features.viewers.components.rememberZoomState + +internal data class FullscreenViewerHostState( + val rootState: DismissRootState, + val zoomState: ZoomState, + val screenHeightPx: Float, + val dismissDistancePx: Float, + val dismissVelocityThreshold: Float +) -private const val TAG = "MediaViewer" - -@OptIn(ExperimentalFoundationApi::class, UnstableApi::class) @Composable -fun MediaViewer( - mediaItems: List, - startIndex: Int = 0, - onDismiss: () -> Unit, - autoDownload: Boolean = true, - onPageChanged: ((Int) -> Unit)? = null, - onForward: (String) -> Unit = {}, - onDelete: ((String) -> Unit)? = null, - onCopyLink: ((String) -> Unit)? = null, - onCopyText: ((String) -> Unit)? = null, - onSaveGif: ((String) -> Unit)? = null, - captions: List = emptyList(), - fileIds: List = emptyList(), - imageDownloadingStates: List = emptyList(), - imageDownloadProgressStates: List = emptyList(), - supportsStreaming: Boolean = false, - downloadUtils: IDownloadUtils, - showImageNumber: Boolean = true, - isGesturesEnabled: Boolean = true, - isDoubleTapSeekEnabled: Boolean = true, - seekDuration: Int = 10, - isZoomEnabled: Boolean = true, - isAlwaysVideo: Boolean = false -) { - require(mediaItems.isNotEmpty()) { "mediaItems can't be empty" } - - val scope = rememberCoroutineScope() - val pagerState = rememberPagerState( - initialPage = startIndex, - pageCount = { mediaItems.size } - ) - +internal fun rememberFullscreenViewerHostState(): FullscreenViewerHostState { val rootState = rememberDismissRootState() val zoomState = rememberZoomState() + val containerSize = LocalWindowInfo.current.containerSize + val density = LocalDensity.current - var showControls by remember { mutableStateOf(true) } - var showSettingsMenu by remember { mutableStateOf(false) } - var currentVideoInPipMode by remember { mutableStateOf(false) } + return remember(rootState, zoomState, containerSize, density) { + FullscreenViewerHostState( + rootState = rootState, + zoomState = zoomState, + screenHeightPx = containerSize.height.toFloat(), + dismissDistancePx = with(density) { 160.dp.toPx() }, + dismissVelocityThreshold = with(density) { 1000.dp.toPx() } + ) + } +} +@Composable +internal fun FullscreenViewerHost( + onDismiss: () -> Unit, + showControls: Boolean, + showSettingsMenu: Boolean = false, + isInPictureInPicture: Boolean = false, + onCloseSettingsMenu: (() -> Unit)? = null, + hostState: FullscreenViewerHostState = rememberFullscreenViewerHostState(), + content: @Composable FullscreenViewerHostState.() -> Unit +) { val context = LocalContext.current - - val containerSize = LocalWindowInfo.current.containerSize - val density = LocalDensity.current - val screenHeightPx = containerSize.height.toFloat() - val dismissDistancePx = with(density) { 160.dp.toPx() } - val dismissVelocityThreshold = with(density) { 1000.dp.toPx() } + val currentOnCloseSettingsMenu = onCloseSettingsMenu LaunchedEffect(Unit) { - Log.d(TAG, "Opened with ${mediaItems.size} items, startIndex=$startIndex") launch { - rootState.scale.animateTo(1f, spring(dampingRatio = 0.8f, stiffness = Spring.StiffnessMedium)) + hostState.rootState.scale.animateTo( + 1f, + spring(dampingRatio = 0.8f, stiffness = Spring.StiffnessMedium) + ) } launch { - rootState.backgroundAlpha.animateTo(1f, tween(150)) + hostState.rootState.backgroundAlpha.animateTo(1f, tween(150)) } } - LaunchedEffect(pagerState.currentPage) { - Log.d(TAG, "Page changed to ${pagerState.currentPage}") - onPageChanged?.invoke(pagerState.currentPage) - zoomState.resetInstant(scope) - rootState.resetInstant(scope) - showSettingsMenu = false - currentVideoInPipMode = false - } - - LaunchedEffect(showControls, currentVideoInPipMode) { + LaunchedEffect(showControls, isInPictureInPicture) { if (!showControls) { - showSettingsMenu = false + currentOnCloseSettingsMenu?.invoke() } - val activity = context.findActivity() - activity?.let { + context.findActivity()?.let { val insetsController = WindowCompat.getInsetsController(it.window, it.window.decorView) - insetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + insetsController.systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE - if (showControls && !currentVideoInPipMode) { + if (showControls && !isInPictureInPicture) { insetsController.show(WindowInsetsCompat.Type.systemBars()) } else { insetsController.hide(WindowInsetsCompat.Type.systemBars()) @@ -128,118 +110,106 @@ fun MediaViewer( } BackHandler { - Log.d(TAG, "BackHandler: showSettingsMenu=$showSettingsMenu, currentVideoInPipMode=$currentVideoInPipMode") if (showSettingsMenu) { - showSettingsMenu = false + currentOnCloseSettingsMenu?.invoke() + } else if (isInPictureInPicture) { + context.findActivity()?.finishAndRemoveTask() } else { - if (currentVideoInPipMode) { - context.findActivity()?.finishAndRemoveTask() - } else { - onDismiss() - } + onDismiss() } } Box( modifier = Modifier .fillMaxSize() - .background(Color.Black.copy(alpha = rootState.backgroundAlpha.value)) + .background(Color.Black.copy(alpha = hostState.rootState.backgroundAlpha.value)) .graphicsLayer { - translationY = rootState.offsetY.value - scaleX = rootState.scale.value - scaleY = rootState.scale.value + translationY = hostState.rootState.offsetY.value + scaleX = hostState.rootState.scale.value + scaleY = hostState.rootState.scale.value } ) { - HorizontalPager( - state = pagerState, - key = { page -> "media_page_${page}" }, - pageSize = PageSize.Fill, - pageSpacing = 0.dp, - beyondViewportPageCount = 0, - userScrollEnabled = zoomState.scale.value == 1f && rootState.offsetY.value == 0f - ) { page -> - val path = mediaItems.getOrNull(page) ?: return@HorizontalPager - val mimeType = getMimeType(path) - val isVideo = (isAlwaysVideo && path.isNotBlank()) || isVideoPath(path, mimeType) - - if (isVideo) { - VideoPage( - path = path, - fileId = fileIds.getOrNull(page) ?: 0, - caption = captions.getOrNull(page), - supportsStreaming = supportsStreaming, - downloadUtils = downloadUtils, - onDismiss = onDismiss, - showControls = showControls, - onToggleControls = { showControls = !showControls }, - onForward = onForward, - onDelete = onDelete, - onCopyLink = onCopyLink, - onCopyText = onCopyText, - onSaveGif = onSaveGif, - showSettingsMenu = showSettingsMenu, - onToggleSettings = { showSettingsMenu = !showSettingsMenu }, - isGesturesEnabled = isGesturesEnabled, - isDoubleTapSeekEnabled = isDoubleTapSeekEnabled, - seekDuration = seekDuration, - isZoomEnabled = isZoomEnabled, - isActive = pagerState.currentPage == page, - onCurrentVideoPipModeChanged = { inPip -> - if (pagerState.currentPage == page) { - currentVideoInPipMode = inPip - } - }, - zoomState = zoomState, - rootState = rootState, - screenHeightPx = screenHeightPx, - dismissDistancePx = dismissDistancePx, - dismissVelocityThreshold = dismissVelocityThreshold - ) - } else { - ImagePage( - path = path, - isDownloading = imageDownloadingStates.getOrNull(page) == true, - downloadProgress = imageDownloadProgressStates.getOrNull(page) ?: 0f, - zoomState = zoomState, - rootState = rootState, - screenHeightPx = screenHeightPx, - dismissDistancePx = dismissDistancePx, - dismissVelocityThreshold = dismissVelocityThreshold, - onDismiss = onDismiss, - showControls = showControls, - onToggleControls = { showControls = !showControls }, - pageIndex = page, - pagerIndex = pagerState.currentPage - ) - } - } + hostState.content() + } +} - val currentPath = mediaItems.getOrNull(pagerState.currentPage) ?: "" - val currentMimeType = getMimeType(currentPath) - val isCurrentVideo = (isAlwaysVideo && currentPath.isNotBlank()) || isVideoPath(currentPath, currentMimeType) - - if (!isCurrentVideo) { - ImageOverlay( - showControls = showControls, - rootState = rootState, - pagerState = pagerState, - mediaItems = mediaItems, - captions = captions, - showImageNumber = showImageNumber, - onDismiss = onDismiss, - showSettingsMenu = showSettingsMenu, - onToggleSettings = { showSettingsMenu = !showSettingsMenu }, - downloadUtils = downloadUtils, - onForward = onForward, - onDelete = onDelete, - onCopyLink = onCopyLink, - onCopyText = onCopyText - ) - } +@OptIn(UnstableApi::class) +@Composable +fun MediaViewer( + mediaItems: List, + startIndex: Int = 0, + onDismiss: () -> Unit, + autoDownload: Boolean = true, + onPageChanged: ((Int) -> Unit)? = null, + onForward: (String) -> Unit = {}, + onDelete: ((String) -> Unit)? = null, + onCopyLink: ((String) -> Unit)? = null, + onCopyText: ((String) -> Unit)? = null, + onSaveGif: ((String) -> Unit)? = null, + captions: List = emptyList(), + fileIds: List = emptyList(), + imageDownloadingStates: List = emptyList(), + imageDownloadProgressStates: List = emptyList(), + supportsStreaming: Boolean = false, + downloadUtils: IDownloadUtils, + showImageNumber: Boolean = true, + isGesturesEnabled: Boolean = true, + isDoubleTapSeekEnabled: Boolean = true, + seekDuration: Int = 10, + isZoomEnabled: Boolean = true, + isAlwaysVideo: Boolean = false +) { + require(mediaItems.isNotEmpty()) { "mediaItems can't be empty" } + + val resolvedIndex = startIndex.coerceIn(0, mediaItems.lastIndex.coerceAtLeast(0)) + val currentPath = mediaItems[resolvedIndex] + val currentMimeType = getMimeType(currentPath) + val shouldRenderSingleVideo = + mediaItems.size == 1 && ((isAlwaysVideo && currentPath.isNotBlank()) || isVideoPath( + currentPath, + currentMimeType + )) + + if (shouldRenderSingleVideo) { + VideoViewer( + path = currentPath, + onDismiss = onDismiss, + onForward = onForward, + onDelete = onDelete, + onCopyLink = onCopyLink, + onCopyText = onCopyText, + onSaveGif = onSaveGif, + caption = captions.getOrNull(resolvedIndex), + fileId = fileIds.getOrNull(resolvedIndex) ?: 0, + supportsStreaming = supportsStreaming, + downloadUtils = downloadUtils, + isGesturesEnabled = isGesturesEnabled, + isDoubleTapSeekEnabled = isDoubleTapSeekEnabled, + seekDuration = seekDuration, + isZoomEnabled = isZoomEnabled + ) + return } + + ImageViewer( + images = mediaItems, + startIndex = resolvedIndex, + onDismiss = onDismiss, + autoDownload = autoDownload, + onPageChanged = onPageChanged, + onForward = onForward, + onDelete = onDelete, + onCopyLink = onCopyLink, + onCopyText = onCopyText, + captions = captions, + imageDownloadingStates = imageDownloadingStates, + imageDownloadProgressStates = imageDownloadProgressStates, + downloadUtils = downloadUtils, + showImageNumber = showImageNumber + ) } -private fun isVideoPath(path: String, mimeType: String?): Boolean { +internal fun isVideoPath(path: String, mimeType: String?): Boolean { if (path.isBlank()) return false if (mimeType?.startsWith("image/") == true) return false diff --git a/presentation/src/main/java/org/monogram/presentation/features/viewers/VideoViewer.kt b/presentation/src/main/java/org/monogram/presentation/features/viewers/VideoViewer.kt index d7c3196d..64065d62 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/viewers/VideoViewer.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/viewers/VideoViewer.kt @@ -1,10 +1,18 @@ package org.monogram.presentation.features.viewers -import android.util.Log +import androidx.annotation.OptIn import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.media3.common.util.UnstableApi import org.monogram.presentation.core.util.IDownloadUtils +import org.monogram.presentation.features.viewers.components.VideoPage +@OptIn(UnstableApi::class) @Composable fun VideoViewer( path: String, @@ -36,29 +44,55 @@ fun VideoViewer( return } - Log.d("VideoViewer", "Composing VideoViewer for path=$effectivePath, fileId=$fileId") + val scope = rememberCoroutineScope() + val hostState = rememberFullscreenViewerHostState() - val mediaItems = remember(effectivePath) { listOf(effectivePath) } - val captions = remember(caption) { listOf(caption) } - val fileIds = remember(fileId) { listOf(fileId) } + var showControls by remember { mutableStateOf(true) } + var showSettingsMenu by remember { mutableStateOf(false) } + var currentVideoInPipMode by remember { mutableStateOf(false) } - MediaViewer( - mediaItems = mediaItems, - startIndex = 0, + LaunchedEffect(effectivePath, fileId, supportsStreaming) { + hostState.zoomState.resetInstant(scope) + hostState.rootState.resetInstant(scope) + showSettingsMenu = false + currentVideoInPipMode = false + } + + FullscreenViewerHost( onDismiss = onDismiss, - onForward = onForward, - onDelete = onDelete, - onCopyLink = onCopyLink, - onCopyText = onCopyText, - onSaveGif = onSaveGif, - captions = captions, - fileIds = fileIds, - supportsStreaming = supportsStreaming, - downloadUtils = downloadUtils, - isGesturesEnabled = isGesturesEnabled, - isDoubleTapSeekEnabled = isDoubleTapSeekEnabled, - seekDuration = seekDuration, - isZoomEnabled = isZoomEnabled, - isAlwaysVideo = true - ) + showControls = showControls, + showSettingsMenu = showSettingsMenu, + isInPictureInPicture = currentVideoInPipMode, + onCloseSettingsMenu = { showSettingsMenu = false }, + hostState = hostState + ) { + VideoPage( + path = effectivePath, + fileId = fileId, + caption = caption, + supportsStreaming = supportsStreaming, + downloadUtils = downloadUtils, + onDismiss = onDismiss, + showControls = showControls, + onToggleControls = { showControls = !showControls }, + onForward = onForward, + onDelete = onDelete, + onCopyLink = onCopyLink, + onCopyText = onCopyText, + onSaveGif = onSaveGif, + showSettingsMenu = showSettingsMenu, + onToggleSettings = { showSettingsMenu = !showSettingsMenu }, + isGesturesEnabled = isGesturesEnabled, + isDoubleTapSeekEnabled = isDoubleTapSeekEnabled, + seekDuration = seekDuration, + isZoomEnabled = isZoomEnabled, + isActive = true, + onCurrentVideoPipModeChanged = { currentVideoInPipMode = it }, + zoomState = zoomState, + rootState = rootState, + screenHeightPx = screenHeightPx, + dismissDistancePx = dismissDistancePx, + dismissVelocityThreshold = dismissVelocityThreshold + ) + } } diff --git a/presentation/src/main/java/org/monogram/presentation/features/viewers/components/ImageViewerComponents.kt b/presentation/src/main/java/org/monogram/presentation/features/viewers/components/ImageViewerComponents.kt index 3599dfa8..97473d72 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/viewers/components/ImageViewerComponents.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/viewers/components/ImageViewerComponents.kt @@ -103,6 +103,7 @@ fun ImagePage( pagerIndex: Int ) { val scope = rememberCoroutineScope() + Box( modifier = Modifier .fillMaxSize() diff --git a/presentation/src/main/java/org/monogram/presentation/features/viewers/components/VideoViewerComponents.kt b/presentation/src/main/java/org/monogram/presentation/features/viewers/components/VideoViewerComponents.kt index f6b7679f..7a3d2133 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/viewers/components/VideoViewerComponents.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/viewers/components/VideoViewerComponents.kt @@ -294,7 +294,6 @@ fun VideoPage( exoPlayer.addListener(listener) lifecycleOwner.lifecycle.addObserver(observer) onDispose { - Log.d(TAG, "Disposing ExoPlayer for $path") lifecycleOwner.lifecycle.removeObserver(observer) exoPlayer.removeListener(listener) exoPlayer.release() From afd2a0855e32e1a5b5a55eac98f1a74a8026b000 Mon Sep 17 00:00:00 2001 From: Artur Skubei <41114720+gdlbo@users.noreply.github.com> Date: Sat, 25 Apr 2026 11:50:32 +0300 Subject: [PATCH 7/8] fix cancel reply after send sticker --- .../chats/conversation/logic/message-actions/MessageActions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/message-actions/MessageActions.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/message-actions/MessageActions.kt index db633521..a473fa63 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/message-actions/MessageActions.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/logic/message-actions/MessageActions.kt @@ -54,13 +54,13 @@ internal fun DefaultChatComponent.handleSendSticker(stickerId: String) { val replyId = currentState.replyMessage?.id val threadId = currentState.effectiveThreadId() val targetChatId = currentState.effectiveThreadChatId(chatId) + onCancelReply() repositoryMessage.sendSticker( targetChatId, stickerId, replyToMsgId = replyId, threadId = threadId ) - onCancelReply() if (shouldAutoScrollAfterSend(currentState.isAtBottom)) { onScrollToBottom() } From a0d0bf3c8ae1402ab021236944d92d76ed099087 Mon Sep 17 00:00:00 2001 From: Artur Skubei <41114720+gdlbo@users.noreply.github.com> Date: Sat, 25 Apr 2026 11:57:13 +0300 Subject: [PATCH 8/8] better counter of unread --- .../chats/conversation/ui/content/ChatContentList.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentList.kt b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentList.kt index 1ae2928f..f6f79859 100644 --- a/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentList.kt +++ b/presentation/src/main/java/org/monogram/presentation/features/chats/conversation/ui/content/ChatContentList.kt @@ -213,17 +213,22 @@ fun ChatContentList( } } } + val unreadBoundaryGroupId = remember(unreadBoundaryIndex, groupedMessages) { + unreadBoundaryIndex + ?.takeIf { it in groupedMessages.indices } + ?.let { groupedMessages[it].firstMessageId } + } var hasUnreadSeparatorBeenVisible by rememberSaveable( state.chatId, state.currentTopicId, - unreadBoundaryIndex, + unreadBoundaryGroupId, state.unreadSeparatorLastReadInboxMessageId, state.unreadSeparatorCount ) { mutableStateOf(false) } var hasUnreadSeparatorDismissed by rememberSaveable( state.chatId, state.currentTopicId, - unreadBoundaryIndex, + unreadBoundaryGroupId, state.unreadSeparatorLastReadInboxMessageId, state.unreadSeparatorCount ) { mutableStateOf(false) } @@ -697,7 +702,7 @@ private fun MessageRowItem( Spacer(modifier = Modifier.height(16.dp)) } - AnimatedVisibility(visible = uiFlags.showUnreadSeparator && !isScrolling) { + AnimatedVisibility(visible = uiFlags.showUnreadSeparator) { Column { UnreadMessagesSeparator(unreadCount = uiFlags.unreadCount) Spacer(modifier = Modifier.height(16.dp))