Przeglądaj źródła

Merge pull request #3904 from nextcloud/issue-3898-ban

🚫 Allow Banning Users and Guests
Julius Linus 9 miesięcy temu
rodzic
commit
9866062704

+ 21 - 3
app/src/main/java/com/nextcloud/talk/api/NcApi.java

@@ -25,6 +25,8 @@ import com.nextcloud.talk.models.json.notifications.NotificationOverall;
 import com.nextcloud.talk.models.json.opengraph.OpenGraphOverall;
 import com.nextcloud.talk.models.json.participants.AddParticipantOverall;
 import com.nextcloud.talk.models.json.participants.ParticipantsOverall;
+import com.nextcloud.talk.models.json.participants.TalkBan;
+import com.nextcloud.talk.models.json.participants.TalkBanOverall;
 import com.nextcloud.talk.models.json.push.PushRegistrationOverall;
 import com.nextcloud.talk.models.json.reactions.ReactionsOverall;
 import com.nextcloud.talk.models.json.reminder.ReminderOverall;
@@ -333,7 +335,7 @@ public interface NcApi {
     */
     @DELETE
     Observable<Void> unregisterDeviceForNotificationsWithProxy(@Url String url,
-                                                               @QueryMap Map<String,String> fields);
+                                                               @QueryMap Map<String, String> fields);
 
     @FormUrlEncoded
     @PUT
@@ -704,9 +706,25 @@ public interface NcApi {
 
     @POST
     Observable<GenericOverall> acceptInvitation(@Header("Authorization") String authorization,
-                                                 @Url String url);
+                                                @Url String url);
 
     @DELETE
     Observable<GenericOverall> rejectInvitation(@Header("Authorization") String authorization,
-                                                   @Url String url);
+                                                @Url String url);
+
+    @GET
+    Observable<TalkBanOverall> listBans(@Header("Authorization") String authorization,
+                                        @Url String url);
+
+    @FormUrlEncoded
+    @POST
+    Observable<TalkBan> banActor(@Header("Authorization") String authorization,
+                                 @Url String url,
+                                 @Field("actorType") String actorType,
+                                 @Field("actorId") String actorId,
+                                 @Field("internalNote") String internalNote);
+
+    @DELETE
+    Observable<GenericOverall> unbanActor(@Header("Authorization") String authorization,
+                                          @Url String url);
 }

+ 15 - 0
app/src/main/java/com/nextcloud/talk/chat/data/ChatRepository.kt

@@ -13,6 +13,7 @@ import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage
 import com.nextcloud.talk.models.json.conversations.RoomOverall
 import com.nextcloud.talk.models.json.conversations.RoomsOverall
 import com.nextcloud.talk.models.json.generic.GenericOverall
+import com.nextcloud.talk.models.json.participants.TalkBan
 import com.nextcloud.talk.models.json.reminder.Reminder
 import io.reactivex.Observable
 import retrofit2.Response
@@ -29,6 +30,7 @@ interface ChatRepository {
         timeStamp: Int,
         chatApiVersion: Int
     ): Observable<Reminder>
