diff --git a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt index 62fc0823f4..305a4f8b22 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -232,7 +232,11 @@ import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -652,55 +656,54 @@ class ChatActivity : this.lifecycle.removeObserver(chatViewModel) } + @OptIn(ExperimentalCoroutinesApi::class) @SuppressLint("NotifyDataSetChanged", "SetTextI18n", "ResourceAsColor") @Suppress("LongMethod") private fun initObservers() { Log.d(TAG, "initObservers Called") - - this.lifecycleScope.launch { + lifecycleScope.launch { chatViewModel.getConversationFlow - .collect { conversationModel -> + .onEach { conversationModel -> currentConversation = conversationModel - chatViewModel.updateConversation( - currentConversation!! - ) - + chatViewModel.updateConversation(conversationModel) logConversationInfos("GetRoomSuccessState") if (adapter == null) { initAdapter() binding.messagesListView.setAdapter(adapter) - layoutManager = binding.messagesListView.layoutManager as LinearLayoutManager? + layoutManager = binding.messagesListView.layoutManager as? LinearLayoutManager } - chatViewModel.getCapabilities(conversationUser!!, roomToken, currentConversation!!) - + chatViewModel.getCapabilities(conversationUser!!, roomToken, conversationModel) + } + .flatMapLatest { conversationModel -> if (conversationModel.lastPinnedId != null && conversationModel.lastPinnedId != 0L && conversationModel.lastPinnedId != conversationModel.hiddenPinnedId ) { - chatViewModel - .getIndividualMessageFromServer( - credentials!!, - conversationUser?.baseUrl!!, - roomToken, - conversationModel.lastPinnedId.toString() + chatViewModel.getIndividualMessageFromServer( + credentials!!, + conversationUser?.baseUrl!!, + roomToken, + conversationModel.lastPinnedId.toString() + ) + } else { + flowOf(null) + } + } + .collectLatest { message -> + if (message != null) { + binding.pinnedMessageContainer.visibility = View.VISIBLE + binding.pinnedMessageComposeView.setContent { + PinnedMessageView( + message, + viewThemeUtils, + currentConversation, + scrollToMessageWithIdWithOffset = ::scrollToMessageWithIdWithOffset, + hidePinnedMessage = ::hidePinnedMessage, + unPinMessage = ::unPinMessage ) - .collect { message -> - message?.let { - binding.pinnedMessageContainer.visibility = View.VISIBLE - binding.pinnedMessageComposeView.setContent { - PinnedMessageView( - message, - viewThemeUtils, - currentConversation, - scrollToMessageWithIdWithOffset = ::scrollToMessageWithIdWithOffset, - hidePinnedMessage = ::hidePinnedMessage, - unPinMessage = ::unPinMessage - ) - } - } - } + } } else { binding.pinnedMessageContainer.visibility = View.GONE } diff --git a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt index 32fa780281..41c0dd1b57 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt @@ -55,6 +55,7 @@ import io.reactivex.Observer import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -62,6 +63,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import java.io.File @@ -887,7 +889,7 @@ class ChatViewModel @Inject constructor( } else { emit(null) } - } + }.flowOn(Dispatchers.IO) suspend fun getNumberOfThreadReplies(threadId: Long): Int = chatRepository.getNumberOfThreadReplies(threadId) diff --git a/app/src/main/java/com/nextcloud/talk/ui/PinnedMessage.kt b/app/src/main/java/com/nextcloud/talk/ui/PinnedMessage.kt index a35f4f9159..a39930a9d7 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/PinnedMessage.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/PinnedMessage.kt @@ -10,50 +10,58 @@ package com.nextcloud.talk.ui import android.text.format.DateFormat import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Menu -import androidx.compose.material3.Divider +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.Text import androidx.compose.runtime.Composable 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.draw.shadow import androidx.compose.ui.graphics.Color 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.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.nextcloud.talk.R import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.json.conversations.Conversation +import com.nextcloud.talk.models.json.conversations.ConversationEnums +import com.nextcloud.talk.models.json.participants.Participant import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.utils.ConversationUtils +import com.nextcloud.talk.utils.preview.ComposePreviewUtils import java.time.Instant import java.time.ZoneId import java.time.format.DateTimeFormatter const val SPACE_16 = 16 +const val SPACE_0 = 0 const val CORNER_RADIUS = 16 -const val ICON_SIZE = 24 -const val ELEVATION = 4 +val ELEVATION = 2.dp const val MAX_HEIGHT = 100 @Suppress("LongMethod", "LongParameterList") @@ -68,11 +76,23 @@ fun PinnedMessageView( ) { message.incoming = true - val pinnedBy = stringResource(R.string.pinned_by) - - message.actorDisplayName = remember(message.pinnedActorDisplayName) { - "${message.actorDisplayName}\n$pinnedBy ${message.pinnedActorDisplayName}" + val pinnedHeadline = if (message.pinnedActorId != message.actorId) { + if (message.pinnedActorId == currentConversation?.actorId) { + stringResource( + R.string.pinned_by_you, + message.actorDisplayName.orEmpty() + ) + } else { + stringResource( + R.string.pinned_by_author, + message.actorDisplayName.orEmpty(), + message.pinnedActorDisplayName.orEmpty() + ) + } + } else { + "${message.actorDisplayName}" } + val scrollState = rememberScrollState() val context = LocalContext.current @@ -97,31 +117,31 @@ fun PinnedMessageView( ConversationUtils.isParticipantOwnerOrModerator(currentConversation!!) } - val adapter = remember { - ComposeChatAdapter() - } + val interactionSource = remember { MutableInteractionSource() } - Column( - verticalArrangement = Arrangement.spacedBy((-SPACE_16).dp), + Box( modifier = Modifier + .fillMaxWidth() + .padding(4.dp) + .shadow( + elevation = ELEVATION, + shape = RoundedCornerShape(CORNER_RADIUS.dp), + clip = false + ) + .background( + incomingBubbleColor, + RoundedCornerShape(CORNER_RADIUS.dp) + ) + .padding(SPACE_16.dp, SPACE_0.dp, SPACE_0.dp, SPACE_16.dp) + .heightIn(max = MAX_HEIGHT.dp) + .clickable( + interactionSource = interactionSource, + indication = null + ) { + scrollToMessageWithIdWithOffset(message.id) + } ) { - Box( - modifier = Modifier - .shadow(ELEVATION.dp, shape = RoundedCornerShape(CORNER_RADIUS.dp)) - .background(incomingBubbleColor, RoundedCornerShape(CORNER_RADIUS.dp)) - .padding(SPACE_16.dp) - .heightIn(max = MAX_HEIGHT.dp) - .verticalScroll(scrollState) - .clickable { - scrollToMessageWithIdWithOffset(message.id) - } - - ) { - adapter.GetComposableForMessage(message) - } - var expanded by remember { mutableStateOf(false) } - val pinnedUntilStr = stringResource(R.string.pinned_until) val untilUnpin = stringResource(R.string.until_unpin) val pinnedText = remember(message.pinnedUntil) { @@ -142,15 +162,32 @@ fun PinnedMessageView( } ?: untilUnpin } + Column( + modifier = Modifier + .verticalScroll(scrollState) + .padding(top = SPACE_16.dp, end = 40.dp) + ) { + Text( + text = pinnedHeadline, + color = colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.labelMedium + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = message.text, + color = colorScheme.onSurface + ) + } + Box( modifier = Modifier - .offset(SPACE_16.dp, 0.dp) - .background(outgoingBubbleColor, RoundedCornerShape(CORNER_RADIUS.dp)) + .align(Alignment.TopEnd) + .padding(top = 2.dp) ) { IconButton(onClick = { expanded = true }) { Icon( - imageVector = Icons.Default.Menu, // Or use a Pin icon here - contentDescription = "Pinned Message Options", + imageVector = Icons.Default.MoreVert, + contentDescription = stringResource(R.string.pinned_message_options), tint = highEmphasisColor ) } @@ -167,22 +204,14 @@ fun PinnedMessageView( color = highEmphasisColor ) }, - onClick = { /* No-op or toggle expansion */ }, - enabled = false // Visually distinct as information, not action + onClick = {}, + enabled = false ) - Divider() + HorizontalDivider() DropdownMenuItem( - text = { Text("Go to message", color = highEmphasisColor) }, - leadingIcon = { - Icon( - painter = painterResource(R.drawable.baseline_chat_bubble_outline_24), - contentDescription = null, - modifier = Modifier.size(ICON_SIZE.dp), - tint = highEmphasisColor - ) - }, + text = { Text(stringResource(R.string.pinned_go_to_message), color = highEmphasisColor) }, onClick = { expanded = false scrollToMessageWithIdWithOffset(message.id) @@ -190,15 +219,7 @@ fun PinnedMessageView( ) DropdownMenuItem( - text = { Text("Dismiss", color = highEmphasisColor) }, - leadingIcon = { - Icon( - painter = painterResource(R.drawable.ic_eye_off), - contentDescription = null, - modifier = Modifier.size(ICON_SIZE.dp), - tint = highEmphasisColor - ) - }, + text = { Text(stringResource(R.string.pinned_dismiss), color = highEmphasisColor) }, onClick = { expanded = false hidePinnedMessage(message) @@ -207,15 +228,7 @@ fun PinnedMessageView( if (canPin) { DropdownMenuItem( - text = { Text("Unpin", color = highEmphasisColor) }, - leadingIcon = { - Icon( - painter = painterResource(R.drawable.keep_off_24px), - contentDescription = null, - modifier = Modifier.size(ICON_SIZE.dp), - tint = highEmphasisColor - ) - }, + text = { Text(stringResource(R.string.unpin_message), color = highEmphasisColor) }, onClick = { expanded = false unPinMessage(message) @@ -226,3 +239,64 @@ fun PinnedMessageView( } } } + +@Preview(name = "Long Content") +@Composable +fun PinnedMessageLongContentPreview() { + PinnedMessagePreview( + messageContent = "This is a **very long** _pinned_ ??\ncontent that should demonstrate how the " + + "scrollable box behaves when there is more text than what can fit in the maximum height of the pinned " + + "message view. It should show a scrollbar or at least allow vertical scrolling to see the rest of " + + "the message. Adding even more text here to ensure it exceeds 100dp." + ) +} + +@Preview( + name = "Dark Mode / R-t-L", + uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES, + locale = "ar" +) +@Composable +fun PinnedMessagePreviewDarkRtl() { + PinnedMessagePreview() +} + +@Suppress("MagicNumber") +@Preview(name = "Light Mode") +@Composable +fun PinnedMessagePreview(messageContent: String = "This is a **pinned** message _content_") { + val context = LocalContext.current + val previewUtils = ComposePreviewUtils.getInstance(context) + val viewThemeUtils = previewUtils.viewThemeUtils + val colorScheme = viewThemeUtils.getColorScheme(context) + + val user = User(id = 1L, userId = "user_id") + val conversation = Conversation( + token = "token", + participantType = Participant.ParticipantType.OWNER, + type = ConversationEnums.ConversationType.ROOM_GROUP_CALL + ) + val currentConversation = ConversationModel.mapToConversationModel(conversation, user) + + val message = ChatMessage().apply { + jsonMessageId = 1 + actorDisplayName = "Author One" + pinnedActorDisplayName = "User Two" + message = messageContent + timestamp = System.currentTimeMillis() / 1000 + pinnedAt = System.currentTimeMillis() / 1000 + } + + MaterialTheme(colorScheme = colorScheme) { + Box(modifier = Modifier.padding(16.dp)) { + PinnedMessageView( + message = message, + viewThemeUtils = viewThemeUtils, + currentConversation = currentConversation, + scrollToMessageWithIdWithOffset = {}, + hidePinnedMessage = {}, + unPinMessage = {} + ) + } + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 52c6b7eca9..bca74f7d95 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -914,9 +914,13 @@ How to translate with transifex: Pin indefinitely Pinned indefinitely Pinned until - Pinned by + %1$s (pinned by you) + %1$s (pinned by %2$s) Pinned Until unpin + Go to message + Dismiss + Message options Send later Schedule message