From 46d3fd1569b951998848f0e0a0eae749aeb92c3d Mon Sep 17 00:00:00 2001 From: rapterjet2004 Date: Wed, 8 May 2024 11:27:21 -0500 Subject: [PATCH] Allows Banning - New option to ban participant if your a moderator and not in one-2-one - New fragment to see previous bans, unban if wanted Signed-off-by: rapterjet2004 --- .../java/com/nextcloud/talk/api/NcApi.java | 24 ++- .../talk/chat/data/ChatRepository.kt | 15 ++ .../data/network/NetworkChatRepositoryImpl.kt | 19 +++ .../ConversationInfoActivity.kt | 113 ++++++++++++-- .../viewmodel/ConversationInfoViewModel.kt | 99 +++++++++++- .../talk/models/json/participants/TalkBan.kt | 40 +++++ .../models/json/participants/TalkBanOCS.kt | 26 ++++ .../json/participants/TalkBanOverall.kt | 23 +++ .../talk/ui/dialog/DialogBanListFragment.kt | 146 ++++++++++++++++++ .../java/com/nextcloud/talk/utils/ApiUtils.kt | 8 + .../nextcloud/talk/utils/CapabilitiesUtil.kt | 7 +- .../main/res/drawable/baseline_block_24.xml | 19 +++ .../res/layout/activity_conversation_info.xml | 29 ++++ app/src/main/res/layout/ban_item_list.xml | 72 +++++++++ app/src/main/res/layout/dialog_ban_actor.xml | 107 +++++++++++++ .../res/layout/fragment_dialog_ban_list.xml | 56 +++++++ app/src/main/res/values/dimens.xml | 1 + app/src/main/res/values/strings.xml | 9 +- 18 files changed, 796 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/participants/TalkBan.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/participants/TalkBanOCS.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/participants/TalkBanOverall.kt create mode 100644 app/src/main/java/com/nextcloud/talk/ui/dialog/DialogBanListFragment.kt create mode 100644 app/src/main/res/drawable/baseline_block_24.xml create mode 100644 app/src/main/res/layout/ban_item_list.xml create mode 100644 app/src/main/res/layout/dialog_ban_actor.xml create mode 100644 app/src/main/res/layout/fragment_dialog_ban_list.xml diff --git a/app/src/main/java/com/nextcloud/talk/api/NcApi.java b/app/src/main/java/com/nextcloud/talk/api/NcApi.java index ea3f247181..f0957ef4d3 100644 --- a/app/src/main/java/com/nextcloud/talk/api/NcApi.java +++ b/app/src/main/java/com/nextcloud/talk/api/NcApi.java @@ -25,6 +25,8 @@ 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 @@ Observable registerDeviceForNotificationsWithPushProxy(@Url String url, */ @DELETE Observable unregisterDeviceForNotificationsWithProxy(@Url String url, - @QueryMap Map fields); + @QueryMap Map fields); @FormUrlEncoded @PUT @@ -704,9 +706,25 @@ Observable getInvitations(@Header("Authorization") String aut @POST Observable acceptInvitation(@Header("Authorization") String authorization, - @Url String url); + @Url String url); @DELETE Observable rejectInvitation(@Header("Authorization") String authorization, - @Url String url); + @Url String url); + + @GET + Observable listBans(@Header("Authorization") String authorization, + @Url String url); + + @FormUrlEncoded + @POST + Observable banActor(@Header("Authorization") String authorization, + @Url String url, + @Field("actorType") String actorType, + @Field("actorId") String actorId, + @Field("internalNote") String internalNote); + + @DELETE + Observable unbanActor(@Header("Authorization") String authorization, + @Url String url); } \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/ChatRepository.kt b/app/src/main/java/com/nextcloud/talk/chat/data/ChatRepository.kt index 5858e38d15..bd85f5fb07 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/data/ChatRepository.kt +++ b/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 + fun getReminder(user: User, roomToken: String, messageId: String, apiVersion: Int): Observable fun deleteReminder(user: User, roomToken: String, messageId: String, apiVersion: Int): Observable fun shareToNotes( @@ -37,6 +39,7 @@ interface ChatRepository { message: String, displayName: String ): Observable // last two fields are false + fun checkForNoteToSelf(credentials: String, url: String, includeStatus: Boolean): Observable fun shareLocationToNotes( credentials: String, @@ -45,6 +48,7 @@ interface ChatRepository { objectId: String, metadata: String ): Observable + fun leaveRoom(credentials: String, url: String): Observable fun sendChatMessage( credentials: String, @@ -54,9 +58,20 @@ interface ChatRepository { replyTo: Int, sendWithoutNotification: Boolean ): Observable + 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 fun setChatReadMarker(credentials: String, url: String, previousMessageId: Int): Observable fun editChatMessage(credentials: String, url: String, text: String): Observable + fun listBans(credentials: String, url: String): Observable> + fun banActor( + credentials: String, + url: String, + actorType: String, + actorId: String, + internalNote: String + ): Observable + + fun unbanActor(credentials: String, url: String): Observable } diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/network/NetworkChatRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/chat/data/network/NetworkChatRepositoryImpl.kt index 57e3fd98f8..b922f6c2c7 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/data/network/NetworkChatRepositoryImpl.kt +++ b/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 { return ncApi.editChatMessage(credentials, url, text).map { it } } + + override fun listBans(credentials: String, url: String): Observable> { + return ncApi.listBans(credentials, url).map { it.ocs?.data } + } + + override fun banActor( + credentials: String, + url: String, + actorType: String, + actorId: String, + internalNote: String + ): Observable { + return ncApi.banActor(credentials, url, actorType, actorId, internalNote) + } + + override fun unbanActor(credentials: String, url: String): Observable { + return ncApi.unbanActor(credentials, url) + } } diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt index cdfcc110bd..dad95186b0 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt +++ b/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() @@ -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 } /** diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfo/viewmodel/ConversationInfoViewModel.kt b/app/src/main/java/com/nextcloud/talk/conversationinfo/viewmodel/ConversationInfoViewModel.kt index a81e3ab187..d8b4ec63aa 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationinfo/viewmodel/ConversationInfoViewModel.kt +++ b/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() + val disposableSet = mutableSetOf() override fun onResume(owner: LifecycleOwner) { super.onResume(owner) @@ -49,6 +53,27 @@ class ConversationInfoViewModel @Inject constructor( sealed interface ViewState + class ListBansSuccessState(val talkBans: List) : ViewState + object ListBansErrorState : ViewState + + private val _getTalkBanState: MutableLiveData = MutableLiveData() + val getTalkBanState: LiveData + get() = _getTalkBanState + + class BanActorSuccessState(val talkBan: TalkBan) : ViewState + object BanActorErrorState : ViewState + + private val _getBanActorState: MutableLiveData = MutableLiveData() + val getBanActorState: LiveData + get() = _getBanActorState + + object UnBanActorSuccessState : ViewState + object UnBanActorErrorState : ViewState + + private val _getUnBanActorState: MutableLiveData = MutableLiveData() + val getUnBanActorState: LiveData + 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> { + 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) { + _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 { + 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 { + 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 { override fun onSubscribe(d: Disposable) { // unused atm diff --git a/app/src/main/java/com/nextcloud/talk/models/json/participants/TalkBan.kt b/app/src/main/java/com/nextcloud/talk/models/json/participants/TalkBan.kt new file mode 100644 index 0000000000..63d60f4752 --- /dev/null +++ b/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 + * 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) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/participants/TalkBanOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/participants/TalkBanOCS.kt new file mode 100644 index 0000000000..9dd62a2d9a --- /dev/null +++ b/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 + * 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? = 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/participants/TalkBanOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/participants/TalkBanOverall.kt new file mode 100644 index 0000000000..08174ec776 --- /dev/null +++ b/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 + * 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) +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/DialogBanListFragment.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/DialogBanListFragment.kt new file mode 100644 index 0000000000..074bf4843a --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/DialogBanListFragment.kt @@ -0,0 +1,146 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Julius Linus + * 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 = mutableListOf() + + fun setItems(items: List) { + 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 + } +} 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 d010344fe2..28d4e4ff7b 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.kt +++ b/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" + } } diff --git a/app/src/main/java/com/nextcloud/talk/utils/CapabilitiesUtil.kt b/app/src/main/java/com/nextcloud/talk/utils/CapabilitiesUtil.kt index c00d48a5ea..5048e5656b 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/CapabilitiesUtil.kt +++ b/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 diff --git a/app/src/main/res/drawable/baseline_block_24.xml b/app/src/main/res/drawable/baseline_block_24.xml new file mode 100644 index 0000000000..920379cb37 --- /dev/null +++ b/app/src/main/res/drawable/baseline_block_24.xml @@ -0,0 +1,19 @@ + + + + + + + diff --git a/app/src/main/res/layout/activity_conversation_info.xml b/app/src/main/res/layout/activity_conversation_info.xml index 8efae01e0d..63738ce3d8 100644 --- a/app/src/main/res/layout/activity_conversation_info.xml +++ b/app/src/main/res/layout/activity_conversation_info.xml @@ -354,6 +354,35 @@ tools:listitem="@layout/rv_item_conversation_info_participant" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_ban_actor.xml b/app/src/main/res/layout/dialog_ban_actor.xml new file mode 100644 index 0000000000..0bcf22574a --- /dev/null +++ b/app/src/main/res/layout/dialog_ban_actor.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_dialog_ban_list.xml b/app/src/main/res/layout/fragment_dialog_ban_list.xml new file mode 100644 index 0000000000..6324cd3527 --- /dev/null +++ b/app/src/main/res/layout/fragment_dialog_ban_list.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 3e7f741711..9b878c0d0c 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -83,6 +83,7 @@ 24dp 24dp 21dp + 12sp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ffa22fa1d3..a89e0dd50a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -336,7 +336,6 @@ How to translate with transifex: Add emojis - Push-to-talk With microphone disabled, click&hold to use Push-to-talk Select authentication certificate @@ -793,4 +792,12 @@ How to translate with transifex: Edited by %1$s Join conversation %1$s at %2$s Conversation settings + Show Banned Participants + Bans List + Unban + Internal Note + Ban Actor + Ban + Show ban reason + Error occured when unbanning actor