mirror of
https://github.com/nextcloud/talk-android
synced 2025-07-08 21:40:43 +01:00
add option to create thread
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
This commit is contained in:
parent
fd10937d68
commit
db62bd051d
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
@ -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
|
@ -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
|
@ -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)
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
@ -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 {
|
||||||
|
@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
Loading…
Reference in New Issue
Block a user