+
     fun getReminder(user: User, roomToken: String, messageId: String, apiVersion: Int): Observable<Reminder>
     fun deleteReminder(user: User, roomToken: String, messageId: String, apiVersion: Int): Observable<GenericOverall>
     fun shareToNotes(
@@ -37,6 +39,7 @@ interface ChatRepository {
         message: String,
         displayName: String
     ): Observable<GenericOverall> // last two fields are false
+
     fun checkForNoteToSelf(credentials: String, url: String, includeStatus: Boolean): Observable<RoomsOverall>
     fun shareLocationToNotes(
         credentials: String,
@@ -45,6 +48,7 @@ interface ChatRepository {
         objectId: String,
         metadata: String
     ): Observable<GenericOverall>
+
     fun leaveRoom(credentials: String, url: String): Observable<GenericOverall>
     fun sendChatMessage(
         credentials: String,
@@ -54,9 +58,20 @@ interface ChatRepository {
         replyTo: Int,
         sendWithoutNotification: Boolean
     ): Observable<GenericOverall>
+
     fun pullChatMessages(credentials: String, url: String, fieldMap: HashMap<String, Int>): Observable<Response<*>>
     fun deleteChatMessage(credentials: String, url: String): Observable<ChatOverallSingleMessage>
     fun createRoom(credentials: String, url: String, map: Map<String, String>): Observable<RoomOverall>
     fun setChatReadMarker(credentials: String, url: String, previousMessageId: Int): Observable<GenericOverall>
     fun editChatMessage(credentials: String, url: String, text: String): Observable<ChatOverallSingleMessage>
+    fun listBans(credentials: String, url: String): Observable<List<TalkBan>>
+    fun banActor(
+        credentials: String,
+        url: String,
+        actorType: String,
+        actorId: String,
+        internalNote: String
+    ): Observable<TalkBan>
+
+    fun unbanActor(credentials: String, url: String): Observable<GenericOverall>
 }

+ 19 - 0
app/src/main/java/com/nextcloud/talk/chat/data/network/NetworkChatRepositoryImpl.kt

@@ -15,6 +15,7 @@ import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage
 import com.nextcloud.talk.models.json.conversations.RoomOverall
 import com.nextcloud.talk.models.json.conversations.RoomsOverall
 import com.nextcloud.talk.models.json.generic.GenericOverall
+import com.nextcloud.talk.models.json.participants.TalkBan
 import com.nextcloud.talk.models.json.reminder.Reminder
 import com.nextcloud.talk.utils.ApiUtils
 import io.reactivex.Observable
@@ -179,4 +180,22 @@ class NetworkChatRepositoryImpl(private val ncApi: NcApi) : ChatRepository {
     override fun editChatMessage(credentials: String, url: String, text: String): Observable<ChatOverallSingleMessage> {
         return ncApi.editChatMessage(credentials, url, text).map { it }
     }
+
+    override fun listBans(credentials: String, url: String): Observable<List<TalkBan>> {
+        return ncApi.listBans(credentials, url).map { it.ocs?.data }
+    }
+
+    override fun banActor(
+        credentials: String,
+        url: String,
+        actorType: String,
+        actorId: String,
+        internalNote: String
+    ): Observable<TalkBan> {
+        return ncApi.banActor(credentials, url, actorType, actorId, internalNote)
+    }
+
+    override fun unbanActor(credentials: String, url: String): Observable<GenericOverall> {
+        return ncApi.unbanActor(credentials, url)
+    }
 }

+ 102 - 11
app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt

@@ -22,6 +22,7 @@ import android.view.View
 import android.view.View.GONE
 import android.view.View.VISIBLE
 import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.FragmentTransaction
 import androidx.lifecycle.ViewModelProvider
 import androidx.work.Data
 import androidx.work.OneTimeWorkRequest
@@ -47,6 +48,7 @@ import com.nextcloud.talk.conversationinfo.viewmodel.ConversationInfoViewModel
 import com.nextcloud.talk.conversationinfoedit.ConversationInfoEditActivity
 import com.nextcloud.talk.data.user.model.User
 import com.nextcloud.talk.databinding.ActivityConversationInfoBinding
+import com.nextcloud.talk.databinding.DialogBanActorBinding
 import com.nextcloud.talk.events.EventStatus
 import com.nextcloud.talk.extensions.loadConversationAvatar
 import com.nextcloud.talk.extensions.loadNoteToSelfAvatar
@@ -60,6 +62,7 @@ import com.nextcloud.talk.models.domain.LobbyState
 import com.nextcloud.talk.models.domain.NotificationLevel
 import com.nextcloud.talk.models.domain.converters.DomainEnumNotificationLevelConverter
 import com.nextcloud.talk.models.json.capabilities.SpreedCapability
+import com.nextcloud.talk.models.json.converters.EnumActorTypeConverter
 import com.nextcloud.talk.models.json.generic.GenericOverall
 import com.nextcloud.talk.models.json.participants.Participant
 import com.nextcloud.talk.models.json.participants.Participant.ActorType.CIRCLES
@@ -68,6 +71,7 @@ import com.nextcloud.talk.models.json.participants.Participant.ActorType.USERS
 import com.nextcloud.talk.models.json.participants.ParticipantsOverall
 import com.nextcloud.talk.repositories.conversations.ConversationsRepository
 import com.nextcloud.talk.shareditems.activities.SharedItemsActivity
+import com.nextcloud.talk.ui.dialog.DialogBanListFragment
 import com.nextcloud.talk.utils.ApiUtils
 import com.nextcloud.talk.utils.CapabilitiesUtil
 import com.nextcloud.talk.utils.ConversationUtils
@@ -181,6 +185,7 @@ class ConversationInfoActivity :
         binding.leaveConversationAction.setOnClickListener { leaveConversation() }
         binding.clearConversationHistory.setOnClickListener { showClearHistoryDialog() }
         binding.addParticipantsAction.setOnClickListener { addParticipants() }
+        binding.listBansButton.setOnClickListener { listBans() }
 
         viewModel.getRoom(conversationUser, conversationToken)
 
@@ -233,6 +238,20 @@ class ConversationInfoActivity :
                 else -> {}
             }
         }
+
+        viewModel.getBanActorState.observe(this) { state ->
+            when (state) {
+                is ConversationInfoViewModel.BanActorSuccessState -> {
+                    getListOfParticipants() // Refresh the list of participants
+                }
+
+                ConversationInfoViewModel.BanActorErrorState -> {
+                    Snackbar.make(binding.root, "Error banning actor", Snackbar.LENGTH_SHORT).show()
+                }
+
+                else -> {}
+            }
+        }
     }
 
     private fun setupActionBar() {
@@ -569,6 +588,17 @@ class ConversationInfoActivity :
             })
     }
 
