updated to 22.0.0

This commit is contained in:
Fr4nz D13trich 2025-10-04 10:41:35 +02:00
parent 93184d21d1
commit 356462d6ab
60 changed files with 1198 additions and 469 deletions

View file

@ -1,7 +1,7 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Your Name <your@email.com>
* SPDX-FileCopyrightText: 2025 Julius Linus <juliuslinus1@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.account

View file

@ -1,7 +1,7 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Your Name <your@email.com>
* SPDX-FileCopyrightText: 2025 Julius Linus <juliuslinus1@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/

View file

@ -277,7 +277,8 @@ interface NcApiCoroutines {
suspend fun getContextOfChatMessage(
@Header("Authorization") authorization: String,
@Url url: String,
@Query("limit") limit: Int
@Query("limit") limit: Int,
@Query("threadId") threadId: Int?
): ChatOverall
@GET

View file

@ -65,6 +65,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.ComposeView
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.content.PermissionChecker
import androidx.core.content.PermissionChecker.PERMISSION_GRANTED
@ -134,6 +135,8 @@ import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.chat.data.model.ChatMessage
import com.nextcloud.talk.chat.viewmodels.ChatViewModel
import com.nextcloud.talk.chat.viewmodels.MessageInputViewModel
import com.nextcloud.talk.contextchat.ContextChatView
import com.nextcloud.talk.contextchat.ContextChatViewModel
import com.nextcloud.talk.conversationinfo.ConversationInfoActivity
import com.nextcloud.talk.conversationinfo.viewmodel.ConversationInfoViewModel
import com.nextcloud.talk.conversationlist.ConversationsListActivity
@ -168,7 +171,6 @@ 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
@ -203,6 +205,7 @@ import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FILE_PATHS
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_IS_BREAKOUT_ROOM
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_IS_MODERATOR
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_OPENED_VIA_NOTIFICATION
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_RECORDING_STATE
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_START_CALL_AFTER_ROOM_SWITCH
@ -246,7 +249,6 @@ import java.util.Locale
import java.util.concurrent.ExecutionException
import javax.inject.Inject
import kotlin.math.roundToInt
import androidx.core.content.ContextCompat
@Suppress("TooManyFunctions")
@AutoInjector(NextcloudTalkApplication::class)
@ -287,6 +289,7 @@ class ChatActivity :
lateinit var chatViewModel: ChatViewModel
lateinit var conversationInfoViewModel: ConversationInfoViewModel
lateinit var contextChatViewModel: ContextChatViewModel
lateinit var messageInputViewModel: MessageInputViewModel
private var chatMenu: Menu? = null
@ -323,28 +326,27 @@ class ChatActivity :
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
executeIfResultOk(it) { intent ->
runBlocking {
val id = intent?.getStringExtra(MessageSearchActivity.RESULT_KEY_MESSAGE_ID)
id?.let {
startContextChatWindowForMessage(id)
val messageId = intent?.getStringExtra(MessageSearchActivity.RESULT_KEY_MESSAGE_ID)
val threadId = intent?.getStringExtra(MessageSearchActivity.RESULT_KEY_THREAD_ID)
messageId?.let {
startContextChatWindowForMessage(messageId, threadId)
}
}
}
}
private fun startContextChatWindowForMessage(id: String?) {
private fun startContextChatWindowForMessage(messageId: String?, threadId: String?) {
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(
KEY_CONVERSATION_NAME,
currentConversation!!.displayName
contextChatViewModel.getContextForChatMessages(
credentials = credentials!!,
baseUrl = conversationUser!!.baseUrl!!,
token = roomToken,
threadId = threadId,
messageId = messageId!!,
title = currentConversation!!.displayName
)
ContextChatCompose(bundle).GetDialogView(shouldDismiss, context)
ContextChatView(context, contextChatViewModel)
}
}
Log.d(TAG, "Should open something else")
@ -366,6 +368,7 @@ class ChatActivity :
var sessionIdAfterRoomJoined: String? = null
lateinit var roomToken: String
var conversationThreadId: Long? = null
var openedViaNotification: Boolean = false
var conversationThreadInfo: ThreadInfo? = null
var conversationUser: User? = null
lateinit var spreedCapabilities: SpreedCapability
@ -408,12 +411,11 @@ class ChatActivity :
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (isChatThread()) {
if (!openedViaNotification && isChatThread()) {
isEnabled = false
onBackPressedDispatcher.onBackPressed()
} else {
val intent = Intent(this@ChatActivity, ConversationsListActivity::class.java)
intent.putExtras(Bundle())
startActivity(intent)
}
}
@ -514,6 +516,8 @@ class ChatActivity :
conversationInfoViewModel = ViewModelProvider(this, viewModelFactory)[ConversationInfoViewModel::class.java]
contextChatViewModel = ViewModelProvider(this, viewModelFactory)[ContextChatViewModel::class.java]
val urlForChatting = ApiUtils.getUrlForChat(chatApiVersion, conversationUser?.baseUrl, roomToken)
val credentials = ApiUtils.getCredentials(conversationUser!!.username, conversationUser!!.token)
chatViewModel.initData(
@ -592,6 +596,8 @@ class ChatActivity :
null
}
openedViaNotification = extras?.getBoolean(KEY_OPENED_VIA_NOTIFICATION) ?: false
sharedText = extras?.getString(BundleKeys.KEY_SHARED_TEXT).orEmpty()
Log.d(TAG, " roomToken = $roomToken")
@ -4431,7 +4437,7 @@ class ChatActivity :
}
if (!foundMessage) {
Log.d(TAG, "quoted message with id " + parentMessage.id + " was not found in adapter")
startContextChatWindowForMessage(parentMessage.id)
startContextChatWindowForMessage(parentMessage.id, conversationThreadId.toString())
}
}

