Browse Source

Merge pull request #1225 from nextcloud/feature/apiv4/fix-participant-list

[apiv4] 👥 Fix participant list
Andy Scherzinger 4 years ago
parent
commit
16255b885f

+ 7 - 6
app/src/main/java/com/nextcloud/talk/adapters/items/AdvancedUserItem.java

@@ -21,6 +21,7 @@
 package com.nextcloud.talk.adapters.items;
 
 import android.accounts.Account;
+import android.net.Uri;
 import android.view.View;
 import android.widget.*;
 import androidx.annotation.Nullable;
@@ -106,14 +107,14 @@ public class AdvancedUserItem extends AbstractFlexibleItem<AdvancedUserItem.User
         holder.avatarImageView.setController(null);
 
         if (adapter.hasFilter()) {
-            FlexibleUtils.highlightText(holder.contactDisplayName, participant.getName(),
+            FlexibleUtils.highlightText(holder.contactDisplayName, participant.getDisplayName(),
                     String.valueOf(adapter.getFilter(String.class)), NextcloudTalkApplication.Companion.getSharedApplication()
                             .getResources().getColor(R.color.colorPrimary));
         } else {
-            holder.contactDisplayName.setText(participant.getName());
+            holder.contactDisplayName.setText(participant.getDisplayName());
         }
 
-        holder.serverUrl.setText(userEntity.getBaseUrl());
+        holder.serverUrl.setText((Uri.parse(userEntity.getBaseUrl()).getHost()));
 
         if (userEntity != null && userEntity.getBaseUrl() != null && userEntity.getBaseUrl().startsWith("http://") || userEntity.getBaseUrl().startsWith("https://")) {
             holder.avatarImageView.setVisibility(View.VISIBLE);
@@ -122,7 +123,7 @@ public class AdvancedUserItem extends AbstractFlexibleItem<AdvancedUserItem.User
                     .setOldController(holder.avatarImageView.getController())
                     .setAutoPlayAnimations(true)
                     .setImageRequest(DisplayUtils.getImageRequestForUrl(ApiUtils.getUrlForAvatarWithName(userEntity.getBaseUrl(),
-                            participant.getUserId(), R.dimen.avatar_size), null))
+                            participant.getActorId(), R.dimen.avatar_size), null))
                     .build();
             holder.avatarImageView.setController(draweeController);
 
@@ -138,8 +139,8 @@ public class AdvancedUserItem extends AbstractFlexibleItem<AdvancedUserItem.User
 
     @Override
     public boolean filter(String constraint) {
-        return participant.getName() != null &&
-                Pattern.compile(constraint, Pattern.CASE_INSENSITIVE | Pattern.LITERAL).matcher(participant.getName().trim()).find();
+        return participant.getDisplayName() != null &&
+                Pattern.compile(constraint, Pattern.CASE_INSENSITIVE | Pattern.LITERAL).matcher(participant.getDisplayName().trim()).find();
     }
 
 

+ 36 - 29
app/src/main/java/com/nextcloud/talk/adapters/items/UserItem.java

@@ -21,7 +21,6 @@
 package com.nextcloud.talk.adapters.items;
 
 import android.content.res.Resources;
-import android.graphics.drawable.BitmapDrawable;
 import android.text.TextUtils;
 import android.view.View;
 import android.widget.ImageView;
@@ -150,37 +149,38 @@ public class UserItem extends AbstractFlexibleItem<UserItem.UserItemViewHolder>
             holder.contactDisplayName.setText(NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest));
         }
 
-        if (TextUtils.isEmpty(participant.getSource()) || participant.getSource().equals("users")) {
-            if (Participant.ParticipantType.GUEST.equals(participant.getType()) ||
-                    Participant.ParticipantType.USER_FOLLOWING_LINK.equals(participant.getType())) {
-                String displayName = NextcloudTalkApplication.Companion.getSharedApplication()
-                        .getResources().getString(R.string.nc_guest);
-
-                if (!TextUtils.isEmpty(participant.getDisplayName())) {
-                    displayName = participant.getDisplayName();
-                }
-
-                DraweeController draweeController = Fresco.newDraweeControllerBuilder()
-                        .setOldController(holder.simpleDraweeView.getController())
-                        .setAutoPlayAnimations(true)
-                        .setImageRequest(DisplayUtils.getImageRequestForUrl(ApiUtils.getUrlForAvatarWithNameForGuests(userEntity.getBaseUrl(),
-                                displayName, R.dimen.avatar_size), null))
-                        .build();
-                holder.simpleDraweeView.setController(draweeController);
-
-            } else {
+        if (participant.getActorType() == Participant.ActorType.GROUPS || "groups".equals(participant.getSource())) {
+            holder.simpleDraweeView.setImageResource(R.drawable.ic_circular_group);
+        } else if (participant.getActorType() == Participant.ActorType.EMAILS) {
+            // FIXME use an email icon
+            holder.simpleDraweeView.setImageResource(R.drawable.ic_circular_group);
+        } else if (participant.getActorType() == Participant.ActorType.GUESTS ||
+                Participant.ParticipantType.GUEST.equals(participant.getType()) ||
+                Participant.ParticipantType.GUEST_MODERATOR.equals(participant.getType())) {
 
-                DraweeController draweeController = Fresco.newDraweeControllerBuilder()
-                        .setOldController(holder.simpleDraweeView.getController())
-                        .setAutoPlayAnimations(true)
-                        .setImageRequest(DisplayUtils.getImageRequestForUrl(ApiUtils.getUrlForAvatarWithName(userEntity.getBaseUrl(),
-                                participant.getUserId(), R.dimen.avatar_size), null))
-                        .build();
-                holder.simpleDraweeView.setController(draweeController);
+            String displayName = NextcloudTalkApplication.Companion.getSharedApplication()
+                    .getResources().getString(R.string.nc_guest);
 
+            if (!TextUtils.isEmpty(participant.getDisplayName())) {
+                displayName = participant.getDisplayName();
             }
-        } else if ("groups".equals(participant.getSource())) {
-            holder.simpleDraweeView.setImageResource(R.drawable.ic_circular_group);
+
+            DraweeController draweeController = Fresco.newDraweeControllerBuilder()
+                    .setOldController(holder.simpleDraweeView.getController())
+                    .setAutoPlayAnimations(true)
+                    .setImageRequest(DisplayUtils.getImageRequestForUrl(ApiUtils.getUrlForAvatarWithNameForGuests(userEntity.getBaseUrl(),
+                                                                                                                  displayName, R.dimen.avatar_size), null))
+                    .build();
+            holder.simpleDraweeView.setController(draweeController);
+
+        } else if (participant.getActorType() == Participant.ActorType.USERS || participant.getSource().equals("users")) {
+            DraweeController draweeController = Fresco.newDraweeControllerBuilder()
+                    .setOldController(holder.simpleDraweeView.getController())
+                    .setAutoPlayAnimations(true)
+                    .setImageRequest(DisplayUtils.getImageRequestForUrl(ApiUtils.getUrlForAvatarWithName(userEntity.getBaseUrl(),
+                                                                                                         participant.getUserId(), R.dimen.avatar_size), null))
+                    .build();
+            holder.simpleDraweeView.setController(draweeController);
         }
 
         Resources resources = NextcloudTalkApplication.Companion.getSharedApplication().getResources();
@@ -246,13 +246,20 @@ public class UserItem extends AbstractFlexibleItem<UserItem.UserItemViewHolder>
                         //userType = NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_owner);
                         //break;
                     case 2:
+                    case 6: // Guest moderator
                         userType = NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_moderator);
                         break;
                     case 3:
                         userType = NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_user);
+                        if (participant.getActorType() == Participant.ActorType.GROUPS) {
+                            userType = NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_group);
+                        }
                         break;
                     case 4:
                         userType = NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest);
+                        if (participant.getActorType() == Participant.ActorType.EMAILS) {
+                            userType = NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_email);
+                        }
                         break;
                     case 5:
                         userType = NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_following_link);

+ 12 - 0
app/src/main/java/com/nextcloud/talk/api/NcApi.java

