diff --git a/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt b/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt index a2e58dcd1..1135f15d9 100644 --- a/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt +++ b/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt @@ -19,6 +19,7 @@ import com.nextcloud.talk.models.json.participants.TalkBan import com.nextcloud.talk.models.json.participants.TalkBanOverall import com.nextcloud.talk.models.json.profile.ProfileOverall import com.nextcloud.talk.models.json.testNotification.TestNotificationOverall +import com.nextcloud.talk.models.json.threads.ThreadOverall import com.nextcloud.talk.models.json.userAbsence.UserAbsenceOverall import okhttp3.MultipartBody import okhttp3.RequestBody @@ -285,4 +286,7 @@ interface NcApiCoroutines { @DELETE suspend fun unbindRoom(@Header("Authorization") authorization: String, @Url url: String): GenericOverall + + @POST + suspend fun createThread(@Header("Authorization") authorization: String, @Url url: String): ThreadOverall } diff --git a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt index 250cd3df2..335fafef6 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -4141,6 +4141,36 @@ class ChatActivity : } } + fun createThread(chatMessage: ChatMessage) { + chatViewModel.createThread( + credentials = conversationUser!!.getCredentials(), + url = ApiUtils.getUrlForThread( + version = chatApiVersion, + baseUrl = conversationUser!!.baseUrl!!, + token = roomToken, + threadId = chatMessage.jsonMessageId + ) + ) + + this.lifecycleScope.launch { + chatViewModel.threadCreationState.collect { uiState -> + when (uiState) { + ChatViewModel.ThreadCreationUiState.None -> { + } + + is ChatViewModel.ThreadCreationUiState.Error -> { + Log.e(TAG, "Error when creating thread") + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + } + + is ChatViewModel.ThreadCreationUiState.Success -> { + openThread(chatMessage) + } + } + } + } + } + override fun joinAudioCall() { startACall(true, false) } diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/network/ChatNetworkDataSource.kt b/app/src/main/java/com/nextcloud/talk/chat/data/network/ChatNetworkDataSource.kt index 7276f4489..2fc854f37 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/data/network/ChatNetworkDataSource.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/data/network/ChatNetworkDataSource.kt @@ -15,6 +15,7 @@ import com.nextcloud.talk.models.json.conversations.RoomOverall import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.opengraph.Reference import com.nextcloud.talk.models.json.reminder.Reminder +import com.nextcloud.talk.models.json.threads.ThreadOverall import com.nextcloud.talk.models.json.userAbsence.UserAbsenceOverall import io.reactivex.Observable import retrofit2.Response @@ -65,6 +66,7 @@ interface ChatNetworkDataSource { fun pullChatMessages(credentials: String, url: String, fieldMap: HashMap): Observable> fun deleteChatMessage(credentials: String, url: String): Observable fun createRoom(credentials: String, url: String, map: Map): Observable + suspend fun createThread(credentials: String, url: String): ThreadOverall fun setChatReadMarker(credentials: String, url: String, previousMessageId: Int): Observable suspend fun editChatMessage(credentials: String, url: String, text: String): ChatOverallSingleMessage suspend fun getOutOfOfficeStatusForUser(credentials: String, baseUrl: String, userId: String): UserAbsenceOverall diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/network/RetrofitChatNetwork.kt b/app/src/main/java/com/nextcloud/talk/chat/data/network/RetrofitChatNetwork.kt index bf36274f6..467879559 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/data/network/RetrofitChatNetwork.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/data/network/RetrofitChatNetwork.kt @@ -17,6 +17,7 @@ import com.nextcloud.talk.models.json.conversations.RoomOverall import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.opengraph.Reference import com.nextcloud.talk.models.json.reminder.Reminder +import com.nextcloud.talk.models.json.threads.ThreadOverall import com.nextcloud.talk.models.json.userAbsence.UserAbsenceOverall import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.message.SendMessageUtils @@ -174,6 +175,9 @@ class RetrofitChatNetwork( it } + override suspend fun createThread(credentials: String, url: String): ThreadOverall = + ncApiCoroutines.createThread(credentials, url) + override fun setChatReadMarker( credentials: String, url: String, diff --git a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt index c829c0542..cf2ff4c23 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt @@ -37,6 +37,7 @@ import com.nextcloud.talk.models.json.conversations.RoomOverall import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.opengraph.Reference import com.nextcloud.talk.models.json.reminder.Reminder +import com.nextcloud.talk.models.json.threads.ThreadInfo import com.nextcloud.talk.models.json.userAbsence.UserAbsenceData import com.nextcloud.talk.repositories.reactions.ReactionsRepository import com.nextcloud.talk.ui.PlaybackSpeed @@ -51,6 +52,8 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow @@ -158,6 +161,9 @@ class ChatViewModel @Inject constructor( val getContextChatMessages: LiveData> get() = _getContextChatMessages + private val _threadCreationState = MutableStateFlow(ThreadCreationUiState.None) + val threadCreationState: StateFlow = _threadCreationState + val getOpenGraph: LiveData get() = _getOpenGraph private val _getOpenGraph: MutableLiveData = MutableLiveData() @@ -424,6 +430,13 @@ class ChatViewModel @Inject constructor( }) } + fun createThread(credentials: String, url: String) { + viewModelScope.launch { + val thread = chatNetworkDataSource.createThread(credentials, url) + _threadCreationState.value = ThreadCreationUiState.Success(thread.ocs?.data) + } + } + fun loadMessages(withCredentials: String, withUrl: String) { val bundle = Bundle() bundle.putString(BundleKeys.KEY_CHAT_URL, withUrl) @@ -873,4 +886,10 @@ class ChatViewModel @Inject constructor( data class Success(val statusCode: Int) : UnbindRoomUiState() data class Error(val message: String) : UnbindRoomUiState() } + + sealed class ThreadCreationUiState { + data object None : ThreadCreationUiState() + data class Success(val thread: ThreadInfo?) : ThreadCreationUiState() + data class Error(val message: String) : ThreadCreationUiState() + } } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/threads/Thread.kt b/app/src/main/java/com/nextcloud/talk/models/json/threads/Thread.kt new file mode 100644 index 000000000..6a912868e --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/threads/Thread.kt @@ -0,0 +1,28 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.threads + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class Thread( + @JsonField(name = ["id"]) + var id: Int = 0, + + @JsonField(name = ["roomId"]) + var roomId: Int = 0, + + @JsonField(name = ["lastMessageId"]) + var lastMessageId: Int = 0, + + @JsonField(name = ["numReplies"]) + var numReplies: Int = 0 +) : Parcelable diff --git a/app/src/main/java/com/nextcloud/talk/models/json/threads/ThreadAttendee.kt b/app/src/main/java/com/nextcloud/talk/models/json/threads/ThreadAttendee.kt new file mode 100644 index 000000000..a9202d1c3 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/threads/ThreadAttendee.kt @@ -0,0 +1,31 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.threads + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class ThreadAttendee( + @JsonField(name = ["notificationLevel"]) + var notificationLevel: Int = 0, + + @JsonField(name = ["lastReadMessage"]) + var lastReadMessage: Int = 0, + + @JsonField(name = ["lastMentionMessage"]) + var lastMentionMessage: Int = 0, + + @JsonField(name = ["lastMentionDirect"]) + var lastMentionDirect: Int = 0, + + @JsonField(name = ["readPrivacy"]) + var readPrivacy: Int = 0 +) : Parcelable diff --git a/app/src/main/java/com/nextcloud/talk/models/json/threads/ThreadInfo.kt b/app/src/main/java/com/nextcloud/talk/models/json/threads/ThreadInfo.kt new file mode 100644 index 000000000..50ea57dbf --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/threads/ThreadInfo.kt @@ -0,0 +1,29 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.threads + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.chat.ChatMessageJson +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class ThreadInfo( + @JsonField(name = ["thread"]) + var thread: Thread? = null, + + @JsonField(name = ["attendee"]) + var attendee: ThreadAttendee? = null, + + @JsonField(name = ["first"]) + var first: ChatMessageJson? = null, + + @JsonField(name = ["last"]) + var last: ChatMessageJson? = null +) : Parcelable diff --git a/app/src/main/java/com/nextcloud/talk/models/json/threads/ThreadOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/threads/ThreadOCS.kt new file mode 100644 index 000000000..56b2d654f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/threads/ThreadOCS.kt @@ -0,0 +1,25 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.threads + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.generic.GenericMeta +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class ThreadOCS( + @JsonField(name = ["meta"]) + var meta: GenericMeta?, + @JsonField(name = ["data"]) + var data: ThreadInfo? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/threads/ThreadOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/threads/ThreadOverall.kt new file mode 100644 index 000000000..c622fcfa4 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/threads/ThreadOverall.kt @@ -0,0 +1,22 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.threads + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class ThreadOverall( + @JsonField(name = ["ocs"]) + var ocs: ThreadOCS? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt index dd744a16a..d08c27aad 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt @@ -144,6 +144,7 @@ class MessageActionsDialog( currentConversation?.type != ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL && isOnline ) + initMenuReplyInThread(true) // TODO: set visibility initMenuOpenThread(message.isThread && chatActivity.threadId == null) initMenuEditMessage(isMessageEditable) initMenuDeleteMessage(showMessageDeletionButton && isOnline) @@ -412,6 +413,17 @@ class MessageActionsDialog( dialogMessageActionsBinding.menuReplyPrivately.visibility = getVisibility(visible) } + private fun initMenuReplyInThread(visible: Boolean) { + if (visible) { + dialogMessageActionsBinding.menuReplyInThread.setOnClickListener { + chatActivity.createThread(message) + dismiss() + } + } + + dialogMessageActionsBinding.menuReplyInThread.visibility = getVisibility(visible) + } + private fun initMenuOpenThread(visible: Boolean) { if (visible) { dialogMessageActionsBinding.menuOpenThread.setOnClickListener { diff --git a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.kt index fe258be3c..b18fe98ad 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.kt @@ -641,4 +641,12 @@ object ApiUtils { fun getUrlForProfile(baseUrl: String, userId: String): String { return "$baseUrl$OCS_API_VERSION/profile/$userId" } + + fun getUrlForThreads(version: Int, baseUrl: String?, token: String): String { + return getUrlForApi(version, baseUrl) + "/chat/" + token + "/threads" + } + + fun getUrlForThread(version: Int, baseUrl: String?, token: String, threadId: Int): String { + return getUrlForThreads(version, baseUrl, token) + "/$threadId" + } } diff --git a/app/src/main/res/layout/dialog_message_actions.xml b/app/src/main/res/layout/dialog_message_actions.xml index cc3a081e7..6d20dc4be 100644 --- a/app/src/main/res/layout/dialog_message_actions.xml +++ b/app/src/main/res/layout/dialog_message_actions.xml @@ -282,6 +282,39 @@ + + + + + + + +