+    private fun listBans() {
+        val fragmentManager = supportFragmentManager
+        val newFragment = DialogBanListFragment(conversationToken)
+        val transaction = fragmentManager.beginTransaction()
+        transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
+        transaction
+            .add(android.R.id.content, newFragment)
+            .addToBackStack(null)
+            .commit()
+    }
+
     private fun addParticipants() {
         val bundle = Bundle()
         val existingParticipantsId = arrayListOf<String>()
@@ -734,6 +764,15 @@ class ConversationInfoActivity :
                 binding.notificationSettingsView.callNotificationsSwitch.visibility = GONE
             }
 
+            binding.listBansButton.visibility =
+                if (ConversationUtils.canModerate(conversationCopy, spreedCapabilities) &&
+                    ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL != conversation!!.type
+                ) {
+                    VISIBLE
+                } else {
+                    GONE
+                }
+
             if (conversation!!.notificationCalls === null) {
                 binding.notificationSettingsView.callNotificationsSwitch.visibility = GONE
             } else {
@@ -1068,6 +1107,10 @@ class ConversationInfoActivity :
         }
     }
 
+    private fun banActor(actorType: String, actorId: String, internalNote: String) {
+        viewModel.banActor(conversationUser, conversationToken, actorType, actorId, internalNote)
+    }
+
     private fun removeAttendeeFromConversation(apiVersion: Int, participant: Participant) {
         if (apiVersion >= ApiUtils.API_V4) {
             ncApi.removeAttendeeFromConversation(
@@ -1264,6 +1307,15 @@ class ConversationInfoActivity :
             )
         )
 
+        if (CapabilitiesUtil.isBanningAvailable(conversationUser.capabilities?.spreedCapability!!)) {
+            items.add(
+                BasicListItemWithImage(
+                    R.drawable.baseline_block_24,
+                    "Ban Participant"
+                )
+            )
+        }
+
         if (participant.type == Participant.ParticipantType.MODERATOR ||
             participant.type == Participant.ParticipantType.GUEST_MODERATOR
         ) {
@@ -1296,18 +1348,24 @@ class ConversationInfoActivity :
                         actionToTrigger++
                     }
 
-                    if (actionToTrigger == 0) {
-                        // Pin, nothing to do
-                    } else if (actionToTrigger == 1) {
-                        // Promote/demote
-                        if (apiVersion >= ApiUtils.API_V4) {
-                            toggleModeratorStatus(apiVersion, participant)
-                        } else {
-                            toggleModeratorStatusLegacy(apiVersion, participant)
+                    when (actionToTrigger) {
+                        DEMOTE_OR_PROMOTE -> {
+                            if (apiVersion >= ApiUtils.API_V4) {
+                                toggleModeratorStatus(apiVersion, participant)
+                            } else {
+                                toggleModeratorStatusLegacy(apiVersion, participant)
+                            }
                         }
-                    } else if (actionToTrigger == 2) {
-                        // Remove from conversation
-                        removeAttendeeFromConversation(apiVersion, participant)
+
+                        REMOVE_FROM_CONVERSATION -> {
+                            removeAttendeeFromConversation(apiVersion, participant)
+                        }
+
+                        BAN_FROM_CONVERSATION -> {
+                            handleBan(participant)
+                        }
+
+                        else -> {}
                     }
                 }
             }
@@ -1315,6 +1373,36 @@ class ConversationInfoActivity :
         return true
     }
 
