mirror of
https://github.com/anyproto/anytype-kotlin.git
synced 2025-06-08 05:47:05 +09:00
DROID-3044 Chats | Enhancement | Basics for chat replies (#1854)
This commit is contained in:
parent
81394a3589
commit
3994d456d5
18 changed files with 352 additions and 43 deletions
|
@ -594,7 +594,7 @@ sealed class Command {
|
|||
sealed class ChatCommand {
|
||||
data class AddMessage(
|
||||
val chat: Id,
|
||||
val message: Chat.Message
|
||||
val message: Chat.Message,
|
||||
) : ChatCommand()
|
||||
|
||||
data class DeleteMessage(
|
||||
|
@ -613,6 +613,11 @@ sealed class Command {
|
|||
val limit: Int
|
||||
) : ChatCommand()
|
||||
|
||||
data class GetMessagesByIds(
|
||||
val chat: Id,
|
||||
val messages: List<Id>
|
||||
) : ChatCommand()
|
||||
|
||||
data class SubscribeLastMessages(
|
||||
val chat: Id,
|
||||
val limit: Int
|
||||
|
|
|
@ -41,16 +41,17 @@ sealed class Chat {
|
|||
*/
|
||||
fun new(
|
||||
text: String,
|
||||
attachments: List<Attachment> = emptyList()
|
||||
) : Message = Chat.Message(
|
||||
attachments: List<Attachment> = emptyList(),
|
||||
replyToMessageId: Id? = null
|
||||
) : Message = Message(
|
||||
id = "",
|
||||
createdAt = 0L,
|
||||
modifiedAt = 0L,
|
||||
attachments = attachments,
|
||||
reactions = emptyMap(),
|
||||
creator = "",
|
||||
replyToMessageId = "",
|
||||
content = Chat.Message.Content(
|
||||
replyToMessageId = replyToMessageId,
|
||||
content = Content(
|
||||
text = text,
|
||||
marks = emptyList(),
|
||||
style = Block.Content.Text.Style.P
|
||||
|
|
|
@ -1067,6 +1067,10 @@ class BlockDataRepository(
|
|||
return remote.getChatMessages(command)
|
||||
}
|
||||
|
||||
override suspend fun getChatMessagesByIds(command: Command.ChatCommand.GetMessagesByIds): List<Chat.Message> {
|
||||
return remote.getChatMessagesByIds(command)
|
||||
}
|
||||
|
||||
override suspend fun subscribeLastChatMessages(
|
||||
command: Command.ChatCommand.SubscribeLastMessages
|
||||
): Command.ChatCommand.SubscribeLastMessages.Response {
|
||||
|
|
|
@ -452,6 +452,7 @@ interface BlockRemote {
|
|||
suspend fun editChatMessage(command: Command.ChatCommand.EditMessage)
|
||||
suspend fun deleteChatMessage(command: Command.ChatCommand.DeleteMessage)
|
||||
suspend fun getChatMessages(command: Command.ChatCommand.GetMessages): List<Chat.Message>
|
||||
suspend fun getChatMessagesByIds(command: Command.ChatCommand.GetMessagesByIds): List<Chat.Message>
|
||||
suspend fun subscribeLastChatMessages(command: Command.ChatCommand.SubscribeLastMessages): Command.ChatCommand.SubscribeLastMessages.Response
|
||||
suspend fun toggleChatMessageReaction(command: Command.ChatCommand.ToggleMessageReaction)
|
||||
suspend fun unsubscribeChat(chat: Id)
|
||||
|
|
|
@ -495,6 +495,7 @@ interface BlockRepository {
|
|||
suspend fun editChatMessage(command: Command.ChatCommand.EditMessage)
|
||||
suspend fun deleteChatMessage(command: Command.ChatCommand.DeleteMessage)
|
||||
suspend fun getChatMessages(command: Command.ChatCommand.GetMessages): List<Chat.Message>
|
||||
suspend fun getChatMessagesByIds(command: Command.ChatCommand.GetMessagesByIds): List<Chat.Message>
|
||||
suspend fun subscribeLastChatMessages(command: Command.ChatCommand.SubscribeLastMessages): Command.ChatCommand.SubscribeLastMessages.Response
|
||||
suspend fun toggleChatMessageReaction(command: Command.ChatCommand.ToggleMessageReaction)
|
||||
suspend fun unsubscribeChat(chat: Id)
|
||||
|
|
|
@ -33,6 +33,7 @@ class ChatContainer @Inject constructor(
|
|||
private val payloads = MutableSharedFlow<List<Event.Command.Chats>>()
|
||||
|
||||
private val attachments = MutableSharedFlow<Set<Id>>(replay = 0)
|
||||
private val replies = MutableSharedFlow<Set<Id>>(replay = 0)
|
||||
|
||||
@Deprecated("Naive implementation. Add caching logic - maybe store for wrappers")
|
||||
fun fetchAttachments(space: Space) : Flow<Map<Id, ObjectWrapper.Basic>> {
|
||||
|
@ -64,18 +65,39 @@ class ChatContainer @Inject constructor(
|
|||
.map { wrappers -> wrappers.associate { it.id to it } }
|
||||
}
|
||||
|
||||
@Deprecated("Naive implementation. Add caching logic")
|
||||
fun fetchReplies(chat: Id) : Flow<Map<Id, Chat.Message>> {
|
||||
return replies
|
||||
.distinctUntilChanged()
|
||||
.map { ids ->
|
||||
if (ids.isNotEmpty()) {
|
||||
repo.getChatMessagesByIds(
|
||||
command = Command.ChatCommand.GetMessagesByIds(
|
||||
chat = chat,
|
||||
messages = ids.toList()
|
||||
)
|
||||
)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.map { messages -> messages.associate { it.id to it } }
|
||||
}
|
||||
|
||||
fun watchWhileTrackingAttachments(chat: Id): Flow<List<Chat.Message>> {
|
||||
return watch(chat)
|
||||
.onEach { messages ->
|
||||
val ids = messages
|
||||
.map { msg ->
|
||||
msg.attachments.map {
|
||||
it.target
|
||||
}
|
||||
val repliesIds = mutableSetOf<Id>()
|
||||
val attachmentsIds = mutableSetOf<Id>()
|
||||
messages.forEach { msg ->
|
||||
attachmentsIds.addAll(msg.attachments.map { it.target })
|
||||
if (!msg.replyToMessageId.isNullOrEmpty()) {
|
||||
repliesIds.add(msg.replyToMessageId.orEmpty())
|
||||
}
|
||||
.flatten()
|
||||
.toSet()
|
||||
attachments.emit(ids)
|
||||
}
|
||||
attachments.emit(attachmentsIds)
|
||||
replies.emit(repliesIds)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
package com.anytypeio.anytype.domain.chats
|
||||
|
||||
import com.anytypeio.anytype.core_models.Command
|
||||
import com.anytypeio.anytype.core_models.chats.Chat
|
||||
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
|
||||
import com.anytypeio.anytype.domain.base.ResultInteractor
|
||||
import com.anytypeio.anytype.domain.block.repo.BlockRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
class GetChatMessagesByIds @Inject constructor(
|
||||
private val repo: BlockRepository,
|
||||
dispatchers: AppCoroutineDispatchers
|
||||
) : ResultInteractor<Command.ChatCommand.GetMessagesByIds, List<Chat.Message>>(dispatchers.io) {
|
||||
|
||||
override suspend fun doWork(params: Command.ChatCommand.GetMessagesByIds): List<Chat.Message> {
|
||||
return repo.getChatMessagesByIds(params)
|
||||
}
|
||||
}
|
|
@ -16,7 +16,8 @@ sealed interface DiscussionView {
|
|||
val reactions: List<Reaction> = emptyList(),
|
||||
val isUserAuthor: Boolean = false,
|
||||
val isEdited: Boolean = false,
|
||||
val avatar: Avatar = Avatar.Initials()
|
||||
val avatar: Avatar = Avatar.Initials(),
|
||||
val reply: Reply? = null
|
||||
) : DiscussionView {
|
||||
|
||||
data class Content(val msg: String, val parts: List<Part>) {
|
||||
|
@ -32,6 +33,12 @@ sealed interface DiscussionView {
|
|||
}
|
||||
}
|
||||
|
||||
data class Reply(
|
||||
val msg: Id,
|
||||
val text: String,
|
||||
val author: String
|
||||
)
|
||||
|
||||
sealed class Attachment {
|
||||
data class Image(
|
||||
val target: Id,
|
||||
|
|
|
@ -11,6 +11,7 @@ import com.anytypeio.anytype.core_models.primitives.Space
|
|||
import com.anytypeio.anytype.core_ui.text.splitByMarks
|
||||
import com.anytypeio.anytype.core_utils.ext.withLatestFrom
|
||||
import com.anytypeio.anytype.domain.auth.interactor.GetAccount
|
||||
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
|
||||
import com.anytypeio.anytype.domain.base.fold
|
||||
import com.anytypeio.anytype.domain.base.onFailure
|
||||
import com.anytypeio.anytype.domain.base.onSuccess
|
||||
|
@ -33,6 +34,7 @@ import javax.inject.Inject
|
|||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
|
@ -48,7 +50,8 @@ class DiscussionViewModel @Inject constructor(
|
|||
private val members: ActiveSpaceMemberSubscriptionContainer,
|
||||
private val getAccount: GetAccount,
|
||||
private val urlBuilder: UrlBuilder,
|
||||
private val spaceViews: SpaceViewSubscriptionContainer
|
||||
private val spaceViews: SpaceViewSubscriptionContainer,
|
||||
private val dispatchers: AppCoroutineDispatchers
|
||||
) : BaseViewModel() {
|
||||
|
||||
val name = MutableStateFlow<String?>(null)
|
||||
|
@ -102,15 +105,20 @@ class DiscussionViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
// TODO move to IO thread.
|
||||
private suspend fun proceedWithObservingChatMessages(
|
||||
account: Id,
|
||||
chat: Id
|
||||
) {
|
||||
chatContainer
|
||||
.watchWhileTrackingAttachments(chat = chat)
|
||||
.withLatestFrom(chatContainer.fetchAttachments(vmParams.space)) { result, dependencies ->
|
||||
.withLatestFrom(
|
||||
chatContainer.fetchAttachments(vmParams.space),
|
||||
chatContainer.fetchReplies(chat = chat)
|
||||
) { result, dependencies, replies ->
|
||||
result.map { msg ->
|
||||
val member = members.get().let { type ->
|
||||
val allMembers = members.get()
|
||||
val member = allMembers.let { type ->
|
||||
when (type) {
|
||||
is Store.Data -> type.members.find { member ->
|
||||
member.identity == msg.creator
|
||||
|
@ -121,6 +129,30 @@ class DiscussionViewModel @Inject constructor(
|
|||
|
||||
val content = msg.content
|
||||
|
||||
val replyToId = msg.replyToMessageId
|
||||
|
||||
val reply = if (replyToId.isNullOrEmpty()) {
|
||||
null
|
||||
} else {
|
||||
val msg = replies[replyToId]
|
||||
if (msg != null) {
|
||||
DiscussionView.Message.Reply(
|
||||
msg = msg.id,
|
||||
text = msg.content?.text.orEmpty(),
|
||||
author = allMembers.let { type ->
|
||||
when (type) {
|
||||
is Store.Data -> type.members.find { member ->
|
||||
member.identity == msg.creator
|
||||
}?.name.orEmpty()
|
||||
is Store.Empty -> ""
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
DiscussionView.Message(
|
||||
id = msg.id,
|
||||
timestamp = msg.createdAt * 1000,
|
||||
|
@ -136,6 +168,7 @@ class DiscussionViewModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
),
|
||||
reply = reply,
|
||||
author = member?.name ?: msg.creator.takeLast(5),
|
||||
isUserAuthor = msg.creator == account,
|
||||
isEdited = msg.modifiedAt > msg.createdAt,
|
||||
|
@ -182,6 +215,7 @@ class DiscussionViewModel @Inject constructor(
|
|||
)
|
||||
}.reversed()
|
||||
}
|
||||
// .flowOn(dispatchers.io)
|
||||
.collect { result ->
|
||||
messages.value = result
|
||||
}
|
||||
|
@ -215,7 +249,6 @@ class DiscussionViewModel @Inject constructor(
|
|||
Timber.e(it, "Error while adding message")
|
||||
}
|
||||
}
|
||||
|
||||
is ChatBoxMode.EditMessage -> {
|
||||
editChatMessage.async(
|
||||
params = Command.ChatCommand.EditMessage(
|
||||
|
@ -234,6 +267,31 @@ class DiscussionViewModel @Inject constructor(
|
|||
chatBoxMode.value = ChatBoxMode.Default
|
||||
}
|
||||
}
|
||||
is ChatBoxMode.Reply -> {
|
||||
addChatMessage.async(
|
||||
params = Command.ChatCommand.AddMessage(
|
||||
chat = chat,
|
||||
message = Chat.Message.new(
|
||||
text = msg,
|
||||
replyToMessageId = mode.msg,
|
||||
attachments = attachments.value.map { a ->
|
||||
Chat.Message.Attachment(
|
||||
target = a.id,
|
||||
type = Chat.Message.Attachment.Type.Link
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
).onSuccess { (id, payload) ->
|
||||
attachments.value = emptyList()
|
||||
chatContainer.onPayload(payload)
|
||||
delay(JUMP_TO_BOTTOM_DELAY)
|
||||
commands.emit(UXCommand.JumpToBottom)
|
||||
}.onFailure {
|
||||
Timber.e(it, "Error while adding message")
|
||||
}
|
||||
chatBoxMode.value = ChatBoxMode.Default
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -272,6 +330,12 @@ class DiscussionViewModel @Inject constructor(
|
|||
attachments.value = emptyList()
|
||||
}
|
||||
|
||||
fun onClearReplyClicked() {
|
||||
viewModelScope.launch {
|
||||
chatBoxMode.value = ChatBoxMode.Default
|
||||
}
|
||||
}
|
||||
|
||||
fun onReacted(msg: Id, reaction: String) {
|
||||
Timber.d("onReacted")
|
||||
viewModelScope.launch {
|
||||
|
@ -292,6 +356,16 @@ class DiscussionViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun onReplyMessage(msg: DiscussionView.Message) {
|
||||
viewModelScope.launch {
|
||||
chatBoxMode.value = ChatBoxMode.Reply(
|
||||
msg = msg.id,
|
||||
text = msg.content.msg,
|
||||
author = msg.author
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onDeleteMessage(msg: DiscussionView.Message) {
|
||||
Timber.d("onDeleteMessageClicked")
|
||||
viewModelScope.launch {
|
||||
|
@ -339,6 +413,11 @@ class DiscussionViewModel @Inject constructor(
|
|||
sealed class ChatBoxMode {
|
||||
data object Default : ChatBoxMode()
|
||||
data class EditMessage(val msg: Id) : ChatBoxMode()
|
||||
data class Reply(
|
||||
val msg: Id,
|
||||
val text: String,
|
||||
val author: String
|
||||
): ChatBoxMode()
|
||||
}
|
||||
|
||||
sealed class Params {
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.anytypeio.anytype.feature_discussions.presentation
|
|||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.anytypeio.anytype.domain.auth.interactor.GetAccount
|
||||
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
|
||||
import com.anytypeio.anytype.domain.chats.AddChatMessage
|
||||
import com.anytypeio.anytype.domain.chats.ChatContainer
|
||||
import com.anytypeio.anytype.domain.chats.DeleteChatMessage
|
||||
|
@ -28,7 +29,8 @@ class DiscussionViewModelFactory @Inject constructor(
|
|||
private val members: ActiveSpaceMemberSubscriptionContainer,
|
||||
private val getAccount: GetAccount,
|
||||
private val urlBuilder: UrlBuilder,
|
||||
private val spaceViews: SpaceViewSubscriptionContainer
|
||||
private val spaceViews: SpaceViewSubscriptionContainer,
|
||||
private val dispatchers: AppCoroutineDispatchers
|
||||
) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T = DiscussionViewModel(
|
||||
|
@ -43,6 +45,7 @@ class DiscussionViewModelFactory @Inject constructor(
|
|||
deleteChatMessage = deleteChatMessage,
|
||||
urlBuilder = urlBuilder,
|
||||
editChatMessage = editChatMessage,
|
||||
spaceViews = spaceViews
|
||||
spaceViews = spaceViews,
|
||||
dispatchers = dispatchers
|
||||
) as T
|
||||
}
|
|
@ -8,6 +8,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import com.anytypeio.anytype.core_models.chats.Chat
|
||||
import com.anytypeio.anytype.feature_discussions.R
|
||||
import com.anytypeio.anytype.feature_discussions.presentation.DiscussionView
|
||||
import com.anytypeio.anytype.feature_discussions.presentation.DiscussionViewModel
|
||||
import kotlin.time.DurationUnit
|
||||
import kotlin.time.toDuration
|
||||
|
||||
|
@ -66,9 +67,8 @@ fun DiscussionPreview() {
|
|||
onCopyMessage = {},
|
||||
onAttachmentClicked = {},
|
||||
onEditMessage = {},
|
||||
onMarkupLinkClicked = {
|
||||
|
||||
}
|
||||
onMarkupLinkClicked = {},
|
||||
onReplyMessage = {}
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -118,7 +118,10 @@ fun DiscussionScreenPreview() {
|
|||
onAttachFileClicked = {},
|
||||
onUploadAttachmentClicked = {},
|
||||
onAttachMediaClicked = {},
|
||||
onAttachObjectClicked = {}
|
||||
onAttachObjectClicked = {},
|
||||
onReplyMessage = {},
|
||||
chatBoxMode = DiscussionViewModel.ChatBoxMode.Default,
|
||||
onClearReplyClicked = {}
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -142,7 +145,8 @@ fun BubblePreview() {
|
|||
onCopyMessage = {},
|
||||
onAttachmentClicked = {},
|
||||
onEditMessage = {},
|
||||
onMarkupLinkClicked = {}
|
||||
onMarkupLinkClicked = {},
|
||||
onReply = {}
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -167,7 +171,8 @@ fun BubbleEditedPreview() {
|
|||
onCopyMessage = {},
|
||||
onAttachmentClicked = {},
|
||||
onEditMessage = {},
|
||||
onMarkupLinkClicked = {}
|
||||
onMarkupLinkClicked = {},
|
||||
onReply = {}
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -199,6 +204,7 @@ fun BubbleWithAttachmentPreview() {
|
|||
},
|
||||
onAttachmentClicked = {},
|
||||
onEditMessage = {},
|
||||
onMarkupLinkClicked = {}
|
||||
onMarkupLinkClicked = {},
|
||||
onReply = {}
|
||||
)
|
||||
}
|
|
@ -93,10 +93,8 @@ import androidx.navigation.compose.NavHost
|
|||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import coil.compose.AsyncImage
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
import coil.request.ImageRequest
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.core_models.chats.Chat
|
||||
import com.anytypeio.anytype.core_ui.foundation.AlertConfig
|
||||
import com.anytypeio.anytype.core_ui.foundation.AlertIcon
|
||||
import com.anytypeio.anytype.core_ui.foundation.Divider
|
||||
|
@ -158,6 +156,7 @@ fun DiscussionScreenWrapper(
|
|||
val clipboard = LocalClipboardManager.current
|
||||
val lazyListState = rememberLazyListState()
|
||||
DiscussionScreen(
|
||||
chatBoxMode = vm.chatBoxMode.collectAsState().value,
|
||||
isSpaceLevelChat = isSpaceLevelChat,
|
||||
title = vm.name.collectAsState().value,
|
||||
messages = vm.messages.collectAsState().value,
|
||||
|
@ -187,7 +186,9 @@ fun DiscussionScreenWrapper(
|
|||
},
|
||||
onUploadAttachmentClicked = {
|
||||
|
||||
}
|
||||
},
|
||||
onReplyMessage = vm::onReplyMessage,
|
||||
onClearReplyClicked = vm::onClearReplyClicked
|
||||
)
|
||||
LaunchedEffect(Unit) {
|
||||
vm.commands.collect { command ->
|
||||
|
@ -211,6 +212,7 @@ fun DiscussionScreenWrapper(
|
|||
*/
|
||||
@Composable
|
||||
fun DiscussionScreen(
|
||||
chatBoxMode: ChatBoxMode,
|
||||
isSpaceLevelChat: Boolean,
|
||||
isInEditMessageMode: Boolean = false,
|
||||
lazyListState: LazyListState,
|
||||
|
@ -222,10 +224,12 @@ fun DiscussionScreen(
|
|||
onAttachClicked: () -> Unit,
|
||||
onBackButtonClicked: () -> Unit,
|
||||
onClearAttachmentClicked: () -> Unit,
|
||||
onClearReplyClicked: () -> Unit,
|
||||
onReacted: (Id, String) -> Unit,
|
||||
onDeleteMessage: (DiscussionView.Message) -> Unit,
|
||||
onCopyMessage: (DiscussionView.Message) -> Unit,
|
||||
onEditMessage: (DiscussionView.Message) -> Unit,
|
||||
onReplyMessage: (DiscussionView.Message) -> Unit,
|
||||
onAttachmentClicked: (DiscussionView.Message.Attachment) -> Unit,
|
||||
onExitEditMessageMode: () -> Unit,
|
||||
onMarkupLinkClicked: (String) -> Unit,
|
||||
|
@ -284,6 +288,7 @@ fun DiscussionScreen(
|
|||
chatBoxFocusRequester.requestFocus()
|
||||
}
|
||||
},
|
||||
onReplyMessage = onReplyMessage,
|
||||
onMarkupLinkClicked = onMarkupLinkClicked
|
||||
)
|
||||
// Jump to bottom button shows up when user scrolls past a threshold.
|
||||
|
@ -324,6 +329,7 @@ fun DiscussionScreen(
|
|||
}
|
||||
|
||||
ChatBox(
|
||||
mode = chatBoxMode,
|
||||
modifier = Modifier
|
||||
.imePadding()
|
||||
.navigationBarsPadding(),
|
||||
|
@ -349,7 +355,8 @@ fun DiscussionScreen(
|
|||
onAttachMediaClicked = onAttachMediaClicked,
|
||||
onUploadAttachmentClicked = onUploadAttachmentClicked,
|
||||
onAttachObjectClicked = onAttachObjectClicked,
|
||||
onClearAttachmentClicked = onClearAttachmentClicked
|
||||
onClearAttachmentClicked = onClearAttachmentClicked,
|
||||
onClearReplyClicked = onClearReplyClicked
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -482,6 +489,7 @@ private fun OldChatBox(
|
|||
|
||||
@Composable
|
||||
private fun ChatBox(
|
||||
mode: ChatBoxMode = ChatBoxMode.Default,
|
||||
modifier: Modifier = Modifier,
|
||||
onBackButtonClicked: () -> Unit,
|
||||
chatBoxFocusRequester: FocusRequester,
|
||||
|
@ -497,7 +505,8 @@ private fun ChatBox(
|
|||
onAttachMediaClicked: () -> Unit,
|
||||
onAttachFileClicked: () -> Unit,
|
||||
onUploadAttachmentClicked: () -> Unit,
|
||||
onClearAttachmentClicked: () -> Unit
|
||||
onClearAttachmentClicked: () -> Unit,
|
||||
onClearReplyClicked: () -> Unit
|
||||
) {
|
||||
|
||||
var showDropdownMenu by remember { mutableStateOf(false) }
|
||||
|
@ -552,8 +561,57 @@ private fun ChatBox(
|
|||
)
|
||||
}
|
||||
}
|
||||
Row(
|
||||
) {
|
||||
when(mode) {
|
||||
is ChatBoxMode.Default -> {
|
||||
|
||||
}
|
||||
is ChatBoxMode.EditMessage -> {
|
||||
|
||||
}
|
||||
is ChatBoxMode.Reply -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(54.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Reply to ${mode.author}",
|
||||
modifier = Modifier.padding(
|
||||
start = 12.dp,
|
||||
top = 8.dp,
|
||||
end = 44.dp
|
||||
),
|
||||
style = Caption1Medium,
|
||||
color = colorResource(R.color.text_primary),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
text = mode.text,
|
||||
modifier = Modifier.padding(
|
||||
start = 12.dp,
|
||||
top = 28.dp,
|
||||
end = 44.dp
|
||||
),
|
||||
style = Caption1Regular,
|
||||
color = colorResource(R.color.text_primary),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Image(
|
||||
painter = painterResource(R.drawable.ic_chat_close_chat_box_reply),
|
||||
contentDescription = "Clear reply to icon",
|
||||
modifier = Modifier
|
||||
.padding(end = 12.dp)
|
||||
.align(Alignment.CenterEnd)
|
||||
.clickable {
|
||||
onClearReplyClicked()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Row {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 4.dp, vertical = 8.dp)
|
||||
|
@ -829,6 +887,7 @@ fun Messages(
|
|||
onCopyMessage: (DiscussionView.Message) -> Unit,
|
||||
onAttachmentClicked: (DiscussionView.Message.Attachment) -> Unit,
|
||||
onEditMessage: (DiscussionView.Message) -> Unit,
|
||||
onReplyMessage: (DiscussionView.Message) -> Unit,
|
||||
onMarkupLinkClicked: (String) -> Unit
|
||||
) {
|
||||
LazyColumn(
|
||||
|
@ -879,7 +938,11 @@ fun Messages(
|
|||
onEditMessage = {
|
||||
onEditMessage(msg)
|
||||
},
|
||||
onMarkupLinkClicked = onMarkupLinkClicked
|
||||
onMarkupLinkClicked = onMarkupLinkClicked,
|
||||
onReply = {
|
||||
onReplyMessage(msg)
|
||||
},
|
||||
reply = msg.reply
|
||||
)
|
||||
if (msg.isUserAuthor) {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
@ -998,6 +1061,7 @@ val userMessageBubbleColor = Color(0x66000000)
|
|||
fun Bubble(
|
||||
modifier: Modifier = Modifier,
|
||||
name: String,
|
||||
reply: DiscussionView.Message.Reply? = null,
|
||||
content: DiscussionView.Message.Content,
|
||||
timestamp: Long,
|
||||
attachments: List<DiscussionView.Message.Attachment> = emptyList(),
|
||||
|
@ -1008,6 +1072,7 @@ fun Bubble(
|
|||
onDeleteMessage: () -> Unit,
|
||||
onCopyMessage: () -> Unit,
|
||||
onEditMessage: () -> Unit,
|
||||
onReply: () -> Unit,
|
||||
onAttachmentClicked: (DiscussionView.Message.Attachment) -> Unit,
|
||||
onMarkupLinkClicked: (String) -> Unit
|
||||
) {
|
||||
|
@ -1020,13 +1085,54 @@ fun Bubble(
|
|||
userMessageBubbleColor
|
||||
else
|
||||
defaultBubbleColor,
|
||||
shape = RoundedCornerShape(24.dp)
|
||||
shape = RoundedCornerShape(20.dp)
|
||||
)
|
||||
.clip(RoundedCornerShape(24.dp))
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.clickable {
|
||||
showDropdownMenu = !showDropdownMenu
|
||||
}
|
||||
) {
|
||||
if (reply != null) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(4.dp)
|
||||
.fillMaxWidth()
|
||||
.height(54.dp)
|
||||
.background(
|
||||
color = colorResource(R.color.navigation_panel_icon),
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = reply.author,
|
||||
modifier = Modifier.padding(
|
||||
start = 12.dp,
|
||||
top = 8.dp,
|
||||
end = 12.dp
|
||||
),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = if (isUserAuthor)
|
||||
colorResource(id = R.color.text_white)
|
||||
else
|
||||
colorResource(id = R.color.text_primary),
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(
|
||||
start = 12.dp,
|
||||
top = 26.dp,
|
||||
end = 12.dp
|
||||
),
|
||||
text = reply.text,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = if (isUserAuthor)
|
||||
colorResource(id = R.color.text_white)
|
||||
else
|
||||
colorResource(id = R.color.text_primary),
|
||||
)
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.padding(
|
||||
start = 16.dp,
|
||||
|
@ -1100,12 +1206,7 @@ fun Bubble(
|
|||
|
||||
if (isEdited) {
|
||||
withStyle(
|
||||
style = SpanStyle(
|
||||
color = if (isUserAuthor)
|
||||
colorResource(id = R.color.text_white)
|
||||
else
|
||||
colorResource(id = R.color.text_primary),
|
||||
)
|
||||
style = SpanStyle(color = colorResource(id = R.color.text_tertiary))
|
||||
) {
|
||||
append(
|
||||
" (${stringResource(R.string.chats_message_edited)})"
|
||||
|
@ -1221,6 +1322,18 @@ fun Bubble(
|
|||
// Do nothing.
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(R.string.chats_reply),
|
||||
color = colorResource(id = R.color.text_primary)
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
onReply()
|
||||
showDropdownMenu = false
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M7,7L12,12L7,17"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="@color/glyph_button"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M17,7L12,12L17,17"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="@color/glyph_button"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
|
@ -1827,5 +1827,6 @@ Please provide specific details of your needs here.</string>
|
|||
<string name="chat_attachment_file">File</string>
|
||||
<string name="chat_attachment_media">Media</string>
|
||||
<string name="chat_attachment_upload">Upload</string>
|
||||
<string name="chats_reply">Reply</string>
|
||||
|
||||
</resources>
|
|
@ -1038,6 +1038,10 @@ class BlockMiddleware(
|
|||
return middleware.chatGetMessages(command)
|
||||
}
|
||||
|
||||
override suspend fun getChatMessagesByIds(command: Command.ChatCommand.GetMessagesByIds): List<Chat.Message> {
|
||||
return middleware.chatGetMessagesByIds(command)
|
||||
}
|
||||
|
||||
override suspend fun subscribeLastChatMessages(
|
||||
command: Command.ChatCommand.SubscribeLastMessages
|
||||
): Command.ChatCommand.SubscribeLastMessages.Response {
|
||||
|
|
|
@ -2744,6 +2744,18 @@ class Middleware @Inject constructor(
|
|||
return response.messages.map { it.core() }
|
||||
}
|
||||
|
||||
@Throws
|
||||
fun chatGetMessagesByIds(command: Command.ChatCommand.GetMessagesByIds) : List<Chat.Message> {
|
||||
val request = Rpc.Chat.GetMessagesByIds.Request(
|
||||
chatObjectId = command.chat,
|
||||
messageIds = command.messages
|
||||
)
|
||||
logRequestIfDebug(request)
|
||||
val (response, time) = measureTimedValue { service.chatGetMessagesByIds(request) }
|
||||
logResponseIfDebug(response, time)
|
||||
return response.messages.map { it.core() }
|
||||
}
|
||||
|
||||
@Throws
|
||||
fun chatDeleteMessage(command: Command.ChatCommand.DeleteMessage) {
|
||||
val request = Rpc.Chat.DeleteMessage.Request(
|
||||
|
|
|
@ -593,6 +593,7 @@ interface MiddlewareService {
|
|||
fun chatAddMessage(request: Rpc.Chat.AddMessage.Request): Rpc.Chat.AddMessage.Response
|
||||
fun chatEditMessage(request: Rpc.Chat.EditMessageContent.Request): Rpc.Chat.EditMessageContent.Response
|
||||
fun chatGetMessages(request: Rpc.Chat.GetMessages.Request): Rpc.Chat.GetMessages.Response
|
||||
fun chatGetMessagesByIds(request: Rpc.Chat.GetMessagesByIds.Request): Rpc.Chat.GetMessagesByIds.Response
|
||||
fun chatDeleteMessage(request: Rpc.Chat.DeleteMessage.Request): Rpc.Chat.DeleteMessage.Response
|
||||
fun chatSubscribeLastMessages(request: Rpc.Chat.SubscribeLastMessages.Request): Rpc.Chat.SubscribeLastMessages.Response
|
||||
fun chatToggleMessageReaction(request: Rpc.Chat.ToggleMessageReaction.Request): Rpc.Chat.ToggleMessageReaction.Response
|
||||
|
|
|
@ -2358,6 +2358,19 @@ class MiddlewareServiceImplementation @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun chatGetMessagesByIds(request: Rpc.Chat.GetMessagesByIds.Request): Rpc.Chat.GetMessagesByIds.Response {
|
||||
val encoded = Service.chatGetMessagesByIds(
|
||||
Rpc.Chat.GetMessagesByIds.Request.ADAPTER.encode(request)
|
||||
)
|
||||
val response = Rpc.Chat.GetMessagesByIds.Response.ADAPTER.decode(encoded)
|
||||
val error = response.error
|
||||
if (error != null && error.code != Rpc.Chat.GetMessagesByIds.Response.Error.Code.NULL) {
|
||||
throw Exception(error.description)
|
||||
} else {
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
override fun chatSubscribeLastMessages(request: Rpc.Chat.SubscribeLastMessages.Request): Rpc.Chat.SubscribeLastMessages.Response {
|
||||
val encoded = Service.chatSubscribeLastMessages(
|
||||
Rpc.Chat.SubscribeLastMessages.Request.ADAPTER.encode(request)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue