Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt
Original file line number Diff line number Diff line change
Expand Up @@ -323,4 +323,18 @@ interface NcApiCoroutines {

@GET
suspend fun status(@Header("Authorization") authorization: String, @Url url: String): StatusOverall

@FormUrlEncoded
@POST
suspend fun pinMessage(
@Header("Authorization") authorization: String,
@Url url: String,
@Field("pinUntil") pinUntil: Int
): ChatOverallSingleMessage

@DELETE
suspend fun unPinMessage(@Header("Authorization") authorization: String, @Url url: String): ChatOverallSingleMessage

@DELETE
suspend fun hidePinnedMessage(@Header("Authorization") authorization: String, @Url url: String): GenericOverall
}
172 changes: 169 additions & 3 deletions app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import android.provider.MediaStore
import android.provider.Settings
import android.text.SpannableStringBuilder
import android.text.TextUtils
import android.text.format.DateFormat
import android.util.Log
import android.view.Gravity
import android.view.Menu
Expand All @@ -59,11 +60,33 @@ import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.view.ContextThemeWrapper
import androidx.cardview.widget.CardView
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
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.material3.Icon
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.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
Expand Down Expand Up @@ -167,12 +190,14 @@ import com.nextcloud.talk.signaling.SignalingMessageReceiver
import com.nextcloud.talk.signaling.SignalingMessageSender
import com.nextcloud.talk.threadsoverview.ThreadsOverviewActivity
import com.nextcloud.talk.translate.ui.TranslateActivity
import com.nextcloud.talk.ui.ComposeChatAdapter
import com.nextcloud.talk.ui.PlaybackSpeed
import com.nextcloud.talk.ui.PlaybackSpeedControl
import com.nextcloud.talk.ui.StatusDrawable
import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet
import com.nextcloud.talk.ui.dialog.DateTimeCompose
import com.nextcloud.talk.ui.dialog.FileAttachmentPreviewFragment
import com.nextcloud.talk.ui.dialog.GetPinnedOptionsDialog
import com.nextcloud.talk.ui.dialog.MessageActionsDialog
import com.nextcloud.talk.ui.dialog.SaveToStorageDialogFragment
import com.nextcloud.talk.ui.dialog.ShowReactionsDialog
Expand Down Expand Up @@ -250,7 +275,7 @@ import java.util.concurrent.ExecutionException
import javax.inject.Inject
import kotlin.math.roundToInt

@Suppress("TooManyFunctions")
@Suppress("TooManyFunctions", "LargeClass", "LongMethod")
@AutoInjector(NextcloudTalkApplication::class)
class ChatActivity :
BaseActivity(),
Expand Down Expand Up @@ -648,7 +673,7 @@ class ChatActivity :

this.lifecycleScope.launch {
chatViewModel.getConversationFlow
.onEach { conversationModel ->
.collect { conversationModel ->
currentConversation = conversationModel
chatViewModel.updateConversation(
currentConversation!!
Expand All @@ -663,7 +688,30 @@ class ChatActivity :
}

chatViewModel.getCapabilities(conversationUser!!, roomToken, currentConversation!!)
}.collect()

if (conversationModel.lastPinnedId != null &&
conversationModel.lastPinnedId != 0L &&
conversationModel.lastPinnedId != conversationModel.hiddenPinnedId
) {
chatViewModel
.getIndividualMessageFromServer(
credentials!!,
conversationUser?.baseUrl!!,
roomToken,
conversationModel.lastPinnedId.toString()
)
.collect { message ->
message?.let {
binding.pinnedMessageContainer.visibility = View.VISIBLE
binding.pinnedMessageComposeView.setContent {
PinnedMessageView(message)
}
}
}
} else {
binding.pinnedMessageContainer.visibility = View.GONE
}
}
}

chatViewModel.getRoomViewState.observe(this) { state ->
Expand Down Expand Up @@ -1130,6 +1178,10 @@ class ChatActivity :
val item = adapter?.items?.get(index)?.item
item?.let {
setMessageAsEdited(item as ChatMessage, newString)

if (item.jsonMessageId.toLong() == currentConversation?.lastPinnedId) {
chatViewModel.getRoom(roomToken)
}
}
}

Expand Down Expand Up @@ -1313,6 +1365,94 @@ class ChatActivity :
}
}

