diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/chats/ChatContainer.kt b/domain/src/main/java/com/anytypeio/anytype/domain/chats/ChatContainer.kt index f38d41b04b..3f7bbe76ba 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/chats/ChatContainer.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/chats/ChatContainer.kt @@ -1,8 +1,6 @@ package com.anytypeio.anytype.domain.chats import com.anytypeio.anytype.core_models.Command -import com.anytypeio.anytype.core_models.DVFilter -import com.anytypeio.anytype.core_models.DVFilterCondition import com.anytypeio.anytype.core_models.Event import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_models.ObjectWrapper @@ -12,12 +10,8 @@ import com.anytypeio.anytype.core_models.primitives.Space import com.anytypeio.anytype.domain.block.repo.BlockRepository import com.anytypeio.anytype.domain.debugging.Logger import com.anytypeio.anytype.domain.library.StoreSearchByIdsParams -import com.anytypeio.anytype.domain.library.StoreSearchParams import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer import javax.inject.Inject -import kotlin.collections.isNotEmpty -import kotlin.collections.toList -import kotlin.math.log import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -116,7 +110,7 @@ class ChatContainer @Inject constructor( listOf("$chat/$ATTACHMENT_SUBSCRIPTION_POSTFIX") ) }.onFailure { - logger.logWarning("DROID-2966 Error while unsubscribing from chat") + logger.logWarning("DROID-2966 Error while unsubscribing from chat:\n${it.message}") }.onSuccess { logger.logInfo("DROID-2966 Successfully unsubscribed from chat") } @@ -136,6 +130,8 @@ class ChatContainer @Inject constructor( var intent: Intent = Intent.None + var initialUnreadSectionMessageId: Id? = null + val initial = buildList { if (initialState.hasUnReadMessages && !initialState.oldestMessageOrderId.isNullOrEmpty()) { // Starting from the unread-messages window. @@ -147,8 +143,10 @@ class ChatContainer @Inject constructor( if (target != null) { intent = Intent.ScrollToMessage( id = target.id, - smooth = false + smooth = false, + startOfUnreadMessageSection = true ) + initialUnreadSectionMessageId = target.id } } addAll(aroundUnread) @@ -169,7 +167,8 @@ class ChatContainer @Inject constructor( initial = ChatStreamState( messages = initial, state = initialState, - intent = intent + intent = intent, + initialUnreadSectionMessageId = initialUnreadSectionMessageId ) ) { state, transform -> when (transform) { @@ -184,7 +183,8 @@ class ChatContainer @Inject constructor( ChatStreamState( messages = loadTheNextPage(state.messages, chat), intent = Intent.None, - state = state.state + state = state.state, + initialUnreadSectionMessageId = null ) } is Transformation.Commands.LoadAround -> { @@ -230,7 +230,8 @@ class ChatContainer @Inject constructor( ChatStreamState( messages = messages, intent = Intent.ScrollToBottom, - state = state.state + state = state.state, + initialUnreadSectionMessageId = initialUnreadSectionMessageId ) } else { val messages = try { @@ -243,7 +244,8 @@ class ChatContainer @Inject constructor( ChatStreamState( messages = messages, intent = Intent.ScrollToBottom, - state = state.state + state = state.state, + initialUnreadSectionMessageId = initialUnreadSectionMessageId ) } } else { @@ -257,7 +259,8 @@ class ChatContainer @Inject constructor( ChatStreamState( messages = messages, intent = Intent.ScrollToBottom, - state = state.state + state = state.state, + initialUnreadSectionMessageId = null ) } } else { @@ -281,7 +284,8 @@ class ChatContainer @Inject constructor( ChatStreamState( messages = messages, intent = Intent.ScrollToBottom, - state = state.state + state = state.state, + initialUnreadSectionMessageId = null ) } } @@ -589,7 +593,8 @@ class ChatContainer @Inject constructor( return ChatStreamState( messages = messageList, - state = countersState + state = countersState, + initialUnreadSectionMessageId = initialUnreadSectionMessageId ) } @@ -711,11 +716,13 @@ class ChatContainer @Inject constructor( /** * Messages sorted — from the oldest to the latest. + * @property [initialUnreadSectionMessageId] used when opening chat with unread messages. */ data class ChatStreamState( val messages: List, val state: Chat.State = Chat.State(), - val intent: Intent = Intent.None + val intent: Intent = Intent.None, + val initialUnreadSectionMessageId: String? = null ) sealed class Intent { @@ -727,7 +734,11 @@ class ChatContainer @Inject constructor( * Defaults to `false` for performance reasons, as smooth scrolling may introduce * delays or unnecessary animations in certain scenarios. */ - data class ScrollToMessage(val id: Id, val smooth: Boolean = false) : Intent() + data class ScrollToMessage( + val id: Id, + val smooth: Boolean = false, + val startOfUnreadMessageSection: Boolean = false + ) : Intent() data class Highlight(val id: Id) : Intent() data object ScrollToBottom : Intent() data object None : Intent() diff --git a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatView.kt b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatView.kt index 34068bfebf..b087dad0ec 100644 --- a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatView.kt +++ b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatView.kt @@ -31,7 +31,8 @@ sealed interface ChatView { val shouldHideUsername: Boolean = false, val isEdited: Boolean = false, val avatar: Avatar = Avatar.Initials(), - val reply: Reply? = null + val reply: Reply? = null, + val startOfUnreadMessageSection: Boolean = false ) : ChatView { val isMaxReactionCountReached: Boolean = diff --git a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModel.kt b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModel.kt index 7afb8ef4af..893c97da5e 100644 --- a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModel.kt +++ b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModel.kt @@ -190,7 +190,7 @@ class ChatViewModel @Inject constructor( chatContainer.subscribeToAttachments(vmParams.ctx, vmParams.space).distinctUntilChanged(), chatContainer.fetchReplies(chat = chat).distinctUntilChanged() ) { result, dependencies, replies -> - Timber.d("DROID-2966 Chat counter state from container: ${result.state}") + Timber.d("DROID-2966 Chat counter state from container: ${result.state}, unread section: ${result.initialUnreadSectionMessageId}") Timber.d("DROID-2966 Intent from container: ${result.intent}") Timber.d("DROID-2966 Message results size from container: ${result.messages.size}") var previousDate: ChatView.DateSection? = null @@ -371,7 +371,8 @@ class ChatViewModel @Inject constructor( ) } else { ChatView.Message.Avatar.Initials(member?.name.orEmpty()) - } + }, + startOfUnreadMessageSection = result.initialUnreadSectionMessageId == msg.id ) val currDate = ChatView.DateSection( formattedDate = dateFormatter.format(msg.createdAt * 1000), diff --git a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/Attachments.kt b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/Attachments.kt index ef97fbedac..fedd908cff 100644 --- a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/Attachments.kt +++ b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/Attachments.kt @@ -1,14 +1,11 @@ package com.anytypeio.anytype.feature_chats.ui -import android.content.res.Configuration import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border -import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth @@ -18,22 +15,18 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Text import androidx.compose.material3.Card -import androidx.compose.material3.CardColors import androidx.compose.material3.CardDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import coil3.compose.rememberAsyncImagePainter -import com.anytypeio.anytype.core_models.Url import com.anytypeio.anytype.core_ui.common.DefaultPreviews import com.anytypeio.anytype.core_ui.views.PreviewTitle2Medium import com.anytypeio.anytype.core_ui.views.Relations3 @@ -46,6 +39,7 @@ import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi import com.bumptech.glide.integration.compose.GlideImage import com.bumptech.glide.load.DecodeFormat import com.bumptech.glide.load.engine.DiskCacheStrategy + @Composable @OptIn(ExperimentalGlideComposeApi::class, ExperimentalFoundationApi::class) fun BubbleAttachments( @@ -266,21 +260,27 @@ fun Bookmark( text = url, modifier = Modifier.padding(horizontal = 12.dp), style = Relations3, - color = colorResource(R.color.transparent_active) + color = colorResource(R.color.transparent_active), + maxLines = 1, + overflow = TextOverflow.Ellipsis ) Spacer(modifier = Modifier.height(2.dp)) Text( text = title, modifier = Modifier.padding(horizontal = 12.dp), style = Title2, - color = colorResource(R.color.text_primary) + color = colorResource(R.color.text_primary), + maxLines = 2, + overflow = TextOverflow.Ellipsis ) Spacer(modifier = Modifier.height(2.dp)) Text( text = description, modifier = Modifier.padding(horizontal = 12.dp), style = Relations3, - color = colorResource(R.color.transparent_active) + color = colorResource(R.color.transparent_active), + maxLines = 3, + overflow = TextOverflow.Ellipsis ) Spacer(modifier = Modifier.height(8.dp)) } diff --git a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatBox.kt b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatBox.kt index fd40c84919..0d95c86e63 100644 --- a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatBox.kt +++ b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatBox.kt @@ -29,6 +29,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.DropdownMenu import androidx.compose.material.MaterialTheme import androidx.compose.material.Text @@ -56,6 +57,7 @@ import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow @@ -581,7 +583,10 @@ private fun ChatBoxUserInput( textStyle = ContentMiscChat.copy(color = colorResource(R.color.text_tertiary)) ) }, - visualTransformation = AnnotatedTextTransformation(spans) + visualTransformation = AnnotatedTextTransformation(spans), + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.Sentences + ) ) } diff --git a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatScreen.kt b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatScreen.kt index 141dd82cdb..3fea041ed0 100644 --- a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatScreen.kt +++ b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatScreen.kt @@ -3,6 +3,7 @@ package com.anytypeio.anytype.feature_chats.ui import android.content.res.Configuration import android.net.Uri import android.provider.OpenableColumns +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -405,6 +406,8 @@ fun ChatScreen( val isPerformingScrollIntent = remember { mutableStateOf(false) } + val offsetPx = with(LocalDensity.current) { 50.dp.toPx().toInt() } + // Applying view model intents LaunchedEffect(intent) { when (intent) { @@ -419,7 +422,11 @@ fun ChatScreen( if (intent.smooth) { lazyListState.animateScrollToItem(index) } else { - lazyListState.scrollToItem(index) + if (intent.startOfUnreadMessageSection) { + lazyListState.scrollToItem(index, scrollOffset = -offsetPx) + } else { + lazyListState.scrollToItem(index) + } } awaitFrame() } else { @@ -597,10 +604,12 @@ fun ChatScreen( ) { Text( text = counter.mentions.toString(), - modifier = Modifier.align(Alignment.Center).padding( - horizontal = 5.dp, - vertical = 2.dp - ), + modifier = Modifier + .align(Alignment.Center) + .padding( + horizontal = 5.dp, + vertical = 2.dp + ), color = colorResource(R.color.glyph_white), style = Caption1Regular ) @@ -642,10 +651,12 @@ fun ChatScreen( ) { Text( text = counter.messages.toString(), - modifier = Modifier.align(Alignment.Center).padding( - horizontal = 5.dp, - vertical = 2.dp - ), + modifier = Modifier + .align(Alignment.Center) + .padding( + horizontal = 5.dp, + vertical = 2.dp + ), color = colorResource(R.color.glyph_white), style = Caption1Regular ) @@ -704,6 +715,7 @@ fun ChatScreen( end = span.end + lengthDifference ) } + is ChatBoxSpan.Markup -> { span.copy( start = span.start + lengthDifference, @@ -801,6 +813,7 @@ fun ChatScreen( } } +@OptIn(ExperimentalFoundationApi::class) @Composable fun Messages( modifier: Modifier = Modifier, @@ -824,6 +837,7 @@ fun Messages( ) { // Timber.d("DROID-2966 Messages composition: ${messages.map { if (it is ChatView.Message) it.content.msg else it }}") val scope = rememberCoroutineScope() + LazyColumn( modifier = modifier, reverseLayout = true, @@ -917,6 +931,25 @@ fun Messages( if (idx == messages.lastIndex) { Spacer(modifier = Modifier.height(36.dp)) } + + if (msg.startOfUnreadMessageSection) { + Box( + modifier = Modifier + .height(50.dp) + .fillParentMaxWidth() + ) { + Text( + text = stringResource(R.string.chat_new_messages_section_text), + modifier = Modifier + .align(Alignment.Center) + .fillMaxWidth(), + style = Caption1Medium, + color = colorResource(R.color.transparent_active), + textAlign = TextAlign.Center + ) + } + } + } else if (msg is ChatView.DateSection) { Text( text = msg.formattedDate, diff --git a/localization/src/main/res/values/strings.xml b/localization/src/main/res/values/strings.xml index 037c2f49e2..70db930ad7 100644 --- a/localization/src/main/res/values/strings.xml +++ b/localization/src/main/res/values/strings.xml @@ -2085,5 +2085,6 @@ Please provide specific details of your needs here. For organized content and data There are no spaces yet + New messages \ No newline at end of file