View file

@ -1,7 +1,7 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Your Name <your@email.com>
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/

View file

@ -74,7 +74,8 @@ interface ChatNetworkDataSource {
baseUrl: String,
token: String,
messageId: String,
limit: Int
limit: Int,
threadId: Int?
): List<ChatMessageJson>
suspend fun getOpenGraph(credentials: String, baseUrl: String, extractedLinkToPreview: String): Reference?
suspend fun unbindRoom(credentials: String, baseUrl: String, roomToken: String): GenericOverall

View file

@ -198,10 +198,11 @@ class RetrofitChatNetwork(private val ncApi: NcApi, private val ncApiCoroutines:
baseUrl: String,
token: String,
messageId: String,
limit: Int
limit: Int,
threadId: Int?
): List<ChatMessageJson> {
val url = ApiUtils.getUrlForChatMessageContext(baseUrl, token, messageId)
return ncApiCoroutines.getContextOfChatMessage(credentials, url, limit).ocs?.data ?: listOf()
return ncApiCoroutines.getContextOfChatMessage(credentials, url, limit, threadId).ocs?.data ?: listOf()
}
override suspend fun getOpenGraph(

View file

@ -35,7 +35,6 @@ import com.nextcloud.talk.models.domain.ConversationModel
import com.nextcloud.talk.models.domain.ReactionAddedModel
import com.nextcloud.talk.models.domain.ReactionDeletedModel
import com.nextcloud.talk.models.json.capabilities.SpreedCapability
import com.nextcloud.talk.models.json.chat.ChatMessageJson
import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage
import com.nextcloud.talk.models.json.conversations.RoomOverall
import com.nextcloud.talk.models.json.generic.GenericOverall
@ -171,10 +170,6 @@ class ChatViewModel @Inject constructor(
val voiceMessagePlaybackSpeedPreferences: LiveData<Map<String, PlaybackSpeed>>
get() = _voiceMessagePlaybackSpeedPreferences
private val _getContextChatMessages: MutableLiveData<List<ChatMessageJson>> = MutableLiveData()
val getContextChatMessages: LiveData<List<ChatMessageJson>>
get() = _getContextChatMessages
private val _threadRetrieveState = MutableStateFlow<ThreadRetrieveUiState>(ThreadRetrieveUiState.None)
val threadRetrieveState: StateFlow<ThreadRetrieveUiState> = _threadRetrieveState
@ -944,20 +939,6 @@ class ChatViewModel @Inject constructor(
}
}
fun getContextForChatMessages(credentials: String, baseUrl: String, token: String, messageId: String, limit: Int) {
viewModelScope.launch {
val messages = chatNetworkDataSource.getContextForChatMessage(
credentials,
baseUrl,
token,
messageId,
limit
)
_getContextChatMessages.value = messages
}
}
fun getOpenGraph(credentials: String, baseUrl: String, urlToPreview: String) {
viewModelScope.launch {
_getOpenGraph.value = chatNetworkDataSource.getOpenGraph(credentials, baseUrl, urlToPreview)

View file

@ -1,7 +1,7 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Your Name <your@email.com>
* SPDX-FileCopyrightText: 2024 Sowjanya Kota <sowjanya.kch@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/

View file

@ -0,0 +1,240 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Julius Linus <juliuslinus1@gmail.com>
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.contextchat
import android.content.Context
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.nextcloud.talk.R
import com.nextcloud.talk.data.database.mappers.asModel
import com.nextcloud.talk.models.json.chat.ChatMessageJson
import com.nextcloud.talk.ui.ComposeChatAdapter
import com.nextcloud.talk.utils.preview.ComposePreviewUtils
@Composable
fun ContextChatView(context: Context, contextViewModel: ContextChatViewModel) {
val contextChatMessagesState = contextViewModel.getContextChatMessagesState.collectAsState().value
when (contextChatMessagesState) {
ContextChatViewModel.ContextChatRetrieveUiState.None -> {}
is ContextChatViewModel.ContextChatRetrieveUiState.Success -> {
ContextChatSuccessView(
visible = true,
context = context,
contextChatRetrieveUiStateSuccess = contextChatMessagesState,
onDismiss = {
contextViewModel.clearContextChatState()
}
)
}
is ContextChatViewModel.ContextChatRetrieveUiState.Error -> {
ContextChatErrorView()
}
}
}
@Composable
fun ContextChatErrorView() {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
Icons.Filled.Info,
contentDescription = "Info Icon"
)
Text(
stringResource(R.string.nc_capabilities_failed)
)
}
}
@Composable
fun ContextChatSuccessView(
visible: Boolean,
context: Context,
contextChatRetrieveUiStateSuccess: ContextChatViewModel.ContextChatRetrieveUiState.Success,
onDismiss: () -> Unit
) {
val previewUtils = ComposePreviewUtils.getInstance(LocalContext.current)
val colorScheme = previewUtils.viewThemeUtils.getColorScheme(context)
if (visible) {
MaterialTheme(colorScheme) {
Dialog(
onDismissRequest = onDismiss,
properties = DialogProperties(
dismissOnBackPress = true,
dismissOnClickOutside = true,
usePlatformDefaultWidth = false
)
) {
Surface {
Column(
modifier = Modifier.Companion
.fillMaxWidth()
.fillMaxHeight()
.padding(top = 16.dp)
) {
Row(
modifier = Modifier.Companion.align(Alignment.Companion.Start),
verticalAlignment = Alignment.Companion.CenterVertically
) {
IconButton(onClick = onDismiss) {
Icon(
Icons.Filled.Close,
stringResource(R.string.close),
modifier = Modifier.Companion
.size(24.dp)
)
}
Column(verticalArrangement = Arrangement.Center) {
Text(contextChatRetrieveUiStateSuccess.title ?: "", fontSize = 18.sp)
if (!contextChatRetrieveUiStateSuccess.subTitle.isNullOrEmpty()) {
Text(contextChatRetrieveUiStateSuccess.subTitle, fontSize = 12.sp)
}
}
// This code was written back then but not needed yet, but it's not deleted yet
// because it may be used soon when further migrating to Compose...
// Spacer(modifier = Modifier.weight(1f))
// val cInt = context.resources.getColor(R.color.high_emphasis_text, null)
// Icon(
// painterResource(R.drawable.ic_call_black_24dp),
// "",
// tint = Color(cInt),
// modifier = Modifier
// .padding()
// .padding(end = 16.dp)
// .alpha(HALF_ALPHA)
// )
//
// Icon(
// painterResource(R.drawable.ic_baseline_videocam_24),
// "",
// tint = Color(cInt),
// modifier = Modifier
// .padding()
// .alpha(HALF_ALPHA)
// )
//
// ComposeChatMenu(colorScheme.background, false)
}
val messages = contextChatRetrieveUiStateSuccess.messages.map(ChatMessageJson::asModel)
val messageId = contextChatRetrieveUiStateSuccess.messageId
val threadId = contextChatRetrieveUiStateSuccess.threadId
val adapter = ComposeChatAdapter(
messagesJson = contextChatRetrieveUiStateSuccess.messages,
messageId = messageId,
threadId = threadId
)
SideEffect {
adapter.addMessages(messages.toMutableList(), true)
}
adapter.GetView()
}
}
}
}
}
}
// This code was written back then but not needed yet, but it's not deleted yet
// because it may be used soon when further migrating to Compose...
@Composable
private fun ComposeChatMenu(backgroundColor: Color, enabled: Boolean = true) {
var expanded by remember { mutableStateOf(false) }
Box(
modifier = Modifier.Companion.wrapContentSize(Alignment.Companion.TopStart)
) {
IconButton(onClick = { expanded = true }) {
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = "More options"
)
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
modifier = Modifier.Companion.background(backgroundColor)
) {
DropdownMenuItem(
text = { Text(stringResource(R.string.nc_search)) },
onClick = {
expanded = false
},
enabled = enabled
)
HorizontalDivider()
DropdownMenuItem(
text = { Text(stringResource(R.string.nc_conversation_menu_conversation_info)) },
onClick = {
expanded = false
},
enabled = enabled
)
HorizontalDivider()
DropdownMenuItem(
text = { Text(stringResource(R.string.nc_shared_items)) },
onClick = {
expanded = false
},
enabled = enabled
)
}
}
}

