Skip to content

Commit 4e0363d

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 <juliuslinus1@gmail.com>
1 parent c3b38f3 commit 4e0363d

29 files changed

+863
-83
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: 169 additions & 3 deletions
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(),
@@ -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) {

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
@@ -16,6 +16,7 @@ import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage
1616
import kotlinx.coroutines.Job
1717
import kotlinx.coroutines.flow.Flow
1818

19+
@Suppress("TooManyFunctions")
1920
interface ChatMessageRepository : LifecycleAwareManager {
2021

2122
/**
@@ -117,4 +118,10 @@ interface ChatMessageRepository : LifecycleAwareManager {
117118
suspend fun sendUnsentChatMessages(credentials: String, url: String)
118119

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

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
@@ -1026,6 +1026,36 @@ class OfflineFirstChatRepository @Inject constructor(
10261026
_removeMessageFlow.emit(chatMessage)
10271027
}
10281028

1029+
override suspend fun pinMessage(credentials: String, url: String, pinUntil: Int): Flow<ChatMessage?> =
1030+
flow {
1031+
runCatching {
1032+
val overall = network.pinMessage(credentials, url, pinUntil)
1033+
emit(overall.ocs?.data?.asModel())
1034+
}.getOrElse { throwable ->
1035+
Log.e(TAG, "Error in pinMessage: $throwable")
1036+
}
1037+
}
1038+
1039+
override suspend fun unPinMessage(credentials: String, url: String): Flow<ChatMessage?> =
1040+
flow {
1041+
runCatching {
1042+
val overall = network.unPinMessage(credentials, url)
1043+
emit(overall.ocs?.data?.asModel())
1044+
}.getOrElse { throwable ->
1045+
Log.e(TAG, "Error in unPinMessage: $throwable")
1046+
}
1047+
}
1048+
1049+
override suspend fun hidePinnedMessage(credentials: String, url: String): Flow<Boolean> =
1050+
flow {
1051+
runCatching {
1052+
network.hidePinnedMessage(credentials, url)
1053+
emit(true)
1054+
}.getOrElse { throwable ->
1055+
Log.e(TAG, "Error in hidePinnedMessage: $throwable")
1056+
}
1057+
}
1058+
10291059
@Suppress("Detekt.TooGenericExceptionCaught")
10301060
override suspend fun addTemporaryMessage(
10311061
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)