add option to create thread

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
This commit is contained in:
Marcel Hibbe 2025-07-04 16:32:57 +02:00
parent fd10937d68
commit db62bd051d
No known key found for this signature in database
GPG Key ID: C793F8B59F43CE7B
13 changed files with 247 additions and 0 deletions

View File

@ -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.participants.TalkBanOverall
import com.nextcloud.talk.models.json.profile.ProfileOverall import com.nextcloud.talk.models.json.profile.ProfileOverall
import com.nextcloud.talk.models.json.testNotification.TestNotificationOverall 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 com.nextcloud.talk.models.json.userAbsence.UserAbsenceOverall
import okhttp3.MultipartBody import okhttp3.MultipartBody
import okhttp3.RequestBody import okhttp3.RequestBody
@ -285,4 +286,7 @@ interface NcApiCoroutines {
@DELETE @DELETE
suspend fun unbindRoom(@Header("Authorization") authorization: String, @Url url: String): GenericOverall suspend fun unbindRoom(@Header("Authorization") authorization: String, @Url url: String): GenericOverall
@POST
suspend fun createThread(@Header("Authorization") authorization: String, @Url url: String): ThreadOverall
} }

View File

@ -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() { override fun joinAudioCall() {
startACall(true, false) startACall(true, false)
} }

View File

@ -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.generic.GenericOverall
import com.nextcloud.talk.models.json.opengraph.Reference import com.nextcloud.talk.models.json.opengraph.Reference
import com.nextcloud.talk.models.json.reminder.Reminder 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.models.json.userAbsence.UserAbsenceOverall
import io.reactivex.Observable import io.reactivex.Observable
import retrofit2.Response import retrofit2.Response
@ -65,6 +66,7 @@ interface ChatNetworkDataSource {
fun pullChatMessages(credentials: String, url: String, fieldMap: HashMap<String, Int>): Observable<Response<*>> fun pullChatMessages(credentials: String, url: String, fieldMap: HashMap<String, Int>): Observable<Response<*>>
fun deleteChatMessage(credentials: String, url: String): Observable<ChatOverallSingleMessage> fun deleteChatMessage(credentials: String, url: String): Observable<ChatOverallSingleMessage>
fun createRoom(credentials: String, url: String, map: Map<String, String>): Observable<RoomOverall> fun createRoom(credentials: String, url: String, map: Map<String, String>): Observable<RoomOverall>
suspend fun createThread(credentials: String, url: String): ThreadOverall
fun setChatReadMarker(credentials: String, url: String, previousMessageId: Int): Observable<GenericOverall> fun setChatReadMarker(credentials: String, url: String, previousMessageId: Int): Observable<GenericOverall>
suspend fun editChatMessage(credentials: String, url: String, text: String): ChatOverallSingleMessage suspend fun editChatMessage(credentials: String, url: String, text: String): ChatOverallSingleMessage
suspend fun getOutOfOfficeStatusForUser(credentials: String, baseUrl: String, userId: String): UserAbsenceOverall suspend fun getOutOfOfficeStatusForUser(credentials: String, baseUrl: String, userId: String): UserAbsenceOverall

View File

@ -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.generic.GenericOverall
import com.nextcloud.talk.models.json.opengraph.Reference import com.nextcloud.talk.models.json.opengraph.Reference
import com.nextcloud.talk.models.json.reminder.Reminder 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.models.json.userAbsence.UserAbsenceOverall
import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.message.SendMessageUtils import com.nextcloud.talk.utils.message.SendMessageUtils
@ -174,6 +175,9 @@ class RetrofitChatNetwork(
it it
} }
override suspend fun createThread(credentials: String, url: String): ThreadOverall =
ncApiCoroutines.createThread(credentials, url)
override fun setChatReadMarker( override fun setChatReadMarker(
credentials: String, credentials: String,
url: String, url: String,

View File

@ -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.generic.GenericOverall
import com.nextcloud.talk.models.json.opengraph.Reference import com.nextcloud.talk.models.json.opengraph.Reference
import com.nextcloud.talk.models.json.reminder.Reminder 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.models.json.userAbsence.UserAbsenceData
import com.nextcloud.talk.repositories.reactions.ReactionsRepository import com.nextcloud.talk.repositories.reactions.ReactionsRepository
import com.nextcloud.talk.ui.PlaybackSpeed import com.nextcloud.talk.ui.PlaybackSpeed
@ -51,6 +52,8 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
@ -158,6 +161,9 @@ class ChatViewModel @Inject constructor(
val getContextChatMessages: LiveData<List<ChatMessageJson>> val getContextChatMessages: LiveData<List<ChatMessageJson>>
get() = _getContextChatMessages get() = _getContextChatMessages
private val _threadCreationState = MutableStateFlow<ThreadCreationUiState>(ThreadCreationUiState.None)
val threadCreationState: StateFlow<ThreadCreationUiState> = _threadCreationState
val getOpenGraph: LiveData<Reference> val getOpenGraph: LiveData<Reference>
get() = _getOpenGraph get() = _getOpenGraph
private val _getOpenGraph: MutableLiveData<Reference> = MutableLiveData() private val _getOpenGraph: MutableLiveData<Reference> = 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) { fun loadMessages(withCredentials: String, withUrl: String) {
val bundle = Bundle() val bundle = Bundle()
bundle.putString(BundleKeys.KEY_CHAT_URL, withUrl) bundle.putString(BundleKeys.KEY_CHAT_URL, withUrl)
@ -873,4 +886,10 @@ class ChatViewModel @Inject constructor(
data class Success(val statusCode: Int) : UnbindRoomUiState() data class Success(val statusCode: Int) : UnbindRoomUiState()
data class Error(val message: String) : 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()
}
} }

View File

@ -0,0 +1,28 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
* 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

View File

@ -0,0 +1,31 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
* 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

View File

@ -0,0 +1,29 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
* 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

View File

@ -0,0 +1,25 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
* 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)
}