View file

@ -0,0 +1,109 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.contextchat
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import autodagger.AutoInjector
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.chat.data.network.ChatNetworkDataSource
import com.nextcloud.talk.chat.viewmodels.ChatViewModel
import com.nextcloud.talk.models.json.chat.ChatMessageJson
import com.nextcloud.talk.users.UserManager
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class ContextChatViewModel @Inject constructor(private val chatNetworkDataSource: ChatNetworkDataSource) :
ViewModel() {
@Inject
lateinit var chatViewModel: ChatViewModel
@Inject
lateinit var userManager: UserManager
var threadId: String? = null
private val _getContextChatMessagesState =
MutableStateFlow<ContextChatRetrieveUiState>(ContextChatRetrieveUiState.None)
val getContextChatMessagesState: StateFlow<ContextChatRetrieveUiState> = _getContextChatMessagesState
@Suppress("LongParameterList")
fun getContextForChatMessages(
credentials: String,
baseUrl: String,
token: String,
threadId: String?,
messageId: String,
title: String
) {
viewModelScope.launch {
val user = userManager.currentUser.blockingGet()
if (!user.hasSpreedFeatureCapability("chat-get-context") ||
!user.hasSpreedFeatureCapability("federation-v1")
) {
_getContextChatMessagesState.value = ContextChatRetrieveUiState.Error
}
var messages = chatNetworkDataSource.getContextForChatMessage(
credentials = credentials,
baseUrl = baseUrl,
token = token,
messageId = messageId,
limit = LIMIT,
threadId = threadId?.toInt()
)
if (threadId.isNullOrEmpty()) {
messages = messages.filter { !isThreadChildMessage(it) }
}
val subTitle = if (threadId?.isNotEmpty() == true) {
messages.firstOrNull()?.threadTitle
} else {
""
}
_getContextChatMessagesState.value = ContextChatRetrieveUiState.Success(
messageId = messageId,
threadId = threadId,
messages = messages,
title = title,
subTitle = subTitle
)
}
}
fun isThreadChildMessage(currentMessage: ChatMessageJson): Boolean =
currentMessage.hasThread &&
currentMessage.threadId != currentMessage.id
fun clearContextChatState() {
_getContextChatMessagesState.value = ContextChatRetrieveUiState.None
}
sealed class ContextChatRetrieveUiState {
data object None : ContextChatRetrieveUiState()
data class Success(
val messageId: String,
val threadId: String?,
val messages: List<ChatMessageJson>,
val title: String?,
val subTitle: String?
) : ContextChatRetrieveUiState()
data object Error : ContextChatRetrieveUiState()
}
companion object {
private const val LIMIT = 50
}
}

