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.util.DebugLogger
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.ContextModule
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.UtilsModule
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.CapabilitiesWorker
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.StatusDrawable
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.FileAttachmentPreviewFragment
import com.nextcloud.talk.ui.dialog.MessageActionsDialog
@ -208,6 +209,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
@ -296,7 +298,33 @@ class ChatActivity :
private val startMessageSearchForResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
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 checkIfMessageIsSaved(messageId: Long): Boolean
@Suppress("LongParameterList")
suspend fun sendChatMessage(
credentials: String,

View File

@ -475,6 +475,15 @@ class OfflineFirstChatRepository @Inject constructor(
.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")
private fun getMessagesFromServer(bundle: Bundle): Pair<Int, List<ChatMessageJson>>? {
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)
}
suspend fun isMessageSaved(messageId: Long): Boolean {
return chatRepository.checkIfMessageIsSaved(messageId)
}
fun getCapabilities(user: User, token: String, conversationModel: ConversationModel) {
Log.d(TAG, "Remote server ${conversationModel.remoteServer}")
if (conversationModel.remoteServer.isNullOrEmpty()) {

View File

@ -8,6 +8,7 @@
package com.nextcloud.talk.ui
import android.content.Context
import android.content.ContextWrapper
import android.util.Log
import android.view.View.TEXT_ALIGNMENT_VIEW_START
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.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
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.filled.PlayArrow
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
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.toArgb
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
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.sp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.graphics.ColorUtils
import androidx.emoji2.widget.EmojiTextView
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asFlow
import autodagger.AutoInjector
@ -84,6 +90,8 @@ import coil.compose.AsyncImage
import com.elyeproj.loaderviewlibrary.LoaderImageView
import com.elyeproj.loaderviewlibrary.LoaderTextView
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.Companion.sharedApplication
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.users.UserManager
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.preview.ComposePreviewUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import org.osmdroid.config.Configuration
@ -116,41 +126,54 @@ import kotlin.random.Random
@Suppress("FunctionNaming", "TooManyFunctions", "LongMethod", "StaticFieldLeak", "LargeClass")
class ComposeChatAdapter(
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)
inner class ComposeChatAdapterViewModel : ViewModel() {
inner class ComposeChatAdapterViewModel : ViewModel(), PreviewAble {
@Inject
lateinit var viewThemeUtils: ViewThemeUtils
override lateinit var viewThemeUtils: ViewThemeUtils
@Inject
lateinit var messageUtils: MessageUtils
override lateinit var messageUtils: MessageUtils
@Inject
lateinit var contactsViewModel: ContactsViewModel
override lateinit var contactsViewModel: ContactsViewModel
@Inject
lateinit var chatViewModel: ChatViewModel
override lateinit var chatViewModel: ChatViewModel
@Inject
lateinit var context: Context
override lateinit var context: Context
@Inject
lateinit var userManager: UserManager
val items = mutableStateListOf<ChatMessage>()
override lateinit var userManager: UserManager
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 {
val TAG: String = ComposeChatAdapter::class.java.simpleName
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 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) {
if (messages.isEmpty()) return
val processedMessages = messages.toMutableList()
if (viewModel.items.isNotEmpty()) {
if (items.isNotEmpty()) {
if (append) {
processedMessages.add(viewModel.items.first())
processedMessages.add(items.first())
} 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)
@ -202,7 +252,7 @@ class ComposeChatAdapter(
modifier = Modifier.padding(16.dp)
) {
stickyHeader {
if (viewModel.items.size == 0) {
if (items.size == 0) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
@ -211,11 +261,11 @@ class ComposeChatAdapter(
ShimmerGroup()
}
} else {
val timestamp = viewModel.items[listState.firstVisibleItemIndex].timestamp
val timestamp = items[listState.firstVisibleItemIndex].timestamp
val dateString = formatTime(timestamp * LONG_1000)
val color = Color(viewModel.highEmphasisColorInt)
val color = Color(highEmphasisColorInt)
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(
horizontalArrangement = Arrangement.Absolute.Center,
verticalAlignment = Alignment.CenterVertically
@ -229,8 +279,8 @@ class ComposeChatAdapter(
.padding(8.dp)
.shadow(
16.dp,
spotColor = viewModel.colorScheme.primary,
ambientColor = viewModel.colorScheme.primary
spotColor = colorScheme.primary,
ambientColor = colorScheme.primary
)
.background(color = Color(backgroundColor), shape = RoundedCornerShape(8.dp))
.padding(8.dp)
@ -240,8 +290,8 @@ class ComposeChatAdapter(
}
}
items(viewModel.items) { message ->
message.activeUser = viewModel.currentUser
items(items) { message ->
message.activeUser = currentUser
when (val type = message.getCalculateMessageType()) {
ChatMessage.MessageType.SYSTEM_MESSAGE -> {
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) {
delay(SCROLL_DELAY)
val pos = searchMessages(messageId!!)
@ -326,7 +376,7 @@ class ComposeChatAdapter(
}
private fun searchMessages(searchId: String): Int {
viewModel.items.forEachIndexed { index, message ->
items.forEachIndexed { index, message ->
if (message.id == searchId) return index
}
return -1
@ -380,18 +430,18 @@ class ComposeChatAdapter(
@Composable
(RowScope.() -> Unit)
) {
val incoming = message.actorId != viewModel.currentUser.userId
val incoming = message.actorId != currentUser.userId
val color = if (incoming) {
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 {
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 {
if (message.isDeleted) {
ColorUtils.setAlphaComponent(viewModel.colorScheme.surfaceVariant.toArgb(), HALF_OPACITY)
ColorUtils.setAlphaComponent(colorScheme.surfaceVariant.toArgb(), HALF_OPACITY)
} else {
viewModel.colorScheme.surfaceVariant.toArgb()
colorScheme.surfaceVariant.toArgb()
}
}
val shape = if (incoming) incomingShape else outgoingShape
@ -405,7 +455,7 @@ class ComposeChatAdapter(
if (incoming) {
val imageUri = message.actorId?.let { viewModel.contactsViewModel.getImageUri(it, true) }
val errorPlaceholderImage: Int = R.drawable.account_circle_96dp
val loadedImage = loadImage(imageUri, viewModel.context, errorPlaceholderImage)
val loadedImage = loadImage(imageUri, LocalContext.current, errorPlaceholderImage)
AsyncImage(
model = loadedImage,
contentDescription = stringResource(R.string.user_avatar),
@ -427,13 +477,13 @@ class ComposeChatAdapter(
color = Color(color),
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
Column(modifier = modifier) {
if (message.parentMessageId != null && !message.isDeleted && messagesJson != null) {
messagesJson!!
.find { it.parentMessage?.id == message.parentMessageId }
?.parentMessage!!.asModel().let { CommonMessageQuote(viewModel.context, it) }
?.parentMessage!!.asModel().let { CommonMessageQuote(LocalContext.current, it) }
}
if (incoming) {
@ -470,8 +520,8 @@ class ComposeChatAdapter(
private fun Modifier.withCustomAnimation(incoming: Boolean): Modifier {
val infiniteTransition = rememberInfiniteTransition()
val borderColor by infiniteTransition.animateColor(
initialValue = viewModel.colorScheme.primary,
targetValue = viewModel.colorScheme.background,
initialValue = colorScheme.primary,
targetValue = colorScheme.background,
animationSpec = infiniteRepeatable(
animation = tween(ANIMATED_BLINK, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
@ -542,7 +592,7 @@ class ComposeChatAdapter(
LoaderTextView(ctx).apply {
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
val color = if (outgoing) {
viewModel.colorScheme.primary.toArgb()
colorScheme.primary.toArgb()
} else {
resources.getColor(R.color.nc_shimmer_default_color, null)
}
@ -562,7 +612,7 @@ class ComposeChatAdapter(
@Composable
private fun EnrichedText(message: ChatMessage) {
AndroidView(factory = { ctx ->
val incoming = message.actorId != viewModel.currentUser.userId
val incoming = message.actorId != currentUser.userId
var processedMessageText = viewModel.messageUtils.enrichChatMessageText(
ctx,
message,
@ -574,7 +624,7 @@ class ComposeChatAdapter(
ctx, viewModel.viewThemeUtils, processedMessageText!!, message, null
)
androidx.emoji2.widget.EmojiTextView(ctx).apply {
EmojiTextView(ctx).apply {
layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
setLineSpacing(0F, LINE_SPACING)
textAlignment = TEXT_ALIGNMENT_VIEW_START
@ -592,14 +642,14 @@ class ComposeChatAdapter(
}
@Composable
private fun SystemMessage(message: ChatMessage) {
fun SystemMessage(message: ChatMessage) {
val similarMessages = sharedApplication!!.resources.getQuantityString(
R.plurals.see_similar_system_messages,
message.expandableChildrenAmount,
message.expandableChildrenAmount
)
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) {
Spacer(modifier = Modifier.weight(1f))
Text(
@ -632,7 +682,7 @@ class ComposeChatAdapter(
Text(
text,
fontSize = AUTHOR_TEXT_SIZE,
color = Color(viewModel.highEmphasisColorInt)
color = Color(highEmphasisColorInt)
)
}
}
@ -640,14 +690,15 @@ class ComposeChatAdapter(
@Composable
private fun ImageMessage(message: ChatMessage, state: MutableState<Boolean>) {
val hasCaption = (message.message != "{file}")
val incoming = message.actorId != viewModel.currentUser.userId
val timeString = DateUtils(viewModel.context).getLocalTimeStringFromTimestamp(message.timestamp)
val incoming = message.actorId != currentUser.userId
val timeString = DateUtils(LocalContext.current).getLocalTimeStringFromTimestamp(message.timestamp)
CommonMessageBody(message, includePadding = false, playAnimation = state.value) {
Column {
message.activeUser = viewModel.currentUser
message.activeUser = currentUser
val imageUri = message.imageUrl
val errorPlaceholderImage: Int = R.drawable.ic_mimetype_image
val loadedImage = load(imageUri, viewModel.context, errorPlaceholderImage)
val mimetype = message.selectedIndividualHashMap!![KEY_MIMETYPE]
val drawableResourceId = getDrawableResourceIdForMimeType(mimetype)
val loadedImage = load(imageUri, LocalContext.current, drawableResourceId)
AsyncImage(
model = loadedImage,
@ -717,8 +768,8 @@ class ComposeChatAdapter(
WaveformSeekBar(ctx).apply {
setWaveData(FloatArray(DEFAULT_WAVE_SIZE) { Random.nextFloat() }) // READ ONLY for now
setColors(
viewModel.colorScheme.inversePrimary.toArgb(),
viewModel.colorScheme.onPrimaryContainer.toArgb()
colorScheme.inversePrimary.toArgb(),
colorScheme.onPrimaryContainer.toArgb()
)
}
},
@ -793,8 +844,8 @@ class ComposeChatAdapter(
private fun LinkMessage(message: ChatMessage, state: MutableState<Boolean>) {
val color = colorResource(R.color.high_emphasis_text)
viewModel.chatViewModel.getOpenGraph(
viewModel.currentUser.getCredentials(),
viewModel.currentUser.baseUrl!!,
currentUser.getCredentials(),
currentUser.baseUrl!!,
message.extractedUrlToPreview!!
)
CommonMessageBody(message, playAnimation = state.value) {
@ -828,7 +879,7 @@ class ComposeChatAdapter(
it.link?.let { Text(it, fontSize = TIME_TEXT_SIZE) }
it.thumb?.let {
val errorPlaceholderImage: Int = R.drawable.ic_mimetype_image
val loadedImage = loadImage(it, viewModel.context, errorPlaceholderImage)
val loadedImage = loadImage(it, LocalContext.current, errorPlaceholderImage)
AsyncImage(
model = loadedImage,
contentDescription = stringResource(R.string.nc_sent_an_image),
@ -882,7 +933,7 @@ class ComposeChatAdapter(
if (cardName?.isNotEmpty() == true) {
val cardDescription = String.format(
viewModel.context.resources.getString(R.string.deck_card_description),
LocalContext.current.resources.getString(R.string.deck_card_description),
stackName,
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
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.content.pm.ActivityInfo
import android.os.Bundle
import androidx.compose.foundation.background
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
fun GetDialogView(
shouldDismiss: MutableState<Boolean>,
@ -101,9 +113,11 @@ class ContextChatCompose(val bundle: Bundle) {
contextViewModel: ContextChatComposeViewModel = ContextChatComposeViewModel()
) {
if (shouldDismiss.value) {
context.requireActivity().requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
return
}
context.requireActivity().requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
val colorScheme = contextViewModel.viewThemeUtils.getColorScheme(context)
MaterialTheme(colorScheme) {
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) { /* */ }
}