1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-07 21:37:02 +09:00

DROID-2966 Chats | Enhancement | Misc. design fixes (#2498)

This commit is contained in:
Evgenii Kozlov 2025-06-05 15:59:42 +02:00 committed by GitHub
parent 7517060aa9
commit f55144794a
Signed by: github
GPG key ID: B5690EEEBB952194
7 changed files with 92 additions and 40 deletions

View file

@ -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<Chat.Message>,
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()

View file

@ -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 =

View file

@ -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),

View file

@ -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))
}

View file

@ -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
)
)
}

View file

@ -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,

View file

@ -2085,5 +2085,6 @@ Please provide specific details of your needs here.</string>
<string name="vault_create_space_description">For organized content and data</string>
<string name="vault_empty_state_text">There are no spaces yet</string>
<string name="chat_new_messages_section_text">New messages</string>
</resources>