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