Skip to content

Commit 9a043b0

Browse files
committed
Added Pinned Messages Support
- ChatMessage now has ChatMessageMetaData - Conversation now has updated fields from server - Added PinnedMessageOptionsDialog - API, viewmodel, class functions Signed-off-by: rapterjet2004 <[email protected]>
1 parent f544343 commit 9a043b0

29 files changed

+855
-81
lines changed

app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,4 +323,18 @@ interface NcApiCoroutines {
323323

324324
@GET
325325
suspend fun status(@Header("Authorization") authorization: String, @Url url: String): StatusOverall
326+
327+
@FormUrlEncoded
328+
@POST
329+
suspend fun pinMessage(
330+
@Header("Authorization") authorization: String,
331+
@Url url: String,
332+
@Field("pinUntil") pinUntil: Int
333+
): ChatOverallSingleMessage
334+
335+
@DELETE
336+
suspend fun unPinMessage(@Header("Authorization") authorization: String, @Url url: String): ChatOverallSingleMessage
337+
338+
@DELETE
339+
suspend fun hidePinnedMessage(@Header("Authorization") authorization: String, @Url url: String): GenericOverall
326340
}

app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt

Lines changed: 165 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import android.provider.MediaStore
3434
import android.provider.Settings
3535
import android.text.SpannableStringBuilder
3636
import android.text.TextUtils
37+
import android.text.format.DateFormat
3738
import android.util.Log
3839
import android.view.Gravity
3940
import android.view.Menu
@@ -59,11 +60,33 @@ import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia
5960
import androidx.appcompat.app.AlertDialog
6061
import androidx.appcompat.view.ContextThemeWrapper
6162
import 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
6276
import androidx.compose.material3.MaterialTheme
77+
import androidx.compose.material3.Text
78+
import androidx.compose.runtime.Composable
6379
import androidx.compose.runtime.getValue
6480
import androidx.compose.runtime.mutableStateOf
81+
import androidx.compose.runtime.remember
6582
import androidx.compose.runtime.setValue
83+
import androidx.compose.ui.Modifier
84+
import androidx.compose.ui.draw.shadow
85+
import androidx.compose.ui.graphics.Color
6686
import 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
6790
import androidx.coordinatorlayout.widget.CoordinatorLayout
6891
import androidx.core.content.ContextCompat
6992
import androidx.core.content.FileProvider
@@ -167,12 +190,14 @@ import com.nextcloud.talk.signaling.SignalingMessageReceiver
167190
import com.nextcloud.talk.signaling.SignalingMessageSender
168191
import com.nextcloud.talk.threadsoverview.ThreadsOverviewActivity
169192
import com.nextcloud.talk.translate.ui.TranslateActivity
193+
import com.nextcloud.talk.ui.ComposeChatAdapter
170194
import com.nextcloud.talk.ui.PlaybackSpeed
171195
import com.nextcloud.talk.ui.PlaybackSpeedControl
172196
import com.nextcloud.talk.ui.StatusDrawable
173197
import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet
174198
import com.nextcloud.talk.ui.dialog.DateTimeCompose
175199
import com.nextcloud.talk.ui.dialog.FileAttachmentPreviewFragment
200+
import com.nextcloud.talk.ui.dialog.GetPinnedOptionsDialog
176201
import com.nextcloud.talk.ui.dialog.MessageActionsDialog
177202
import com.nextcloud.talk.ui.dialog.SaveToStorageDialogFragment
178203
import com.nextcloud.talk.ui.dialog.ShowReactionsDialog
@@ -250,7 +275,7 @@ import java.util.concurrent.ExecutionException
250275
import javax.inject.Inject
251276
import kotlin.math.roundToInt
252277

253-
@Suppress("TooManyFunctions")
278+
@Suppress("TooManyFunctions", "LargeClass", "LongMethod")
254279
@AutoInjector(NextcloudTalkApplication::class)
255280
class ChatActivity :
256281
BaseActivity(),
@@ -663,6 +688,27 @@ class ChatActivity :
663688
}
664689