View file

@ -38,14 +38,12 @@ import androidx.activity.OnBackPressedCallback
import androidx.annotation.OptIn
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.SearchView
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.core.graphics.drawable.toDrawable
import androidx.core.net.toUri
import androidx.core.os.bundleOf
import androidx.core.view.MenuItemCompat
import androidx.core.view.isVisible
import androidx.fragment.app.DialogFragment
@ -115,7 +113,8 @@ import com.nextcloud.talk.threadsoverview.ThreadsOverviewActivity
import com.nextcloud.talk.ui.BackgroundVoiceMessageCard
import com.nextcloud.talk.ui.dialog.ChooseAccountDialogFragment
import com.nextcloud.talk.ui.dialog.ChooseAccountShareToDialogFragment
import com.nextcloud.talk.ui.dialog.ContextChatCompose
import com.nextcloud.talk.contextchat.ContextChatView
import com.nextcloud.talk.contextchat.ContextChatViewModel
import com.nextcloud.talk.ui.dialog.ConversationsListBottomDialog
import com.nextcloud.talk.ui.dialog.FilterConversationFragment
import com.nextcloud.talk.ui.dialog.FilterConversationFragment.Companion.ARCHIVE
@ -204,6 +203,7 @@ class ConversationsListActivity :
lateinit var contactsViewModel: ContactsViewModel
lateinit var conversationsListViewModel: ConversationsListViewModel
lateinit var contextChatViewModel: ContextChatViewModel
override val appBarLayoutType: AppBarLayoutType
get() = AppBarLayoutType.SEARCH_BAR
@ -263,6 +263,7 @@ class ConversationsListActivity :
currentUser = currentUserProvider.currentUser.blockingGet()
conversationsListViewModel = ViewModelProvider(this, viewModelFactory)[ConversationsListViewModel::class.java]
contextChatViewModel = ViewModelProvider(this, viewModelFactory)[ContextChatViewModel::class.java]
binding = ActivityConversationsBinding.inflate(layoutInflater)
setupActionBar()
@ -1533,15 +1534,16 @@ class ConversationsListActivity :
).model.displayName
binding.genericComposeView.apply {
val shouldDismiss = mutableStateOf(false)
setContent {
val bundle = bundleOf()
bundle.putString(BundleKeys.KEY_CREDENTIALS, credentials!!)
bundle.putString(BundleKeys.KEY_BASE_URL, currentUser!!.baseUrl)
bundle.putString(KEY_ROOM_TOKEN, token)
bundle.putString(BundleKeys.KEY_MESSAGE_ID, item.messageEntry.messageId)
bundle.putString(BundleKeys.KEY_CONVERSATION_NAME, conversationName)
ContextChatCompose(bundle).GetDialogView(shouldDismiss, context)
contextChatViewModel.getContextForChatMessages(
credentials = credentials!!,
baseUrl = currentUser!!.baseUrl!!,
token = token,
threadId = item.messageEntry.threadId,
messageId = item.messageEntry.messageId!!,
title = item.messageEntry.title
)
ContextChatView(context, contextChatViewModel)
}
}
}
@ -2244,7 +2246,7 @@ class ConversationsListActivity :
)
val bundle = Bundle()
bundle.putString(ThreadsOverviewActivity.KEY_APPBAR_TITLE, getString(R.string.followed_threads))
bundle.putString(ThreadsOverviewActivity.KEY_APPBAR_TITLE, getString(R.string.threads))
bundle.putString(ThreadsOverviewActivity.KEY_THREADS_SOURCE_URL, threadsUrl)
val threadsOverviewIntent = Intent(context, ThreadsOverviewActivity::class.java)
threadsOverviewIntent.putExtras(bundle)

View file