+    private fun MaterialDialog.handleBan(participant: Participant) {
+        val apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.API_V4, 1))
+        val binding = DialogBanActorBinding.inflate(layoutInflater)
+        val actorTypeConverter = EnumActorTypeConverter()
+        val dialog = MaterialAlertDialogBuilder(context)
+            .setView(binding.root)
+            .create()
+        binding.avatarImage.loadUserAvatar(
+            conversationUser,
+            participant.actorId!!,
+            true,
+            false
+        )
+        binding.displayNameText.text = participant.actorId
+        binding.buttonBan.setOnClickListener {
+            banActor(
+                actorTypeConverter.convertToString(participant.actorType!!),
+                participant.actorId!!,
+                binding.banActorEdit.text.toString()
+            )
+            removeAttendeeFromConversation(apiVersion, participant)
+            dialog.dismiss()
+        }
+        binding.buttonClose.setOnClickListener { dialog.dismiss() }
+        viewThemeUtils.material.colorTextInputLayout(binding.banActorEditLayout)
+        viewThemeUtils.material.colorMaterialButtonPrimaryFilled(binding.buttonBan)
+        viewThemeUtils.material.colorMaterialButtonText(binding.buttonClose)
+        dialog.show()
+    }
+
     private fun setUpNotificationSettings(module: DatabaseStorageModule) {
         binding.notificationSettingsView.notificationSettingsImportantConversation.setOnClickListener {
             val isChecked = binding.notificationSettingsView.importantConversationSwitch.isChecked
@@ -1353,6 +1441,9 @@ class ConversationInfoActivity :
         private const val LOW_EMPHASIS_OPACITY: Float = 0.38f
         private const val RECORDING_CONSENT_NOT_REQUIRED_FOR_CONVERSATION: Int = 0
         private const val RECORDING_CONSENT_REQUIRED_FOR_CONVERSATION: Int = 1
+        private const val DEMOTE_OR_PROMOTE = 1
+        private const val REMOVE_FROM_CONVERSATION = 2
+        private const val BAN_FROM_CONVERSATION = 3
     }
 
     /**

+ 98 - 1
app/src/main/java/com/nextcloud/talk/conversationinfo/viewmodel/ConversationInfoViewModel.kt

@@ -16,6 +16,9 @@ import com.nextcloud.talk.chat.data.ChatRepository
 import com.nextcloud.talk.data.user.model.User
 import com.nextcloud.talk.models.domain.ConversationModel
 import com.nextcloud.talk.models.json.capabilities.SpreedCapability
+import com.nextcloud.talk.models.json.generic.GenericOverall
+import com.nextcloud.talk.models.json.participants.TalkBan
+import com.nextcloud.talk.utils.ApiUtils
 import io.reactivex.Observer
 import io.reactivex.android.schedulers.AndroidSchedulers
 import io.reactivex.disposables.Disposable
@@ -31,8 +34,9 @@ class ConversationInfoViewModel @Inject constructor(
             PAUSED,
             RESUMED
         }
+
         lateinit var currentLifeCycleFlag: LifeCycleFlag
-        public val disposableSet = mutableSetOf<Disposable>()
+        val disposableSet = mutableSetOf<Disposable>()
 
         override fun onResume(owner: LifecycleOwner) {
             super.onResume(owner)
@@ -49,6 +53,27 @@ class ConversationInfoViewModel @Inject constructor(
 
     sealed interface ViewState
 
+    class ListBansSuccessState(val talkBans: List<TalkBan>) : ViewState
+    object ListBansErrorState : ViewState
+
+    private val _getTalkBanState: MutableLiveData<ViewState> = MutableLiveData()
+    val getTalkBanState: LiveData<ViewState>
+        get() = _getTalkBanState
+
+    class BanActorSuccessState(val talkBan: TalkBan) : ViewState
+    object BanActorErrorState : ViewState
+
+    private val _getBanActorState: MutableLiveData<ViewState> = MutableLiveData()
+    val getBanActorState: LiveData<ViewState>
+        get() = _getBanActorState
+
+    object UnBanActorSuccessState : ViewState
+    object UnBanActorErrorState : ViewState
+
+    private val _getUnBanActorState: MutableLiveData<ViewState> = MutableLiveData()
+    val getUnBanActorState: LiveData<ViewState>
+        get() = _getUnBanActorState
+
     object GetRoomStartState : ViewState
     object GetRoomErrorState : ViewState
     open class GetRoomSuccessState(val conversationModel: ConversationModel) : ViewState
@@ -103,6 +128,78 @@ class ConversationInfoViewModel @Inject constructor(
         }
     }
 
+    fun listBans(user: User, token: String) {
+        val url = ApiUtils.getUrlForBans(user.baseUrl!!, token)
+        chatRepository.listBans(user.getCredentials(), url)
+            .subscribeOn(Schedulers.io())
+            ?.observeOn(AndroidSchedulers.mainThread())
+            ?.subscribe(object : Observer<List<TalkBan>> {
+                override fun onSubscribe(p0: Disposable) {
+                    // unused atm
+                }
+
+                override fun onError(e: Throwable) {
+                    _getTalkBanState.value = ListBansErrorState
+                }
+
+                override fun onComplete() {
+                    // unused atm
+                }
+
+                override fun onNext(talkBans: List<TalkBan>) {
+                    _getTalkBanState.value = ListBansSuccessState(talkBans)
+                }
+            })
+    }
+
+    fun banActor(user: User, token: String, actorType: String, actorId: String, internalNote: String) {
+        val url = ApiUtils.getUrlForBans(user.baseUrl!!, token)
+        chatRepository.banActor(user.getCredentials(), url, actorType, actorId, internalNote)
+            .subscribeOn(Schedulers.io())
+            ?.observeOn(AndroidSchedulers.mainThread())
+            ?.subscribe(object : Observer<TalkBan> {
+                override fun onSubscribe(p0: Disposable) {
+                    // unused atm
+                }
+
+                override fun onError(e: Throwable) {
+                    _getBanActorState.value = BanActorErrorState
+                }
+
+                override fun onComplete() {
+                    // unused atm
+                }
+
+                override fun onNext(talkBan: TalkBan) {
+                    _getBanActorState.value = BanActorSuccessState(talkBan)
+                }
+            })
+    }
+
+    fun unbanActor(user: User, token: String, banId: Int) {
+        val url = ApiUtils.getUrlForUnban(user.baseUrl!!, token, banId)
+        chatRepository.unbanActor(user.getCredentials(), url)
+            .subscribeOn(Schedulers.io())
+            ?.observeOn(AndroidSchedulers.mainThread())
+            ?.subscribe(object : Observer<GenericOverall> {
+                override fun onSubscribe(p0: Disposable) {
+                    // unused atm
+                }
+
+                override fun onError(p0: Throwable) {
+                    _getUnBanActorState.value = UnBanActorErrorState
+                }
+
+                override fun onComplete() {
+                    // unused atm
+                }
+
+                override fun onNext(p0: GenericOverall) {
+                    _getUnBanActorState.value = UnBanActorSuccessState
+                }
+            })
+    }
+
     inner class GetRoomObserver : Observer<ConversationModel> {
         override fun onSubscribe(d: Disposable) {
             // unused atm

+ 40 - 0
app/src/main/java/com/nextcloud/talk/models/json/participants/TalkBan.kt

@@ -0,0 +1,40 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Julius Linus <juliuslinus1@gmail.com>
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package com.nextcloud.talk.models.json.participants
+
+import android.os.Parcelable
+import com.bluelinelabs.logansquare.annotation.JsonField
+import com.bluelinelabs.logansquare.annotation.JsonObject
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+@JsonObject
+data class TalkBan(
+    @JsonField(name = ["id"])
+    var id: String?,
+    @JsonField(name = ["moderatorActorType"])
+    var moderatorActorType: String?,
+    @JsonField(name = ["moderatorActorId"])
+    var moderatorActorId: String?,
+    @JsonField(name = ["moderatorDisplayName"])
+    var moderatorDisplayName: String?,
+    @JsonField(name = ["bannedActorType"])
+    var bannedActorType: String?,
+    @JsonField(name = ["bannedActorId"])
+    var bannedActorId: String?,
+    @JsonField(name = ["bannedDisplayName"])
+    var bannedDisplayName: String?,
+    @JsonField(name = ["bannedTime"])
+    var bannedTime: Int?,
+    @JsonField(name = ["internalNote"])
+    var internalNote: String?
+) : Parcelable {
+    // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
+    constructor() :
+        this(null, null, null, null, null, null, null, null, null)
+}

+ 26 - 0
app/src/main/java/com/nextcloud/talk/models/json/participants/TalkBanOCS.kt

@@ -0,0 +1,26 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Julius Linus <juliuslinus1@gmail.com>
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package com.nextcloud.talk.models.json.participants
+
+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 TalkBanOCS(
+    @JsonField(name = ["meta"])
+    var meta: GenericMeta?,
+    @JsonField(name = ["data"])
+    var data: List<TalkBan>? = null
+) : Parcelable {
+    // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
+    constructor() : this(null, null)
+}

+ 23 - 0
app/src/main/java/com/nextcloud/talk/models/json/participants/TalkBanOverall.kt

@@ -0,0 +1,23 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Julius Linus <juliuslinus1@gmail.com>
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package com.nextcloud.talk.models.json.participants
+
+import android.os.Parcelable
+import com.bluelinelabs.logansquare.annotation.JsonField
+import com.bluelinelabs.logansquare.annotation.JsonObject
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+@JsonObject
+data class TalkBanOverall(
+    @JsonField(name = ["ocs"])
+    var ocs: TalkBanOCS? = null
+) : Parcelable {
+    // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
+    constructor() : this(null)
+}

+ 146 - 0
app/src/main/java/com/nextcloud/talk/ui/dialog/DialogBanListFragment.kt

@@ -0,0 +1,146 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Julius Linus <juliuslinus1@gmail.com>
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package com.nextcloud.talk.ui.dialog
+
+import android.os.Bundle
+import android.text.format.DateUtils
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.BaseAdapter
+import androidx.fragment.app.DialogFragment
+import androidx.lifecycle.ViewModelProvider
+import autodagger.AutoInjector
+import com.google.android.material.snackbar.Snackbar
+import com.nextcloud.talk.R
+import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.conversationinfo.viewmodel.ConversationInfoViewModel
+import com.nextcloud.talk.data.user.model.User
+import com.nextcloud.talk.databinding.BanItemListBinding
+import com.nextcloud.talk.databinding.FragmentDialogBanListBinding
+import com.nextcloud.talk.models.json.participants.TalkBan
+import com.nextcloud.talk.ui.theme.ViewThemeUtils
+import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
+import javax.inject.Inject
+
+@AutoInjector(NextcloudTalkApplication::class)
+class DialogBanListFragment(val roomToken: String) : DialogFragment() {
+
+    lateinit var binding: FragmentDialogBanListBinding
+
+    @Inject
+    lateinit var viewThemeUtils: ViewThemeUtils
+
+    @Inject
+    lateinit var viewModelFactory: ViewModelProvider.Factory
+
+    @Inject
+    lateinit var currentUserProvider: CurrentUserProviderNew
+
+    lateinit var viewModel: ConversationInfoViewModel
+    private lateinit var conversationUser: User
+
+    private val adapter = object : BaseAdapter() {
+        private var bans: List<TalkBan> = mutableListOf()
+
+        fun setItems(items: List<TalkBan>) {
+            bans = items
+        }
+
+        override fun getCount(): Int {
+            return bans.size
+        }
+
+        override fun getItem(position: Int): Any {
+            return bans[position]
+        }
+
+        override fun getItemId(position: Int): Long {
+            return bans[position].bannedTime!!.toLong()
+        }
+
+        override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
+            val binding = BanItemListBinding.inflate(LayoutInflater.from(context))
+            binding.banActorName.text = bans[position].bannedDisplayName
+            val time = bans[position].bannedTime!!.toLong() * ONE_SEC
+            binding.banTime.text = DateUtils.formatDateTime(
+                requireContext(),
+                time,
+                (DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_TIME)
+            )
+            binding.banReason.text = bans[position].internalNote
+            binding.unbanBtn.setOnClickListener {
+                unBanActor(bans[position].id!!.toInt())
+            }
+            return binding.root
+        }
+    }
+
+    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+        NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
+        binding = FragmentDialogBanListBinding.inflate(LayoutInflater.from(context))
+        viewModel =
+            ViewModelProvider(this, viewModelFactory)[ConversationInfoViewModel::class.java]
+        conversationUser = currentUserProvider.currentUser.blockingGet()
+
+        themeView()
+        initObservers()
+        initListeners()
+        getBanList()
+        return binding.root
+    }
+
+    private fun initObservers() {
+        viewModel.getTalkBanState.observe(viewLifecycleOwner) { state ->
+            when (state) {
+                is ConversationInfoViewModel.ListBansSuccessState -> {
+                    adapter.setItems(state.talkBans)
+                    binding.banListView.adapter = adapter
+                }
+
+                is ConversationInfoViewModel.ListBansErrorState -> {}
+                else -> {}
+            }
+        }
+
+        viewModel.getUnBanActorState.observe(viewLifecycleOwner) { state ->
+            when (state) {
+                is ConversationInfoViewModel.UnBanActorSuccessState -> {
+                    getBanList()
+                }
+
+                is ConversationInfoViewModel.UnBanActorErrorState -> {
+                    Snackbar.make(binding.root, getString(R.string.error_unbanning), Snackbar.LENGTH_SHORT).show()
+                }
+
+                else -> {}
+            }
+        }
+    }
+
+    private fun themeView() {
+        viewThemeUtils.platform.colorViewBackground(binding.root)
+    }
+
+    private fun initListeners() {
+        binding.closeBtn.setOnClickListener { dismiss() }
+    }
+
+    private fun getBanList() {
+        viewModel.listBans(conversationUser, roomToken)
+    }
+
+    private fun unBanActor(banId: Int) {
+        viewModel.unbanActor(conversationUser, roomToken, banId)
+    }
+
+    companion object {
+        @JvmStatic
+        fun newInstance(roomToken: String) = DialogBanListFragment(roomToken)
+        const val ONE_SEC = 1000L
+    }
+}

+ 8 - 0
app/src/main/java/com/nextcloud/talk/utils/ApiUtils.kt

@@ -572,4 +572,12 @@ object ApiUtils {
     fun getUrlForRoomCapabilities(version: Int, baseUrl: String?, token: String?): String {
         return getUrlForRooms(version, baseUrl) + "/" + token + "/capabilities"
     }
+
+    fun getUrlForBans(baseUrl: String, token: String): String {
+        return "$baseUrl/ocs/v1.php$SPREED_API_VERSION/ban/$token"
+    }
+
+    fun getUrlForUnban(baseUrl: String, token: String, banId: Int): String {
+        return "${getUrlForBans(baseUrl, token)}/$banId"
+    }
 }

+ 6 - 1
app/src/main/java/com/nextcloud/talk/utils/CapabilitiesUtil.kt

@@ -53,7 +53,8 @@ enum class SpreedFeatures(val value: String) {
     CHAT_PERMISSION("chat-permission"),
     CONVERSATION_PERMISSION("conversation-permissions"),
     FEDERATION_V1("federation-v1"),
-    DELETE_MESSAGES_UNLIMITED("delete-messages-unlimited")
+    DELETE_MESSAGES_UNLIMITED("delete-messages-unlimited"),
+    BAN_V1("ban-v1")
 }
 
 @Suppress("TooManyFunctions")
@@ -213,6 +214,10 @@ object CapabilitiesUtil {
         return RECORDING_CONSENT_NOT_REQUIRED
     }
 
+    fun isBanningAvailable(spreedCapabilities: SpreedCapability): Boolean {
+        return hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.BAN_V1)
+    }
+
     // endregion
 
     //region SpreedCapabilities that can't be used with federation as the settings for them are global

+ 19 - 0
app/src/main/res/drawable/baseline_block_24.xml

@@ -0,0 +1,19 @@
+<!--
+  ~ Nextcloud Talk - Android Client
+  ~
+  ~ SPDX-FileCopyrightText: 2024 Google LLC
+  ~ SPDX-License-Identifier: Apache-2.0
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:height="24dp"
+    android:tint="@color/design_default_color_error"
+    android:viewportHeight="24"
+    android:viewportWidth="24"
+    android:width="24dp">
+
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM4,12c0,-4.42 3.58,-8 8,-8 1.85,0 3.55,0.63 4.9,1.69L5.69,16.9C4.63,15.55 4,13.85 4,12zM12,20c-1.85,0 -3.55,-0.63 -4.9,-1.69L18.31,7.1C19.37,8.45 20,10.15 20,12c0,4.42 -3.58,8 -8,8z" />
+
+</vector>

+ 29 - 0
app/src/main/res/layout/activity_conversation_info.xml

@@ -354,6 +354,35 @@
                     tools:listitem="@layout/rv_item_conversation_info_participant" />
             </LinearLayout>
 
+            <LinearLayout
+                android:id="@+id/list_bans_button"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:paddingStart="@dimen/standard_margin"
+                android:paddingTop="@dimen/standard_half_margin"
+                android:paddingEnd="@dimen/standard_margin"
+                android:paddingBottom="@dimen/standard_half_margin"
+                android:orientation="horizontal"
+                android:background="?android:attr/selectableItemBackground">
+
+
+                <ImageView
+                    android:layout_width="24dp"
+                    android:layout_height="40dp"
+                    android:layout_marginEnd="@dimen/standard_margin"
+                    android:contentDescription="@null"
+                    android:src="@drawable/baseline_block_24"
+                    app:tint="@color/grey_600" />
+
+                <com.google.android.material.textview.MaterialTextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="match_parent"
+                    android:gravity="center_vertical"
+                    android:text="@string/show_banned_participants"
+                    android:textSize="@dimen/headline_text_size" />
+
+            </LinearLayout>
+
             <LinearLayout
                 android:id="@+id/danger_zone_options"
                 android:layout_width="match_parent"

+ 72 - 0
app/src/main/res/layout/ban_item_list.xml

@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Nextcloud Talk - Android Client
+  ~
+  ~ SPDX-FileCopyrightText: 2024 Julius Linus <juliuslinus1@gmail.com>
+  ~ SPDX-License-Identifier: GPL-3.0-or-later
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/ban_item"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="horizontal">
+
+        <LinearLayout
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:orientation="vertical"
+            android:layout_weight="1">
+
+            <com.google.android.material.textview.MaterialTextView
+                android:id="@+id/ban_actor_name"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:ellipsize="end"
+                android:lines="1"
+                android:textAlignment="viewStart"
+                android:textAppearance="@style/ListItem"
+                android:textSize="@dimen/md_title_textsize"
+                tools:text="User 2" />
+
+            <com.google.android.material.textview.MaterialTextView
+                android:id="@+id/ban_time"
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                android:textSize="@dimen/sm_text_size"
+                tools:text="11th August, 2023" />
+
+
+            <com.google.android.material.textview.MaterialTextView
+                android:id="@+id/ban_reason"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                tools:text="Was being mean"
+                android:padding="@dimen/standard_half_padding" />
+
+        </LinearLayout>
+
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/unban_btn"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="@dimen/standard_half_margin"
+            android:layout_gravity="center"
+            android:text="@string/unban"
+            android:textColor="@color/hwSecurityRed"
+            style="@style/Widget.Material3.Button.TextButton"
+            />
+
+</LinearLayout>
+
+
+
+
+
+
+
+
+
+
+
+

+ 107 - 0
app/src/main/res/layout/dialog_ban_actor.xml

@@ -0,0 +1,107 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Nextcloud Talk - Android Client
+  ~
+  ~ SPDX-FileCopyrightText: 2024 Julius Linus <julius.linus@nextcloud.com>
+  ~ SPDX-License-Identifier: GPL-3.0-or-later
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    tools:background="@color/white"
+    tools:visibility="visible">
+
+    <com.google.android.material.textview.MaterialTextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_margin="@dimen/standard_margin"
+        android:text="@string/ban_actor"
+        android:textSize="@dimen/md_title_textsize" />
+
+    <com.google.android.material.divider.MaterialDivider
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" />
+
+    <RelativeLayout
+        android:id="@+id/ban_actor_profile"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="@dimen/standard_margin"
+        android:layout_marginTop="@dimen/standard_quarter_margin"
+        android:layout_marginEnd="@dimen/standard_margin"
+        android:animateLayoutChanges="true"
+        tools:visibility="visible">
+
+        <ImageView
+            android:id="@+id/avatar_image"
+            android:layout_width="@dimen/avatar_size_big"
+            android:layout_height="@dimen/avatar_size_big"
+            android:layout_centerHorizontal="true"
+            android:layout_marginTop="@dimen/standard_margin"
+            android:contentDescription="@string/avatar"
+            tools:src="@drawable/account_circle_48dp" />
+
+        <androidx.emoji2.widget.EmojiTextView
+            android:id="@+id/display_name_text"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_below="@id/avatar_image"
+            android:layout_centerHorizontal="true"
+            android:layout_marginTop="@dimen/margin_between_elements"
+            android:textSize="@dimen/headline_text_size"
+            tools:text="Jane Doe" />
+
+    </RelativeLayout>
+
+    <com.google.android.material.textfield.TextInputLayout
+        android:id="@+id/ban_actor_edit_layout"
+        style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_margin="@dimen/standard_margin"
+        android:hint="@string/internal_note">
+
+        <com.google.android.material.textfield.MaterialAutoCompleteTextView
+            android:id="@+id/ban_actor_edit"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:inputType="textShortMessage"
+            android:lines="1"
+            android:padding="@dimen/standard_half_padding" />
+
+    </com.google.android.material.textfield.TextInputLayout>
+
+    <com.google.android.material.divider.MaterialDivider
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" />
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/standard_half_margin"
+        android:gravity="end"
+        android:orientation="horizontal"
+        android:paddingStart="@dimen/dialog_padding"
+        android:paddingEnd="@dimen/dialog_padding"
+        android:paddingBottom="@dimen/dialog_padding_top_bottom">
+
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/button_close"
+            style="@style/Button.Borderless"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:minHeight="@dimen/min_size_clickable_area"
+            android:text="@string/close" />
+
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/button_ban"
+            style="@style/Button.Primary"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:minHeight="@dimen/min_size_clickable_area"
+            android:text="@string/ban" />
+
+    </LinearLayout>
+
+</LinearLayout>

+ 56 - 0
app/src/main/res/layout/fragment_dialog_ban_list.xml

@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Nextcloud Talk - Android Client
+  ~
+  ~ SPDX-FileCopyrightText: 2024 Julius Linus <juliuslinus1@gmail.com>
+  ~ SPDX-License-Identifier: GPL-3.0-or-later
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    tools:context=".ui.dialog.DialogBanListFragment"
+    tools:background="@color/white">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        android:layout_marginHorizontal="@dimen/standard_margin">
+
+        <ImageView
+            android:id="@+id/close_btn"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:contentDescription="@string/close"
+            android:src="@drawable/ic_close_search"/>
+
+        <com.google.android.material.textview.MaterialTextView
+            android:id="@+id/ban_title"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:layout_margin="@dimen/standard_margin"
+            android:text="@string/bans_list"
+            android:textSize="@dimen/md_title_textsize" />
+    </LinearLayout>
+
+
+    <com.google.android.material.divider.MaterialDivider
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" />
+
+    <ListView
+        android:id="@+id/ban_list_view"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:padding="@dimen/standard_padding">
+
+    </ListView>
+
+
+
+</LinearLayout>

+ 1 - 0
app/src/main/res/values/dimens.xml

@@ -83,6 +83,7 @@
     <dimen name="notification_icon_width">24dp</dimen>
     <dimen name="notification_icon_height">24dp</dimen>
     <dimen name="notification_icon_layout_right_end_margin">21dp</dimen>
+    <dimen name="sm_text_size">12sp</dimen>
 
 
 </resources>

+ 8 - 1
app/src/main/res/values/strings.xml

@@ -336,7 +336,6 @@ How to translate with transifex:
     <string name="nc_add_emojis">Add emojis</string>
 
 
-
     <string name="nc_push_to_talk">Push-to-talk</string>
     <string name="nc_push_to_talk_desc">With microphone disabled, click&amp;hold to use Push-to-talk</string>
     <string name="nc_configure_cert_auth">Select authentication certificate</string>
@@ -793,4 +792,12 @@ How to translate with transifex:
     <string name="message_last_edited_by">Edited by %1$s</string>
     <string name="share_link_to_conversation">Join conversation %1$s at %2$s</string>
     <string name="nc_conversation_settings">Conversation settings</string>
+    <string name="show_banned_participants">Show Banned Participants</string>
+    <string name="bans_list">Bans List</string>
+    <string name="unban">Unban</string>
+    <string name="internal_note">Internal Note</string>
+    <string name="ban_actor">Ban Actor</string>
+    <string name="ban">Ban</string>
+    <string name="show_ban_reason">Show ban reason</string>
+    <string name="error_unbanning">Error occured when unbanning actor</string>
 </resources>