@@ -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 (),
@@ -655,7 +680,7 @@ class ChatActivity :
655680
656681 this .lifecycleScope.launch {
657682 chatViewModel.getConversationFlow
658- .onEach { conversationModel ->
683+ .collect { conversationModel ->
659684 currentConversation = conversationModel
660685 chatViewModel.updateConversation(
661686 currentConversation!!
@@ -670,7 +695,30 @@ class ChatActivity :
670695 }
671696
672697 chatViewModel.getCapabilities(conversationUser!! , roomToken, currentConversation!! )
673- }.collect()
698+
699+ if (conversationModel.lastPinnedId != null &&
700+ conversationModel.lastPinnedId != 0L &&
701+ conversationModel.lastPinnedId != conversationModel.hiddenPinnedId
702+ ) {
703+ chatViewModel
704+ .getIndividualMessageFromServer(
705+ credentials!! ,
706+ conversationUser?.baseUrl!! ,
707+ roomToken,
708+ conversationModel.lastPinnedId.toString()
709+ )
710+ .collect { message ->
711+ message?.let {
712+ binding.pinnedMessageContainer.visibility = View .VISIBLE
713+ binding.pinnedMessageComposeView.setContent {
714+ PinnedMessageView (message)
715+ }
716+ }
717+ }
718+ } else {
719+ binding.pinnedMessageContainer.visibility = View .GONE
720+ }
721+ }
674722 }
675723
676724 chatViewModel.getRoomViewState.observe(this ) { state ->
@@ -1137,6 +1185,10 @@ class ChatActivity :
11371185 val item = adapter?.items?.get(index)?.item
11381186 item?.let {
11391187 setMessageAsEdited(item as ChatMessage , newString)
1188+
1189+ if (item.jsonMessageId.toLong() == currentConversation?.lastPinnedId) {
1190+ chatViewModel.getRoom(roomToken)
1191+ }
11401192 }
11411193 }
11421194
@@ -1320,6 +1372,94 @@ class ChatActivity :
13201372 }
13211373 }
13221374
1375+ @Composable
1376+ private fun PinnedMessageView (message : ChatMessage ) {
1377+ message.incoming = true
1378+ val pinnedBy = stringResource(R .string.pinned_by)
1379+ message.actorDisplayName = " ${message.actorDisplayName} \n $pinnedBy ${message.pinnedActorDisplayName} "
1380+ val scrollState = rememberScrollState()
1381+
1382+ val outgoingBubbleColor = remember {
1383+ val colorInt = viewThemeUtils.talk
1384+ .getOutgoingMessageBubbleColor(context, message.isDeleted, false )
1385+
1386+ Color (colorInt)
1387+ }
1388+
1389+ val incomingBubbleColor = remember {
1390+ val colorInt = resources
1391+ .getColor(R .color.bg_message_list_incoming_bubble, null )
1392+
1393+ Color (colorInt)
1394+ }
1395+
1396+ val isAllowed = remember {
1397+ ConversationUtils .isParticipantOwnerOrModerator(currentConversation!! )
1398+ }
1399+
1400+ Column (
1401+ verticalArrangement = Arrangement .spacedBy((- 16 ).dp),
1402+ modifier = Modifier
1403+ ) {
1404+ Box (
1405+ modifier = Modifier
1406+ .shadow(4 .dp, shape = RoundedCornerShape (16 .dp))
1407+ .background(incomingBubbleColor, RoundedCornerShape (16 .dp))
1408+ .padding(16 .dp)
1409+ .verticalScroll(scrollState)
1410+ ) {
1411+ ComposeChatAdapter ().GetComposableForMessage (message)
1412+ }
1413+
1414+ Row (
1415+ modifier = Modifier
1416+ .padding(start = 16 .dp)
1417+ .background(outgoingBubbleColor, RoundedCornerShape (16 .dp))
1418+ .padding(16 .dp)
1419+ ) {
1420+ val hiddenEye = painterResource(R .drawable.ic_eye_off)
1421+ Icon (
1422+ hiddenEye,
1423+ " Hide pin" ,
1424+ modifier = Modifier
1425+ .size(16 .dp)
1426+ .clickable {
1427+ hidePinnedMessage(message)
1428+ }
1429+ )
1430+
1431+ if (isAllowed) {
1432+ Spacer (modifier = Modifier .size(16 .dp))
1433+ val read = painterResource(R .drawable.keep_off_24px)
1434+ Icon (
1435+ read,
1436+ " Unpin" ,
1437+ modifier = Modifier
1438+ .size(16 .dp)
1439+ .clickable {
1440+ unPinMessage(message)
1441+ }
1442+ )
1443+ }
1444+
1445+ val pinnedUntilStr = stringResource(R .string.pinned_until)
1446+ val pinnedIndefinitely = stringResource(R .string.pinned_indefinitely)
1447+ val pinnedText = message.pinnedUntil?.let {
1448+ val format = if (DateFormat .is24HourFormat(context)) " EEE, HH:mm" else " EEE, hh:mm a"
1449+ val localDateTime = Instant .ofEpochMilli(it)
1450+ .atZone(ZoneId .systemDefault())
1451+ .toLocalDateTime()
1452+
1453+ val timeString = localDateTime.format(DateTimeFormatter .ofPattern(format))
1454+
1455+ " $pinnedUntilStr $timeString "
1456+ } ? : pinnedIndefinitely
1457+
1458+ Text (pinnedText, modifier = Modifier .padding(start = 16 .dp))
1459+ }
1460+ }
1461+ }
1462+
13231463 private fun removeUnreadMessagesMarker () {
13241464 removeMessageById(UNREAD_MESSAGES_MARKER_ID .toString())
13251465 }
@@ -3931,6 +4071,32 @@ class ChatActivity :
39314071 }
39324072 }
39334073
4074+ fun hidePinnedMessage (message : ChatMessage ) {
4075+ val url = ApiUtils .getUrlForChatMessagePinning(chatApiVersion, conversationUser?.baseUrl, roomToken, message.id)
4076+ chatViewModel.hidePinnedMessage(credentials!! , url)
4077+ }
4078+
4079+ fun pinMessage (message : ChatMessage ) {
4080+ val url = ApiUtils .getUrlForChatMessagePinning(chatApiVersion, conversationUser?.baseUrl, roomToken, message.id)
4081+ binding.genericComposeView.apply {
4082+ val shouldDismiss = mutableStateOf(false )
4083+ setContent {
4084+ GetPinnedOptionsDialog (shouldDismiss, context, viewThemeUtils) { zonedDateTime ->
4085+ zonedDateTime?.let {
4086+ chatViewModel.pinMessage(credentials!! , url, pinUntil = zonedDateTime.toEpochSecond().toInt())
4087+ } ? : chatViewModel.pinMessage(credentials!! , url)
4088+
4089+ shouldDismiss.value = true
4090+ }
4091+ }
4092+ }
4093+ }
4094+
4095+ fun unPinMessage (message : ChatMessage ) {
4096+ val url = ApiUtils .getUrlForChatMessagePinning(chatApiVersion, conversationUser?.baseUrl, roomToken, message.id)
4097+ chatViewModel.unPinMessage(credentials!! , url)
4098+ }
4099+
39344100 fun markAsUnread (message : IMessage ? ) {
39354101 val chatMessage = message as ChatMessage ?
39364102 if (chatMessage!! .previousMessageId > NO_PREVIOUS_MESSAGE_ID ) {
0 commit comments