Allow replies to maintain state on orientation change

Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>
This commit is contained in:
rapterjet2004 2025-06-05 16:38:35 -05:00
parent 7e72032738
commit 71bd381828
No known key found for this signature in database
GPG Key ID: 3AA5FDFED7944099
11 changed files with 222 additions and 79 deletions

View File

@ -35,7 +35,6 @@ import android.widget.LinearLayout
import android.widget.PopupMenu import android.widget.PopupMenu
import android.widget.RelativeLayout import android.widget.RelativeLayout
import android.widget.SeekBar import android.widget.SeekBar
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.view.ContextThemeWrapper import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@ -81,15 +80,15 @@ import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
import com.nextcloud.talk.utils.message.MessageUtils import com.nextcloud.talk.utils.message.MessageUtils
import com.nextcloud.talk.utils.text.Spans import com.nextcloud.talk.utils.text.Spans
import com.otaliastudios.autocomplete.Autocomplete import com.otaliastudios.autocomplete.Autocomplete
import com.stfalcon.chatkit.commons.models.IMessage
import com.vanniktech.emoji.EmojiPopup import com.vanniktech.emoji.EmojiPopup
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.util.Objects import java.util.Objects
import javax.inject.Inject import javax.inject.Inject
@Suppress("LongParameterList", "TooManyFunctions") @Suppress("LongParameterList", "TooManyFunctions", "LargeClass", "LongMethod")
@AutoInjector(NextcloudTalkApplication::class) @AutoInjector(NextcloudTalkApplication::class)
class MessageInputFragment : Fragment() { class MessageInputFragment : Fragment() {
@ -112,6 +111,10 @@ class MessageInputFragment : Fragment() {
private const val CONNECTION_ESTABLISHED_ANIM_DURATION: Long = 3000 private const val CONNECTION_ESTABLISHED_ANIM_DURATION: Long = 3000
private const val FULLY_OPAQUE: Float = 1.0f private const val FULLY_OPAQUE: Float = 1.0f
private const val FULLY_TRANSPARENT: Float = 0.0f private const val FULLY_TRANSPARENT: Float = 0.0f
const val QUOTED_MESSAGE_TEXT = "QUOTED_MESSAGE_TEXT"
const val QUOTED_MESSAGE_ID = "QUOTED_MESSAGE_ID"
const val QUOTED_MESSAGE_URL = "QUOTED_MESSAGE_URL"
const val QUOTED_MESSAGE_NAME = "QUOTED_MESSAGE_NAME"
} }
@Inject @Inject
@ -163,7 +166,6 @@ class MessageInputFragment : Fragment() {
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
saveState()
} }
override fun onDestroyView() { override fun onDestroyView() {
@ -172,7 +174,6 @@ class MessageInputFragment : Fragment() {
mentionAutocomplete?.dismissPopup() mentionAutocomplete?.dismissPopup()
} }
clearEditUI() clearEditUI()
cancelReply()
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -183,7 +184,13 @@ class MessageInputFragment : Fragment() {
private fun initObservers() { private fun initObservers() {
Log.d(TAG, "LifeCyclerOwner is: ${viewLifecycleOwner.lifecycle}") Log.d(TAG, "LifeCyclerOwner is: ${viewLifecycleOwner.lifecycle}")
chatActivity.messageInputViewModel.getReplyChatMessage.observe(viewLifecycleOwner) { message -> chatActivity.messageInputViewModel.getReplyChatMessage.observe(viewLifecycleOwner) { message ->
message?.let { replyToMessage(message) } (message as ChatMessage?)?.let {
chatActivity.chatViewModel.messageDraft.quotedMessageText = message.text
chatActivity.chatViewModel.messageDraft.quotedDisplayName = message.actorDisplayName
chatActivity.chatViewModel.messageDraft.quotedImageUrl = message.imageUrl
chatActivity.chatViewModel.messageDraft.quotedJsonId = message.jsonMessageId
replyToMessage(message.text, message.actorDisplayName, message.imageUrl, message.jsonMessageId)
}
} }
chatActivity.messageInputViewModel.getEditChatMessage.observe(viewLifecycleOwner) { message -> chatActivity.messageInputViewModel.getEditChatMessage.observe(viewLifecycleOwner) { message ->
@ -299,34 +306,24 @@ class MessageInputFragment : Fragment() {
} }
private fun restoreState() { private fun restoreState() {
if (binding.fragmentMessageInputView.inputEditText.text.isEmpty()) { runBlocking {
requireContext().getSharedPreferences(chatActivity.localClassName, AppCompatActivity.MODE_PRIVATE).apply { chatActivity.chatViewModel.updateMessageDraft()
val text = getString(chatActivity.roomToken, "")
val cursor = getInt(chatActivity.roomToken + CURSOR_KEY, 0)
binding.fragmentMessageInputView.messageInput.setText(text)
binding.fragmentMessageInputView.messageInput.setSelection(cursor)
}
} }
}
private fun saveState() { val draft = chatActivity.chatViewModel.messageDraft
val text = binding.fragmentMessageInputView.messageInput.text.toString() binding.fragmentMessageInputView.messageInput.setText(draft.messageText)
val cursor = binding.fragmentMessageInputView.messageInput.selectionStart binding.fragmentMessageInputView.messageInput.setSelection(draft.messageCursor)
val previous = requireContext().getSharedPreferences( if (draft.messageText != "") {
chatActivity.localClassName, binding.fragmentMessageInputView.messageInput.requestFocus()
AppCompatActivity }
.MODE_PRIVATE
).getString(chatActivity.roomToken, "null")
if (text != previous) { if (isInReplyState()) {
requireContext().getSharedPreferences( replyToMessage(
chatActivity.localClassName, chatActivity.chatViewModel.messageDraft.quotedMessageText,
AppCompatActivity.MODE_PRIVATE chatActivity.chatViewModel.messageDraft.quotedDisplayName,
).edit().apply { chatActivity.chatViewModel.messageDraft.quotedImageUrl,
putString(chatActivity.roomToken, text) chatActivity.chatViewModel.messageDraft.quotedJsonId ?: 0
putInt(chatActivity.roomToken + CURSOR_KEY, cursor) )
apply()
}
} }
} }
@ -388,7 +385,10 @@ class MessageInputFragment : Fragment() {
} }
override fun afterTextChanged(s: Editable) { override fun afterTextChanged(s: Editable) {
// unused atm val cursor = binding.fragmentMessageInputView.messageInput.selectionStart
val text = binding.fragmentMessageInputView.messageInput.text.toString()
chatActivity.chatViewModel.messageDraft.messageCursor = cursor
chatActivity.chatViewModel.messageDraft.messageText = text
} }
}) })
@ -615,7 +615,7 @@ class MessageInputFragment : Fragment() {
} }
} }
} }
v?.onTouchEvent(event) ?: true v?.onTouchEvent(event) != false
} }
} }
@ -717,52 +717,54 @@ class MessageInputFragment : Fragment() {
} }
} }
private fun replyToMessage(message: IMessage?) { private fun replyToMessage(
quotedMessageText: String?,
quotedActorDisplayName: String?,
quotedImageUrl: String?,
quotedJsonId: Int
) {
Log.d(TAG, "Reply") Log.d(TAG, "Reply")
val chatMessage = message as ChatMessage? val view = binding.fragmentMessageInputView
chatMessage?.let { view.findViewById<ImageButton>(R.id.attachmentButton)?.visibility =
val view = binding.fragmentMessageInputView View.GONE
view.findViewById<ImageButton>(R.id.attachmentButton)?.visibility = view.findViewById<ImageButton>(R.id.cancelReplyButton)?.visibility =
View.GONE View.VISIBLE
view.findViewById<ImageButton>(R.id.cancelReplyButton)?.visibility =
View.VISIBLE
val quotedMessage = view.findViewById<EmojiTextView>(R.id.quotedMessage) val quotedMessage = view.findViewById<EmojiTextView>(R.id.quotedMessage)
quotedMessage?.maxLines = 2 quotedMessage?.maxLines = 2
quotedMessage?.ellipsize = TextUtils.TruncateAt.END quotedMessage?.ellipsize = TextUtils.TruncateAt.END
quotedMessage?.text = it.text quotedMessage?.text = quotedMessageText
view.findViewById<EmojiTextView>(R.id.quotedMessageAuthor)?.text = view.findViewById<EmojiTextView>(R.id.quotedMessageAuthor)?.text =
it.actorDisplayName ?: requireContext().getText(R.string.nc_nick_guest) quotedActorDisplayName ?: requireContext().getText(R.string.nc_nick_guest)
chatActivity.conversationUser?.let { chatActivity.conversationUser?.let {
val quotedMessageImage = view.findViewById<ImageView>(R.id.quotedMessageImage) val quotedMessageImage = view.findViewById<ImageView>(R.id.quotedMessageImage)
chatMessage.imageUrl?.let { previewImageUrl -> quotedImageUrl?.let { previewImageUrl ->
quotedMessageImage?.visibility = View.VISIBLE quotedMessageImage?.visibility = View.VISIBLE
val px = TypedValue.applyDimension( val px = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, TypedValue.COMPLEX_UNIT_DIP,
QUOTED_MESSAGE_IMAGE_MAX_HEIGHT, QUOTED_MESSAGE_IMAGE_MAX_HEIGHT,
resources.displayMetrics resources.displayMetrics
) )
quotedMessageImage?.maxHeight = px.toInt() quotedMessageImage?.maxHeight = px.toInt()
val layoutParams = quotedMessageImage?.layoutParams as FlexboxLayout.LayoutParams val layoutParams = quotedMessageImage?.layoutParams as FlexboxLayout.LayoutParams
layoutParams.flexGrow = 0f layoutParams.flexGrow = 0f
quotedMessageImage.layoutParams = layoutParams quotedMessageImage.layoutParams = layoutParams
quotedMessageImage.load(previewImageUrl) { quotedMessageImage.load(previewImageUrl) {
addHeader("Authorization", chatActivity.credentials!!) addHeader("Authorization", chatActivity.credentials!!)
}
} ?: run {
view.findViewById<ImageView>(R.id.quotedMessageImage)?.visibility = View.GONE
} }
} ?: run {
view.findViewById<ImageView>(R.id.quotedMessageImage)?.visibility = View.GONE
} }
val quotedChatMessageView =
view.findViewById<RelativeLayout>(R.id.quotedChatMessageView)
quotedChatMessageView?.tag = message?.jsonMessageId
quotedChatMessageView?.visibility = View.VISIBLE
} }
val quotedChatMessageView =
view.findViewById<RelativeLayout>(R.id.quotedChatMessageView)
quotedChatMessageView?.tag = quotedJsonId
quotedChatMessageView?.visibility = View.VISIBLE
} }
fun updateOwnTypingStatus(typedText: CharSequence) { fun updateOwnTypingStatus(typedText: CharSequence) {
@ -1051,5 +1053,15 @@ class MessageInputFragment : Fragment() {
quote.tag = null quote.tag = null
binding.fragmentMessageInputView.findViewById<ImageButton>(R.id.attachmentButton)?.visibility = View.VISIBLE binding.fragmentMessageInputView.findViewById<ImageButton>(R.id.attachmentButton)?.visibility = View.VISIBLE
chatActivity.messageInputViewModel.reply(null) chatActivity.messageInputViewModel.reply(null)
chatActivity.chatViewModel.messageDraft.quotedMessageText = null
chatActivity.chatViewModel.messageDraft.quotedDisplayName = null
chatActivity.chatViewModel.messageDraft.quotedImageUrl = null
chatActivity.chatViewModel.messageDraft.quotedJsonId = null
}
private fun isInReplyState(): Boolean {
val jsonId = chatActivity.chatViewModel.messageDraft.quotedJsonId
return jsonId != null
} }
} }

