@@ -14,7 +14,6 @@ import android.view.LayoutInflater
1414import androidx.activity.compose.setContent
1515import androidx.activity.result.ActivityResult
1616import androidx.activity.result.contract.ActivityResultContracts
17- import androidx.annotation.DrawableRes
1817import androidx.appcompat.app.AlertDialog
1918import androidx.compose.material3.MaterialTheme
2019import androidx.compose.material3.SnackbarHostState
@@ -43,14 +42,14 @@ import com.nextcloud.talk.activities.BaseActivity
4342import com.nextcloud.talk.activities.MainActivity
4443import com.nextcloud.talk.api.NcApi
4544import com.nextcloud.talk.application.NextcloudTalkApplication
46- import com.nextcloud.talk.bottomsheet.items.BasicListItemWithImage
47- import com.nextcloud.talk.bottomsheet.items.listItemsWithImage
4845import com.nextcloud.talk.chat.ChatActivity
4946import com.nextcloud.talk.components.ColoredStatusBar
5047import com.nextcloud.talk.contacts.CompanionClass.Companion.KEY_HIDE_ALREADY_EXISTING_PARTICIPANTS
5148import com.nextcloud.talk.contacts.ContactsActivity
49+ import com.nextcloud.talk.conversationinfo.model.ParticipantModel
5250import com.nextcloud.talk.conversationinfo.ui.ConversationInfoScreen
5351import com.nextcloud.talk.conversationinfo.ui.ConversationInfoScreenCallbacks
52+ import com.nextcloud.talk.conversationinfo.ui.ParticipantOpsAction
5453import com.nextcloud.talk.conversationinfo.viewmodel.ConversationInfoViewModel
5554import com.nextcloud.talk.conversationinfoedit.ConversationInfoEditActivity
5655import com.nextcloud.talk.data.user.model.User
@@ -70,7 +69,6 @@ import com.nextcloud.talk.models.json.generic.GenericOverall
7069import com.nextcloud.talk.models.json.participants.Participant
7170import com.nextcloud.talk.models.json.participants.Participant.ActorType.CIRCLES
7271import com.nextcloud.talk.models.json.participants.Participant.ActorType.GROUPS
73- import com.nextcloud.talk.models.json.participants.Participant.ActorType.USERS
7472import com.nextcloud.talk.models.json.upcomingEvents.UpcomingEvent
7573import com.nextcloud.talk.shareditems.activities.SharedItemsActivity
7674import com.nextcloud.talk.threadsoverview.ThreadsOverviewActivity
@@ -91,6 +89,8 @@ import io.reactivex.schedulers.Schedulers
9189import kotlinx.coroutines.launch
9290import org.greenrobot.eventbus.Subscribe
9391import org.greenrobot.eventbus.ThreadMode
92+ import org.json.JSONObject
93+ import retrofit2.HttpException
9494import java.time.Instant
9595import java.time.ZoneId
9696import java.time.ZonedDateTime
@@ -312,7 +312,9 @@ class ConversationInfoActivity : BaseActivity() {
312312 onLockConversationClick = {
313313 conversationUser?.let { viewModel.toggleLock(it, conversationToken) }
314314 },
315- onParticipantClick = { model -> handleParticipantClick(model.participant) },
315+ onParticipantClick = { model -> handleParticipantClick(model) },
316+ onParticipantOpsDismiss = { viewModel.setParticipantForOps(null ) },
317+ onParticipantOpsAction = { action, model -> handleParticipantOpsAction(action, model) },
316318 onAddParticipantsClick = {
317319 startGroupChat = false
318320 selectParticipantsToAdd()
@@ -645,15 +647,17 @@ class ConversationInfoActivity : BaseActivity() {
645647 ncApi.demoteAttendeeFromModerator(
646648 credentials,
647649 ApiUtils .getUrlForRoomModerators(apiVersion, user.baseUrl!! , conversationToken),
648- participant.attendeeId
650+ participant.attendeeId,
651+ null
649652 )?.subscribeOn(Schedulers .io())?.observeOn(AndroidSchedulers .mainThread())?.subscribe(subscriber)
650653 } else if (participant.type == Participant .ParticipantType .USER ||
651654 participant.type == Participant .ParticipantType .GUEST
652655 ) {
653656 ncApi.promoteAttendeeToModerator(
654657 credentials,
655658 ApiUtils .getUrlForRoomModerators(apiVersion, user.baseUrl!! , conversationToken),
656- participant.attendeeId
659+ participant.attendeeId,
660+ null
657661 )?.subscribeOn(Schedulers .io())?.observeOn(AndroidSchedulers .mainThread())?.subscribe(subscriber)
658662 }
659663 }
@@ -685,7 +689,8 @@ class ConversationInfoActivity : BaseActivity() {
685689
686690 @SuppressLint(" LongLogTag" )
687691 override fun onError (e : Throwable ) {
688- Log .e(TAG , " Error toggling moderator status" , e)
692+ Log .e(TAG , " Error changing the participant type" , e)
693+ showParticipantActionError(e)
689694 }
690695 override fun onComplete () { /* unused */ }
691696 }
@@ -722,113 +727,85 @@ class ConversationInfoActivity : BaseActivity() {
722727 conversationUser?.let { viewModel.banActor(it, conversationToken, actorType, actorId, internalNote) }
723728 }
724729
725- @SuppressLint(" CheckResult" , " StringFormatInvalid" )
730+ private fun handleParticipantClick (model : ParticipantModel ) {
731+ viewModel.setParticipantForOps(model)
732+ }
733+
726734 @Suppress(" ReturnCount" )
727- private fun handleParticipantClick ( participant : Participant ) {
735+ private fun handleParticipantOpsAction ( action : ParticipantOpsAction , model : ParticipantModel ) {
728736 val state = viewModel.uiState.value
729737 val conv = state.conversation ? : return
730738 val caps = state.spreedCapabilities ? : return
731739 if (! ConversationUtils .canModerate(conv, caps)) return
732740 val user = conversationUser ? : return
741+ val participant = model.participant
733742 val apiVersion = ApiUtils .getConversationApiVersion(user, intArrayOf(ApiUtils .API_V4 , 1 ))
743+ when (action) {
744+ ParticipantOpsAction .PromoteToModerator ,
745+ ParticipantOpsAction .DemoteFromModerator ->
746+ if (apiVersion >= ApiUtils .API_V4 ) {
747+ toggleModeratorStatus(apiVersion, participant)
748+ } else {
749+ toggleModeratorStatusLegacy(apiVersion, participant)
750+ }
734751
735- if (participant.calculatedActorType == USERS && participant.calculatedActorId == user.userId) {
736- if (participant.attendeePin?.isNotEmpty() == true ) {
737- launchRemoveAttendeeFromConversationDialog(
738- participant,
739- apiVersion,
740- context.getString(R .string.nc_attendee_pin, participant.attendeePin),
741- R .drawable.ic_lock_grey600_24px
742- )
743- }
744- } else if (participant.type == Participant .ParticipantType .OWNER ) {
745- // Cannot moderate owner
746- } else if (participant.calculatedActorType == GROUPS ) {
747- launchRemoveAttendeeFromConversationDialog(
748- participant,
749- apiVersion,
750- context.getString(R .string.nc_remove_group_and_members)
751- )
752- } else if (participant.calculatedActorType == CIRCLES ) {
753- launchRemoveAttendeeFromConversationDialog(
754- participant,
755- apiVersion,
756- context.getString(R .string.nc_remove_team_and_members)
757- )
758- } else {
759- launchDefaultActions(participant, apiVersion)
752+ ParticipantOpsAction .PromoteToOwner ->
753+ changeParticipantType(apiVersion, participant, promote = true , PARTICIPANT_TYPE_OWNER )
754+
755+ ParticipantOpsAction .DemoteOwnerToModerator ->
756+ changeParticipantType(apiVersion, participant, promote = false , PARTICIPANT_TYPE_MODERATOR )
757+
758+ ParticipantOpsAction .DemoteOwnerToUser ->
759+ changeParticipantType(apiVersion, participant, promote = false , PARTICIPANT_TYPE_USER )
760+
761+ ParticipantOpsAction .RemoveFromConversation -> removeAttendeeFromConversation(apiVersion, participant)
762+ ParticipantOpsAction .Ban -> handleBan(participant)
760763 }
761764 }
762765
763- @SuppressLint(" CheckResult" )
764- @Suppress(" CyclomaticComplexMethod" )
765- private fun launchDefaultActions (participant : Participant , apiVersion : Int ) {
766- val items = getDefaultActionItems(participant)
767- if (CapabilitiesUtil .isBanningAvailable(conversationUser?.capabilities?.spreedCapability!! )) {
768- items.add(BasicListItemWithImage (R .drawable.baseline_block_24, context.getString(R .string.ban_participant)))
769- }
770- when (participant.type) {
771- Participant .ParticipantType .MODERATOR , Participant .ParticipantType .GUEST_MODERATOR -> items.removeAt(1 )
772- Participant .ParticipantType .USER , Participant .ParticipantType .GUEST -> items.removeAt(2 )
773- else -> {
774- items.removeAt(2 )
775- items.removeAt(1 )
776- }
766+ private fun showParticipantActionError (e : Throwable ) {
767+ val messageRes = if (participantActionErrorReason(e) == ERROR_LAST_MODERATOR ) {
768+ R .string.nc_last_moderator_cannot_be_demoted
769+ } else {
770+ R .string.nc_participant_type_change_failed
777771 }
778- if (participant.attendeePin == null || participant.attendeePin!! .isEmpty()) items.removeAt(0 )
779- if (items.isNotEmpty()) {
780- MaterialDialog (this , BottomSheet (WRAP_CONTENT )).show {
781- cornerRadius(res = R .dimen.corner_radius)
782- title(text = participant.displayName)
783- listItemsWithImage(items = items) { _, index, _ ->
784- var actionToTrigger = index
785- if (participant.attendeePin == null || participant.attendeePin!! .isEmpty()) actionToTrigger++
786- if (participant.type == Participant .ParticipantType .USER_FOLLOWING_LINK ) actionToTrigger++
787- when (actionToTrigger) {
788- DEMOTE_OR_PROMOTE -> {
789- if (apiVersion >= ApiUtils .API_V4 ) {
790- toggleModeratorStatus(apiVersion, participant)
791- } else {
792- toggleModeratorStatusLegacy(apiVersion, participant)
793- }
794- }
795- REMOVE_FROM_CONVERSATION -> removeAttendeeFromConversation(apiVersion, participant)
796- BAN_FROM_CONVERSATION -> handleBan(participant)
797- else -> { /* unused */ }
798- }
799- }
800- }
772+ lifecycleScope.launch { viewModel.emitSnackbar(messageRes) }
773+ }
774+
775+ /* *
776+ * The moderators endpoint reports why it refused in the OCS data as
777+ * `{"ocs":{"data":{"error":"<reason>"}}}`.
778+ */
779+ @Suppress(" Detekt.TooGenericExceptionCaught" )
780+ private fun participantActionErrorReason (e : Throwable ): String? =
781+ try {
782+ (e as ? HttpException )?.response()?.errorBody()?.string()
783+ ?.let { JSONObject (it) }
784+ ?.optJSONObject(" ocs" )
785+ ?.optJSONObject(" data" )
786+ ?.optString(" error" )
787+ ?.takeIf { it.isNotEmpty() }
788+ } catch (exception: Exception ) {
789+ Log .w(TAG , " Could not read the participant action error" , exception)
790+ null
801791 }
802- }
803-
804- @SuppressLint(" StringFormatInvalid" )
805- private fun getDefaultActionItems (participant : Participant ): MutableList <BasicListItemWithImage > =
806- mutableListOf (
807- BasicListItemWithImage (
808- R .drawable.ic_lock_grey600_24px,
809- context.getString(R .string.nc_attendee_pin, participant.attendeePin)
810- ),
811- BasicListItemWithImage (R .drawable.ic_pencil_grey600_24dp, context.getString(R .string.nc_promote)),
812- BasicListItemWithImage (R .drawable.ic_pencil_grey600_24dp, context.getString(R .string.nc_demote)),
813- BasicListItemWithImage (R .drawable.ic_delete_grey600_24dp, context.getString(R .string.nc_remove_participant))
814- )
815792
816- @SuppressLint(" CheckResult" )
817- private fun launchRemoveAttendeeFromConversationDialog (
818- participant : Participant ,
793+ private fun changeParticipantType (
819794 apiVersion : Int ,
820- itemText : String ,
821- @DrawableRes itemIcon : Int = R .drawable.ic_delete_grey600_24dp
795+ participant : Participant ,
796+ promote : Boolean ,
797+ participantType : Int
822798 ) {
823- MaterialDialog (this , BottomSheet (WRAP_CONTENT )).show {
824- cornerRadius(res = R .dimen.corner_radius)
825- title(text = participant.displayName)
826- listItemsWithImage(
827- items = mutableListOf (BasicListItemWithImage (itemIcon, itemText))
828- ) { _, index, _ ->
829- if (index == 0 ) removeAttendeeFromConversation(apiVersion, participant)
830- }
799+ val user = conversationUser ? : return
800+ val url = ApiUtils .getUrlForRoomModerators(apiVersion, user.baseUrl!! , conversationToken)
801+ val call = if (promote) {
802+ ncApi.promoteAttendeeToModerator(credentials, url, participant.attendeeId, participantType)
803+ } else {
804+ ncApi.demoteAttendeeFromModerator(credentials, url, participant.attendeeId, participantType)
831805 }
806+ call?.subscribeOn(Schedulers .io())
807+ ?.observeOn(AndroidSchedulers .mainThread())
808+ ?.subscribe(participantActionObserver())
832809 }
833810
834811 private fun handleBan (participant : Participant ) {
@@ -861,8 +838,12 @@ class ConversationInfoActivity : BaseActivity() {
861838
862839 companion object {
863840 private val TAG = ConversationInfoActivity ::class .java.simpleName
864- private const val DEMOTE_OR_PROMOTE = 1
865- private const val REMOVE_FROM_CONVERSATION = 2
866- private const val BAN_FROM_CONVERSATION = 3
841+
842+ private const val ERROR_LAST_MODERATOR = " last-moderator"
843+
844+ // Participant types the moderators endpoint accepts as a target level
845+ private const val PARTICIPANT_TYPE_OWNER : Int = 1
846+ private const val PARTICIPANT_TYPE_MODERATOR : Int = 2
847+ private const val PARTICIPANT_TYPE_USER : Int = 3
867848 }
868849}
0 commit comments