@@ -34,6 +34,7 @@ import android.provider.MediaStore
3434import android.provider.Settings
3535import android.text.SpannableStringBuilder
3636import android.text.TextUtils
37+ import android.text.format.DateFormat
3738import android.util.Log
3839import android.view.Gravity
3940import android.view.Menu
@@ -59,11 +60,33 @@ import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia
5960import androidx.appcompat.app.AlertDialog
6061import androidx.appcompat.view.ContextThemeWrapper
6162import androidx.cardview.widget.CardView
63+ import androidx.compose.foundation.background
64+ import androidx.compose.foundation.clickable
65+ import androidx.compose.foundation.layout.Arrangement
66+ import androidx.compose.foundation.layout.Box
67+ import androidx.compose.foundation.layout.Column
68+ import androidx.compose.foundation.layout.Row
69+ import androidx.compose.foundation.layout.Spacer
70+ import androidx.compose.foundation.layout.padding
71+ import androidx.compose.foundation.layout.size
72+ import androidx.compose.foundation.rememberScrollState
73+ import androidx.compose.foundation.shape.RoundedCornerShape
74+ import androidx.compose.foundation.verticalScroll
75+ import androidx.compose.material3.Icon
6276import androidx.compose.material3.MaterialTheme
77+ import androidx.compose.material3.Text
78+ import androidx.compose.runtime.Composable
6379import androidx.compose.runtime.getValue
6480import androidx.compose.runtime.mutableStateOf
81+ import androidx.compose.runtime.remember
6582import androidx.compose.runtime.setValue
83+ import androidx.compose.ui.Modifier
84+ import androidx.compose.ui.draw.shadow
85+ import androidx.compose.ui.graphics.Color
6686import androidx.compose.ui.platform.ComposeView
87+ import androidx.compose.ui.res.painterResource
88+ import androidx.compose.ui.res.stringResource
89+ import androidx.compose.ui.unit.dp
6790import androidx.coordinatorlayout.widget.CoordinatorLayout
6891import androidx.core.content.ContextCompat
6992import androidx.core.content.FileProvider
@@ -167,12 +190,14 @@ import com.nextcloud.talk.signaling.SignalingMessageReceiver
167190import com.nextcloud.talk.signaling.SignalingMessageSender
168191import com.nextcloud.talk.threadsoverview.ThreadsOverviewActivity
169192import com.nextcloud.talk.translate.ui.TranslateActivity
193+ import com.nextcloud.talk.ui.ComposeChatAdapter
170194import com.nextcloud.talk.ui.PlaybackSpeed
171195import com.nextcloud.talk.ui.PlaybackSpeedControl
172196import com.nextcloud.talk.ui.StatusDrawable
173197import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet
174198import com.nextcloud.talk.ui.dialog.DateTimeCompose
175199import com.nextcloud.talk.ui.dialog.FileAttachmentPreviewFragment
200+ import com.nextcloud.talk.ui.dialog.GetPinnedOptionsDialog
176201import com.nextcloud.talk.ui.dialog.MessageActionsDialog
177202import com.nextcloud.talk.ui.dialog.SaveToStorageDialogFragment
178203import com.nextcloud.talk.ui.dialog.ShowReactionsDialog
@@ -250,7 +275,7 @@ import java.util.concurrent.ExecutionException
250275import javax.inject.Inject
251276import kotlin.math.roundToInt
252277
253- @Suppress(" TooManyFunctions" )
278+ @Suppress(" TooManyFunctions" , " LargeClass " , " LongMethod " )
254279@AutoInjector(NextcloudTalkApplication ::class )
255280class ChatActivity :
256281 BaseActivity (),
@@ -648,7 +673,7 @@ class ChatActivity :
648673
649674 this .lifecycleScope.launch {
650675 chatViewModel.getConversationFlow
651- .onEach { conversationModel ->
676+ .collect { conversationModel ->
652677 currentConversation = conversationModel
653678 chatViewModel.updateConversation(
654679 currentConversation!!
@@ -663,7 +688,30 @@ class ChatActivity :
663688 }
664689
665690 chatViewModel.getCapabilities(conversationUser!! , roomToken, currentConversation!! )
666- }.collect()
691+
692+ if (conversationModel.lastPinnedId != null &&
693+ conversationModel.lastPinnedId != 0L &&
694+ conversationModel.lastPinnedId != conversationModel.hiddenPinnedId
695+ ) {
696+ chatViewModel
697+ .getIndividualMessageFromServer(
698+ credentials!! ,
699+ conversationUser?.baseUrl!! ,
700+ roomToken,
701+ conversationModel.lastPinnedId.toString()
702+ )
703+ .collect { message ->
704+ message?.let {
705+ binding.pinnedMessageContainer.visibility = View .VISIBLE
706+ binding.pinnedMessageComposeView.setContent {
707+ PinnedMessageView (message)
708+ }
709+ }
710+ }
711+ } else {
712+ binding.pinnedMessageContainer.visibility = View .GONE
713+ }
714+ }
667715 }
668716
669717 chatViewModel.getRoomViewState.observe(this ) { state ->
@@ -1130,6 +1178,10 @@ class ChatActivity :
11301178 val item = adapter?.items?.get(index)?.item
11311179 item?.let {
11321180 setMessageAsEdited(item as ChatMessage , newString)
1181+
1182+ if (item.jsonMessageId.toLong() == currentConversation?.lastPinnedId) {
1183+ chatViewModel.getRoom(roomToken)
1184+ }
11331185 }
11341186 }
11351187
@@ -1313,6 +1365,94 @@ class ChatActivity :
13131365 }
13141366 }
13151367
1368+ @Composable
1369+ private fun PinnedMessageView (message : ChatMessage ) {
1370+ message.incoming = true
1371+ val pinnedBy = stringResource(R .string.pinned_by)
1372+ message.actorDisplayName = " ${message.actorDisplayName} \n $pinnedBy ${message.pinnedActorDisplayName} "
1373+ val scrollState = rememberScrollState()
1374+
1375+ val outgoingBubbleColor = remember {
1376+ val colorInt = viewThemeUtils.talk
1377+ .getOutgoingMessageBubbleColor(context, message.isDeleted, false )
1378+
1379+ Color (colorInt)
1380+ }
1381+
1382+ val incomingBubbleColor = remember {
1383+ val colorInt = resources
1384+ .getColor(R .color.bg_message_list_incoming_bubble, null )
1385+
1386+ Color (colorInt)
1387+ }
1388+
1389+ val isAllowed = remember {
1390+ ConversationUtils .isParticipantOwnerOrModerator(currentConversation!! )
1391+ }
1392+
1393+ Column (
1394+ verticalArrangement = Arrangement .spacedBy((- 16 ).dp),
1395+ modifier = Modifier
1396+ ) {
1397+ Box (
1398+ modifier = Modifier
1399+ .shadow(4 .dp, shape = RoundedCornerShape (16 .dp))
1400+ .background(incomingBubbleColor, RoundedCornerShape (16 .dp))
1401+ .padding(16 .dp)
1402+ .verticalScroll(scrollState)
1403+ ) {
1404+ ComposeChatAdapter ().GetComposableForMessage (message)
1405+ }
1406+
1407+ Row (
1408+ modifier = Modifier
1409+ .padding(start = 16 .dp)
1410+ .background(outgoingBubbleColor, RoundedCornerShape (16 .dp))
1411+ .padding(16 .dp)
1412+ ) {
1413+ val hiddenEye = painterResource(R .drawable.ic_eye_off)
1414+ Icon (
1415+ hiddenEye,
1416+ " Hide pin" ,
1417+ modifier = Modifier
1418+ .size(16 .dp)
1419+ .clickable {
1420+ hidePinnedMessage(message)
1421+ }
1422+ )
1423+
1424+ if (isAllowed) {
1425+ Spacer (modifier = Modifier .size(16 .dp))
1426+ val read = painterResource(R .drawable.keep_off_24px)
1427+ Icon (
1428+ read,
1429+ " Unpin" ,
1430+ modifier = Modifier
1431+ .size(16 .dp)
1432+ .clickable {
1433+ unPinMessage(message)
1434+ }
1435+ )
1436+ }
1437+
1438+ val pinnedUntilStr = stringResource(R .string.pinned_until)
1439+ val pinnedIndefinitely = stringResource(R .string.pinned_indefinitely)
1440+ val pinnedText = message.pinnedUntil?.let {
1441+ val format = if (DateFormat .is24HourFormat(context)) " EEE, HH:mm" else " EEE, hh:mm a"
1442+ val localDateTime = Instant .ofEpochMilli(it)
1443+ .atZone(ZoneId .systemDefault())
1444+ .toLocalDateTime()
1445+
1446+ val timeString = localDateTime.format(DateTimeFormatter .ofPattern(format))
1447+
1448+ " $pinnedUntilStr $timeString "
1449+ } ? : pinnedIndefinitely
1450+
1451+ Text (pinnedText, modifier = Modifier .padding(start = 16 .dp))
1452+ }
1453+ }
1454+ }
1455+
13161456 private fun removeUnreadMessagesMarker () {
13171457 removeMessageById(UNREAD_MESSAGES_MARKER_ID .toString())
13181458 }
@@ -3915,6 +4055,32 @@ class ChatActivity :
39154055 }
39164056 }
39174057
4058+ fun hidePinnedMessage (message : ChatMessage ) {
4059+ val url = ApiUtils .getUrlForChatMessagePinning(chatApiVersion, conversationUser?.baseUrl, roomToken, message.id)
4060+ chatViewModel.hidePinnedMessage(credentials!! , url)
4061+ }
4062+
4063+ fun pinMessage (message : ChatMessage ) {
4064+ val url = ApiUtils .getUrlForChatMessagePinning(chatApiVersion, conversationUser?.baseUrl, roomToken, message.id)
4065+ binding.genericComposeView.apply {
4066+ val shouldDismiss = mutableStateOf(false )
4067+ setContent {
4068+ GetPinnedOptionsDialog (shouldDismiss, context, viewThemeUtils) { zonedDateTime ->
4069+ zonedDateTime?.let {
4070+ chatViewModel.pinMessage(credentials!! , url, pinUntil = zonedDateTime.toEpochSecond().toInt())
4071+ } ? : chatViewModel.pinMessage(credentials!! , url)
4072+
4073+ shouldDismiss.value = true
4074+ }
4075+ }
4076+ }
4077+ }
4078+
4079+ fun unPinMessage (message : ChatMessage ) {
4080+ val url = ApiUtils .getUrlForChatMessagePinning(chatApiVersion, conversationUser?.baseUrl, roomToken, message.id)
4081+ chatViewModel.unPinMessage(credentials!! , url)
4082+ }
4083+
39184084 fun markAsUnread (message : IMessage ? ) {
39194085 val chatMessage = message as ChatMessage ?
39204086 if (chatMessage!! .previousMessageId > NO_PREVIOUS_MESSAGE_ID ) {
0 commit comments