@Composable
private fun PinnedMessageView(message: ChatMessage) {
message.incoming = true
val pinnedBy = stringResource(R.string.pinned_by)
message.actorDisplayName = "${message.actorDisplayName}\n$pinnedBy ${message.pinnedActorDisplayName}"
val scrollState = rememberScrollState()

val outgoingBubbleColor = remember {
val colorInt = viewThemeUtils.talk
.getOutgoingMessageBubbleColor(context, message.isDeleted, false)

Color(colorInt)
}

val incomingBubbleColor = remember {
val colorInt = resources
.getColor(R.color.bg_message_list_incoming_bubble, null)

Color(colorInt)
}

val isAllowed = remember {
ConversationUtils.isParticipantOwnerOrModerator(currentConversation!!)
}

Column(
verticalArrangement = Arrangement.spacedBy((-16).dp),
modifier = Modifier
) {
Box(
modifier = Modifier
.shadow(4.dp, shape = RoundedCornerShape(16.dp))
.background(incomingBubbleColor, RoundedCornerShape(16.dp))
.padding(16.dp)
.verticalScroll(scrollState)
) {
ComposeChatAdapter().GetComposableForMessage(message)
}

Row(
modifier = Modifier
.padding(start = 16.dp)
.background(outgoingBubbleColor, RoundedCornerShape(16.dp))
.padding(16.dp)
) {
val hiddenEye = painterResource(R.drawable.ic_eye_off)
Icon(
hiddenEye,
"Hide pin",
modifier = Modifier
.size(16.dp)
.clickable {
hidePinnedMessage(message)
}
)

if (isAllowed) {
Spacer(modifier = Modifier.size(16.dp))
val read = painterResource(R.drawable.keep_off_24px)
Icon(
read,
"Unpin",
modifier = Modifier
.size(16.dp)
.clickable {
unPinMessage(message)
}
)
}

val pinnedUntilStr = stringResource(R.string.pinned_until)
val pinnedIndefinitely = stringResource(R.string.pinned_indefinitely)
val pinnedText = message.pinnedUntil?.let {
val format = if (DateFormat.is24HourFormat(context)) "EEE, HH:mm" else "EEE, hh:mm a"
val localDateTime = Instant.ofEpochMilli(it)
.atZone(ZoneId.systemDefault())
.toLocalDateTime()

val timeString = localDateTime.format(DateTimeFormatter.ofPattern(format))

"$pinnedUntilStr $timeString"
} ?: pinnedIndefinitely

Text(pinnedText, modifier = Modifier.padding(start = 16.dp))
}
}
}

private fun removeUnreadMessagesMarker() {
removeMessageById(UNREAD_MESSAGES_MARKER_ID.toString())
}
Expand Down Expand Up @@ -3915,6 +4055,32 @@ class ChatActivity :
}
}

fun hidePinnedMessage(message: ChatMessage) {
val url = ApiUtils.getUrlForChatMessagePinning(chatApiVersion, conversationUser?.baseUrl, roomToken, message.id)
chatViewModel.hidePinnedMessage(credentials!!, url)
}

fun pinMessage(message: ChatMessage) {
val url = ApiUtils.getUrlForChatMessagePinning(chatApiVersion, conversationUser?.baseUrl, roomToken, message.id)
binding.genericComposeView.apply {
val shouldDismiss = mutableStateOf(false)
setContent {
GetPinnedOptionsDialog(shouldDismiss, context, viewThemeUtils) { zonedDateTime ->
zonedDateTime?.let {
chatViewModel.pinMessage(credentials!!, url, pinUntil = zonedDateTime.toEpochSecond().toInt())
} ?: chatViewModel.pinMessage(credentials!!, url)

shouldDismiss.value = true
}
}
}
}

fun unPinMessage(message: ChatMessage) {
val url = ApiUtils.getUrlForChatMessagePinning(chatApiVersion, conversationUser?.baseUrl, roomToken, message.id)
chatViewModel.unPinMessage(credentials!!, url)
}

