Follow up improvements

- Added ComposePreviewUtils
- Added ComposePreviewUtilsDao (both for previewing w/ dependencies)
- Additional fixes

Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>
This commit is contained in:
rapterjet2004 2025-04-21 14:31:59 -05:00 committed by Marcel Hibbe
parent fdfa58dcdd
commit fd7afccbc4
No known key found for this signature in database
GPG Key ID: C793F8B59F43CE7B
9 changed files with 604 additions and 59 deletions

View File

@ -33,7 +33,6 @@ import coil.decode.SvgDecoder
import coil.memory.MemoryCache import coil.memory.MemoryCache
import coil.util.DebugLogger import coil.util.DebugLogger
import com.nextcloud.talk.BuildConfig import com.nextcloud.talk.BuildConfig
import com.nextcloud.talk.filebrowser.webdav.DavUtils
import com.nextcloud.talk.dagger.modules.BusModule import com.nextcloud.talk.dagger.modules.BusModule
import com.nextcloud.talk.dagger.modules.ContextModule import com.nextcloud.talk.dagger.modules.ContextModule
import com.nextcloud.talk.dagger.modules.DaosModule import com.nextcloud.talk.dagger.modules.DaosModule
@ -43,6 +42,7 @@ import com.nextcloud.talk.dagger.modules.RepositoryModule
import com.nextcloud.talk.dagger.modules.RestModule import com.nextcloud.talk.dagger.modules.RestModule
import com.nextcloud.talk.dagger.modules.UtilsModule import com.nextcloud.talk.dagger.modules.UtilsModule
import com.nextcloud.talk.dagger.modules.ViewModelModule import com.nextcloud.talk.dagger.modules.ViewModelModule
import com.nextcloud.talk.filebrowser.webdav.DavUtils
import com.nextcloud.talk.jobs.AccountRemovalWorker import com.nextcloud.talk.jobs.AccountRemovalWorker
import com.nextcloud.talk.jobs.CapabilitiesWorker import com.nextcloud.talk.jobs.CapabilitiesWorker
import com.nextcloud.talk.jobs.SignalingSettingsWorker import com.nextcloud.talk.jobs.SignalingSettingsWorker

View File

@ -153,6 +153,7 @@ import com.nextcloud.talk.ui.PlaybackSpeed
import com.nextcloud.talk.ui.PlaybackSpeedControl import com.nextcloud.talk.ui.PlaybackSpeedControl
import com.nextcloud.talk.ui.StatusDrawable import com.nextcloud.talk.ui.StatusDrawable
import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet
import com.nextcloud.talk.ui.dialog.ContextChatCompose
import com.nextcloud.talk.ui.dialog.DateTimeCompose import com.nextcloud.talk.ui.dialog.DateTimeCompose
import com.nextcloud.talk.ui.dialog.FileAttachmentPreviewFragment import com.nextcloud.talk.ui.dialog.FileAttachmentPreviewFragment
import com.nextcloud.talk.ui.dialog.MessageActionsDialog import com.nextcloud.talk.ui.dialog.MessageActionsDialog
@ -208,6 +209,7 @@ import kotlinx.coroutines.Dispatchers
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 kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode import org.greenrobot.eventbus.ThreadMode
@ -296,7 +298,33 @@ class ChatActivity :
private val startMessageSearchForResult = private val startMessageSearchForResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
executeIfResultOk(it) { intent -> executeIfResultOk(it) { intent ->
onMessageSearchResult(intent) runBlocking {
val id = intent?.getStringExtra(MessageSearchActivity.RESULT_KEY_MESSAGE_ID)
id?.let {
val long = id.toLong()
val isSaved = chatViewModel.isMessageSaved(id.toLong())
if (isSaved) {
onMessageSearchResult(intent)
} else {
binding.genericComposeView.apply {
val shouldDismiss = mutableStateOf(false)
setContent {
val bundle = bundleOf()
bundle.putString(BundleKeys.KEY_CREDENTIALS, credentials!!)
bundle.putString(BundleKeys.KEY_BASE_URL, conversationUser!!.baseUrl)
bundle.putString(KEY_ROOM_TOKEN, roomToken)
bundle.putString(BundleKeys.KEY_MESSAGE_ID, id)
bundle.putString(
BundleKeys.KEY_CONVERSATION_NAME,
currentConversation!!.displayName
)
ContextChatCompose(bundle).GetDialogView(shouldDismiss, context)
}
}
Log.d("Julius", "Should open something else")
}
}
}
} }
} }

View File