@@ -135,15 +135,27 @@ public interface NcApi {
 
 
     // also used for removing a guest from a conversation
+    @Deprecated
     @DELETE
     Observable<GenericOverall> removeParticipantFromConversation(@Header("Authorization") String authorization, @Url String url, @Query("participant") String participantId);
 
+    @DELETE
+    Observable<GenericOverall> removeAttendeeFromConversation(@Header("Authorization") String authorization, @Url String url, @Query("attendeeId") Long attendeeId);
+
+    @Deprecated
     @POST
     Observable<GenericOverall> promoteUserToModerator(@Header("Authorization") String authorization, @Url String url, @Query("participant") String participantId);
 
+    @Deprecated
     @DELETE
     Observable<GenericOverall> demoteModeratorToUser(@Header("Authorization") String authorization, @Url String url, @Query("participant") String participantId);
 
+    @POST
+    Observable<GenericOverall> promoteAttendeeToModerator(@Header("Authorization") String authorization, @Url String url, @Query("attendeeId") Long attendeeId);
+
+    @DELETE
+    Observable<GenericOverall> demoteAttendeeFromModerator(@Header("Authorization") String authorization, @Url String url, @Query("attendeeId") Long attendeeId);
+
     /*
         Server URL is: baseUrl + ocsApiVersion + spreedApiVersion + /room/roomToken/participants/self
      */

+ 3 - 3
app/src/main/java/com/nextcloud/talk/controllers/CallController.java

@@ -1273,7 +1273,7 @@ public class CallController extends BaseController {
             inCallFlag = (int) Participant.ParticipantFlags.IN_CALL_WITH_AUDIO_AND_VIDEO.getValue();
         }
 
-        int apiVersion = ApiUtils.getConversationApiVersion(conversationUser, new int[] {1});
+        int apiVersion = ApiUtils.getCallApiVersion(conversationUser, new int[] {1});
 
         ncApi.joinCall(credentials, ApiUtils.getUrlForCall(apiVersion, baseUrl, roomToken), inCallFlag)
                 .subscribeOn(Schedulers.io())
@@ -1558,7 +1558,7 @@ public class CallController extends BaseController {
     }
 
     private void hangupNetworkCalls(boolean shutDownView) {
-        int apiVersion = ApiUtils.getConversationApiVersion(conversationUser, new int[] {1});
+        int apiVersion = ApiUtils.getCallApiVersion(conversationUser, new int[] {1});
 
         ncApi.leaveCall(credentials, ApiUtils.getUrlForCall(apiVersion, baseUrl, roomToken))
                 .subscribeOn(Schedulers.io())
@@ -1686,7 +1686,7 @@ public class CallController extends BaseController {
 
     private void getPeersForCall() {
         Log.d(TAG, "getPeersForCall");
-        int apiVersion = ApiUtils.getConversationApiVersion(conversationUser, new int[] {1});
+        int apiVersion = ApiUtils.getCallApiVersion(conversationUser, new int[] {1});
 
         ncApi.getPeersForCall(credentials, ApiUtils.getUrlForCall(apiVersion, baseUrl, roomToken))
                 .subscribeOn(Schedulers.io())

+ 1 - 1
app/src/main/java/com/nextcloud/talk/controllers/CallNotificationController.java

@@ -209,7 +209,7 @@ public class CallNotificationController extends BaseController {
     }
 
     private void checkIfAnyParticipantsRemainInRoom() {
-        int apiVersion = ApiUtils.getConversationApiVersion(userBeingCalled, new int[] {1});
+        int apiVersion = ApiUtils.getCallApiVersion(userBeingCalled, new int[] {1});
 
         ncApi.getPeersForCall(credentials, ApiUtils.getUrlForCall(apiVersion, userBeingCalled.getBaseUrl(),
                                                                   currentConversation.getToken()))

+ 5 - 2
app/src/main/java/com/nextcloud/talk/controllers/ContactsController.java

@@ -61,6 +61,7 @@ import com.nextcloud.talk.models.json.autocomplete.AutocompleteOverall;
 import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser;
 import com.nextcloud.talk.models.json.conversations.Conversation;
 import com.nextcloud.talk.models.json.conversations.RoomOverall;
+import com.nextcloud.talk.models.json.converters.EnumActorTypeConverter;
 import com.nextcloud.talk.models.json.participants.Participant;
 import com.nextcloud.talk.utils.ApiUtils;
 import com.nextcloud.talk.utils.ConductorRemapping;
@@ -508,6 +509,7 @@ public class ContactsController extends BaseController implements SearchView.OnQ
                             Participant participant;
 
                             List<AbstractFlexibleItem> newUserItemList = new ArrayList<>();
+                            EnumActorTypeConverter actorTypeConverter = new EnumActorTypeConverter();
 
                             try {
                                     AutocompleteOverall autocompleteOverall = LoganSquare.parse(responseBody.string(), AutocompleteOverall.class);
@@ -516,13 +518,14 @@ public class ContactsController extends BaseController implements SearchView.OnQ
                                     for (AutocompleteUser autocompleteUser : autocompleteUsersHashSet) {
                                         if (!autocompleteUser.getId().equals(currentUser.getUserId()) && !existingParticipants.contains(autocompleteUser.getId())) {
                                             participant = new Participant();
-                                            participant.setUserId(autocompleteUser.getId());
+                                            participant.setActorId(autocompleteUser.getId());
+                                            participant.setActorType(actorTypeConverter.getFromString(autocompleteUser.getSource()));
                                             participant.setDisplayName(autocompleteUser.getLabel());
                                             participant.setSource(autocompleteUser.getSource());
 
                                             String headerTitle;
 
-                                            if (!autocompleteUser.getSource().equals("groups")) {
+                                            if (participant.getActorType() != Participant.ActorType.GROUPS) {
                                                 headerTitle = participant.getDisplayName().substring(0, 1).toUpperCase();
                                             } else {
                                                 headerTitle = getResources().getString(R.string.nc_groups);

+ 312 - 91
app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt

@@ -20,11 +20,13 @@
 
 package com.nextcloud.talk.controllers
 
+import android.annotation.SuppressLint
 import android.content.Context
 import android.graphics.drawable.Drawable
 import android.graphics.drawable.LayerDrawable
 import android.os.Bundle
 import android.text.TextUtils
+import android.util.Log
 import android.view.LayoutInflater
 import android.view.MenuItem
 import android.view.View
@@ -63,6 +65,8 @@ import com.nextcloud.talk.models.json.conversations.RoomOverall
 import com.nextcloud.talk.models.json.converters.EnumNotificationLevelConverter
 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.GROUPS
+import com.nextcloud.talk.models.json.participants.Participant.ActorType.USERS
 import com.nextcloud.talk.models.json.participants.ParticipantsOverall
 import com.nextcloud.talk.utils.ApiUtils
 import com.nextcloud.talk.utils.DateUtils
@@ -410,8 +414,13 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
         for (i in participants.indices) {
             participant = participants[i]
             userItem = UserItem(participant, conversationUser, null)
-            userItem.isOnline = !participant.sessionId.equals("0")
-            if (!TextUtils.isEmpty(participant.userId) && participant.userId == conversationUser!!.userId) {
+            if (participant.sessionId != null) {
+                userItem.isOnline = !participant.sessionId.equals("0")
+            } else {
+                userItem.isOnline = !participant.sessionIds!!.isEmpty()
+            }
+
+            if (participant.getActorType() == USERS && participant.getActorId() == conversationUser!!.userId) {
                 ownUserItem = userItem
                 ownUserItem.model.sessionId = "-1"
                 ownUserItem.isOnline = true
@@ -444,7 +453,7 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
         var apiVersion = 1
         // FIXME Fix API checking with guests?
         if (conversationUser != null) {
-            apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(1))
+            apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
         }
 
         ncApi.getPeersForCall(
@@ -478,7 +487,9 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
 
         recyclerViewItems.forEach {
             val userItem = it as UserItem
-            existingParticipantsId.add(userItem.model.userId)
+            if (userItem.model.getActorType() == USERS) {
+                existingParticipantsId.add(userItem.model.getActorId())
+            }
         }
 
         bundle.putBoolean(BundleKeys.KEY_ADD_PARTICIPANTS, true)
@@ -572,7 +583,7 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
                             leaveConversationAction.visibility = View.VISIBLE
                         }
 
-                        if (!conversation!!.canModerate(conversationUser)) {
+                        if (!conversation!!.canDelete(conversationUser)) {
                             deleteConversationAction.visibility = View.GONE
                         } else {
                             deleteConversationAction.visibility = View.VISIBLE
@@ -685,117 +696,320 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
         }
     }
 
+    private fun toggleModeratorStatus(apiVersion: Int, participant: Participant) {
+        val subscriber = object : Observer<GenericOverall> {
+            override fun onSubscribe(d: Disposable) {
+            }
+
+            override fun onNext(genericOverall: GenericOverall) {
+                getListOfParticipants()
+            }
+
+            @SuppressLint("LongLogTag")
+            override fun onError(e: Throwable) {
+                Log.e(TAG, "Error toggling moderator status", e)
+            }
+
+            override fun onComplete() {
+            }
+        }
+
+        if (participant.type == Participant.ParticipantType.MODERATOR ||
+            participant.type == Participant.ParticipantType.GUEST_MODERATOR
+        ) {
+            ncApi.demoteAttendeeFromModerator(
+                credentials,
+                ApiUtils.getUrlForRoomModerators(
+                    apiVersion,
+                    conversationUser!!.baseUrl,
+                    conversation!!.token
+                ),
+                participant.attendeeId
+            )
+                .subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe(subscriber)
+        } else if (participant.type == Participant.ParticipantType.USER ||
+            participant.type == Participant.ParticipantType.GUEST
+        ) {
+            ncApi.promoteAttendeeToModerator(
+                credentials,
+                ApiUtils.getUrlForRoomModerators(
+                    apiVersion,
+                    conversationUser!!.baseUrl,
+                    conversation!!.token
+                ),
+                participant.attendeeId
+            )
+                .subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe(subscriber)
+        }
+    }
+
+    private fun toggleModeratorStatusLegacy(apiVersion: Int, participant: Participant) {
+        val subscriber = object : Observer<GenericOverall> {
+            override fun onSubscribe(d: Disposable) {
+            }
+
+            override fun onNext(genericOverall: GenericOverall) {
+                getListOfParticipants()
+            }
+
+            @SuppressLint("LongLogTag")
+            override fun onError(e: Throwable) {
+                Log.e(TAG, "Error toggling moderator status", e)
+            }
+
+            override fun onComplete() {
+            }
+        }
+
+        if (participant.type == Participant.ParticipantType.MODERATOR) {
+            ncApi.demoteModeratorToUser(
+                credentials,
+                ApiUtils.getUrlForRoomModerators(
+                    apiVersion,
+                    conversationUser!!.baseUrl,
+                    conversation!!.token
+                ),
+                participant.userId
+            )
+                .subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe(subscriber)
+        } else if (participant.type == Participant.ParticipantType.USER) {
+            ncApi.promoteUserToModerator(
+                credentials,
+                ApiUtils.getUrlForRoomModerators(
+                    apiVersion,
+                    conversationUser!!.baseUrl,
+                    conversation!!.token
+                ),
+                participant.userId
+            )
+                .subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe(subscriber)
+        }
+    }
+
+    fun removeAttendeeFromConversation(apiVersion: Int, participant: Participant) {
+        if (apiVersion >= ApiUtils.APIv4) {
+            ncApi.removeAttendeeFromConversation(
+                credentials,
+                ApiUtils.getUrlForAttendees(
+                    apiVersion,
+                    conversationUser!!.baseUrl,
+                    conversation!!.token
+                ),
+                participant.attendeeId
+            )
+                .subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe(object : Observer<GenericOverall> {
+                    override fun onSubscribe(d: Disposable) {
+                    }
+
+                    override fun onNext(genericOverall: GenericOverall) {
+                        getListOfParticipants()
+                    }
+
+                    @SuppressLint("LongLogTag")
+                    override fun onError(e: Throwable) {
+                        Log.e(TAG, "Error removing attendee from conversation", e)
+                    }
+
+                    override fun onComplete() {
+                    }
+                })
+        } else {
+            if (participant.type == Participant.ParticipantType.GUEST ||
+                participant.type == Participant.ParticipantType.USER_FOLLOWING_LINK
+            ) {
+                ncApi.removeParticipantFromConversation(
+                    credentials,
+                    ApiUtils.getUrlForRemovingParticipantFromConversation(
+                        conversationUser!!.baseUrl,
+                        conversation!!.token,
+                        true
+                    ),
+                    participant.sessionId
+                )
+                    .subscribeOn(Schedulers.io())
+                    .observeOn(AndroidSchedulers.mainThread())
+                    .subscribe(object : Observer<GenericOverall> {
+                        override fun onSubscribe(d: Disposable) {
+                        }
+
+                        override fun onNext(genericOverall: GenericOverall) {
+                            getListOfParticipants()
+                        }
+
+                        @SuppressLint("LongLogTag")
+                        override fun onError(e: Throwable) {
+                            Log.e(TAG, "Error removing guest from conversation", e)
+                        }
+
+                        override fun onComplete() {
+                        }
+                    })
+            } else {
+                ncApi.removeParticipantFromConversation(
+                    credentials,
+                    ApiUtils.getUrlForRemovingParticipantFromConversation(
+                        conversationUser!!.baseUrl,
+                        conversation!!.token,
+                        false
+                    ),
+                    participant.userId
+                )
+                    .subscribeOn(Schedulers.io())
+                    .observeOn(AndroidSchedulers.mainThread())
+                    .subscribe(object : Observer<GenericOverall> {
+                        override fun onSubscribe(d: Disposable) {
+                        }
+
+                        override fun onNext(genericOverall: GenericOverall) {
+                            getListOfParticipants()
+                        }
+
+                        @SuppressLint("LongLogTag")
+                        override fun onError(e: Throwable) {
+                            Log.e(TAG, "Error removing user from conversation", e)
+                        }
+
+                        override fun onComplete() {
+                        }
+                    })
+            }
+        }
+    }
+
     override fun onItemClick(view: View?, position: Int): Boolean {
+        if (!conversation!!.canModerate(conversationUser)) {
+            return true
+        }
+
         val userItem = adapter?.getItem(position) as UserItem
         val participant = userItem.model
 
-        if (participant.userId != conversationUser!!.userId) {
-            var items = mutableListOf(
-                BasicListItemWithImage(R.drawable.ic_pencil_grey600_24dp, context.getString(R.string.nc_promote)),
-                BasicListItemWithImage(R.drawable.ic_pencil_grey600_24dp, context.getString(R.string.nc_demote)),
+        val apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
+
+        if (participant.getActorType() == USERS && participant.getActorId() == conversationUser!!.userId) {
+            if (participant.attendeePin.isNotEmpty()) {
+                val items = mutableListOf(
+                    BasicListItemWithImage(
+                        R.drawable.ic_lock_grey600_24px,
+                        context.getString(R.string.nc_attendee_pin, participant.attendeePin)
+                    )
+                )
+                MaterialDialog(activity!!, BottomSheet(WRAP_CONTENT)).show {
+                    cornerRadius(res = R.dimen.corner_radius)
+
+                    title(text = participant.displayName)
+                    listItemsWithImage(items = items) { dialog, index, _ ->
+                        if (index == 0) {
+                            removeAttendeeFromConversation(apiVersion, participant)
+                        }
+                    }
+                }
+            }
+            return true
+        }
+
+        if (participant.type == Participant.ParticipantType.OWNER) {
+            // Can not moderate owner
+            return true
+        }
+
+        if (participant.getActorType() == GROUPS) {
+            val items = mutableListOf(
                 BasicListItemWithImage(
                     R.drawable.ic_delete_grey600_24dp,
-                    context.getString(R.string.nc_remove_participant)
+                    context.getString(R.string.nc_remove_group_and_members)
                 )
             )
+            MaterialDialog(activity!!, BottomSheet(WRAP_CONTENT)).show {
+                cornerRadius(res = R.dimen.corner_radius)
 
-            if (!conversation!!.canModerate(conversationUser)) {
-                items = mutableListOf()
-            } else {
-                if (participant.type == Participant.ParticipantType.MODERATOR || participant.type == Participant.ParticipantType.OWNER) {
-                    items.removeAt(0)
-                } else if (participant.type == Participant.ParticipantType.USER) {
-                    items.removeAt(1)
+                title(text = participant.displayName)
+                listItemsWithImage(items = items) { dialog, index, _ ->
+                    if (index == 0) {
+                        removeAttendeeFromConversation(apiVersion, participant)
+                    }
                 }
             }
+            return true
+        }
 
-            if (items.isNotEmpty()) {
-                MaterialDialog(activity!!, BottomSheet(WRAP_CONTENT)).show {
-                    cornerRadius(res = R.dimen.corner_radius)
+        var items = mutableListOf(
+            BasicListItemWithImage(
+                R.drawable.ic_lock_grey600_24px,
+                context.getString(R.string.nc_attendee_pin, participant.attendeePin)
+            ),
+            BasicListItemWithImage(R.drawable.ic_pencil_grey600_24dp, context.getString(R.string.nc_promote)),
+            BasicListItemWithImage(R.drawable.ic_pencil_grey600_24dp, context.getString(R.string.nc_demote)),
+            BasicListItemWithImage(
+                R.drawable.ic_delete_grey600_24dp,
+                context.getString(R.string.nc_remove_participant)
+            )
+        )
 
-                    title(text = participant.displayName)
-                    listItemsWithImage(items = items) { dialog, index, _ ->
+        if (participant.type == Participant.ParticipantType.MODERATOR ||
+            participant.type == Participant.ParticipantType.GUEST_MODERATOR
+        ) {
+            items.removeAt(1)
+        } else if (participant.type == Participant.ParticipantType.USER ||
+            participant.type == Participant.ParticipantType.GUEST
+        ) {
+            items.removeAt(2)
+        } else {
+            // Self joined users can not be promoted nor demoted
+            items.removeAt(2)
+            items.removeAt(1)
+        }
 
-                        val apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(1))
+        if (participant.attendeePin.isEmpty()) {
+            items.removeAt(0)
+        }
 
-                        if (index == 0) {
-                            if (participant.type == Participant.ParticipantType.MODERATOR) {
-                                ncApi.demoteModeratorToUser(
-                                    credentials,
-                                    ApiUtils.getUrlForRoomModerators(
-                                        apiVersion,
-                                        conversationUser.baseUrl,
-                                        conversation!!.token
-                                    ),
-                                    participant.userId
-                                )
-                                    .subscribeOn(Schedulers.io())
-                                    .observeOn(AndroidSchedulers.mainThread())
-                                    .subscribe {
-                                        getListOfParticipants()
-                                    }
-                            } else if (participant.type == Participant.ParticipantType.USER) {
-                                ncApi.promoteUserToModerator(
-                                    credentials,
-                                    ApiUtils.getUrlForRoomModerators(
-                                        apiVersion,
-                                        conversationUser.baseUrl,
-                                        conversation!!.token
-                                    ),
-                                    participant.userId
-                                )
-                                    .subscribeOn(Schedulers.io())
-                                    .observeOn(AndroidSchedulers.mainThread())
-                                    .subscribe {
-                                        getListOfParticipants()
-                                    }
-                            }
-                        } else if (index == 1) {
-                            if (participant.type == Participant.ParticipantType.GUEST ||
-                                participant.type == Participant.ParticipantType.USER_FOLLOWING_LINK
-                            ) {
-                                ncApi.removeParticipantFromConversation(
-                                    credentials,
-                                    ApiUtils.getUrlForRemovingParticipantFromConversation(
-                                        conversationUser.baseUrl,
-                                        conversation!!.token,
-                                        true
-                                    ),
-                                    participant.sessionId
-                                )
-                                    .subscribeOn(Schedulers.io())
-                                    .observeOn(AndroidSchedulers.mainThread())
-                                    .subscribe {
-                                        getListOfParticipants()
-                                    }
-                            } else {
-                                ncApi.removeParticipantFromConversation(
-                                    credentials,
-                                    ApiUtils.getUrlForRemovingParticipantFromConversation(
-                                        conversationUser.baseUrl,
-                                        conversation!!.token,
-                                        false
-                                    ),
-                                    participant.userId
-                                )
-                                    .subscribeOn(Schedulers.io())
-                                    .observeOn(AndroidSchedulers.mainThread())
-                                    .subscribe {
-                                        getListOfParticipants()
-                                        // get participants again
-                                    }
-                            }
+        if (items.isNotEmpty()) {
+            MaterialDialog(activity!!, BottomSheet(WRAP_CONTENT)).show {
+                cornerRadius(res = R.dimen.corner_radius)
+
+                title(text = participant.displayName)
+                listItemsWithImage(items = items) { dialog, index, _ ->
+                    var actionToTrigger = index
+                    if (participant.attendeePin.isEmpty()) {
+                        actionToTrigger++
+                    }
+                    if (participant.type == Participant.ParticipantType.USER_FOLLOWING_LINK) {
+                        actionToTrigger++
+                    }
+
+                    if (actionToTrigger == 0) {
+                        // Pin, nothing to do
+                    } else if (actionToTrigger == 1) {
+                        // Promote/demote
+                        if (apiVersion >= ApiUtils.APIv4) {
+                            toggleModeratorStatus(apiVersion, participant)
+                        } else {
+                            toggleModeratorStatusLegacy(apiVersion, participant)
                         }
+                    } else if (actionToTrigger == 2) {
+                        // Remove from conversation
+                        removeAttendeeFromConversation(apiVersion, participant)
                     }
                 }
             }
         }
-
         return true
     }
 
     companion object {
 
+        private const val TAG = "ConversationInfoController"
         private const val ID_DELETE_CONVERSATION_DIALOG = 0
     }
 
@@ -804,6 +1018,13 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
      */
     class UserItemComparator : Comparator<UserItem> {
         override fun compare(left: UserItem, right: UserItem): Int {
+            val leftIsGroup = left.model.actorType == GROUPS
+            val rightIsGroup = right.model.actorType == GROUPS
+            if (leftIsGroup != rightIsGroup) {
+                // Groups below participants
+                return if (rightIsGroup) { -1 } else { 1 }
+            }
+
             if (left.isOnline && !right.isOnline) {
                 return -1
             } else if (!left.isOnline && right.isOnline) {

+ 8 - 7
app/src/main/java/com/nextcloud/talk/controllers/SwitchAccountController.java

@@ -178,17 +178,17 @@ public class SwitchAccountController extends BaseController {
                 for (Object userEntityObject : userUtils.getUsers()) {
                     userEntity = (UserEntity) userEntityObject;
                     if (!userEntity.getCurrent()) {
-                        participant = new Participant();
-                        participant.setName(userEntity.getDisplayName());
-
                         String userId;
-
                         if (userEntity.getUserId() != null) {
                             userId = userEntity.getUserId();
                         } else {
                             userId = userEntity.getUsername();
                         }
-                        participant.setUserId(userId);
+
+                        participant = new Participant();
+                        participant.setActorType(Participant.ActorType.USERS);
+                        participant.setActorId(userId);
+                        participant.setDisplayName(userEntity.getDisplayName());
                         userItems.add(new AdvancedUserItem(participant, userEntity, null));
                     }
                 }
@@ -203,8 +203,9 @@ public class SwitchAccountController extends BaseController {
                     importAccount = AccountUtils.INSTANCE.getInformationFromAccount(account);
 
                     participant = new Participant();
-                    participant.setName(importAccount.getUsername());
-                    participant.setUserId(importAccount.getUsername());
+                    participant.setActorType(Participant.ActorType.USERS);
+                    participant.setActorId(importAccount.getUsername());
+                    participant.setDisplayName(importAccount.getUsername());
                     userEntity = new UserEntity();
                     userEntity.setBaseUrl(importAccount.getBaseUrl());
                     userItems.add(new AdvancedUserItem(participant, userEntity, account));

+ 5 - 53
app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/OperationsMenuController.java

@@ -54,7 +54,6 @@ import com.nextcloud.talk.models.json.capabilities.Capabilities;
 import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall;
 import com.nextcloud.talk.models.json.conversations.Conversation;
 import com.nextcloud.talk.models.json.conversations.RoomOverall;
-import com.nextcloud.talk.models.json.generic.GenericOverall;
 import com.nextcloud.talk.models.json.participants.AddParticipantOverall;
 import com.nextcloud.talk.utils.ApiUtils;
 import com.nextcloud.talk.utils.ConductorRemapping;
@@ -270,29 +269,20 @@ public class OperationsMenuController extends BaseController {
                     break;
                 case 11:
                     RetrofitBucket retrofitBucket;
-                    boolean isGroupCallWorkaround = false;
                     String invite = null;
 
                     if (invitedGroups.size() > 0) {
                         invite = invitedGroups.get(0);
                     }
 
-                    if (conversationType.equals(Conversation.ConversationType.ROOM_PUBLIC_CALL) ||
-                            !currentUser.hasSpreedFeatureCapability("empty-group-room")) {
+                    if (conversationType.equals(Conversation.ConversationType.ROOM_PUBLIC_CALL)) {
                         retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(apiVersion, currentUser.getBaseUrl(),
-                                "3", invite, conversationName);
+                                                                                 "3", invite, conversationName);
                     } else {
-                        String roomType = "2";
-                        if (!currentUser.hasSpreedFeatureCapability("empty-group-room")) {
-                            isGroupCallWorkaround = true;
-                            roomType = "3";
-                        }
-
                         retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(apiVersion, currentUser.getBaseUrl(),
-                                roomType, invite, conversationName);
+                                                                                 "2", invite, conversationName);
                     }
 
-                    final boolean isGroupCallWorkaroundFinal = isGroupCallWorkaround;
                     ncApi.createRoom(credentials, retrofitBucket.getUrl(), retrofitBucket.getQueryMap())
                             .subscribeOn(Schedulers.io())
                             .observeOn(AndroidSchedulers.mainThread())
@@ -321,11 +311,7 @@ public class OperationsMenuController extends BaseController {
                                                 @Override
                                                 public void onNext(RoomOverall roomOverall) {
                                                     conversation = roomOverall.getOcs().getData();
-                                                    if (conversationType.equals(Conversation.ConversationType.ROOM_PUBLIC_CALL) && isGroupCallWorkaroundFinal) {
-                                                        performGroupCallWorkaround(credentials);
-                                                    } else {
-                                                        inviteUsersToAConversation();
-                                                    }
+                                                    inviteUsersToAConversation();
                                                 }
 
                                                 @Override
@@ -393,40 +379,6 @@ public class OperationsMenuController extends BaseController {
         }
     }
 
-    private void performGroupCallWorkaround(String credentials) {
-        int apiVersion = ApiUtils.getConversationApiVersion(currentUser, new int[] {ApiUtils.APIv4, 1});
-
-        ncApi.makeRoomPrivate(credentials, ApiUtils.getUrlForRoomPublic(apiVersion, currentUser.getBaseUrl(),
-                                                                            conversation.getToken()))
-                .subscribeOn(Schedulers.io())
-                .observeOn(AndroidSchedulers.mainThread())
-                .retry(1)
-                .subscribe(new Observer<GenericOverall>() {
-                    @Override
-                    public void onSubscribe(Disposable d) {
-
-                    }
-
-                    @Override
-                    public void onNext(GenericOverall genericOverall) {
-                        inviteUsersToAConversation();
-                    }
-
-                    @Override
-                    public void onError(Throwable e) {
-                        showResultImage(false, false);
-                        dispose();
-                    }
-
-                    @Override
-                    public void onComplete() {
-                        dispose();
-                    }
-                });
-
-
-    }
-
     private void showResultImage(boolean everythingOK, boolean isGuestSupportError) {
         progressBar.setVisibility(View.GONE);
 
@@ -552,7 +504,7 @@ public class OperationsMenuController extends BaseController {
             localInvitedGroups.remove(0);
         }
 
-        int apiVersion = ApiUtils.getConversationApiVersion(currentUser, new int[] {1});
+        int apiVersion = ApiUtils.getConversationApiVersion(currentUser, new int[] {4, 1});
 
         if (localInvitedUsers.size() > 0 || (localInvitedGroups.size() > 0 && currentUser.hasSpreedFeatureCapability("invite-groups-and-mails"))) {
             if ((localInvitedGroups.size() > 0 && currentUser.hasSpreedFeatureCapability("invite-groups-and-mails"))) {

+ 1 - 1
app/src/main/java/com/nextcloud/talk/jobs/AddParticipantsToConversation.java

@@ -66,7 +66,7 @@ public class AddParticipantsToConversation extends Worker {
         String[] selectedGroupIds = data.getStringArray(BundleKeys.INSTANCE.getKEY_SELECTED_GROUPS());
         UserEntity user = userUtils.getUserWithInternalId(data.getLong(BundleKeys.INSTANCE.getKEY_INTERNAL_USER_ID(), -1));
 
-        int apiVersion = ApiUtils.getConversationApiVersion(user, new int[] {1});
+        int apiVersion = ApiUtils.getConversationApiVersion(user, new int[] {ApiUtils.APIv4, 1});
 
         String conversationToken = data.getString(BundleKeys.INSTANCE.getKEY_TOKEN());
         String credentials = ApiUtils.getCredentials(user.getUsername(), user.getToken());

+ 101 - 102
app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.java

@@ -84,9 +84,13 @@ public class Conversation {
     public int lastReadMessage;
     @JsonField(name = "callFlag")
     public int callFlag;
+
     @JsonField(name = "canLeaveConversation")
     public Boolean canLeaveConversation;
 
+    @JsonField(name = "canDeleteConversation")
+    public Boolean canDeleteConversation;
+
     public boolean isPublic() {
         return (ConversationType.ROOM_PUBLIC_CALL.equals(type));
     }
@@ -133,6 +137,15 @@ public class Conversation {
                 (getType() != ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL && this.participants.size() > 1);
     }
 
+    public boolean canDelete(UserEntity conversationUser) {
+        if (canDeleteConversation != null) {
+            // Available since APIv2
+            return canDeleteConversation;
+        }
+        // Fallback for APIv1
+        return canModerate(conversationUser);
+    }
+
     public String getRoomId() {
         return this.roomId;
     }
@@ -314,169 +327,155 @@ public class Conversation {
         this.callFlag = callFlag;
     }
 
-    public boolean equals(final Object o) {
-        if (o == this) {
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
             return true;
         }
-        if (!(o instanceof Conversation)) {
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        Conversation that = (Conversation) o;
+
+        if (lastPing != that.lastPing) {
             return false;
         }
-        final Conversation other = (Conversation) o;
-        if (!other.canEqual((Object) this)) {
+        if (hasPassword != that.hasPassword) {
             return false;
         }
-        final Object this$roomId = this.getRoomId();
-        final Object other$roomId = other.getRoomId();
-        if (this$roomId == null ? other$roomId != null : !this$roomId.equals(other$roomId)) {
+        if (isFavorite != that.isFavorite) {
             return false;
         }
-        final Object this$token = this.getToken();
-        final Object other$token = other.getToken();
-        if (this$token == null ? other$token != null : !this$token.equals(other$token)) {
+        if (lastActivity != that.lastActivity) {
             return false;
         }
-        final Object this$name = this.getName();
-        final Object other$name = other.getName();
-        if (this$name == null ? other$name != null : !this$name.equals(other$name)) {
+        if (unreadMessages != that.unreadMessages) {
             return false;
         }
-        final Object this$displayName = this.getDisplayName();
-        final Object other$displayName = other.getDisplayName();
-        if (this$displayName == null ? other$displayName != null : !this$displayName.equals(other$displayName)) {
+        if (unreadMention != that.unreadMention) {
             return false;
         }
-        final Object this$type = this.getType();
-        final Object other$type = other.getType();
-        if (this$type == null ? other$type != null : !this$type.equals(other$type)) {
+        if (lastReadMessage != that.lastReadMessage) {
             return false;
         }
-        if (this.getLastPing() != other.getLastPing()) {
+        if (callFlag != that.callFlag) {
             return false;
         }
-        final Object this$participants = this.participants;
-        final Object other$participants = other.participants;
-        if (this$participants == null ? other$participants != null : !this$participants.equals(other$participants)) {
+        if (roomId != null ? !roomId.equals(that.roomId) : that.roomId != null) {
             return false;
         }
-        final Object this$participantType = this.getParticipantType();
-        final Object other$participantType = other.getParticipantType();
-        if (this$participantType == null ? other$participantType != null : !this$participantType.equals(other$participantType)) {
+        if (!token.equals(that.token)) {
             return false;
         }
-        if (this.isHasPassword() != other.isHasPassword()) {
+        if (name != null ? !name.equals(that.name) : that.name != null) {
             return false;
         }
-        final Object this$sessionId = this.getSessionId();
-        final Object other$sessionId = other.getSessionId();
-        if (this$sessionId == null ? other$sessionId != null : !this$sessionId.equals(other$sessionId)) {
+        if (displayName != null ? !displayName.equals(that.displayName) : that.displayName != null) {
             return false;
         }
-        final Object this$password = this.getPassword();
-        final Object other$password = other.getPassword();
-        if (this$password == null ? other$password != null : !this$password.equals(other$password)) {
+        if (type != that.type) {
             return false;
         }
-        if (this.isFavorite() != other.isFavorite()) {
+        if (participants != null ? !participants.equals(that.participants) : that.participants != null) {
             return false;
         }
-        if (this.getLastActivity() != other.getLastActivity()) {
+        if (participantType != that.participantType) {
             return false;
         }
-        if (this.getUnreadMessages() != other.getUnreadMessages()) {
+        if (sessionId != null ? !sessionId.equals(that.sessionId) : that.sessionId != null) {
             return false;
         }
-        if (this.isUnreadMention() != other.isUnreadMention()) {
+        if (password != null ? !password.equals(that.password) : that.password != null) {
             return false;
         }
-        final Object this$lastMessage = this.getLastMessage();
-        final Object other$lastMessage = other.getLastMessage();
-        if (this$lastMessage == null ? other$lastMessage != null : !this$lastMessage.equals(other$lastMessage)) {
+        if (lastMessage != null ? !lastMessage.equals(that.lastMessage) : that.lastMessage != null) {
             return false;
         }
-        final Object this$objectType = this.getObjectType();
-        final Object other$objectType = other.getObjectType();
-        if (this$objectType == null ? other$objectType != null : !this$objectType.equals(other$objectType)) {
+        if (objectType != null ? !objectType.equals(that.objectType) : that.objectType != null) {
             return false;
         }
-        final Object this$notificationLevel = this.getNotificationLevel();
-        final Object other$notificationLevel = other.getNotificationLevel();
-        if (this$notificationLevel == null ? other$notificationLevel != null : !this$notificationLevel.equals(other$notificationLevel)) {
+        if (notificationLevel != that.notificationLevel) {
             return false;
         }
-        final Object this$conversationReadOnlyState = this.getConversationReadOnlyState();
-        final Object other$conversationReadOnlyState = other.getConversationReadOnlyState();
-        if (this$conversationReadOnlyState == null ? other$conversationReadOnlyState != null : !this$conversationReadOnlyState.equals(other$conversationReadOnlyState)) {
+        if (conversationReadOnlyState != that.conversationReadOnlyState) {
             return false;
         }
-        final Object this$lobbyState = this.getLobbyState();
-        final Object other$lobbyState = other.getLobbyState();
-        if (this$lobbyState == null ? other$lobbyState != null : !this$lobbyState.equals(other$lobbyState)) {
+        if (lobbyState != that.lobbyState) {
             return false;
         }
-        final Object this$lobbyTimer = this.getLobbyTimer();
-        final Object other$lobbyTimer = other.getLobbyTimer();
-        if (this$lobbyTimer == null ? other$lobbyTimer != null : !this$lobbyTimer.equals(other$lobbyTimer)) {
+        if (lobbyTimer != null ? !lobbyTimer.equals(that.lobbyTimer) : that.lobbyTimer != null) {
             return false;
         }
-        if (this.getLastReadMessage() != other.getLastReadMessage()) {
+        if (canLeaveConversation != null ? !canLeaveConversation.equals(that.canLeaveConversation) : that.canLeaveConversation != null) {
             return false;
         }
-
-        return this.getCallFlag() == other.getCallFlag();
+        return canDeleteConversation != null ? canDeleteConversation.equals(that.canDeleteConversation) : that.canDeleteConversation == null;
     }
 
     protected boolean canEqual(final Object other) {
         return other instanceof Conversation;
     }
 
+    @Override
     public int hashCode() {
-        final int PRIME = 59;
-        int result = 1;
-        final Object $roomId = this.getRoomId();
-        result = result * PRIME + ($roomId == null ? 43 : $roomId.hashCode());
-        final Object $token = this.getToken();
-        result = result * PRIME + ($token == null ? 43 : $token.hashCode());
-        final Object $name = this.getName();
-        result = result * PRIME + ($name == null ? 43 : $name.hashCode());
-        final Object $displayName = this.getDisplayName();
-        result = result * PRIME + ($displayName == null ? 43 : $displayName.hashCode());
-        final Object $type = this.getType();
-        result = result * PRIME + ($type == null ? 43 : $type.hashCode());
-        final long $lastPing = this.getLastPing();
-        result = result * PRIME + (int) ($lastPing >>> 32 ^ $lastPing);
-        final Object $participants = this.participants;
-        result = result * PRIME + ($participants == null ? 43 : $participants.hashCode());
-        final Object $participantType = this.getParticipantType();
-        result = result * PRIME + ($participantType == null ? 43 : $participantType.hashCode());
-        result = result * PRIME + (this.isHasPassword() ? 79 : 97);
-        final Object $sessionId = this.getSessionId();
-        result = result * PRIME + ($sessionId == null ? 43 : $sessionId.hashCode());
-        final Object $password = this.getPassword();
-        result = result * PRIME + ($password == null ? 43 : $password.hashCode());
-        result = result * PRIME + (this.isFavorite() ? 79 : 97);
-        final long $lastActivity = this.getLastActivity();
-        result = result * PRIME + (int) ($lastActivity >>> 32 ^ $lastActivity);
-        result = result * PRIME + this.getUnreadMessages();
-        result = result * PRIME + (this.isUnreadMention() ? 79 : 97);
-        final Object $lastMessage = this.getLastMessage();
-        result = result * PRIME + ($lastMessage == null ? 43 : $lastMessage.hashCode());
-        final Object $objectType = this.getObjectType();
-        result = result * PRIME + ($objectType == null ? 43 : $objectType.hashCode());
-        final Object $notificationLevel = this.getNotificationLevel();
-        result = result * PRIME + ($notificationLevel == null ? 43 : $notificationLevel.hashCode());
-        final Object $conversationReadOnlyState = this.getConversationReadOnlyState();
-        result = result * PRIME + ($conversationReadOnlyState == null ? 43 : $conversationReadOnlyState.hashCode());
-        final Object $lobbyState = this.getLobbyState();
-        result = result * PRIME + ($lobbyState == null ? 43 : $lobbyState.hashCode());
-        final Object $lobbyTimer = this.getLobbyTimer();
-        result = result * PRIME + ($lobbyTimer == null ? 43 : $lobbyTimer.hashCode());
-        result = result * PRIME + this.getLastReadMessage();
-        result = result * PRIME + this.getCallFlag();
+        int result = roomId != null ? roomId.hashCode() : 0;
+        result = 31 * result + token.hashCode();
+        result = 31 * result + (name != null ? name.hashCode() : 0);
+        result = 31 * result + (displayName != null ? displayName.hashCode() : 0);
+        result = 31 * result + type.hashCode();
+        result = 31 * result + (int) (lastPing ^ (lastPing >>> 32));
+        result = 31 * result + (participants != null ? participants.hashCode() : 0);
+        result = 31 * result + (participantType != null ? participantType.hashCode() : 0);
+        result = 31 * result + (hasPassword ? 1 : 0);
+        result = 31 * result + (sessionId != null ? sessionId.hashCode() : 0);
+        result = 31 * result + (password != null ? password.hashCode() : 0);
+        result = 31 * result + (isFavorite ? 1 : 0);
+        result = 31 * result + (int) (lastActivity ^ (lastActivity >>> 32));
+        result = 31 * result + unreadMessages;
+        result = 31 * result + (unreadMention ? 1 : 0);
+        result = 31 * result + (lastMessage != null ? lastMessage.hashCode() : 0);
+        result = 31 * result + (objectType != null ? objectType.hashCode() : 0);
+        result = 31 * result + (notificationLevel != null ? notificationLevel.hashCode() : 0);
+        result = 31 * result + (conversationReadOnlyState != null ? conversationReadOnlyState.hashCode() : 0);
+        result = 31 * result + (lobbyState != null ? lobbyState.hashCode() : 0);
+        result = 31 * result + (lobbyTimer != null ? lobbyTimer.hashCode() : 0);
+        result = 31 * result + lastReadMessage;
+        result = 31 * result + callFlag;
+        result = 31 * result + (canLeaveConversation != null ? canLeaveConversation.hashCode() : 0);
+        result = 31 * result + (canDeleteConversation != null ? canDeleteConversation.hashCode() : 0);
         return result;
     }
 
+    @Override
     public String toString() {
-        return "Conversation(roomId=" + this.getRoomId() + ", token=" + this.getToken() + ", name=" + this.getName() + ", displayName=" + this.getDisplayName() + ", type=" + this.getType() + ", lastPing=" + this.getLastPing() + ", participants=" + this.participants + ", participantType=" + this.getParticipantType() + ", hasPassword=" + this.isHasPassword() + ", sessionId=" + this.getSessionId() + ", password=" + this.getPassword() + ", isFavorite=" + this.isFavorite() + ", lastActivity=" + this.getLastActivity() + ", unreadMessages=" + this.getUnreadMessages() + ", unreadMention=" + this.isUnreadMention() + ", lastMessage=" + this.getLastMessage() + ", objectType=" + this.getObjectType() + ", notificationLevel=" + this.getNotificationLevel() + ", conversationReadOnlyState=" + this.getConversationReadOnlyState() + ", lobbyState=" + this.getLobbyState() + ", lobbyTimer=" + this.getLobbyTimer() + ", lastReadMessage=" + this.getLastReadMessage() + ", callFlag=" + this.getCallFlag() + ")";
+        return "Conversation{" +
+                "roomId='" + roomId + '\'' +
+                ", token='" + token + '\'' +
+                ", name='" + name + '\'' +
+                ", displayName='" + displayName + '\'' +
+                ", type=" + type +
+                ", lastPing=" + lastPing +
+                ", participants=" + participants +
+                ", participantType=" + participantType +
+                ", hasPassword=" + hasPassword +
+                ", sessionId='" + sessionId + '\'' +
+                ", password='" + password + '\'' +
+                ", isFavorite=" + isFavorite +
+                ", lastActivity=" + lastActivity +
+                ", unreadMessages=" + unreadMessages +
+                ", unreadMention=" + unreadMention +
+                ", lastMessage=" + lastMessage +
+                ", objectType='" + objectType + '\'' +
+                ", notificationLevel=" + notificationLevel +
+                ", conversationReadOnlyState=" + conversationReadOnlyState +
+                ", lobbyState=" + lobbyState +
+                ", lobbyTimer=" + lobbyTimer +
+                ", lastReadMessage=" + lastReadMessage +
+                ", callFlag=" + callFlag +
+                ", canLeaveConversation=" + canLeaveConversation +
+                ", canDeleteConversation=" + canDeleteConversation +
+                '}';
     }
 
     public enum NotificationLevel {

+ 58 - 0
app/src/main/java/com/nextcloud/talk/models/json/converters/EnumActorTypeConverter.kt

@@ -0,0 +1,58 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Joas Schilling
+ * @author Andy Scherzinger
+ * Copyright (C) 2021 Joas Schilling <coding@schilljs.com>
+ * Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.models.json.converters
+
+import com.bluelinelabs.logansquare.typeconverters.StringBasedTypeConverter
+import com.nextcloud.talk.models.json.participants.Participant
+import com.nextcloud.talk.models.json.participants.Participant.ActorType.DUMMY
+import com.nextcloud.talk.models.json.participants.Participant.ActorType.EMAILS
+import com.nextcloud.talk.models.json.participants.Participant.ActorType.GROUPS
+import com.nextcloud.talk.models.json.participants.Participant.ActorType.GUESTS
+import com.nextcloud.talk.models.json.participants.Participant.ActorType.USERS
+
+class EnumActorTypeConverter : StringBasedTypeConverter<Participant.ActorType>() {
+    override fun getFromString(string: String): Participant.ActorType {
+        return when (string) {
+            "emails" -> EMAILS
+            "groups" -> GROUPS
+            "guests" -> GUESTS
+            "users" -> USERS
+            else -> DUMMY
+        }
+    }
+
+    override fun convertToString(`object`: Participant.ActorType?): String {
+
+        if (`object` == null) {
+            return ""
+        }
+
+        return when (`object`) {
+            EMAILS -> "emails"
+            GROUPS -> "groups"
+            GUESTS -> "guests"
+            USERS -> "users"
+            else -> ""
+        }
+    }
+}

+ 157 - 52
app/src/main/java/com/nextcloud/talk/models/json/participants/Participant.java

@@ -22,21 +22,38 @@ package com.nextcloud.talk.models.json.participants;
 
 import com.bluelinelabs.logansquare.annotation.JsonField;
 import com.bluelinelabs.logansquare.annotation.JsonObject;
+import com.nextcloud.talk.models.json.converters.EnumActorTypeConverter;
 import com.nextcloud.talk.models.json.converters.EnumParticipantTypeConverter;
 import com.nextcloud.talk.models.json.converters.ObjectParcelConverter;
 
 import org.parceler.Parcel;
 import org.parceler.ParcelPropertyConverter;
 
+import java.util.Arrays;
+
 @Parcel
 @JsonObject
 public class Participant {
+    @JsonField(name = "attendeeId")
+    public Long attendeeId;
+
+    @JsonField(name = "actorType", typeConverter = EnumActorTypeConverter.class)
+    public ActorType actorType;
+
+    @JsonField(name = "actorId")
+    public String actorId;
+
+    @JsonField(name = "attendeePin")
+    public String attendeePin;
+
+    @Deprecated
     @JsonField(name = "userId")
     public String userId;
 
     @JsonField(name = {"type", "participantType"}, typeConverter = EnumParticipantTypeConverter.class)
     public ParticipantType type;
 
+    @Deprecated
     @JsonField(name = "name")
     public String name;
 
@@ -46,15 +63,21 @@ public class Participant {
     @JsonField(name = "lastPing")
     public long lastPing;
 
+    @Deprecated
     @JsonField(name = "sessionId")
     public String sessionId;
 
+    @JsonField(name = "sessionIds")
+    public String[] sessionIds;
+
+    @Deprecated
     @JsonField(name = "roomId")
     public long roomId;
 
     @ParcelPropertyConverter(ObjectParcelConverter.class)
     @JsonField(name = "inCall")
     public Object inCall;
+
     public String source;
 
     public boolean selected;
@@ -76,7 +99,37 @@ public class Participant {
         return participantFlags;
     }
 
+    public Long getAttendeeId() {
+        return attendeeId;
+    }
+
+    public ActorType getActorType() {
+        if (this.actorType == null) {
+            if (this.userId != null) {
+                return ActorType.USERS;
+            } else {
+                return ActorType.GUESTS;
+            }
+        }
+        return actorType;
+    }
+
+    public String getActorId() {
+        if (this.actorId == null) {
+            return this.userId;
+        }
+        return actorId;
+    }
+
+    public String getAttendeePin() {
+        return attendeePin;
+    }
+
+    @Deprecated
     public String getUserId() {
+        if (this.actorType != null && this.actorType == ActorType.USERS) {
+            return this.actorId;
+        }
         return this.userId;
     }
 
@@ -84,6 +137,7 @@ public class Participant {
         return this.type;
     }
 
+    @Deprecated
     public String getName() {
         return this.name;
     }
@@ -96,10 +150,16 @@ public class Participant {
         return this.lastPing;
     }
 
+    @Deprecated
     public String getSessionId() {
         return this.sessionId;
     }
 
+    public String[] getSessionIds() {
+        return sessionIds;
+    }
+
+    @Deprecated
     public long getRoomId() {
         return this.roomId;
     }
@@ -116,14 +176,32 @@ public class Participant {
         return this.selected;
     }
 
+    @Deprecated
     public void setUserId(String userId) {
         this.userId = userId;
     }
 
+    public void setAttendeeId(Long attendeeId) {
+        this.attendeeId = attendeeId;
+    }
+
+    public void setActorType(ActorType actorType) {
+        this.actorType = actorType;
+    }
+
+    public void setActorId(String actorId) {
+        this.actorId = actorId;
+    }
+
+    public void setAttendeePin(String attendeePin) {
+        this.attendeePin = attendeePin;
+    }
+
     public void setType(ParticipantType type) {
         this.type = type;
     }
 
+    @Deprecated
     public void setName(String name) {
         this.name = name;
     }
@@ -136,10 +214,12 @@ public class Participant {
         this.lastPing = lastPing;
     }
 
+    @Deprecated
     public void setSessionId(String sessionId) {
         this.sessionId = sessionId;
     }
 
+    @Deprecated
     public void setRoomId(long roomId) {
         this.roomId = roomId;
     }
@@ -156,93 +236,118 @@ public class Participant {
         this.selected = selected;
     }
 
-    public boolean equals(final Object o) {
-        if (o == this) {
+    public void setSessionIds(String[] sessionIds) {
+        this.sessionIds = sessionIds;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
             return true;
         }
-        if (!(o instanceof Participant)) {
+        if (o == null || getClass() != o.getClass()) {
             return false;
         }
-        final Participant other = (Participant) o;
-        if (!other.canEqual((Object) this)) {
+
+        Participant that = (Participant) o;
+
+        if (lastPing != that.lastPing) {
             return false;
         }
-        final Object this$userId = this.getUserId();
-        final Object other$userId = other.getUserId();
-        if (this$userId == null ? other$userId != null : !this$userId.equals(other$userId)) {
+        if (roomId != that.roomId) {
             return false;
         }
-        final Object this$type = this.getType();
-        final Object other$type = other.getType();
-        if (this$type == null ? other$type != null : !this$type.equals(other$type)) {
+        if (selected != that.selected) {
             return false;
         }
-        final Object this$name = this.getName();
-        final Object other$name = other.getName();
-        if (this$name == null ? other$name != null : !this$name.equals(other$name)) {
+        if (!attendeeId.equals(that.attendeeId)) {
             return false;
         }
-        final Object this$displayName = this.getDisplayName();
-        final Object other$displayName = other.getDisplayName();
-        if (this$displayName == null ? other$displayName != null : !this$displayName.equals(other$displayName)) {
+        if (!actorType.equals(that.actorType)) {
             return false;
         }
-        if (this.getLastPing() != other.getLastPing()) {
+        if (!actorId.equals(that.actorId)) {
             return false;
         }
-        final Object this$sessionId = this.getSessionId();
-        final Object other$sessionId = other.getSessionId();
-        if (this$sessionId == null ? other$sessionId != null : !this$sessionId.equals(other$sessionId)) {
+        if (!attendeePin.equals(that.attendeePin)) {
             return false;
         }
-        if (this.getRoomId() != other.getRoomId()) {
+        if (!userId.equals(that.userId)) {
             return false;
         }
-        final Object this$inCall = this.getInCall();
-        final Object other$inCall = other.getInCall();
-        if (this$inCall == null ? other$inCall != null : !this$inCall.equals(other$inCall)) {
+        if (type != that.type) {
             return false;
         }
-        final Object this$source = this.getSource();
-        final Object other$source = other.getSource();
-        if (this$source == null ? other$source != null : !this$source.equals(other$source)) {
+        if (!name.equals(that.name)) {
             return false;
         }
-
-        return this.isSelected() == other.isSelected();
+        if (displayName != null ? !displayName.equals(that.displayName) : that.displayName != null) {
+            return false;
+        }
+        if (!sessionId.equals(that.sessionId)) {
+            return false;
+        }
+        // Probably incorrect - comparing Object[] arrays with Arrays.equals
+        if (!Arrays.equals(sessionIds, that.sessionIds)) {
+            return false;
+        }
+        if (inCall != null ? !inCall.equals(that.inCall) : that.inCall != null) {
+            return false;
+        }
+        return source != null ? source.equals(that.source) : that.source == null;
     }
 
     protected boolean canEqual(final Object other) {
         return other instanceof Participant;
     }
 
+    @Override
     public int hashCode() {
-        final int PRIME = 59;
-        int result = 1;
-        final Object $userId = this.getUserId();
-        result = result * PRIME + ($userId == null ? 43 : $userId.hashCode());
-        final Object $type = this.getType();
-        result = result * PRIME + ($type == null ? 43 : $type.hashCode());
-        final Object $name = this.getName();
-        result = result * PRIME + ($name == null ? 43 : $name.hashCode());
-        final Object $displayName = this.getDisplayName();
-        result = result * PRIME + ($displayName == null ? 43 : $displayName.hashCode());
-        final long $lastPing = this.getLastPing();
-        result = result * PRIME + (int) ($lastPing >>> 32 ^ $lastPing);
-        final Object $sessionId = this.getSessionId();
-        result = result * PRIME + ($sessionId == null ? 43 : $sessionId.hashCode());
-        final long $roomId = this.getRoomId();
-        result = result * PRIME + (int) ($roomId >>> 32 ^ $roomId);
-        final Object $inCall = this.getInCall();
-        result = result * PRIME + ($inCall == null ? 43 : $inCall.hashCode());
-        final Object $source = this.getSource();
-        result = result * PRIME + ($source == null ? 43 : $source.hashCode());
-        result = result * PRIME + (this.isSelected() ? 79 : 97);
+        int result = (attendeeId != null ? attendeeId.hashCode() : 0);
+        result = 31 * result + (actorType != null ? actorType.hashCode() : 0);
+        result = 31 * result + (actorId != null ? actorId.hashCode() : 0);
+        result = 31 * result + (attendeePin != null ? attendeePin.hashCode() : 0);
+        result = 31 * result + (userId != null ? userId.hashCode() : 0);
+        result = 31 * result + (type != null ? type.hashCode() : 0);
+        result = 31 * result + (name != null ? name.hashCode() : 0);
+        result = 31 * result + (displayName != null ? displayName.hashCode() : 0);
+        result = 31 * result + (int) (lastPing ^ (lastPing >>> 32));
+        result = 31 * result + (sessionId != null ? sessionId.hashCode() : 0);
+        result = 31 * result + Arrays.hashCode(sessionIds);
+        result = 31 * result + (int) (roomId ^ (roomId >>> 32));
+        result = 31 * result + (inCall != null ? inCall.hashCode() : 0);
+        result = 31 * result + (source != null ? source.hashCode() : 0);
+        result = 31 * result + (selected ? 1 : 0);
         return result;
     }
 
+    @Override
     public String toString() {
-        return "Participant(userId=" + this.getUserId() + ", type=" + this.getType() + ", name=" + this.getName() + ", displayName=" + this.getDisplayName() + ", lastPing=" + this.getLastPing() + ", sessionId=" + this.getSessionId() + ", roomId=" + this.getRoomId() + ", inCall=" + this.getInCall() + ", source=" + this.getSource() + ", selected=" + this.isSelected() + ")";
+        return "Participant{" +
+                "attendeeId=" + attendeeId +
+                ", actorType='" + actorType + '\'' +
+                ", actorId='" + actorId + '\'' +
+                ", attendeePin='" + attendeePin + '\'' +
+                ", userId='" + userId + '\'' +
+                ", type=" + type +
+                ", name='" + name + '\'' +
+                ", displayName='" + displayName + '\'' +
+                ", lastPing=" + lastPing +
+                ", sessionId='" + sessionId + '\'' +
+                ", sessionIds=" + Arrays.toString(sessionIds) +
+                ", roomId=" + roomId +
+                ", inCall=" + inCall +
+                ", source='" + source + '\'' +
+                ", selected=" + selected +
+                '}';
+    }
+
+    public enum ActorType {
+        DUMMY,
+        EMAILS,
+        GROUPS,
+        GUESTS,
+        USERS,
     }
 
     public enum ParticipantType {

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

@@ -111,6 +111,10 @@ public class ApiUtils {
         return baseUrl + ocsApiVersion + "/cloud/capabilities";
     }
 
+    public static int getCallApiVersion(UserEntity capabilities, int[] versions) throws NoSupportedApiException {
+        return getConversationApiVersion(capabilities, versions);
+    }
+
     public static int getConversationApiVersion(UserEntity capabilities, int[] versions) throws NoSupportedApiException {
         boolean hasApiV4 = false;
         for (int version : versions) {

+ 4 - 0
app/src/main/res/values/strings.xml

@@ -255,6 +255,8 @@
     <string name="nc_demote">Demote from moderator</string>
     <string name="nc_promote">Promote to moderator</string>
     <string name="nc_remove_participant">Remove participant</string>
+    <string name="nc_remove_group_and_members">Remove group and members</string>
+    <string name="nc_attendee_pin">Pin: %1$s</string>
 
     <!-- Chat -->
     <string name="nc_hint_enter_a_message">Enter a message…</string>
@@ -305,6 +307,8 @@
 
     <!-- Other -->
     <string name="nc_limit_hit">%s characters limit has been hit</string>
+    <string name="nc_email">Email</string>
+    <string name="nc_group">Group</string>
     <string name="nc_groups">Groups</string>
     <string name="nc_participants">Participants</string>
     <string name="nc_participants_add">Add participants</string>

+ 1 - 1
scripts/analysis/findbugs-results.txt

@@ -1 +1 @@
-476
+475