665690
chatViewModel.getCapabilities(conversationUser!!, roomToken, currentConversation!!)
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+
binding.pinnedMessageContainer.visibility = View.VISIBLE
705+
binding.pinnedMessageComposeView.setContent {
706+
PinnedMessageView(message)
707+
}
708+
}
709+
} else {
710+
binding.pinnedMessageContainer.visibility = View.GONE
711+
}
666712
}.collect()
667713
}
668714

@@ -1130,6 +1176,10 @@ class ChatActivity :
11301176
val item = adapter?.items?.get(index)?.item
11311177
item?.let {
11321178
setMessageAsEdited(item as ChatMessage, newString)
1179+
1180+
if (item.jsonMessageId.toLong() == currentConversation?.lastPinnedId) {
1181+
chatViewModel.getRoom(roomToken)
1182+
}
11331183
}
11341184
}
11351185

@@ -1313,6 +1363,94 @@ class ChatActivity :
13131363
}
13141364
}
13151365

1366+
@Composable
1367+
private fun PinnedMessageView(message: ChatMessage) {
1368+
message.incoming = true
1369+
val pinnedBy = stringResource(R.string.pinned_by)
1370+
message.actorDisplayName = "${message.actorDisplayName}\n$pinnedBy ${message.pinnedActorDisplayName}"
1371+
val scrollState = rememberScrollState()
1372+
1373+
val outgoingBubbleColor = remember {
1374+
val colorInt = viewThemeUtils.talk
1375+
.getOutgoingMessageBubbleColor(context, message.isDeleted, false)
1376+
1377+
Color(colorInt)
1378+
}
1379+
1380+
val incomingBubbleColor = remember {
1381+
val colorInt = resources
1382+
.getColor(R.color.bg_message_list_incoming_bubble, null)
1383+
1384+
Color(colorInt)
1385+
}
1386+
1387+
val isAllowed = remember {
1388+
ConversationUtils.isParticipantOwnerOrModerator(currentConversation!!)
1389+
}
1390+
1391+
Column(
1392+
verticalArrangement = Arrangement.spacedBy((-16).dp),
1393+
modifier = Modifier
1394+
) {
1395+
Box(
1396+
modifier = Modifier
1397+
.shadow(4.dp, shape = RoundedCornerShape(16.dp))
1398+
.background(incomingBubbleColor, RoundedCornerShape(16.dp))
1399+
.padding(16.dp)
1400+
.verticalScroll(scrollState)
1401+
) {
1402+
ComposeChatAdapter().GetComposableForMessage(message)
1403+
}
1404+
1405+
Row(
1406+
modifier = Modifier
1407+
.padding(start = 16.dp)
1408+
.background(outgoingBubbleColor, RoundedCornerShape(16.dp))
1409+
.padding(16.dp)
1410+
) {
1411+
val hiddenEye = painterResource(R.drawable.ic_eye_off)
1412+
Icon(
1413+
hiddenEye,
1414+
"Hide pin",
1415+
modifier = Modifier
1416+
.size(16.dp)
1417+
.clickable {
1418+
hidePinnedMessage(message)
1419+
}
1420+
)
1421+
1422+
if (isAllowed) {
1423+
Spacer(modifier = Modifier.size(16.dp))
1424+
val read = painterResource(R.drawable.keep_off_24px)
1425+
Icon(
1426+
read,
1427+
"Unpin",
1428+
modifier = Modifier
1429+
.size(16.dp)
1430+
.clickable {
1431+
unPinMessage(message)
1432+
}
1433+
)
1434+
}
1435+
1436+
val pinnedUntilStr = stringResource(R.string.pinned_until)
1437+
val pinnedIndefinitely = stringResource(R.string.pinned_indefinitely)
1438+
val pinnedText = message.pinnedUntil?.let {
1439+
val format = if (DateFormat.is24HourFormat(context)) "EEE, HH:mm" else "EEE, hh:mm a"
1440+
val localDateTime = Instant.ofEpochMilli(it)
1441+
.atZone(ZoneId.systemDefault())
1442+
.toLocalDateTime()
1443+
1444+
val timeString = localDateTime.format(DateTimeFormatter.ofPattern(format))
1445+
1446+
"$pinnedUntilStr $timeString"
1447+
} ?: pinnedIndefinitely
1448+
1449+
Text(pinnedText, modifier = Modifier.padding(start = 16.dp))
1450+
}
1451+
}
1452+
}
1453+
13161454
private fun removeUnreadMessagesMarker() {
13171455
removeMessageById(UNREAD_MESSAGES_MARKER_ID.toString())
13181456
}
@@ -3915,6 +4053,32 @@ class ChatActivity :
39154053
}
39164054
}
39174055