View File

@ -27,6 +27,7 @@ import com.nextcloud.talk.conversationlist.data.OfflineConversationsRepository
import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.extensions.toIntOrZero import com.nextcloud.talk.extensions.toIntOrZero
import com.nextcloud.talk.jobs.UploadAndShareFilesWorker import com.nextcloud.talk.jobs.UploadAndShareFilesWorker
import com.nextcloud.talk.models.MessageDraft
import com.nextcloud.talk.models.domain.ConversationModel import com.nextcloud.talk.models.domain.ConversationModel
import com.nextcloud.talk.models.domain.ReactionAddedModel import com.nextcloud.talk.models.domain.ReactionAddedModel
import com.nextcloud.talk.models.domain.ReactionDeletedModel import com.nextcloud.talk.models.domain.ReactionDeletedModel
@ -60,6 +61,7 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
@ -89,6 +91,8 @@ class ChatViewModel @Inject constructor(
val disposableSet = mutableSetOf<Disposable>() val disposableSet = mutableSetOf<Disposable>()
var mediaPlayerDuration = mediaPlayerManager.mediaPlayerDuration var mediaPlayerDuration = mediaPlayerManager.mediaPlayerDuration
val mediaPlayerPosition = mediaPlayerManager.mediaPlayerPosition val mediaPlayerPosition = mediaPlayerManager.mediaPlayerPosition
var chatRoomToken: String = ""
var messageDraft: MessageDraft = MessageDraft()
fun getChatRepository(): ChatMessageRepository = chatRepository fun getChatRepository(): ChatMessageRepository = chatRepository
@ -108,6 +112,14 @@ class ChatViewModel @Inject constructor(
mediaRecorderManager.handleOnPause() mediaRecorderManager.handleOnPause()
chatRepository.handleOnPause() chatRepository.handleOnPause()
mediaPlayerManager.handleOnPause() mediaPlayerManager.handleOnPause()
runBlocking {
val model = conversationRepository.getLocallyStoredConversation(chatRoomToken)
model?.let {
it.messageDraft = messageDraft
conversationRepository.updateConversation(it)
}
}
} }
override fun onStop(owner: LifecycleOwner) { override fun onStop(owner: LifecycleOwner) {
@ -889,6 +901,11 @@ class ChatViewModel @Inject constructor(
} }
} }
suspend fun updateMessageDraft() {
val model = conversationRepository.getLocallyStoredConversation(chatRoomToken)
messageDraft = model?.messageDraft!!
}
companion object { companion object {
private val TAG = ChatViewModel::class.simpleName private val TAG = ChatViewModel::class.simpleName
const val JOIN_ROOM_RETRY_COUNT: Long = 3 const val JOIN_ROOM_RETRY_COUNT: Long = 3

View File

@ -307,6 +307,16 @@ class ConversationsListActivity :
showNotificationWarning() showNotificationWarning()
showShareToScreen = hasActivityActionSendIntent() showShareToScreen = hasActivityActionSendIntent()
// context.getSharedPreferences(
// CHAT_ACTIVITY_LOCAL_NAME,
// MODE_PRIVATE
// ).edit().apply {
// putInt(QUOTED_MESSAGE_ID, -1)
// putString(QUOTED_MESSAGE_NAME, null)
// putString(QUOTED_MESSAGE_TEXT, "")
// putString(QUOTED_MESSAGE_URL, null)
// apply()
// }
if (!eventBus.isRegistered(this)) { if (!eventBus.isRegistered(this)) {
eventBus.register(this) eventBus.register(this)
@ -2216,6 +2226,7 @@ class ConversationsListActivity :
const val UNREAD_BUBBLE_DELAY = 2500 const val UNREAD_BUBBLE_DELAY = 2500
const val BOTTOM_SHEET_DELAY: Long = 2500 const val BOTTOM_SHEET_DELAY: Long = 2500
private const val KEY_SEARCH_QUERY = "ConversationsListActivity.searchQuery" private const val KEY_SEARCH_QUERY = "ConversationsListActivity.searchQuery"
private const val CHAT_ACTIVITY_LOCAL_NAME = "com.nextcloud.talk.chat.ChatActivity"
const val SEARCH_DEBOUNCE_INTERVAL_MS = 300 const val SEARCH_DEBOUNCE_INTERVAL_MS = 300
const val SEARCH_MIN_CHARS = 1 const val SEARCH_MIN_CHARS = 1
const val HTTP_UNAUTHORIZED = 401 const val HTTP_UNAUTHORIZED = 401

View File

@ -36,4 +36,8 @@ interface OfflineConversationsRepository {
* to be handled asynchronously. * to be handled asynchronously.
*/ */
fun getRoom(roomToken: String): Job fun getRoom(roomToken: String): Job
suspend fun updateConversation(conversationModel: ConversationModel)
suspend fun getLocallyStoredConversation(roomToken: String): ConversationModel?
} }

View File

@ -98,12 +98,22 @@ class OfflineFirstConversationsRepository @Inject constructor(
runBlocking { runBlocking {
_conversationFlow.emit(model) _conversationFlow.emit(model)
val entityList = listOf(model.asEntity()) val entityList = listOf(model.asEntity())
dao.upsertConversations(entityList) dao.upsertConversations(user.id!!, entityList)
} }
} }
}) })
} }
override suspend fun updateConversation(conversationModel: ConversationModel) {
val entity = conversationModel.asEntity()
dao.updateConversation(entity)
}
override suspend fun getLocallyStoredConversation(roomToken: String): ConversationModel? {
val id = user.id!!
return getConversation(id, roomToken)
}
@Suppress("Detekt.TooGenericExceptionCaught") @Suppress("Detekt.TooGenericExceptionCaught")
private suspend fun getRoomsFromServer(): List<ConversationEntity>? { private suspend fun getRoomsFromServer(): List<ConversationEntity>? {
var conversationsFromSync: List<ConversationEntity>? = null var conversationsFromSync: List<ConversationEntity>? = null
@ -126,7 +136,7 @@ class OfflineFirstConversationsRepository @Inject constructor(
} }
deleteLeftConversations(conversationsFromSync) deleteLeftConversations(conversationsFromSync)
dao.upsertConversations(conversationsFromSync) dao.upsertConversations(user.id!!, conversationsFromSync)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Something went wrong when fetching conversations", e) Log.e(TAG, "Something went wrong when fetching conversations", e)
} }