@ -76,6 +76,8 @@ interface ChatMessageRepository : LifecycleAwareManager {
*/ */
suspend fun getMessage(messageId: Long, bundle: Bundle): Flow<ChatMessage> suspend fun getMessage(messageId: Long, bundle: Bundle): Flow<ChatMessage>
suspend fun checkIfMessageIsSaved(messageId: Long): Boolean
@Suppress("LongParameterList") @Suppress("LongParameterList")
suspend fun sendChatMessage( suspend fun sendChatMessage(
credentials: String, credentials: String,

View File

@ -475,6 +475,15 @@ class OfflineFirstChatRepository @Inject constructor(
.map(ChatMessageEntity::asModel) .map(ChatMessageEntity::asModel)
} }
override suspend fun checkIfMessageIsSaved(messageId: Long): Boolean {
try {
chatDao.getChatMessageForConversation(internalConversationId, messageId)
return true
} catch (_: Exception) {
return false
}
}
@Suppress("UNCHECKED_CAST", "MagicNumber", "Detekt.TooGenericExceptionCaught") @Suppress("UNCHECKED_CAST", "MagicNumber", "Detekt.TooGenericExceptionCaught")
private fun getMessagesFromServer(bundle: Bundle): Pair<Int, List<ChatMessageJson>>? { private fun getMessagesFromServer(bundle: Bundle): Pair<Int, List<ChatMessageJson>>? {
val fieldMap = bundle.getSerializable(BundleKeys.KEY_FIELD_MAP) as HashMap<String, Int> val fieldMap = bundle.getSerializable(BundleKeys.KEY_FIELD_MAP) as HashMap<String, Int>

View File

@ -281,6 +281,10 @@ class ChatViewModel @Inject constructor(
conversationRepository.getRoom(token) conversationRepository.getRoom(token)
} }
suspend fun isMessageSaved(messageId: Long): Boolean {
return chatRepository.checkIfMessageIsSaved(messageId)
}
fun getCapabilities(user: User, token: String, conversationModel: ConversationModel) { fun getCapabilities(user: User, token: String, conversationModel: ConversationModel) {
Log.d(TAG, "Remote server ${conversationModel.remoteServer}") Log.d(TAG, "Remote server ${conversationModel.remoteServer}")
if (conversationModel.remoteServer.isNullOrEmpty()) { if (conversationModel.remoteServer.isNullOrEmpty()) {

View File

@ -8,6 +8,7 @@
package com.nextcloud.talk.ui package com.nextcloud.talk.ui
import android.content.Context import android.content.Context
import android.content.ContextWrapper
import android.util.Log import android.util.Log
import android.view.View.TEXT_ALIGNMENT_VIEW_START import android.view.View.TEXT_ALIGNMENT_VIEW_START
import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.MATCH_PARENT
@ -23,6 +24,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@ -46,6 +48,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
@ -68,15 +71,18 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.graphics.ColorUtils import androidx.core.graphics.ColorUtils
import androidx.emoji2.widget.EmojiTextView
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.asFlow import androidx.lifecycle.asFlow
import autodagger.AutoInjector import autodagger.AutoInjector
@ -84,6 +90,8 @@ import coil.compose.AsyncImage
import com.elyeproj.loaderviewlibrary.LoaderImageView import com.elyeproj.loaderviewlibrary.LoaderImageView
import com.elyeproj.loaderviewlibrary.LoaderTextView import com.elyeproj.loaderviewlibrary.LoaderTextView
import com.nextcloud.talk.R import com.nextcloud.talk.R
import com.nextcloud.talk.activities.MainActivity
import com.nextcloud.talk.adapters.messages.PreviewMessageViewHolder.Companion.KEY_MIMETYPE
import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.chat.data.model.ChatMessage import com.nextcloud.talk.chat.data.model.ChatMessage
@ -99,7 +107,9 @@ import com.nextcloud.talk.models.json.opengraph.Reference
import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.users.UserManager import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.DateUtils import com.nextcloud.talk.utils.DateUtils
import com.nextcloud.talk.utils.DrawableUtils.getDrawableResourceIdForMimeType
import com.nextcloud.talk.utils.message.MessageUtils import com.nextcloud.talk.utils.message.MessageUtils
import com.nextcloud.talk.utils.preview.ComposePreviewUtils
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import org.osmdroid.config.Configuration import org.osmdroid.config.Configuration
@ -116,41 +126,54 @@ import kotlin.random.Random
@Suppress("FunctionNaming", "TooManyFunctions", "LongMethod", "StaticFieldLeak", "LargeClass") @Suppress("FunctionNaming", "TooManyFunctions", "LongMethod", "StaticFieldLeak", "LargeClass")
class ComposeChatAdapter( class ComposeChatAdapter(
private var messagesJson: List<ChatMessageJson>? = null, private var messagesJson: List<ChatMessageJson>? = null,
private var messageId: String? = null private var messageId: String? = null,
private val utils: ComposePreviewUtils? = null
) { ) {
interface PreviewAble {
val viewThemeUtils: ViewThemeUtils
val messageUtils: MessageUtils
val contactsViewModel: ContactsViewModel
val chatViewModel: ChatViewModel
val context: Context
val userManager: UserManager
}
@AutoInjector(NextcloudTalkApplication::class) @AutoInjector(NextcloudTalkApplication::class)
inner class ComposeChatAdapterViewModel : ViewModel() { inner class ComposeChatAdapterViewModel : ViewModel(), PreviewAble {
@Inject @Inject
lateinit var viewThemeUtils: ViewThemeUtils override lateinit var viewThemeUtils: ViewThemeUtils
@Inject @Inject
lateinit var messageUtils: MessageUtils override lateinit var messageUtils: MessageUtils
@Inject @Inject
lateinit var contactsViewModel: ContactsViewModel override lateinit var contactsViewModel: ContactsViewModel
@Inject @Inject
lateinit var chatViewModel: ChatViewModel override lateinit var chatViewModel: ChatViewModel
@Inject @Inject
lateinit var context: Context override lateinit var context: Context
@Inject @Inject
lateinit var userManager: UserManager override lateinit var userManager: UserManager
val items = mutableStateListOf<ChatMessage>()
init { init {
sharedApplication!!.componentApplication.inject(this) sharedApplication?.componentApplication?.inject(this)
} }
val currentUser: User = userManager.currentUser.blockingGet()
val colorScheme = viewThemeUtils.getColorScheme(context)
val highEmphasisColorInt = context.resources.getColor(R.color.high_emphasis_text, null)
} }
inner class ComposeChatAdapterPreviewViewModel(
override val viewThemeUtils: ViewThemeUtils,
override val messageUtils: MessageUtils,
override val contactsViewModel: ContactsViewModel,
override val chatViewModel: ChatViewModel,
override val context: Context,
override val userManager: UserManager
) : ViewModel(), PreviewAble
companion object { companion object {
val TAG: String = ComposeChatAdapter::class.java.simpleName val TAG: String = ComposeChatAdapter::class.java.simpleName
private val REGULAR_TEXT_SIZE = 16.sp private val REGULAR_TEXT_SIZE = 16.sp
@ -173,21 +196,48 @@ class ComposeChatAdapter(
private var incomingShape: RoundedCornerShape = RoundedCornerShape(2.dp, 20.dp, 20.dp, 20.dp) private var incomingShape: RoundedCornerShape = RoundedCornerShape(2.dp, 20.dp, 20.dp, 20.dp)
private var outgoingShape: RoundedCornerShape = RoundedCornerShape(20.dp, 2.dp, 20.dp, 20.dp) private var outgoingShape: RoundedCornerShape = RoundedCornerShape(20.dp, 2.dp, 20.dp, 20.dp)
private val viewModel = ComposeChatAdapterViewModel()
val viewModel: PreviewAble =
if (utils != null) {
ComposeChatAdapterPreviewViewModel(
utils.viewThemeUtils,
utils.messageUtils,
utils.contactsViewModel,
utils.chatViewModel,
utils.context,
utils.userManager
)
} else {
ComposeChatAdapterViewModel()
}
val items = mutableStateListOf<ChatMessage>()
val currentUser: User = viewModel.userManager.currentUser.blockingGet()
val colorScheme = viewModel.viewThemeUtils.getColorScheme(viewModel.context)
val highEmphasisColorInt = viewModel.context.resources.getColor(R.color.high_emphasis_text, null)
fun Context.findMainActivityOrNull(): MainActivity? {
var context = this
while (context is ContextWrapper) {
if (context is MainActivity) return context
context = context.baseContext
}
return null
}
fun addMessages(messages: MutableList<ChatMessage>, append: Boolean) { fun addMessages(messages: MutableList<ChatMessage>, append: Boolean) {
if (messages.isEmpty()) return if (messages.isEmpty()) return
val processedMessages = messages.toMutableList() val processedMessages = messages.toMutableList()
if (viewModel.items.isNotEmpty()) { if (items.isNotEmpty()) {
if (append) { if (append) {
processedMessages.add(viewModel.items.first()) processedMessages.add(items.first())
} else { } else {
processedMessages.add(viewModel.items.last()) processedMessages.add(items.last())
} }
} }
if (append) viewModel.items.addAll(processedMessages) else viewModel.items.addAll(0, processedMessages) if (append) items.addAll(processedMessages) else items.addAll(0, processedMessages)
} }
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@ -202,7 +252,7 @@ class ComposeChatAdapter(
modifier = Modifier.padding(16.dp) modifier = Modifier.padding(16.dp)
) { ) {
stickyHeader { stickyHeader {
if (viewModel.items.size == 0) { if (items.size == 0) {
Column( Column(
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
@ -211,11 +261,11 @@ class ComposeChatAdapter(
ShimmerGroup() ShimmerGroup()
} }
} else { } else {
val timestamp = viewModel.items[listState.firstVisibleItemIndex].timestamp val timestamp = items[listState.firstVisibleItemIndex].timestamp
val dateString = formatTime(timestamp * LONG_1000) val dateString = formatTime(timestamp * LONG_1000)
val color = Color(viewModel.highEmphasisColorInt) val color = Color(highEmphasisColorInt)
val backgroundColor = val backgroundColor =
viewModel.context.resources.getColor(R.color.bg_message_list_incoming_bubble, null) LocalContext.current.resources.getColor(R.color.bg_message_list_incoming_bubble, null)
Row( Row(
horizontalArrangement = Arrangement.Absolute.Center, horizontalArrangement = Arrangement.Absolute.Center,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
@ -229,8 +279,8 @@ class ComposeChatAdapter(
.padding(8.dp) .padding(8.dp)
.shadow( .shadow(
16.dp, 16.dp,
spotColor = viewModel.colorScheme.primary, spotColor = colorScheme.primary,
ambientColor = viewModel.colorScheme.primary ambientColor = colorScheme.primary
) )
.background(color = Color(backgroundColor), shape = RoundedCornerShape(8.dp)) .background(color = Color(backgroundColor), shape = RoundedCornerShape(8.dp))
.padding(8.dp) .padding(8.dp)
@ -240,8 +290,8 @@ class ComposeChatAdapter(
} }
} }
items(viewModel.items) { message -> items(items) { message ->
message.activeUser = viewModel.currentUser message.activeUser = currentUser
when (val type = message.getCalculateMessageType()) { when (val type = message.getCalculateMessageType()) {
ChatMessage.MessageType.SYSTEM_MESSAGE -> { ChatMessage.MessageType.SYSTEM_MESSAGE -> {
if (!message.shouldFilter()) { if (!message.shouldFilter()) {
@ -284,7 +334,7 @@ class ComposeChatAdapter(
} }
} }
if (messageId != null && viewModel.items.size > 0) { if (messageId != null && items.size > 0) {
LaunchedEffect(Dispatchers.Main) { LaunchedEffect(Dispatchers.Main) {
delay(SCROLL_DELAY) delay(SCROLL_DELAY)
val pos = searchMessages(messageId!!) val pos = searchMessages(messageId!!)
@ -326,7 +376,7 @@ class ComposeChatAdapter(
} }
private fun searchMessages(searchId: String): Int { private fun searchMessages(searchId: String): Int {
viewModel.items.forEachIndexed { index, message -> items.forEachIndexed { index, message ->
if (message.id == searchId) return index if (message.id == searchId) return index
} }
return -1 return -1
@ -380,18 +430,18 @@ class ComposeChatAdapter(
@Composable @Composable
(RowScope.() -> Unit) (RowScope.() -> Unit)
) { ) {
val incoming = message.actorId != viewModel.currentUser.userId val incoming = message.actorId != currentUser.userId
val color = if (incoming) { val color = if (incoming) {
if (message.isDeleted) { if (message.isDeleted) {
viewModel.context.resources.getColor(R.color.bg_message_list_incoming_bubble_deleted, null) LocalContext.current.resources.getColor(R.color.bg_message_list_incoming_bubble_deleted, null)
} else { } else {
viewModel.context.resources.getColor(R.color.bg_message_list_incoming_bubble, null) LocalContext.current.resources.getColor(R.color.bg_message_list_incoming_bubble, null)
} }
} else { } else {
if (message.isDeleted) { if (message.isDeleted) {
ColorUtils.setAlphaComponent(viewModel.colorScheme.surfaceVariant.toArgb(), HALF_OPACITY) ColorUtils.setAlphaComponent(colorScheme.surfaceVariant.toArgb(), HALF_OPACITY)
} else { } else {
viewModel.colorScheme.surfaceVariant.toArgb() colorScheme.surfaceVariant.toArgb()
} }
} }
val shape = if (incoming) incomingShape else outgoingShape val shape = if (incoming) incomingShape else outgoingShape
@ -405,7 +455,7 @@ class ComposeChatAdapter(
if (incoming) { if (incoming) {
val imageUri = message.actorId?.let { viewModel.contactsViewModel.getImageUri(it, true) } val imageUri = message.actorId?.let { viewModel.contactsViewModel.getImageUri(it, true) }
val errorPlaceholderImage: Int = R.drawable.account_circle_96dp val errorPlaceholderImage: Int = R.drawable.account_circle_96dp
val loadedImage = loadImage(imageUri, viewModel.context, errorPlaceholderImage) val loadedImage = loadImage(imageUri, LocalContext.current, errorPlaceholderImage)
AsyncImage( AsyncImage(
model = loadedImage, model = loadedImage,
contentDescription = stringResource(R.string.user_avatar), contentDescription = stringResource(R.string.user_avatar),
@ -427,13 +477,13 @@ class ComposeChatAdapter(
color = Color(color), color = Color(color),
shape = shape shape = shape
) { ) {
val timeString = DateUtils(viewModel.context).getLocalTimeStringFromTimestamp(message.timestamp) val timeString = DateUtils(LocalContext.current).getLocalTimeStringFromTimestamp(message.timestamp)
val modifier = if (includePadding) Modifier.padding(8.dp, 4.dp, 8.dp, 4.dp) else Modifier val modifier = if (includePadding) Modifier.padding(8.dp, 4.dp, 8.dp, 4.dp) else Modifier
Column(modifier = modifier) { Column(modifier = modifier) {
if (message.parentMessageId != null && !message.isDeleted && messagesJson != null) { if (message.parentMessageId != null && !message.isDeleted && messagesJson != null) {
messagesJson!! messagesJson!!
.find { it.parentMessage?.id == message.parentMessageId } .find { it.parentMessage?.id == message.parentMessageId }
?.parentMessage!!.asModel().let { CommonMessageQuote(viewModel.context, it) } ?.parentMessage!!.asModel().let { CommonMessageQuote(LocalContext.current, it) }
} }
if (incoming) { if (incoming) {
@ -470,8 +520,8 @@ class ComposeChatAdapter(
private fun Modifier.withCustomAnimation(incoming: Boolean): Modifier { private fun Modifier.withCustomAnimation(incoming: Boolean): Modifier {
val infiniteTransition = rememberInfiniteTransition() val infiniteTransition = rememberInfiniteTransition()
val borderColor by infiniteTransition.animateColor( val borderColor by infiniteTransition.animateColor(
initialValue = viewModel.colorScheme.primary, initialValue = colorScheme.primary,
targetValue = viewModel.colorScheme.background, targetValue = colorScheme.background,
animationSpec = infiniteRepeatable( animationSpec = infiniteRepeatable(
animation = tween(ANIMATED_BLINK, easing = LinearEasing), animation = tween(ANIMATED_BLINK, easing = LinearEasing),
repeatMode = RepeatMode.Reverse repeatMode = RepeatMode.Reverse
@ -542,7 +592,7 @@ class ComposeChatAdapter(
LoaderTextView(ctx).apply { LoaderTextView(ctx).apply {
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT) layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
val color = if (outgoing) { val color = if (outgoing) {
viewModel.colorScheme.primary.toArgb() colorScheme.primary.toArgb()
} else { } else {
resources.getColor(R.color.nc_shimmer_default_color, null) resources.getColor(R.color.nc_shimmer_default_color, null)
} }
@ -562,7 +612,7 @@ class ComposeChatAdapter(
@Composable @Composable
private fun EnrichedText(message: ChatMessage) { private fun EnrichedText(message: ChatMessage) {
AndroidView(factory = { ctx -> AndroidView(factory = { ctx ->
val incoming = message.actorId != viewModel.currentUser.userId val incoming = message.actorId != currentUser.userId
var processedMessageText = viewModel.messageUtils.enrichChatMessageText( var processedMessageText = viewModel.messageUtils.enrichChatMessageText(
ctx, ctx,
message, message,
@ -574,7 +624,7 @@ class ComposeChatAdapter(
ctx, viewModel.viewThemeUtils, processedMessageText!!, message, null ctx, viewModel.viewThemeUtils, processedMessageText!!, message, null
) )
androidx.emoji2.widget.EmojiTextView(ctx).apply { EmojiTextView(ctx).apply {
layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT) layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
setLineSpacing(0F, LINE_SPACING) setLineSpacing(0F, LINE_SPACING)
textAlignment = TEXT_ALIGNMENT_VIEW_START textAlignment = TEXT_ALIGNMENT_VIEW_START
@ -592,14 +642,14 @@ class ComposeChatAdapter(
} }
@Composable @Composable
private fun SystemMessage(message: ChatMessage) { fun SystemMessage(message: ChatMessage) {
val similarMessages = sharedApplication!!.resources.getQuantityString( val similarMessages = sharedApplication!!.resources.getQuantityString(
R.plurals.see_similar_system_messages, R.plurals.see_similar_system_messages,
message.expandableChildrenAmount, message.expandableChildrenAmount,
message.expandableChildrenAmount message.expandableChildrenAmount
) )
Column(horizontalAlignment = Alignment.CenterHorizontally) { Column(horizontalAlignment = Alignment.CenterHorizontally) {
val timeString = DateUtils(viewModel.context).getLocalTimeStringFromTimestamp(message.timestamp) val timeString = DateUtils(LocalContext.current).getLocalTimeStringFromTimestamp(message.timestamp)
Row(horizontalArrangement = Arrangement.Absolute.Center, verticalAlignment = Alignment.CenterVertically) { Row(horizontalArrangement = Arrangement.Absolute.Center, verticalAlignment = Alignment.CenterVertically) {
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
Text( Text(
@ -632,7 +682,7 @@ class ComposeChatAdapter(
Text( Text(
text, text,
fontSize = AUTHOR_TEXT_SIZE, fontSize = AUTHOR_TEXT_SIZE,
color = Color(viewModel.highEmphasisColorInt) color = Color(highEmphasisColorInt)
) )
} }
} }
@ -640,14 +690,15 @@ class ComposeChatAdapter(
@Composable @Composable
private fun ImageMessage(message: ChatMessage, state: MutableState<Boolean>) { private fun ImageMessage(message: ChatMessage, state: MutableState<Boolean>) {
val hasCaption = (message.message != "{file}") val hasCaption = (message.message != "{file}")
val incoming = message.actorId != viewModel.currentUser.userId val incoming = message.actorId != currentUser.userId
val timeString = DateUtils(viewModel.context).getLocalTimeStringFromTimestamp(message.timestamp) val timeString = DateUtils(LocalContext.current).getLocalTimeStringFromTimestamp(message.timestamp)
CommonMessageBody(message, includePadding = false, playAnimation = state.value) { CommonMessageBody(message, includePadding = false, playAnimation = state.value) {
Column { Column {
message.activeUser = viewModel.currentUser message.activeUser = currentUser
val imageUri = message.imageUrl val imageUri = message.imageUrl
val errorPlaceholderImage: Int = R.drawable.ic_mimetype_image val mimetype = message.selectedIndividualHashMap!![KEY_MIMETYPE]
val loadedImage = load(imageUri, viewModel.context, errorPlaceholderImage) val drawableResourceId = getDrawableResourceIdForMimeType(mimetype)
val loadedImage = load(imageUri, LocalContext.current, drawableResourceId)
AsyncImage( AsyncImage(
model = loadedImage, model = loadedImage,
@ -717,8 +768,8 @@ class ComposeChatAdapter(
WaveformSeekBar(ctx).apply { WaveformSeekBar(ctx).apply {
setWaveData(FloatArray(DEFAULT_WAVE_SIZE) { Random.nextFloat() }) // READ ONLY for now setWaveData(FloatArray(DEFAULT_WAVE_SIZE) { Random.nextFloat() }) // READ ONLY for now
setColors( setColors(
viewModel.colorScheme.inversePrimary.toArgb(), colorScheme.inversePrimary.toArgb(),
viewModel.colorScheme.onPrimaryContainer.toArgb() colorScheme.onPrimaryContainer.toArgb()
) )
} }
}, },
@ -793,8 +844,8 @@ class ComposeChatAdapter(
private fun LinkMessage(message: ChatMessage, state: MutableState<Boolean>) { private fun LinkMessage(message: ChatMessage, state: MutableState<Boolean>) {
val color = colorResource(R.color.high_emphasis_text) val color = colorResource(R.color.high_emphasis_text)
viewModel.chatViewModel.getOpenGraph( viewModel.chatViewModel.getOpenGraph(
viewModel.currentUser.getCredentials(), currentUser.getCredentials(),
viewModel.currentUser.baseUrl!!, currentUser.baseUrl!!,
message.extractedUrlToPreview!! message.extractedUrlToPreview!!
) )
CommonMessageBody(message, playAnimation = state.value) { CommonMessageBody(message, playAnimation = state.value) {
@ -828,7 +879,7 @@ class ComposeChatAdapter(
it.link?.let { Text(it, fontSize = TIME_TEXT_SIZE) } it.link?.let { Text(it, fontSize = TIME_TEXT_SIZE) }
it.thumb?.let { it.thumb?.let {
val errorPlaceholderImage: Int = R.drawable.ic_mimetype_image val errorPlaceholderImage: Int = R.drawable.ic_mimetype_image
val loadedImage = loadImage(it, viewModel.context, errorPlaceholderImage) val loadedImage = loadImage(it, LocalContext.current, errorPlaceholderImage)
AsyncImage( AsyncImage(
model = loadedImage, model = loadedImage,
contentDescription = stringResource(R.string.nc_sent_an_image), contentDescription = stringResource(R.string.nc_sent_an_image),
@ -882,7 +933,7 @@ class ComposeChatAdapter(
if (cardName?.isNotEmpty() == true) { if (cardName?.isNotEmpty() == true) {
val cardDescription = String.format( val cardDescription = String.format(
viewModel.context.resources.getString(R.string.deck_card_description), LocalContext.current.resources.getString(R.string.deck_card_description),
stackName, stackName,
boardName boardName
) )
@ -899,3 +950,44 @@ class ComposeChatAdapter(
} }
} }
} }
@Preview(showBackground = true, widthDp = 380, heightDp = 800)
@Composable
fun AllMessageTypesPreview() {
val previewUtils = ComposePreviewUtils.getInstance(LocalContext.current)
val adapter = remember { ComposeChatAdapter(messagesJson = null, messageId = null, previewUtils) }
val sampleMessages = remember {
listOf(
// Text Messages
ChatMessage().apply {
jsonMessageId = 1
actorId = "user1"
message = "I love Nextcloud"
timestamp = System.currentTimeMillis()
actorDisplayName = "User1"
messageType = ChatMessage.MessageType.REGULAR_TEXT_MESSAGE.name
},
ChatMessage().apply {
jsonMessageId = 2
actorId = "user1_id"
message = "I love Nextcloud"
timestamp = System.currentTimeMillis()
actorDisplayName = "User2"
messageType = ChatMessage.MessageType.REGULAR_TEXT_MESSAGE.name
}
)
}
LaunchedEffect(sampleMessages) { // Use LaunchedEffect or similar to update state once
if (adapter.items.isEmpty()) { // Prevent adding multiple times on recomposition
adapter.addMessages(sampleMessages.toMutableList(), append = false) // Add messages
}
}
MaterialTheme(colorScheme = adapter.colorScheme) { // Use the (potentially faked) color scheme
Box(modifier = Modifier.fillMaxSize()) { // Provide a container
adapter.GetView() // Call the main Composable
}
}
}

View File

@ -7,7 +7,10 @@
package com.nextcloud.talk.ui.dialog package com.nextcloud.talk.ui.dialog
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.ContextWrapper
import android.content.pm.ActivityInfo
import android.os.Bundle import android.os.Bundle
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@ -94,6 +97,15 @@ class ContextChatCompose(val bundle: Bundle) {
} }
} }
private fun Context.requireActivity(): Activity {
var context = this
while (context is ContextWrapper) {
if (context is Activity) return context
context = context.baseContext
}
throw IllegalStateException("No activity was present but it is required.")
}
@Composable @Composable
fun GetDialogView( fun GetDialogView(
shouldDismiss: MutableState<Boolean>, shouldDismiss: MutableState<Boolean>,
@ -101,9 +113,11 @@ class ContextChatCompose(val bundle: Bundle) {
contextViewModel: ContextChatComposeViewModel = ContextChatComposeViewModel() contextViewModel: ContextChatComposeViewModel = ContextChatComposeViewModel()
) { ) {
if (shouldDismiss.value) { if (shouldDismiss.value) {
context.requireActivity().requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
return return
} }
context.requireActivity().requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
val colorScheme = contextViewModel.viewThemeUtils.getColorScheme(context) val colorScheme = contextViewModel.viewThemeUtils.getColorScheme(context)
MaterialTheme(colorScheme) { MaterialTheme(colorScheme) {
Dialog( Dialog(

View File

@ -0,0 +1,188 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Your Name <your@email.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.utils.preview
import android.content.Context
import com.github.aurae.retrofit2.LoganSquareConverterFactory
import com.nextcloud.android.common.ui.color.ColorUtil
import com.nextcloud.android.common.ui.theme.MaterialSchemes
import com.nextcloud.android.common.ui.theme.utils.AndroidViewThemeUtils
import com.nextcloud.android.common.ui.theme.utils.AndroidXViewThemeUtils
import com.nextcloud.android.common.ui.theme.utils.DialogViewThemeUtils
import com.nextcloud.android.common.ui.theme.utils.MaterialViewThemeUtils
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.api.NcApiCoroutines
import com.nextcloud.talk.chat.data.ChatMessageRepository
import com.nextcloud.talk.chat.data.io.AudioFocusRequestManager
import com.nextcloud.talk.chat.data.io.MediaRecorderManager
import com.nextcloud.talk.chat.data.network.ChatNetworkDataSource
import com.nextcloud.talk.chat.data.network.OfflineFirstChatRepository
import com.nextcloud.talk.chat.data.network.RetrofitChatNetwork
import com.nextcloud.talk.chat.viewmodels.ChatViewModel
import com.nextcloud.talk.contacts.ContactsRepository
import com.nextcloud.talk.contacts.ContactsRepositoryImpl
import com.nextcloud.talk.contacts.ContactsViewModel
import com.nextcloud.talk.conversationlist.data.OfflineConversationsRepository
import com.nextcloud.talk.conversationlist.data.network.ConversationsNetworkDataSource
import com.nextcloud.talk.conversationlist.data.network.OfflineFirstConversationsRepository
import com.nextcloud.talk.conversationlist.data.network.RetrofitConversationsNetwork
import com.nextcloud.talk.data.database.dao.ChatBlocksDao
import com.nextcloud.talk.data.database.dao.ChatMessagesDao
import com.nextcloud.talk.data.database.dao.ConversationsDao
import com.nextcloud.talk.data.network.NetworkMonitor
import com.nextcloud.talk.data.network.NetworkMonitorImpl
import com.nextcloud.talk.data.user.UsersDao
import com.nextcloud.talk.data.user.UsersRepository
import com.nextcloud.talk.data.user.UsersRepositoryImpl
import com.nextcloud.talk.repositories.reactions.ReactionsRepository
import com.nextcloud.talk.repositories.reactions.ReactionsRepositoryImpl
import com.nextcloud.talk.ui.theme.MaterialSchemesProviderImpl
import com.nextcloud.talk.ui.theme.TalkSpecificViewThemeUtils
import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.database.user.CurrentUserProviderImpl
import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
import com.nextcloud.talk.utils.message.MessageUtils
import com.nextcloud.talk.utils.preferences.AppPreferences
import com.nextcloud.talk.utils.preferences.AppPreferencesImpl
import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
/**
* TODO - basically a reimplementation of common dependencies for use in Previewing Advanced Compose Views
* It's a hard coded Dependency Injector
*
*/
class ComposePreviewUtils private constructor(context: Context) {
private val mContext = context
companion object {
fun getInstance(context: Context) = ComposePreviewUtils(context)
val TAG: String = ComposePreviewUtils::class.java.simpleName
}
@OptIn(ExperimentalCoroutinesApi::class)
val appPreferences: AppPreferences
get() = AppPreferencesImpl(mContext)
val context: Context = mContext
val userRepository: UsersRepository
get() = UsersRepositoryImpl(usersDao)
val userManager: UserManager
get() = UserManager(userRepository)
val userProvider: CurrentUserProviderNew
get() = CurrentUserProviderImpl(userManager)
val colorUtil: ColorUtil
get() = ColorUtil(mContext)
val materialScheme: MaterialSchemes
get() = MaterialSchemesProviderImpl(userProvider, colorUtil).getMaterialSchemesForCurrentUser()
val viewThemeUtils: ViewThemeUtils
get() {
val android = AndroidViewThemeUtils(materialScheme, colorUtil)
val material = MaterialViewThemeUtils(materialScheme, colorUtil)
val androidx = AndroidXViewThemeUtils(materialScheme, android)
val talk = TalkSpecificViewThemeUtils(materialScheme, androidx)
val dialog = DialogViewThemeUtils(materialScheme)
return ViewThemeUtils(materialScheme, android, material, androidx, talk, dialog)
}
val messageUtils: MessageUtils
get() = MessageUtils(mContext)
val retrofit: Retrofit
get() {
val retrofitBuilder = Retrofit.Builder()
.client(OkHttpClient.Builder().build())
.baseUrl("https://nextcloud.com")
.addCallAdapterFactory(RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io()))
.addConverterFactory(LoganSquareConverterFactory.create())
return retrofitBuilder.build()
}
val ncApi: NcApi
get() = retrofit.create(NcApi::class.java)
val ncApiCoroutines: NcApiCoroutines
get() = retrofit.create(NcApiCoroutines::class.java)
val chatNetworkDataSource: ChatNetworkDataSource
get() = RetrofitChatNetwork(ncApi, ncApiCoroutines)
val usersDao: UsersDao
get() = DummyUserDaoImpl()
val chatMessagesDao: ChatMessagesDao
get() = DummyChatMessagesDaoImpl()
val chatBlocksDao: ChatBlocksDao
get() = DummyChatBlocksDaoImpl()
val conversationsDao: ConversationsDao
get() = DummyConversationDaoImpl()
val networkMonitor: NetworkMonitor
get() = NetworkMonitorImpl(mContext)
val chatRepository: ChatMessageRepository
get() = OfflineFirstChatRepository(
chatMessagesDao,
chatBlocksDao,
chatNetworkDataSource,
networkMonitor,
userProvider
)
val conversationNetworkDataSource: ConversationsNetworkDataSource
get() = RetrofitConversationsNetwork(ncApi)
val conversationRepository: OfflineConversationsRepository
get() = OfflineFirstConversationsRepository(
conversationsDao,
conversationNetworkDataSource,
chatNetworkDataSource,
networkMonitor,
userProvider
)
val reactionsRepository: ReactionsRepository
get() = ReactionsRepositoryImpl(ncApi, userProvider, chatMessagesDao)
val mediaRecorderManager: MediaRecorderManager
get() = MediaRecorderManager()
val audioFocusRequestManager: AudioFocusRequestManager
get() = AudioFocusRequestManager(mContext)
val chatViewModel: ChatViewModel
get() = ChatViewModel(
appPreferences,
chatNetworkDataSource,
chatRepository,
conversationRepository,
reactionsRepository,
mediaRecorderManager,
audioFocusRequestManager,
userProvider
)
val contactsRepository: ContactsRepository
get() = ContactsRepositoryImpl(ncApiCoroutines, userProvider)
val contactsViewModel: ContactsViewModel
get() = ContactsViewModel(contactsRepository)
}

View File

@ -0,0 +1,208 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Your Name <your@email.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.utils.preview
import com.nextcloud.talk.data.database.dao.ChatBlocksDao
import com.nextcloud.talk.data.database.dao.ChatMessagesDao
import com.nextcloud.talk.data.database.dao.ConversationsDao
import com.nextcloud.talk.data.database.model.ChatBlockEntity
import com.nextcloud.talk.data.database.model.ChatMessageEntity
import com.nextcloud.talk.data.database.model.ConversationEntity
import com.nextcloud.talk.data.user.UsersDao
import com.nextcloud.talk.data.user.model.UserEntity
import com.nextcloud.talk.models.json.push.PushConfigurationState
import io.reactivex.Maybe
import io.reactivex.Observable
import io.reactivex.Single
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
class DummyChatMessagesDaoImpl : ChatMessagesDao {
override fun getNewestMessageId(internalConversationId: String): Long = 0L
override fun getMessagesForConversation(internalConversationId: String): Flow<List<ChatMessageEntity>> = flowOf()
override fun getTempMessagesForConversation(internalConversationId: String): Flow<List<ChatMessageEntity>> =
flowOf()
override fun getTempMessageForConversation(
internalConversationId: String,
referenceId: String
): Flow<ChatMessageEntity> = flowOf()
override suspend fun upsertChatMessages(chatMessages: List<ChatMessageEntity>) { /* */ }
override suspend fun upsertChatMessage(chatMessage: ChatMessageEntity) { /* */ }
override fun getChatMessageForConversation(
internalConversationId: String,
messageId: Long
): Flow<ChatMessageEntity> = flowOf()
override fun deleteChatMessages(internalIds: List<String>) { /* */ }
override fun deleteTempChatMessages(internalConversationId: String, referenceIds: List<String>) { /* */ }
override fun updateChatMessage(message: ChatMessageEntity) { /* */ }
override fun getMessagesFromIds(messageIds: List<Long>): Flow<List<ChatMessageEntity>> = flowOf()
override fun getMessagesForConversationSince(
internalConversationId: String,
messageId: Long
): Flow<List<ChatMessageEntity>> = flowOf()
override fun getMessagesForConversationBefore(
internalConversationId: String,
messageId: Long,
limit: Int
): Flow<List<ChatMessageEntity>> = flowOf()
override fun getMessagesForConversationBeforeAndEqual(
internalConversationId: String,
messageId: Long,
limit: Int
): Flow<List<ChatMessageEntity>> = flowOf()
override fun getCountBetweenMessageIds(
internalConversationId: String,
oldestMessageId: Long,
newestMessageId: Long
): Int = 0
override fun clearAllMessagesForUser(pattern: String) { /* */ }
override fun deleteMessagesOlderThan(internalConversationId: String, messageId: Long) { /* */ }
}
class DummyUserDaoImpl : UsersDao() {
private val dummyUsers = mutableListOf(
UserEntity(1L, "user1_id", "user1", "server1", "1"),
UserEntity(2L, "user2_id", "user2", "server1", "2"),
UserEntity(0L, "user3_id", "user3", "server2", "3")
)
private var activeUserId: Long? = 1L
override fun getActiveUser(): Maybe<UserEntity> {
return Maybe.fromCallable { dummyUsers.find { it.id == activeUserId && !it.scheduledForDeletion } }
}
override fun getActiveUserObservable(): Observable<UserEntity> {
return Observable.fromCallable { dummyUsers.find { it.id == activeUserId && !it.scheduledForDeletion } }
}
override fun getActiveUserSynchronously(): UserEntity? {
return dummyUsers.find { it.id == activeUserId && !it.scheduledForDeletion }
}
override fun deleteUser(user: UserEntity): Int {
val initialSize = dummyUsers.size
dummyUsers.removeIf { it.id == user.id }
return initialSize - dummyUsers.size
}
override fun updateUser(user: UserEntity): Int {
val index = dummyUsers.indexOfFirst { it.id == user.id }
return if (index != -1) {
dummyUsers[index] = user
1
} else {
0
}
}
override fun saveUser(user: UserEntity): Long {
val newUser = user.copy(id = dummyUsers.size + 1L)
dummyUsers.add(newUser)
return newUser.id
}
override fun saveUsers(vararg users: UserEntity): List<Long> {
return users.map { saveUser(it) }
}
override fun getUsers(): Single<List<UserEntity>> {
return Single.just(dummyUsers.filter { !it.scheduledForDeletion })
}
override fun getUserWithId(id: Long): Maybe<UserEntity> {
return Maybe.fromCallable { dummyUsers.find { it.id == id } }
}
override fun getUserWithIdNotScheduledForDeletion(id: Long): Maybe<UserEntity> {
return Maybe.fromCallable { dummyUsers.find { it.id == id && !it.scheduledForDeletion } }
}
override fun getUserWithUserId(userId: String): Maybe<UserEntity> {
return Maybe.fromCallable { dummyUsers.find { it.userId == userId } }
}
override fun getUsersScheduledForDeletion(): Single<List<UserEntity>> {
return Single.just(dummyUsers.filter { it.scheduledForDeletion })
}
override fun getUsersNotScheduledForDeletion(): Single<List<UserEntity>> {
return Single.just(dummyUsers.filter { !it.scheduledForDeletion })
}
override fun getUserWithUsernameAndServer(username: String, server: String): Maybe<UserEntity> {
return Maybe.fromCallable { dummyUsers.find { it.username == username } }
}
override fun setUserAsActiveWithId(id: Long): Int {
activeUserId = id
return 1
}
override fun updatePushState(id: Long, state: PushConfigurationState): Single<Int> {
val index = dummyUsers.indexOfFirst { it.id == id }
return if (index != -1) {
dummyUsers[index] = dummyUsers[index]
Single.just(1)
} else {
Single.just(0)
}
}
}
class DummyConversationDaoImpl : ConversationsDao {
override fun getConversationsForUser(accountId: Long): Flow<List<ConversationEntity>> = flowOf()
override fun getConversationForUser(accountId: Long, token: String): Flow<ConversationEntity?> = flowOf()
override fun upsertConversations(conversationEntities: List<ConversationEntity>) { /* */ }
override fun deleteConversations(conversationIds: List<String>) { /* */ }
override fun updateConversation(conversationEntity: ConversationEntity) { /* */ }
override fun clearAllConversationsForUser(accountId: Long) { /* */ }
}
class DummyChatBlocksDaoImpl : ChatBlocksDao {
override fun deleteChatBlocks(blocks: List<ChatBlockEntity>) { /* */ }
override fun getChatBlocks(internalConversationId: String): Flow<List<ChatBlockEntity>> = flowOf()
override fun getChatBlocksContainingMessageId(
internalConversationId: String,
messageId: Long
): Flow<List<ChatBlockEntity?>> = flowOf()
override fun getConnectedChatBlocks(
internalConversationId: String,
oldestMessageId: Long,
newestMessageId: Long
): Flow<List<ChatBlockEntity>> = flowOf()
override suspend fun upsertChatBlock(chatBlock: ChatBlockEntity) { /* */ }
override fun clearChatBlocksForUser(pattern: String) { /* */ }
override fun deleteChatBlocksOlderThan(internalConversationId: String, messageId: Long) { /* */ }
}