@ -219,16 +219,6 @@ public class RestModule {
httpClient.addInterceptor(new HeadersInterceptor());
List<ConnectionSpec> specs = new ArrayList<>();
if (BuildConfig.DEBUG) {
specs.add(ConnectionSpec.COMPATIBLE_TLS);
specs.add(ConnectionSpec.CLEARTEXT);
httpClient.connectionSpecs(specs);
} else {
specs.add(ConnectionSpec.COMPATIBLE_TLS);
httpClient.connectionSpecs(specs);
}
if (BuildConfig.DEBUG && !context.getResources().getBoolean(R.bool.nc_is_debug)) {
HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);

View file

@ -13,6 +13,7 @@ import com.nextcloud.talk.account.viewmodels.BrowserLoginActivityViewModel
import com.nextcloud.talk.chat.viewmodels.ChatViewModel
import com.nextcloud.talk.chat.viewmodels.MessageInputViewModel
import com.nextcloud.talk.contacts.ContactsViewModel
import com.nextcloud.talk.contextchat.ContextChatViewModel
import com.nextcloud.talk.conversationcreation.ConversationCreationViewModel
import com.nextcloud.talk.conversationinfo.viewmodel.ConversationInfoViewModel
import com.nextcloud.talk.conversationinfoedit.viewmodel.ConversationInfoEditViewModel
@ -166,4 +167,9 @@ abstract class ViewModelModule {
@IntoMap
@ViewModelKey(BrowserLoginActivityViewModel::class)
abstract fun browserLoginActivityViewModel(viewModel: BrowserLoginActivityViewModel): ViewModel
@Binds
@IntoMap
@ViewModelKey(ContextChatViewModel::class)
abstract fun contextChatViewModel(viewModel: ContextChatViewModel): ViewModel
}

View file

@ -85,6 +85,8 @@ import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_ONE_TO_ONE
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SHARE_RECORDING_TO_CHAT_URL
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SYSTEM_NOTIFICATION_ID
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_THREAD_ID
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_OPENED_VIA_NOTIFICATION
import com.nextcloud.talk.utils.preferences.AppPreferences
import io.reactivex.Observable
import io.reactivex.Observer
@ -397,6 +399,10 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
val ncNotification = notificationOverall.ocs!!.notification
if (ncNotification != null) {
enrichPushMessageByNcNotificationData(ncNotification)
val threadId = parseThreadId(ncNotification.objectId)
threadId?.let { intent.putExtra(KEY_THREAD_ID, it) }
showNotification(intent, ncNotification)
}
}
@ -827,6 +833,8 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
}
}
private fun parseThreadId(objectId: String?): Long? = objectId?.split("/")?.getOrNull(2)?.toLongOrNull()
private fun sendNotification(notificationId: Int, notification: Notification) {
Log.d(TAG, "show notification with id $notificationId")
if (ActivityCompat.checkSelfPermission(
@ -982,6 +990,7 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor
val bundle = Bundle()
bundle.putString(KEY_ROOM_TOKEN, pushMessage.id)
bundle.putLong(KEY_INTERNAL_USER_ID, signatureVerification.user!!.id!!)
bundle.putBoolean(KEY_OPENED_VIA_NOTIFICATION, true)
intent.putExtras(bundle)
return intent
}

View file

@ -166,6 +166,7 @@ class MessageSearchActivity : BaseActivity() {
if (state is MessageSearchViewModel.FinishedState) {
val resultIntent = Intent().apply {
putExtra(RESULT_KEY_MESSAGE_ID, state.selectedMessageId)
putExtra(RESULT_KEY_THREAD_ID, state.selectedThreadId)
}
setResult(Activity.RESULT_OK, resultIntent)
finish()
@ -244,5 +245,6 @@ class MessageSearchActivity : BaseActivity() {
companion object {
const val RESULT_KEY_MESSAGE_ID = "MessageSearchActivity.result.message"
const val RESULT_KEY_THREAD_ID = "MessageSearchActivity.result.thread"
}
}

View file

@ -42,7 +42,7 @@ class MessageSearchViewModel @Inject constructor(private val unifiedSearchReposi
object EmptyState : ViewState()
object ErrorState : ViewState()
class LoadedState(val results: List<SearchMessageEntry>, val hasMore: Boolean) : ViewState()
class FinishedState(val selectedMessageId: String) : ViewState()
class FinishedState(val selectedMessageId: String, val selectedThreadId: String?) : ViewState()
private lateinit var messageSearchHelper: MessageSearchHelper
@ -95,7 +95,7 @@ class MessageSearchViewModel @Inject constructor(private val unifiedSearchReposi
}
fun selectMessage(messageEntry: SearchMessageEntry) {
_state.value = FinishedState(messageEntry.messageId!!)
_state.value = FinishedState(messageEntry.messageId!!, messageEntry.threadId)
}
companion object {

View file

@ -13,5 +13,6 @@ data class SearchMessageEntry(
val title: String,
val messageExcerpt: String,
val conversationToken: String,
val threadId: String?,
val messageId: String?
)

View file

@ -1,7 +1,7 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Your Name <your@email.com>
* SPDX-FileCopyrightText: 2024 Marcel Hibbe <dev@mhibbe.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/

View file

@ -66,6 +66,7 @@ class UnifiedSearchRepositoryImpl(private val api: NcApi, private val userProvid
private const val ATTRIBUTE_CONVERSATION = "conversation"
private const val ATTRIBUTE_MESSAGE_ID = "messageId"
private const val ATTRIBUTE_THREAD_ID = "threadId"
private fun mapToMessageResults(
data: UnifiedSearchResponseData,
@ -81,13 +82,15 @@ class UnifiedSearchRepositoryImpl(private val api: NcApi, private val userProvid
private fun mapToMessage(unifiedSearchEntry: UnifiedSearchEntry, searchTerm: String): SearchMessageEntry {
val conversation = unifiedSearchEntry.attributes?.get(ATTRIBUTE_CONVERSATION)!!
val messageId = unifiedSearchEntry.attributes?.get(ATTRIBUTE_MESSAGE_ID)
val threadId = unifiedSearchEntry.attributes?.get(ATTRIBUTE_THREAD_ID)
return SearchMessageEntry(
searchTerm,
unifiedSearchEntry.thumbnailUrl,
unifiedSearchEntry.title!!,
unifiedSearchEntry.subline!!,
conversation,
messageId
searchTerm = searchTerm,
thumbnailURL = unifiedSearchEntry.thumbnailUrl,
title = unifiedSearchEntry.title!!,
messageExcerpt = unifiedSearchEntry.subline!!,
conversationToken = conversation,
threadId = threadId,
messageId = messageId
)
}
}

View file

@ -1,7 +1,7 @@
/*
* 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
*/

View file

@ -127,6 +127,7 @@ import kotlin.random.Random
class ComposeChatAdapter(
private var messagesJson: List<ChatMessageJson>? = null,
private var messageId: String? = null,
private var threadId: String? = null,
private val utils: ComposePreviewUtils? = null
) {
@ -195,6 +196,7 @@ class ComposeChatAdapter(
private const val ANIMATED_BLINK = 500
private const val FLOAT_06 = 0.6f
private const val HALF_OPACITY = 127
private const val MESSAGE_LENGTH_THRESHOLD = 25
}
private var incomingShape: RoundedCornerShape = RoundedCornerShape(2.dp, 20.dp, 20.dp, 20.dp)
@ -354,7 +356,8 @@ class ComposeChatAdapter(
this.isReaction() ||
this.isPollVotedMessage() ||
this.isEditMessage() ||
this.isInfoMessageAboutDeletion()
this.isInfoMessageAboutDeletion() ||
this.isThreadCreatedMessage()
private fun ChatMessage.isInfoMessageAboutDeletion(): Boolean =
this.parentMessageId != null &&
@ -366,6 +369,9 @@ class ComposeChatAdapter(
private fun ChatMessage.isEditMessage(): Boolean =
this.systemMessageType == ChatMessage.SystemMessageType.MESSAGE_EDITED
private fun ChatMessage.isThreadCreatedMessage(): Boolean =
this.systemMessageType == ChatMessage.SystemMessageType.THREAD_CREATED
private fun ChatMessage.isReaction(): Boolean =
systemMessageType == ChatMessage.SystemMessageType.REACTION ||
systemMessageType == ChatMessage.SystemMessageType.REACTION_DELETED ||
@ -429,16 +435,30 @@ class ComposeChatAdapter(
message: ChatMessage,
includePadding: Boolean = true,
playAnimation: Boolean = false,
content:
@Composable
(RowScope.() -> Unit)
content: @Composable () -> Unit
) {
fun shouldShowTimeNextToContent(message: ChatMessage): Boolean {
val containsLinebreak = message.message?.contains("\n") ?: false ||
message.message?.contains("\r") ?: false
return ((message.message?.length ?: 0) < MESSAGE_LENGTH_THRESHOLD) &&
!isFirstMessageOfThreadInNormalChat(message) &&
message.messageParameters.isNullOrEmpty() &&
!containsLinebreak
}
val incoming = message.actorId != currentUser.userId
val color = if (incoming) {
if (message.isDeleted) {
LocalContext.current.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 {
LocalContext.current.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) {
@ -449,11 +469,15 @@ class ComposeChatAdapter(
}
val shape = if (incoming) incomingShape else outgoingShape
val rowModifier = if (message.id == messageId && playAnimation) {
Modifier.withCustomAnimation(incoming)
} else {
Modifier
}
Row(
modifier = (
if (message.id == messageId && playAnimation) Modifier.withCustomAnimation(incoming) else Modifier
)
.fillMaxWidth(1f)
modifier = rowModifier.fillMaxWidth(),
horizontalArrangement = if (incoming) Arrangement.Start else Arrangement.End
) {
if (incoming) {
val imageUri = message.actorId?.let { viewModel.contactsViewModel.getImageUri(it, true) }
@ -465,11 +489,10 @@ class ComposeChatAdapter(
modifier = Modifier
.size(48.dp)
.align(Alignment.CenterVertically)
.padding()
.padding(end = 8.dp)
)
} else {
Spacer(Modifier.weight(1f))
Spacer(Modifier.width(8.dp))
}
Surface(
@ -480,38 +503,51 @@ class ComposeChatAdapter(
color = Color(color),
shape = shape
) {
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(16.dp, 4.dp, 16.dp, 4.dp)
} else {
Modifier
}
Column(modifier = modifier) {
if (message.parentMessageId != null && !message.isDeleted && messagesJson != null) {
if (messagesJson != null &&
message.parentMessageId != null &&
!message.isDeleted &&
message.parentMessageId.toString() != threadId
) {
messagesJson!!
.find { it.parentMessage?.id == message.parentMessageId }
?.parentMessage!!.asModel().let { CommonMessageQuote(LocalContext.current, it) }
?.parentMessage!!.asModel()
.let { CommonMessageQuote(LocalContext.current, it) }
}
if (incoming) {
Text(message.actorDisplayName.toString(), fontSize = AUTHOR_TEXT_SIZE)
}
Row {
ThreadTitle(message)
if (shouldShowTimeNextToContent(message)) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
content()
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(top = 6.dp, start = 8.dp)
) {
TimeDisplay(message)
ReadStatus(message)
}
}
} else {
content()
Spacer(modifier = Modifier.size(8.dp))
Text(
timeString,
fontSize = TIME_TEXT_SIZE,
textAlign = TextAlign.End,
modifier = Modifier.align(Alignment.CenterVertically)
)
if (message.readStatus == ReadStatus.NONE) {
val read = painterResource(R.drawable.ic_check_all)
Icon(
read,
"",
modifier = Modifier
.padding(start = 2.dp)
.size(12.dp)
.align(Alignment.CenterVertically)
)
Row(
modifier = Modifier.align(Alignment.End),
verticalAlignment = Alignment.CenterVertically
) {
TimeDisplay(message)
ReadStatus(message)
}
}
}
@ -519,6 +555,55 @@ class ComposeChatAdapter(
}
}
@Composable
private fun TimeDisplay(message: ChatMessage) {
val timeString = DateUtils(LocalContext.current)
.getLocalTimeStringFromTimestamp(message.timestamp)
Text(
timeString,
fontSize = TIME_TEXT_SIZE,
textAlign = TextAlign.Center
)
}
@Composable
private fun ReadStatus(message: ChatMessage) {
if (message.readStatus == ReadStatus.NONE) {
val read = painterResource(R.drawable.ic_check_all)
Icon(
read,
"",
modifier = Modifier
.padding(start = 4.dp)
.size(16.dp)
)
}
}
@Composable
private fun ThreadTitle(message: ChatMessage) {
if (isFirstMessageOfThreadInNormalChat(message)) {
Row {
val read = painterResource(R.drawable.outline_forum_24)
Icon(
read,
"",
modifier = Modifier
.padding(end = 6.dp)
.size(18.dp)
.align(Alignment.CenterVertically)
)
Text(
text = message.threadTitle ?: "",
fontSize = REGULAR_TEXT_SIZE,
fontWeight = FontWeight.SemiBold
)
}
}
}
fun isFirstMessageOfThreadInNormalChat(message: ChatMessage): Boolean = threadId == null && message.isThread
@Composable
private fun Modifier.withCustomAnimation(incoming: Boolean): Modifier {
val infiniteTransition = rememberInfiniteTransition()
@ -750,8 +835,8 @@ class ComposeChatAdapter(
read,
"",
modifier = Modifier
.padding(start = 2.dp)
.size(12.dp)
.padding(start = 4.dp)
.size(16.dp)
.align(Alignment.CenterVertically)
)
}
@ -762,29 +847,30 @@ class ComposeChatAdapter(
@Composable
private fun VoiceMessage(message: ChatMessage, state: MutableState<Boolean>) {
CommonMessageBody(message, playAnimation = state.value) {
Icon(
Icons.Filled.PlayArrow,
"play",
modifier = Modifier
.size(24.dp)
.align(Alignment.CenterVertically)
)
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
Icons.Filled.PlayArrow,
contentDescription = "play",
modifier = Modifier.size(24.dp)
)
AndroidView(
factory = { ctx ->
WaveformSeekBar(ctx).apply {
setWaveData(FloatArray(DEFAULT_WAVE_SIZE) { Random.nextFloat() }) // READ ONLY for now
setColors(
colorScheme.inversePrimary.toArgb(),
colorScheme.onPrimaryContainer.toArgb()
)
}
},
modifier = Modifier
.align(Alignment.CenterVertically)
.width(180.dp)
.height(80.dp)
)
AndroidView(
factory = { ctx ->
WaveformSeekBar(ctx).apply {
setWaveData(FloatArray(DEFAULT_WAVE_SIZE) { Random.nextFloat() }) // READ ONLY for now
setColors(
colorScheme.inversePrimary.toArgb(),
colorScheme.onPrimaryContainer.toArgb()
)
}
},
modifier = Modifier
.width(180.dp)
.height(80.dp)
)
}
}
}
@ -856,6 +942,7 @@ class ComposeChatAdapter(
message.extractedUrlToPreview!!
)
CommonMessageBody(message, playAnimation = state.value) {
EnrichedText(message)
Row(
modifier = Modifier
.drawWithCache {
@ -960,9 +1047,17 @@ class ComposeChatAdapter(
@Preview(showBackground = true, widthDp = 380, heightDp = 800)
@Composable
@Suppress("MagicNumber", "LongMethod")
fun AllMessageTypesPreview() {
val previewUtils = ComposePreviewUtils.getInstance(LocalContext.current)
val adapter = remember { ComposeChatAdapter(messagesJson = null, messageId = null, previewUtils) }
val adapter = remember {
ComposeChatAdapter(
messagesJson = null,
messageId = null,
threadId = null,
previewUtils
)
}
val sampleMessages = remember {
listOf(
@ -982,6 +1077,42 @@ fun AllMessageTypesPreview() {
timestamp = System.currentTimeMillis()
actorDisplayName = "User2"
messageType = ChatMessage.MessageType.REGULAR_TEXT_MESSAGE.name
},
ChatMessage().apply {
jsonMessageId = 3
actorId = "user1_id"
message = "This is a really really really really really really really really really long message"
timestamp = System.currentTimeMillis()
actorDisplayName = "User2"
messageType = ChatMessage.MessageType.REGULAR_TEXT_MESSAGE.name
},
ChatMessage().apply {
jsonMessageId = 4
actorId = "user1_id"
message = "some \n linebreak"
timestamp = System.currentTimeMillis()
actorDisplayName = "User2"
messageType = ChatMessage.MessageType.REGULAR_TEXT_MESSAGE.name
},
ChatMessage().apply {
jsonMessageId = 5
actorId = "user1_id"
threadTitle = "Thread title"
isThread = true
message = "Content of a first thread message"
timestamp = System.currentTimeMillis()
actorDisplayName = "User2"
messageType = ChatMessage.MessageType.REGULAR_TEXT_MESSAGE.name
},
ChatMessage().apply {
jsonMessageId = 6
actorId = "user1_id"
threadTitle = "looooooooooooong Thread title"
isThread = true
message = "Content"
timestamp = System.currentTimeMillis()
actorDisplayName = "User2"
messageType = ChatMessage.MessageType.REGULAR_TEXT_MESSAGE.name
}
)
}

View file

@ -1,262 +0,0 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Julius Linus <juliuslinus1@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
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
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asFlow
import autodagger.AutoInjector
import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.chat.viewmodels.ChatViewModel
import com.nextcloud.talk.data.database.mappers.asModel
import com.nextcloud.talk.models.json.chat.ChatMessageJson
import com.nextcloud.talk.ui.ComposeChatAdapter
import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.bundle.BundleKeys
import javax.inject.Inject
@Suppress("FunctionNaming", "LongMethod", "StaticFieldLeak")
class ContextChatCompose(val bundle: Bundle) {
companion object {
const val LIMIT = 50
const val HALF_ALPHA = 0.5f
}
@AutoInjector(NextcloudTalkApplication::class)
inner class ContextChatComposeViewModel : ViewModel() {
@Inject
lateinit var viewThemeUtils: ViewThemeUtils
@Inject
lateinit var chatViewModel: ChatViewModel
@Inject
lateinit var userManager: UserManager
init {
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
val credentials = bundle.getString(BundleKeys.KEY_CREDENTIALS)!!
val baseUrl = bundle.getString(BundleKeys.KEY_BASE_URL)!!
val token = bundle.getString(BundleKeys.KEY_ROOM_TOKEN)!!
val messageId = bundle.getString(BundleKeys.KEY_MESSAGE_ID)!!
chatViewModel.getContextForChatMessages(credentials, baseUrl, token, messageId, LIMIT)
}
}
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>,
context: Context,
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(
onDismissRequest = {
shouldDismiss.value = true
},
properties = DialogProperties(
dismissOnBackPress = true,
dismissOnClickOutside = true,
usePlatformDefaultWidth = false
)
) {
Surface {
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.padding(top = 16.dp)
) {
val user = contextViewModel.userManager.currentUser.blockingGet()
val shouldShow = !user.hasSpreedFeatureCapability("chat-get-context") ||
!user.hasSpreedFeatureCapability("federation-v1")
Row(
modifier = Modifier.align(Alignment.Start),
verticalAlignment = Alignment.CenterVertically
) {
IconButton(onClick = {
shouldDismiss.value = true
}) {
Icon(
Icons.Filled.Close,
stringResource(R.string.close),
modifier = Modifier
.size(24.dp)
)
}
Column(verticalArrangement = Arrangement.Center) {
val name = bundle.getString(BundleKeys.KEY_CONVERSATION_NAME)!!
Text(name, fontSize = 24.sp)
}
// Spacer(modifier = Modifier.weight(1f))
// val cInt = context.resources.getColor(R.color.high_emphasis_text, null)
// Icon(
// painterResource(R.drawable.ic_call_black_24dp),
// "",
// tint = Color(cInt),
// modifier = Modifier
// .padding()
// .padding(end = 16.dp)
// .alpha(HALF_ALPHA)
// )
//
// Icon(
// painterResource(R.drawable.ic_baseline_videocam_24),
// "",
// tint = Color(cInt),
// modifier = Modifier
// .padding()
// .alpha(HALF_ALPHA)
// )
//
// ComposeChatMenu(colorScheme.background, false)
}
if (shouldShow) {
Icon(
Icons.Filled.Info,
"Info Icon",
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Text(
stringResource(R.string.nc_capabilities_failed),
modifier = Modifier.align(Alignment.CenterHorizontally)
)
} else {
val contextState = contextViewModel
.chatViewModel
.getContextChatMessages
.asFlow()
.collectAsState(listOf())
val messagesJson = contextState.value
val messages = messagesJson.map(ChatMessageJson::asModel)
val messageId = bundle.getString(BundleKeys.KEY_MESSAGE_ID)!!
val adapter = ComposeChatAdapter(messagesJson, messageId)
SideEffect {
adapter.addMessages(messages.toMutableList(), true)
}
adapter.GetView()
}
}
}
}
}
}
@Composable
private fun ComposeChatMenu(backgroundColor: Color, enabled: Boolean = true) {
var expanded by remember { mutableStateOf(false) }
Box(
modifier = Modifier.wrapContentSize(Alignment.TopStart)
) {
IconButton(onClick = { expanded = true }) {
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = "More options"
)
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
modifier = Modifier.background(backgroundColor)
) {
DropdownMenuItem(
text = { Text(stringResource(R.string.nc_search)) },
onClick = {
expanded = false
},
enabled = enabled
)
HorizontalDivider()
DropdownMenuItem(
text = { Text(stringResource(R.string.nc_conversation_menu_conversation_info)) },
onClick = {
expanded = false
},
enabled = enabled
)
HorizontalDivider()
DropdownMenuItem(
text = { Text(stringResource(R.string.nc_shared_items)) },
onClick = {
expanded = false
},
enabled = enabled
)
}
}
}
}

View file

@ -84,4 +84,5 @@ object BundleKeys {
const val KEY_FOCUS_INPUT: String = "KEY_FOCUS_INPUT"
const val KEY_THREAD_ID = "KEY_THREAD_ID"
const val KEY_FROM_QR: String = "KEY_FROM_QR"
const val KEY_OPENED_VIA_NOTIFICATION: String = "KEY_OPENED_VIA_NOTIFICATION"
}