View File

@ -8,11 +8,15 @@
package com.nextcloud.talk.data.database.dao package com.nextcloud.talk.data.database.dao
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy.Companion.REPLACE
import androidx.room.Query import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update import androidx.room.Update
import androidx.room.Upsert import androidx.room.Upsert
import com.nextcloud.talk.data.database.model.ConversationEntity import com.nextcloud.talk.data.database.model.ConversationEntity
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
@Dao @Dao
interface ConversationsDao { interface ConversationsDao {
@ -22,9 +26,27 @@ interface ConversationsDao {
@Query("SELECT * FROM Conversations where accountId = :accountId AND token = :token") @Query("SELECT * FROM Conversations where accountId = :accountId AND token = :token")
fun getConversationForUser(accountId: Long, token: String): Flow<ConversationEntity?> fun getConversationForUser(accountId: Long, token: String): Flow<ConversationEntity?>
@Upsert @Upsert()
fun upsertConversations(conversationEntities: List<ConversationEntity>) fun upsertConversations(conversationEntities: List<ConversationEntity>)
@Insert(onConflict = REPLACE)
suspend fun insertOrUpdate(item: ConversationEntity)
@Transaction
suspend fun upsertConversations(accountId: Long, serverItems: List<ConversationEntity>) {
serverItems.forEach { serverItem ->
val existingItem = getConversationForUser(accountId, serverItem.token).first()
if (existingItem != null) {
val mergedItem = serverItem
mergedItem.messageDraft = existingItem.messageDraft
insertOrUpdate(mergedItem)
} else {
// Insert new item directly (local-only fields will be default)
insertOrUpdate(serverItem)
}
}
}
/** /**
* Deletes rows in the db matching the specified [conversationIds] * Deletes rows in the db matching the specified [conversationIds]
*/ */
@ -36,7 +58,7 @@ interface ConversationsDao {
) )
fun deleteConversations(conversationIds: List<String>) fun deleteConversations(conversationIds: List<String>)
@Update @Update(onConflict = REPLACE)
fun updateConversation(conversationEntity: ConversationEntity) fun updateConversation(conversationEntity: ConversationEntity)
@Query( @Query(

View File

@ -1,7 +1,7 @@
/* /*
* Nextcloud Talk - Android Client * Nextcloud Talk - Android Client
* *
* SPDX-FileCopyrightText: 2024 Your Name <your@email.com> * SPDX-FileCopyrightText: 2024 Julius Linus <juliuslinus1@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
@ -63,7 +63,8 @@ fun ConversationModel.asEntity() =
remoteToken = remoteToken, remoteToken = remoteToken,
hasArchived = hasArchived, hasArchived = hasArchived,
hasSensitive = hasSensitive, hasSensitive = hasSensitive,
hasImportant = hasImportant hasImportant = hasImportant,
messageDraft = messageDraft
) )
fun ConversationEntity.asModel() = fun ConversationEntity.asModel() =
@ -117,7 +118,8 @@ fun ConversationEntity.asModel() =
remoteToken = remoteToken, remoteToken = remoteToken,
hasArchived = hasArchived, hasArchived = hasArchived,
hasSensitive = hasSensitive, hasSensitive = hasSensitive,
hasImportant = hasImportant hasImportant = hasImportant,
messageDraft = messageDraft
) )
fun Conversation.asEntity(accountId: Long) = fun Conversation.asEntity(accountId: Long) =

View File

@ -13,6 +13,7 @@ import androidx.room.ForeignKey
import androidx.room.Index import androidx.room.Index
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import com.nextcloud.talk.data.user.model.UserEntity import com.nextcloud.talk.data.user.model.UserEntity
import com.nextcloud.talk.models.MessageDraft
import com.nextcloud.talk.models.json.conversations.ConversationEnums import com.nextcloud.talk.models.json.conversations.ConversationEnums
import com.nextcloud.talk.models.json.participants.Participant import com.nextcloud.talk.models.json.participants.Participant
@ -96,7 +97,8 @@ data class ConversationEntity(
@ColumnInfo(name = "unreadMessages") var unreadMessages: Int = 0, @ColumnInfo(name = "unreadMessages") var unreadMessages: Int = 0,
@ColumnInfo(name = "hasArchived") var hasArchived: Boolean = false, @ColumnInfo(name = "hasArchived") var hasArchived: Boolean = false,
@ColumnInfo(name = "hasSensitive") var hasSensitive: Boolean = false, @ColumnInfo(name = "hasSensitive") var hasSensitive: Boolean = false,
@ColumnInfo(name = "hasImportant") var hasImportant: Boolean = false @ColumnInfo(name = "hasImportant") var hasImportant: Boolean = false,
@ColumnInfo(name = "messageDraft") var messageDraft: MessageDraft? = MessageDraft()
// missing/not needed: attendeeId // missing/not needed: attendeeId
// missing/not needed: attendeePin // missing/not needed: attendeePin
// missing/not needed: attendeePermissions // missing/not needed: attendeePermissions

View File

@ -0,0 +1,57 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Julius Linus <juliuslinus1@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.models
import android.os.Parcelable
import androidx.room.TypeConverter
import com.bluelinelabs.logansquare.LoganSquare
import com.bluelinelabs.logansquare.annotation.JsonField
import com.bluelinelabs.logansquare.annotation.JsonObject
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
@Parcelize
@JsonObject
@Serializable
data class MessageDraft(
@JsonField(name = ["messageText"])
var messageText: String = "",
@JsonField(name = ["messageCursor"])
var messageCursor: Int = 0,
@JsonField(name = ["quotedJsonId"])
var quotedJsonId: Int? = null,
@JsonField(name = ["quotedDisplayName"])
var quotedDisplayName: String? = null,
@JsonField(name = ["quotedMessageText"])
var quotedMessageText: String? = null,
@JsonField(name = ["quoteImageUrl"])
var quotedImageUrl: String? = null
) : Parcelable {
constructor() : this("", 0, null, null, null, null)
}
class MessageDraftConverter {
@TypeConverter
fun fromMessageDraftToString(messageDraft: MessageDraft?): String {
return if (messageDraft == null) {
""
} else {
LoganSquare.serialize(messageDraft)
}
}
@TypeConverter
fun fromStringToMessageDraft(value: String): MessageDraft? {
return if (value.isBlank()) {
null
} else {
return LoganSquare.parse(value, MessageDraft::class.java)
}
}
}

View File

@ -8,6 +8,7 @@
package com.nextcloud.talk.models.domain package com.nextcloud.talk.models.domain
import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.models.MessageDraft
import com.nextcloud.talk.models.json.chat.ChatMessageJson import com.nextcloud.talk.models.json.chat.ChatMessageJson
import com.nextcloud.talk.models.json.conversations.Conversation import com.nextcloud.talk.models.json.conversations.Conversation
import com.nextcloud.talk.models.json.conversations.ConversationEnums import com.nextcloud.talk.models.json.conversations.ConversationEnums
@ -65,7 +66,8 @@ class ConversationModel(
var hasImportant: Boolean = false, var hasImportant: Boolean = false,
// attributes that don't come from API. This should be changed?! // attributes that don't come from API. This should be changed?!
var password: String? = null var password: String? = null,
var messageDraft: MessageDraft? = MessageDraft()
) { ) {
companion object { companion object {

View File

@ -194,6 +194,10 @@ class DummyConversationDaoImpl : ConversationsDao {
override fun upsertConversations(conversationEntities: List<ConversationEntity>) { /* */ } override fun upsertConversations(conversationEntities: List<ConversationEntity>) { /* */ }
override suspend fun insertOrUpdate(item: ConversationEntity) {
/**/
}
override fun deleteConversations(conversationIds: List<String>) { /* */ } override fun deleteConversations(conversationIds: List<String>) { /* */ }
override fun updateConversation(conversationEntity: ConversationEntity) { /* */ } override fun updateConversation(conversationEntity: ConversationEntity) { /* */ }