fun markAsUnread(message: IMessage?) {
val chatMessage = message as ChatMessage?
if (chatMessage!!.previousMessageId > NO_PREVIOUS_MESSAGE_ID) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow

@Suppress("TooManyFunctions")
interface ChatMessageRepository : LifecycleAwareManager {

/**
Expand Down Expand Up @@ -116,4 +117,10 @@ interface ChatMessageRepository : LifecycleAwareManager {
suspend fun sendUnsentChatMessages(credentials: String, url: String)

suspend fun deleteTempMessage(chatMessage: ChatMessage)

suspend fun pinMessage(credentials: String, url: String, pinUntil: Int): Flow<ChatMessage?>

suspend fun unPinMessage(credentials: String, url: String): Flow<ChatMessage?>

suspend fun hidePinnedMessage(credentials: String, url: String): Flow<Boolean>
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ data class ChatMessage(

var lastEditTimestamp: Long? = 0,

var incoming: Boolean = false,

var isDownloadingVoiceMessage: Boolean = false,

var resetVoiceMessage: Boolean = false,
Expand Down Expand Up @@ -130,7 +132,17 @@ data class ChatMessage(

var sendStatus: SendStatus? = null,

var silent: Boolean = false
var silent: Boolean = false,

var pinnedActorType: String? = null,

var pinnedActorId: String? = null,

var pinnedActorDisplayName: String? = null,

var pinnedAt: Long? = null,

var pinnedUntil: Long? = null

) : MessageContentType,
MessageContentType.Image {
Expand Down Expand Up @@ -433,7 +445,9 @@ data class ChatMessage(
FEDERATED_USER_ADDED,
FEDERATED_USER_REMOVED,
PHONE_ADDED,
THREAD_CREATED
THREAD_CREATED,
MESSAGE_PINNED,
MESSAGE_UNPINNED
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,9 @@ interface ChatNetworkDataSource {
): List<ChatMessageJson>
suspend fun getOpenGraph(credentials: String, baseUrl: String, extractedLinkToPreview: String): Reference?
suspend fun unbindRoom(credentials: String, baseUrl: String, roomToken: String): GenericOverall
suspend fun pinMessage(credentials: String, url: String, pinUntil: Int): ChatOverallSingleMessage

suspend fun unPinMessage(credentials: String, url: String): ChatOverallSingleMessage

suspend fun hidePinnedMessage(credentials: String, url: String): GenericOverall
}
Original file line number Diff line number Diff line change
Expand Up @@ -1020,6 +1020,36 @@ class OfflineFirstChatRepository @Inject constructor(
_removeMessageFlow.emit(chatMessage)
}

override suspend fun pinMessage(credentials: String, url: String, pinUntil: Int): Flow<ChatMessage?> =
flow {
runCatching {
val overall = network.pinMessage(credentials, url, pinUntil)
emit(overall.ocs?.data?.asModel())
}.getOrElse { throwable ->
Log.e(TAG, "Error in pinMessage: $throwable")
}
}

override suspend fun unPinMessage(credentials: String, url: String): Flow<ChatMessage?> =
flow {
runCatching {
val overall = network.unPinMessage(credentials, url)
emit(overall.ocs?.data?.asModel())
}.getOrElse { throwable ->
Log.e(TAG, "Error in unPinMessage: $throwable")
}
}

override suspend fun hidePinnedMessage(credentials: String, url: String): Flow<Boolean> =
flow {
runCatching {
network.hidePinnedMessage(credentials, url)
emit(true)
}.getOrElse { throwable ->
Log.e(TAG, "Error in hidePinnedMessage: $throwable")
}
}

@Suppress("Detekt.TooGenericExceptionCaught")
override suspend fun addTemporaryMessage(
message: CharSequence,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,4 +222,13 @@ class RetrofitChatNetwork(private val ncApi: NcApi, private val ncApiCoroutines:
val url = ApiUtils.getUrlForUnbindingRoom(baseUrl, roomToken)
return ncApiCoroutines.unbindRoom(credentials, url)
}

override suspend fun pinMessage(credentials: String, url: String, pinUntil: Int): ChatOverallSingleMessage =
ncApiCoroutines.pinMessage(credentials, url, pinUntil)

override suspend fun unPinMessage(credentials: String, url: String): ChatOverallSingleMessage =
ncApiCoroutines.unPinMessage(credentials, url)

override suspend fun hidePinnedMessage(credentials: String, url: String): GenericOverall =
ncApiCoroutines.hidePinnedMessage(credentials, url)
}
Loading
Loading