1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-08 05:47:05 +09:00

DROID-2966 Chats | Tech | ChatPreviewContainer (#2446)

This commit is contained in:
Evgenii Kozlov 2025-05-23 16:18:59 +02:00 committed by GitHub
parent bd9ccaff50
commit 4783b8e79e
Signed by: github
GPG key ID: B5690EEEBB952194
7 changed files with 272 additions and 7 deletions

View file

@ -8,6 +8,8 @@ import com.anytypeio.anytype.domain.auth.repo.AuthRepository
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.block.interactor.sets.GetObjectTypes
import com.anytypeio.anytype.domain.block.repo.BlockRepository
import com.anytypeio.anytype.domain.chats.ChatEventChannel
import com.anytypeio.anytype.domain.chats.ChatPreviewContainer
import com.anytypeio.anytype.domain.config.ConfigStorage
import com.anytypeio.anytype.domain.debugging.DebugAccountSelectTrace
import com.anytypeio.anytype.domain.debugging.Logger
@ -238,6 +240,23 @@ object SubscriptionsModule {
deviceTokenStoringService = deviceTokenStoringService
)
@JvmStatic
@Provides
@Singleton
fun provideChatPreviewContainer(
@Named(DEFAULT_APP_COROUTINE_SCOPE) scope: CoroutineScope,
dispatchers: AppCoroutineDispatchers,
repo: BlockRepository,
logger: Logger,
events: ChatEventChannel
): ChatPreviewContainer = ChatPreviewContainer.Default(
repo = repo,
dispatchers = dispatchers,
scope = scope,
logger = logger,
events = events
)
@JvmStatic
@Provides
@Singleton

View file

@ -322,6 +322,9 @@ sealed class Event {
sealed class Chats : Command() {
/**
* @property [id] msg ID
*/
data class Add(
override val context: Id,
val id: Id,
@ -329,6 +332,9 @@ sealed class Event {
val message: Chat.Message
) : Chats()
/**
* @property [id] msg ID
*/
data class Update(
override val context: Id,
val id: Id,
@ -337,9 +343,12 @@ sealed class Event {
data class Delete(
override val context: Id,
val id: Id
val message: Id
) : Chats()
/**
* @property [id] msg ID
*/
data class UpdateReactions(
override val context: Id,
val id: Id,

View file

@ -7,6 +7,7 @@ import kotlinx.coroutines.flow.Flow
interface ChatEventRemoteChannel {
fun observe(chat: Id): Flow<List<Event.Command.Chats>>
fun subscribe(subscription: Id): Flow<List<Event.Command.Chats>>
class Default(
private val channel: ChatEventRemoteChannel
) : ChatEventChannel {

View file

@ -478,7 +478,7 @@ class ChatContainer @Inject constructor(
is Event.Command.Chats.Update -> {
if (messageList.isInCurrentWindow(event.id)) {
val index = messageList.indexOfFirst { it.id == event.id }
val index = messageList.indexOfFirst { it.id == event.message.id }
messageList[index] = event.message
}
// Tracking the last message in the chat tail
@ -486,12 +486,12 @@ class ChatContainer @Inject constructor(
}
is Event.Command.Chats.Delete -> {
if (messageList.isInCurrentWindow(event.id)) {
val index = messageList.indexOfFirst { it.id == event.id }
if (messageList.isInCurrentWindow(event.message)) {
val index = messageList.indexOfFirst { it.id == event.message }
messageList.removeAt(index)
}
// Tracking the last message in the chat tail
lastMessages.remove(event.id)
lastMessages.remove(event.message)
}
is Event.Command.Chats.UpdateReactions -> {

View file

@ -0,0 +1,124 @@
package com.anytypeio.anytype.domain.chats
import com.anytypeio.anytype.core_models.Event
import com.anytypeio.anytype.core_models.chats.Chat
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.block.repo.BlockRepository
import com.anytypeio.anytype.domain.debugging.Logger
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.scan
import kotlinx.coroutines.launch
/**
* Cross-space (vault-level) chat previews container
*/
interface ChatPreviewContainer {
fun start()
fun stop()
suspend fun getAll(): List<Chat.Preview>
suspend fun getPreview(space: SpaceId): Chat.Preview?
fun observePreviews() : Flow<List<Chat.Preview>>
class Default @Inject constructor(
private val repo: BlockRepository,
private val events: ChatEventChannel,
private val dispatchers: AppCoroutineDispatchers,
private val scope: CoroutineScope,
private val logger: Logger
) : ChatPreviewContainer {
private var job: Job? = null
private val previews = MutableStateFlow<List<Chat.Preview>>(emptyList())
override fun start() {
job?.cancel()
job = scope.launch(dispatchers.io) {
previews.value = emptyList()
val initial = runCatching { repo.subscribeToMessagePreviews(SUBSCRIPTION_ID) }
.onFailure { logger.logException(it, "DROID-2966 Error while getting initial previews") }
.getOrDefault(emptyList())
events
.observe(SUBSCRIPTION_ID)
.scan(initial = initial) { previews, events ->
events.fold(previews) { state, event ->
when (event) {
is Event.Command.Chats.Update -> {
state.map { preview ->
if (preview.chat == event.context && preview.message?.id == event.id) {
preview.copy(message = event.message)
} else {
preview
}
}
}
is Event.Command.Chats.UpdateState -> {
state.map { preview ->
if (preview.chat == event.context) {
preview.copy(
state = event.state
)
} else {
preview
}
}
}
is Event.Command.Chats.Delete -> {
state.map { preview ->
if (preview.chat == event.context && preview.message?.id == event.message) {
preview.copy(message = null)
} else {
preview
}
}
}
else -> state.also {
logger.logInfo("DROID-2966 Ignoring event: $event")
}
}
}
}
.flowOn(dispatchers.io)
.catch { logger.logException(it, "DROID-2966 Exception in chat preview flow") }
.collect {
previews.value = it
}
}
}
override fun stop() {
job?.cancel()
job = null
scope.launch(dispatchers.io) {
previews.value = emptyList()
runCatching {
repo.unsubscribeFromMessagePreviews(subscription = SUBSCRIPTION_ID)
}.onFailure {
logger.logException(it, "DROID-2966 Error while unsubscribing from message previews")
}
}
}
override suspend fun getAll(): List<Chat.Preview> = previews.value
override suspend fun getPreview(space: SpaceId): Chat.Preview? {
return previews.value.firstOrNull { preview -> preview.space.id == space.id }
}
override fun observePreviews(): Flow<List<Chat.Preview>> {
return previews
}
companion object {
private const val SUBSCRIPTION_ID = "chat-previews-subscription"
}
}
}

View file

@ -299,7 +299,7 @@ fun anytype.Event.Message.toCoreModels(
checkNotNull(event)
Event.Command.Chats.Delete(
context = context,
id = event.id
message = event.id
)
}
chatUpdate != null -> {

View file

@ -24,6 +24,18 @@ class ChatEventMiddlewareChannel(
events.isNotEmpty()
}
}
override fun subscribe(subscription: Id): Flow<List<Event.Command.Chats>> {
return eventProxy
.flow()
.mapNotNull { item ->
item.messages.mapNotNull { msg ->
msg.payload(subscription = subscription, contextId = item.contextId)
}
}.filter { events ->
events.isNotEmpty()
}
}
}
fun MEventMessage.payload(contextId: Id) : Event.Command.Chats? {
@ -78,7 +90,7 @@ fun MEventMessage.payload(contextId: Id) : Event.Command.Chats? {
checkNotNull(event)
Event.Command.Chats.Delete(
context = contextId,
id = event.id
message = event.id
)
}
chatUpdateReactions != null -> {
@ -93,6 +105,106 @@ fun MEventMessage.payload(contextId: Id) : Event.Command.Chats? {
)
}
else -> {
null
}
}
}
fun MEventMessage.payload(subscription: Id, contextId: Id) : Event.Command.Chats? {
return when {
chatAdd != null -> {
val event = chatAdd
checkNotNull(event)
if (event.subIds.contains(subscription)) {
Event.Command.Chats.Add(
context = contextId,
order = event.orderId,
id = event.id,
message = requireNotNull(event.message?.core())
)
} else {
null
}
}
chatStateUpdate != null -> {
val event = chatStateUpdate
checkNotNull(event)
if (event.subIds.contains(subscription)) {
Event.Command.Chats.UpdateState(
context = contextId,
state = event.state?.core()
)
} else {
null
}
}
chatUpdateMessageReadStatus != null -> {
val event = chatUpdateMessageReadStatus
checkNotNull(event)
if (event.subIds.contains(subscription)) {
Event.Command.Chats.UpdateMessageReadStatus(
context = contextId,
messages = event.ids,
isRead = event.isRead
)
} else {
null
}
}
chatUpdateMentionReadStatus != null -> {
val event = chatUpdateMentionReadStatus
checkNotNull(event)
if (event.subIds.contains(subscription)) {
Event.Command.Chats.UpdateMentionReadStatus(
context = contextId,
messages = event.ids,
isRead = event.isRead
)
} else {
null
}
}
chatUpdate != null -> {
val event = chatUpdate
checkNotNull(event)
if (event.subIds.contains(subscription)) {
Event.Command.Chats.Update(
context = contextId,
id = event.id,
message = requireNotNull(event.message?.core())
)
} else {
null
}
}
chatDelete != null -> {
val event = chatDelete
checkNotNull(event)
if (event.subIds.contains(subscription)) {
Event.Command.Chats.Delete(
context = contextId,
message = event.id
)
} else {
null
}
}
chatUpdateReactions != null -> {
val event = chatUpdateReactions
checkNotNull(event)
if (event.subIds.contains(subscription)) {
Event.Command.Chats.UpdateReactions(
context = contextId,
id = event.id,
reactions = event.reactions?.reactions?.mapValues { (unicode, identities) ->
identities.ids
} ?: emptyMap()
)
} else {
null
}
}
else -> {
null
}