4056+
fun hidePinnedMessage(message: ChatMessage) {
4057+
val url = ApiUtils.getUrlForChatMessagePinning(chatApiVersion, conversationUser?.baseUrl, roomToken, message.id)
4058+
chatViewModel.hidePinnedMessage(credentials!!, url)
4059+
}
4060+
4061+
fun pinMessage(message: ChatMessage) {
4062+
val url = ApiUtils.getUrlForChatMessagePinning(chatApiVersion, conversationUser?.baseUrl, roomToken, message.id)
4063+
binding.genericComposeView.apply {
4064+
val shouldDismiss = mutableStateOf(false)
4065+
setContent {
4066+
GetPinnedOptionsDialog(shouldDismiss, context, viewThemeUtils) { zonedDateTime ->
4067+
zonedDateTime?.let {
4068+
chatViewModel.pinMessage(credentials!!, url, pinUntil = zonedDateTime.toEpochSecond().toInt())
4069+
} ?: chatViewModel.pinMessage(credentials!!, url)
4070+
4071+
shouldDismiss.value = true
4072+
}
4073+
}
4074+
}
4075+
}
4076+
4077+
fun unPinMessage(message: ChatMessage) {
4078+
val url = ApiUtils.getUrlForChatMessagePinning(chatApiVersion, conversationUser?.baseUrl, roomToken, message.id)
4079+
chatViewModel.unPinMessage(credentials!!, url)
4080+
}
4081+
39184082
fun markAsUnread(message: IMessage?) {
39194083
val chatMessage = message as ChatMessage?
39204084
if (chatMessage!!.previousMessageId > NO_PREVIOUS_MESSAGE_ID) {

app/src/main/java/com/nextcloud/talk/chat/data/ChatMessageRepository.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage
1515
import kotlinx.coroutines.Job
1616
import kotlinx.coroutines.flow.Flow
1717

18+
@Suppress("TooManyFunctions")
1819
interface ChatMessageRepository : LifecycleAwareManager {
1920

2021
/**
@@ -116,4 +117,10 @@ interface ChatMessageRepository : LifecycleAwareManager {
116117
suspend fun sendUnsentChatMessages(credentials: String, url: String)
117118

118119
suspend fun deleteTempMessage(chatMessage: ChatMessage)
120+
121+
suspend fun pinMessage(credentials: String, url: String, pinUntil: Int): Flow<ChatMessage?>
122+
123+
suspend fun unPinMessage(credentials: String, url: String): Flow<ChatMessage?>
124+
125+
suspend fun hidePinnedMessage(credentials: String, url: String): Flow<Boolean>
119126
}

app/src/main/java/com/nextcloud/talk/chat/data/model/ChatMessage.kt

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ data class ChatMessage(
9494

9595
var lastEditTimestamp: Long? = 0,
9696

97+
var incoming: Boolean = false,
98+
9799
var isDownloadingVoiceMessage: Boolean = false,
98100

99101
var resetVoiceMessage: Boolean = false,
@@ -130,7 +132,17 @@ data class ChatMessage(
130132

131133
var sendStatus: SendStatus? = null,
132134

133-
var silent: Boolean = false
135+
var silent: Boolean = false,
136+
137+
var pinnedActorType: String? = null,
138+
139+
var pinnedActorId: String? = null,
140+
141+
var pinnedActorDisplayName: String? = null,
142+
143+
var pinnedAt: Long? = null,
144+
145+
var pinnedUntil: Long? = null
134146

135147
) : MessageContentType,
136148
MessageContentType.Image {
@@ -433,7 +445,9 @@ data class ChatMessage(
433445
FEDERATED_USER_ADDED,
434446
FEDERATED_USER_REMOVED,
435447
PHONE_ADDED,
436-
THREAD_CREATED
448+
THREAD_CREATED,
449+
MESSAGE_PINNED,
450+
MESSAGE_UNPINNED
437451
}
438452

439453
companion object {

app/src/main/java/com/nextcloud/talk/chat/data/network/ChatNetworkDataSource.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,9 @@ interface ChatNetworkDataSource {
7979
): List<ChatMessageJson>
8080
suspend fun getOpenGraph(credentials: String, baseUrl: String, extractedLinkToPreview: String): Reference?
8181
suspend fun unbindRoom(credentials: String, baseUrl: String, roomToken: String): GenericOverall
82+
suspend fun pinMessage(credentials: String, url: String, pinUntil: Int): ChatOverallSingleMessage
83+
84+
suspend fun unPinMessage(credentials: String, url: String): ChatOverallSingleMessage
85+
86+
suspend fun hidePinnedMessage(credentials: String, url: String): GenericOverall
8287
}

app/src/main/java/com/nextcloud/talk/chat/data/network/OfflineFirstChatRepository.kt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1020,6 +1020,36 @@ class OfflineFirstChatRepository @Inject constructor(
10201020
_removeMessageFlow.emit(chatMessage)
10211021
}
10221022

1023+
override suspend fun pinMessage(credentials: String, url: String, pinUntil: Int): Flow<ChatMessage?> =
1024+
flow {
1025+
runCatching {
1026+
val overall = network.pinMessage(credentials, url, pinUntil)
1027+
emit(overall.ocs?.data?.asModel())
1028+
}.getOrElse { throwable ->
1029+
Log.e(TAG, "Error in pinMessage: $throwable")
1030+
}
1031+
}
1032+
1033+
override suspend fun unPinMessage(credentials: String, url: String): Flow<ChatMessage?> =
1034+
flow {
1035+
runCatching {
1036+
val overall = network.unPinMessage(credentials, url)
1037+
emit(overall.ocs?.data?.asModel())
1038+
}.getOrElse { throwable ->
1039+
Log.e(TAG, "Error in unPinMessage: $throwable")
1040+
}
1041+
}
1042+
1043+
override suspend fun hidePinnedMessage(credentials: String, url: String): Flow<Boolean> =
1044+
flow {
1045+
runCatching {
1046+
network.hidePinnedMessage(credentials, url)
1047+
emit(true)
1048+
}.getOrElse { throwable ->
1049+
Log.e(TAG, "Error in hidePinnedMessage: $throwable")
1050+
}
1051+
}
1052+
10231053
@Suppress("Detekt.TooGenericExceptionCaught")
10241054
override suspend fun addTemporaryMessage(
10251055
message: CharSequence,

app/src/main/java/com/nextcloud/talk/chat/data/network/RetrofitChatNetwork.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,4 +222,13 @@ class RetrofitChatNetwork(private val ncApi: NcApi, private val ncApiCoroutines:
222222
val url = ApiUtils.getUrlForUnbindingRoom(baseUrl, roomToken)
223223
return ncApiCoroutines.unbindRoom(credentials, url)
224224
}
225+
226+
override suspend fun pinMessage(credentials: String, url: String, pinUntil: Int): ChatOverallSingleMessage =
227+
ncApiCoroutines.pinMessage(credentials, url, pinUntil)
228+
229+
override suspend fun unPinMessage(credentials: String, url: String): ChatOverallSingleMessage =
230+
ncApiCoroutines.unPinMessage(credentials, url)
231+
232+
override suspend fun hidePinnedMessage(credentials: String, url: String): GenericOverall =
233+
ncApiCoroutines.hidePinnedMessage(credentials, url)
225234
}

0 commit comments

Comments
 (0)