View File

@ -0,0 +1,22 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
* 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)
}

View File

@ -144,6 +144,7 @@ class MessageActionsDialog(
currentConversation?.type != ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL && currentConversation?.type != ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL &&
isOnline isOnline
) )
initMenuReplyInThread(true) // TODO: set visibility
initMenuOpenThread(message.isThread && chatActivity.threadId == null) initMenuOpenThread(message.isThread && chatActivity.threadId == null)
initMenuEditMessage(isMessageEditable) initMenuEditMessage(isMessageEditable)
initMenuDeleteMessage(showMessageDeletionButton && isOnline) initMenuDeleteMessage(showMessageDeletionButton && isOnline)
@ -412,6 +413,17 @@ class MessageActionsDialog(
dialogMessageActionsBinding.menuReplyPrivately.visibility = getVisibility(visible) 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) { private fun initMenuOpenThread(visible: Boolean) {
if (visible) { if (visible) {
dialogMessageActionsBinding.menuOpenThread.setOnClickListener { dialogMessageActionsBinding.menuOpenThread.setOnClickListener {

View File

@ -641,4 +641,12 @@ object ApiUtils {
fun getUrlForProfile(baseUrl: String, userId: String): String { fun getUrlForProfile(baseUrl: String, userId: String): String {
return "$baseUrl$OCS_API_VERSION/profile/$userId" 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"
}
} }

View File

@ -282,6 +282,39 @@
</LinearLayout> </LinearLayout>
<LinearLayout
android:id="@+id/menu_reply_in_thread"
android:layout_width="match_parent"
android:layout_height="@dimen/bottom_sheet_item_height"
android:background="?android:attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal"
tools:ignore="UseCompoundDrawables">
<ImageView
android:id="@+id/menu_icon_reply_in_thread"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
android:paddingStart="@dimen/standard_padding"
android:paddingEnd="@dimen/zero"
android:src="@drawable/outline_thread_unread_24"
app:tint="@color/high_emphasis_menu_icon" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/menu_text_reply_in_thread"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:paddingStart="@dimen/standard_double_padding"
android:paddingEnd="@dimen/standard_padding"
android:text="@string/reply_in_thread"
android:textAlignment="viewStart"
android:textColor="@color/high_emphasis_text"
android:textSize="@dimen/bottom_sheet_text_size" />
</LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/menu_open_thread" android:id="@+id/menu_open_thread"
android:layout_width="match_parent" android:layout_width="match_parent"