refactored group chat members into seperate screen

Daniel Gultsch created

Change summary

src/main/AndroidManifest.xml                                            | 200 
src/main/java/eu/siacs/conversations/entities/MucOptions.java           |  26 
src/main/java/eu/siacs/conversations/services/AvatarService.java        |   2 
src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java  | 258 
src/main/java/eu/siacs/conversations/ui/MucUsersActivity.java           |  71 
src/main/java/eu/siacs/conversations/ui/adapter/UserAdapter.java        | 147 
src/main/java/eu/siacs/conversations/ui/adapter/UserPreviewAdapter.java |  73 
src/main/java/eu/siacs/conversations/ui/util/GridManager.java           |   3 
src/main/res/layout/account_row.xml                                     |   2 
src/main/res/layout/activity_muc_details.xml                            |  38 
src/main/res/layout/activity_muc_users.xml                              |  33 
src/main/res/layout/contact.xml                                         |   2 
src/main/res/layout/user_preview.xml                                    |  15 
src/main/res/values/strings.xml                                         |   3 
14 files changed, 542 insertions(+), 331 deletions(-)

Detailed changes

src/main/AndroidManifest.xml πŸ”—

@@ -1,52 +1,52 @@
 <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          xmlns:tools="http://schemas.android.com/tools"
-          package="eu.siacs.conversations">
-
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
-    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
-    <uses-permission android:name="android.permission.READ_CONTACTS"/>
-    <uses-permission android:name="android.permission.READ_PROFILE"/>
-    <uses-permission android:name="android.permission.INTERNET"/>
-    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
-    <uses-permission android:name="android.permission.WAKE_LOCK"/>
-    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
-    <uses-permission android:name="android.permission.VIBRATE"/>
-    <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
-    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
-    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
-    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
-    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
+    xmlns:tools="http://schemas.android.com/tools"
+    package="eu.siacs.conversations">
+
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.READ_CONTACTS" />
+    <uses-permission android:name="android.permission.READ_PROFILE" />
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.WAKE_LOCK" />
+    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
+    <uses-permission android:name="android.permission.VIBRATE" />
+    <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
+    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
 
     <uses-feature
         android:name="android.hardware.location"
-        android:required="false"/>
+        android:required="false" />
     <uses-feature
         android:name="android.hardware.location.gps"
-        android:required="false"/>
+        android:required="false" />
     <uses-feature
         android:name="android.hardware.location.network"
-        android:required="false"/>
+        android:required="false" />
 
-    <uses-permission android:name="android.permission.CAMERA"/>
-    <uses-permission android:name="android.permission.RECORD_AUDIO"/>
+    <uses-permission android:name="android.permission.CAMERA" />
+    <uses-permission android:name="android.permission.RECORD_AUDIO" />
 
     <uses-permission
         android:name="android.permission.READ_PHONE_STATE"
-        tools:node="remove"/>
+        tools:node="remove" />
 
-    <uses-sdk tools:overrideLibrary="net.ypresto.androidtranscoder"/>
+    <uses-sdk tools:overrideLibrary="net.ypresto.androidtranscoder" />
 
     <uses-feature
         android:name="android.hardware.camera"
-        android:required="false"/>
+        android:required="false" />
     <uses-feature
         android:name="android.hardware.camera.autofocus"
-        android:required="false"/>
+        android:required="false" />
 
     <uses-feature
         android:name="android.hardware.microphone"
-        android:required="false"/>
+        android:required="false" />
 
 
     <application
@@ -61,82 +61,82 @@
 
         <meta-data
             android:name="com.google.android.gms.car.application"
-            android:resource="@xml/automotive_app_desc"/>
+            android:resource="@xml/automotive_app_desc" />
 
-        <service android:name=".services.XmppConnectionService"/>
+        <service android:name=".services.XmppConnectionService" />
 
         <receiver android:name=".services.EventReceiver">
             <intent-filter>
-                <action android:name="android.intent.action.BOOT_COMPLETED"/>
-                <action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>
-                <action android:name="android.intent.action.ACTION_SHUTDOWN"/>
-                <action android:name="android.media.RINGER_MODE_CHANGED"/>
+                <action android:name="android.intent.action.BOOT_COMPLETED" />
+                <action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
+                <action android:name="android.intent.action.ACTION_SHUTDOWN" />
+                <action android:name="android.media.RINGER_MODE_CHANGED" />
             </intent-filter>
         </receiver>
 
         <activity
             android:name=".ui.ShareLocationActivity"
-            android:label="@string/title_activity_share_location"/>
+            android:label="@string/title_activity_share_location" />
         <activity
             android:name=".ui.SearchActivity"
-            android:label="@string/search_messages"/>
+            android:label="@string/search_messages" />
         <activity
             android:name=".ui.RecordingActivity"
             android:configChanges="orientation|screenSize"
-            android:theme="@style/ConversationsTheme.Dialog"/>
+            android:theme="@style/ConversationsTheme.Dialog" />
         <activity
             android:name=".ui.ShowLocationActivity"
-            android:label="@string/title_activity_show_location"/>
+            android:label="@string/title_activity_show_location" />
         <activity
             android:name=".ui.ConversationActivity"
             android:theme="@style/SplashTheme">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN"/>
+                <action android:name="android.intent.action.MAIN" />
 
-                <category android:name="android.intent.category.LAUNCHER"/>
+                <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
         <activity
             android:name=".ui.ConversationsActivity"
             android:label="@string/app_name"
             android:launchMode="singleTask"
-            android:minHeight="300dp"
             android:minWidth="300dp"
-            android:windowSoftInputMode="stateHidden"/>
+            android:minHeight="300dp"
+            android:windowSoftInputMode="stateHidden" />
         <activity
             android:name=".ui.ScanActivity"
             android:screenOrientation="portrait"
             android:theme="@style/ConversationsTheme.FullScreen"
-            android:windowSoftInputMode="stateAlwaysHidden"/>
+            android:windowSoftInputMode="stateAlwaysHidden" />
         <activity
             android:name=".ui.UriHandlerActivity"
             android:label="@string/app_name">
             <intent-filter>
-                <action android:name="android.intent.action.VIEW"/>
+                <action android:name="android.intent.action.VIEW" />
 
-                <category android:name="android.intent.category.DEFAULT"/>
-                <category android:name="android.intent.category.BROWSABLE"/>
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
 
-                <data android:scheme="xmpp"/>
+                <data android:scheme="xmpp" />
             </intent-filter>
             <intent-filter android:autoVerify="true">
-                <action android:name="android.intent.action.VIEW"/>
+                <action android:name="android.intent.action.VIEW" />
 
-                <category android:name="android.intent.category.DEFAULT"/>
-                <category android:name="android.intent.category.BROWSABLE"/>
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
 
-                <data android:scheme="https"/>
-                <data android:host="conversations.im"/>
-                <data android:pathPrefix="/i/"/>
-                <data android:pathPrefix="/j/"/>
+                <data android:scheme="https" />
+                <data android:host="conversations.im" />
+                <data android:pathPrefix="/i/" />
+                <data android:pathPrefix="/j/" />
             </intent-filter>
             <intent-filter>
-                <action android:name="android.intent.action.SENDTO"/>
+                <action android:name="android.intent.action.SENDTO" />
 
-                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.DEFAULT" />
 
-                <data android:scheme="imto"/>
-                <data android:host="jabber"/>
+                <data android:scheme="imto" />
+                <data android:host="jabber" />
             </intent-filter>
         </activity>
         <activity
@@ -144,7 +144,7 @@
             android:label="@string/title_activity_start_conversation"
             android:launchMode="singleTop">
             <intent-filter>
-                <action android:name="android.intent.action.VIEW"/>
+                <action android:name="android.intent.action.VIEW" />
             </intent-filter>
         </activity>
         <activity
@@ -157,101 +157,104 @@
         </activity>
         <activity
             android:name=".ui.ChooseContactActivity"
-            android:label="@string/title_activity_choose_contact"/>
+            android:label="@string/title_activity_choose_contact" />
         <activity
             android:name=".ui.BlocklistActivity"
-            android:label="@string/title_activity_block_list"/>
+            android:label="@string/title_activity_block_list" />
         <activity
             android:name=".ui.ChangePasswordActivity"
-            android:label="@string/change_password_on_server"/>
-        <activity android:name=".ui.ChooseAccountForProfilePictureActivity"
-            android:label="@string/choose_account"
-            android:enabled="false">
+            android:label="@string/change_password_on_server" />
+        <activity
+            android:name=".ui.ChooseAccountForProfilePictureActivity"
+            android:enabled="false"
+            android:label="@string/choose_account">
             <intent-filter android:label="@string/set_profile_picture">
-                <action android:name="android.intent.action.ATTACH_DATA"/>
-                <category android:name="android.intent.category.DEFAULT"/>
+                <action android:name="android.intent.action.ATTACH_DATA" />
+                <category android:name="android.intent.category.DEFAULT" />
 
-                <data android:mimeType="image/*"/>
+                <data android:mimeType="image/*" />
             </intent-filter>
         </activity>
         <activity
             android:name=".ui.ShareViaAccountActivity"
             android:label="@string/title_activity_share_via_account"
-            android:launchMode="singleTop"/>
+            android:launchMode="singleTop" />
         <activity
             android:name=".ui.EditAccountActivity"
             android:launchMode="singleTop"
-            android:windowSoftInputMode="stateHidden|adjustResize"/>
+            android:windowSoftInputMode="stateHidden|adjustResize" />
         <activity
             android:name=".ui.ConferenceDetailsActivity"
             android:label="@string/action_muc_details"
-            android:windowSoftInputMode="stateHidden"/>
+            android:windowSoftInputMode="stateHidden" />
         <activity
             android:name=".ui.ContactDetailsActivity"
-            android:windowSoftInputMode="stateHidden"/>
+            android:windowSoftInputMode="stateHidden" />
         <activity
             android:name=".ui.PublishProfilePictureActivity"
             android:label="@string/mgmt_account_publish_avatar"
-            android:windowSoftInputMode="stateHidden"/>
+            android:windowSoftInputMode="stateHidden" />
         <activity
             android:name=".ui.PublishGroupChatProfilePictureActivity"
-            android:label="@string/group_chat_avatar"/>
+            android:label="@string/group_chat_avatar" />
         <activity
             android:name=".ui.ShareWithActivity"
             android:label="@string/app_name"
             android:launchMode="singleTop">
 
             <intent-filter>
-                <action android:name="android.intent.action.SEND"/>
-                <action android:name="android.intent.action.SEND_MULTIPLE"/>
+                <action android:name="android.intent.action.SEND" />
+                <action android:name="android.intent.action.SEND_MULTIPLE" />
 
-                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.DEFAULT" />
 
-                <data android:mimeType="text/plain"/>
+                <data android:mimeType="text/plain" />
             </intent-filter>
 
             <intent-filter>
-                <action android:name="android.intent.action.SEND"/>
-                <action android:name="android.intent.action.SEND_MULTIPLE"/>
+                <action android:name="android.intent.action.SEND" />
+                <action android:name="android.intent.action.SEND_MULTIPLE" />
 
-                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.DEFAULT" />
 
-                <data android:mimeType="*/*"/>
+                <data android:mimeType="*/*" />
             </intent-filter>
 
             <meta-data
                 android:name="android.service.chooser.chooser_target_service"
-                android:value=".services.ContactChooserTargetService"/>
+                android:value=".services.ContactChooserTargetService" />
         </activity>
         <activity
             android:name=".ui.TrustKeysActivity"
             android:label="@string/trust_omemo_fingerprints"
-            android:windowSoftInputMode="stateAlwaysHidden"/>
+            android:windowSoftInputMode="stateAlwaysHidden" />
         <activity
             android:name=".ui.AboutActivity"
             android:parentActivityName=".ui.SettingsActivity">
             <meta-data
                 android:name="android.support.PARENT_ACTIVITY"
-                android:value="eu.siacs.conversations.ui.SettingsActivity"/>
+                android:value="eu.siacs.conversations.ui.SettingsActivity" />
             <intent-filter>
-                <action android:name="android.intent.action.VIEW"/>
-                <category android:name="android.intent.category.PREFERENCE"/>
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.PREFERENCE" />
             </intent-filter>
         </activity>
-        <activity android:name="com.theartofdev.edmodo.cropper.CropImageActivity"
-                  android:theme="@style/Base.Theme.AppCompat"/>
-        <activity android:name=".ui.MemorizingActivity"/>
+        <activity
+            android:name="com.theartofdev.edmodo.cropper.CropImageActivity"
+            android:theme="@style/Base.Theme.AppCompat" />
+        <activity android:name=".ui.MemorizingActivity" />
 
-        <activity android:name=".ui.MediaBrowserActivity"
-            android:label="@string/media_browser"/>
+        <activity
+            android:name=".ui.MediaBrowserActivity"
+            android:label="@string/media_browser" />
 
-        <service android:name=".services.ExportBackupService"/>
-        <service android:name=".services.ImportBackupService"/>
+        <service android:name=".services.ExportBackupService" />
+        <service android:name=".services.ImportBackupService" />
         <service
             android:name=".services.ContactChooserTargetService"
             android:permission="android.permission.BIND_CHOOSER_TARGET_SERVICE">
             <intent-filter>
-                <action android:name="android.service.chooser.ChooserTargetService"/>
+                <action android:name="android.service.chooser.ChooserTargetService" />
             </intent-filter>
         </service>
 
@@ -262,21 +265,24 @@
             android:grantUriPermissions="true">
             <meta-data
                 android:name="android.support.FILE_PROVIDER_PATHS"
-                android:resource="@xml/file_paths"/>
+                android:resource="@xml/file_paths" />
         </provider>
         <provider
             android:name=".services.BarcodeProvider"
             android:authorities="${applicationId}.barcodes"
             android:exported="false"
-            android:grantUriPermissions="true"/>
+            android:grantUriPermissions="true" />
 
         <activity
             android:name=".ui.ShortcutActivity"
             android:label="@string/contact">
             <intent-filter>
-                <action android:name="android.intent.action.CREATE_SHORTCUT"/>
+                <action android:name="android.intent.action.CREATE_SHORTCUT" />
             </intent-filter>
         </activity>
+        <activity
+            android:name=".ui.MucUsersActivity"
+            android:label="@string/group_chat_members" />
     </application>
 
 </manifest>

src/main/java/eu/siacs/conversations/entities/MucOptions.java πŸ”—

@@ -13,6 +13,7 @@ import java.util.Set;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
+import eu.siacs.conversations.services.AvatarService;
 import eu.siacs.conversations.services.MessageArchiveService;
 import eu.siacs.conversations.utils.JidHelper;
 import eu.siacs.conversations.utils.UIHelper;
@@ -382,6 +383,21 @@ public class MucOptions {
         return subset;
     }
 
+    public static List<User> sub(List<User> users, int max) {
+        ArrayList<User> subset = new ArrayList<>();
+        HashSet<Jid> jids = new HashSet<>();
+        for (User user : users) {
+            jids.add(user.getAccount().getJid().asBareJid());
+            if (user.getRealJid() == null || (user.getRealJid().getLocal() != null && jids.add(user.getRealJid()))) {
+                subset.add(user);
+            }
+            if (subset.size() >= max) {
+                break;
+            }
+        }
+        return subset;
+    }
+
     public int getUserCount() {
         synchronized (users) {
             return users.size();
@@ -705,7 +721,7 @@ public class MucOptions {
 
     }
 
-    public static class User implements Comparable<User> {
+    public static class User implements Comparable<User>, AvatarService.Avatarable {
         private Role role = Role.NONE;
         private Affiliation affiliation = Affiliation.NONE;
         private Jid realJid;
@@ -841,7 +857,7 @@ public class MucOptions {
             }
         }
 
-        private String getComparableName() {
+        public String getComparableName() {
             Contact contact = getContact();
             if (contact != null) {
                 return contact.getDisplayName();
@@ -866,5 +882,11 @@ public class MucOptions {
             this.chatState = chatState;
             return true;
         }
+
+        @Override
+        public int getAvatarBackgroundColor() {
+            final String seed = realJid != null ? realJid.asBareJid().toString() : null;
+            return UIHelper.getColorForName(seed == null ? getName() : seed);
+        }
     }
 }

src/main/java/eu/siacs/conversations/services/AvatarService.java πŸ”—

@@ -80,6 +80,8 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
 			return get((Message) avatarable, size, cachedOnly);
 		} else if (avatarable instanceof ListItem) {
 			return get((ListItem) avatarable, size, cachedOnly);
+		} else if (avatarable instanceof MucOptions.User) {
+			return get((MucOptions.User) avatarable, size, cachedOnly);
 		}
 		throw new AssertionError("AvatarService does not know how to generate avatar from "+avatarable.getClass().getName());
 

src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java πŸ”—

@@ -4,47 +4,29 @@ import android.app.PendingIntent;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentSender.SendIntentException;
-import android.content.res.Resources;
 import android.databinding.DataBindingUtil;
-import android.graphics.Bitmap;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-import android.os.AsyncTask;
 import android.os.Bundle;
-import android.support.v4.content.ContextCompat;
 import android.support.v7.app.AlertDialog;
 import android.support.v7.widget.Toolbar;
 import android.text.Editable;
 import android.text.SpannableStringBuilder;
 import android.text.TextWatcher;
-import android.util.Log;
-import android.view.ContextMenu;
-import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
 import android.view.View.OnClickListener;
-import android.view.WindowManager;
-import android.widget.ImageView;
 import android.widget.Toast;
 
-import org.openintents.openpgp.util.OpenPgpUtils;
-
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import java.util.concurrent.RejectedExecutionException;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.crypto.PgpEngine;
 import eu.siacs.conversations.databinding.ActivityMucDetailsBinding;
-import eu.siacs.conversations.databinding.ContactBinding;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Bookmark;
-import eu.siacs.conversations.entities.Contact;
 import eu.siacs.conversations.entities.Conversation;
 import eu.siacs.conversations.entities.MucOptions;
 import eu.siacs.conversations.entities.MucOptions.User;
@@ -52,13 +34,12 @@ import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.services.XmppConnectionService.OnConversationUpdate;
 import eu.siacs.conversations.services.XmppConnectionService.OnMucRosterUpdate;
 import eu.siacs.conversations.ui.adapter.MediaAdapter;
+import eu.siacs.conversations.ui.adapter.UserPreviewAdapter;
 import eu.siacs.conversations.ui.interfaces.OnMediaLoaded;
-import eu.siacs.conversations.ui.service.EmojiService;
 import eu.siacs.conversations.ui.util.Attachment;
 import eu.siacs.conversations.ui.util.AvatarWorkerTask;
 import eu.siacs.conversations.ui.util.GridManager;
 import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
-import eu.siacs.conversations.ui.util.MucDetailsContextMenuHelper;
 import eu.siacs.conversations.ui.util.MyLinkify;
 import eu.siacs.conversations.ui.util.SoftKeyboardUtils;
 import eu.siacs.conversations.utils.AccountUtils;
@@ -66,7 +47,6 @@ import eu.siacs.conversations.utils.Compatibility;
 import eu.siacs.conversations.utils.EmojiWrapper;
 import eu.siacs.conversations.utils.StringUtils;
 import eu.siacs.conversations.utils.StylingHelper;
-import eu.siacs.conversations.utils.UIHelper;
 import eu.siacs.conversations.utils.XmppUri;
 import me.drakeet.support.toast.ToastCompat;
 import rocks.xmpp.addr.Jid;
@@ -77,20 +57,11 @@ import static eu.siacs.conversations.utils.StringUtils.changed;
 public class ConferenceDetailsActivity extends XmppActivity implements OnConversationUpdate, OnMucRosterUpdate, XmppConnectionService.OnAffiliationChanged, XmppConnectionService.OnRoleChanged, XmppConnectionService.OnConfigurationPushed, XmppConnectionService.OnRoomDestroy, TextWatcher, OnMediaLoaded {
     public static final String ACTION_VIEW_MUC = "view_muc";
 
-    private static final float INACTIVE_ALPHA = 0.4684f; //compromise between dark and light theme
-
     private Conversation mConversation;
-    private OnClickListener inviteListener = new OnClickListener() {
-
-        @Override
-        public void onClick(View v) {
-            inviteToConversation(mConversation);
-        }
-    };
     private ActivityMucDetailsBinding binding;
     private MediaAdapter mMediaAdapter;
+    private UserPreviewAdapter mUserPreviewAdapter;
     private String uuid = null;
-    private User mSelectedUser = null;
 
     private boolean mAdvancedMode = false;
 
@@ -205,31 +176,6 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
         }
     };
 
-    public static boolean cancelPotentialWork(User user, ImageView imageView) {
-        final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
-
-        if (bitmapWorkerTask != null) {
-            final User old = bitmapWorkerTask.o;
-            if (old == null || user != old) {
-                bitmapWorkerTask.cancel(true);
-            } else {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
-        if (imageView != null) {
-            final Drawable drawable = imageView.getDrawable();
-            if (drawable instanceof AsyncDrawable) {
-                final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
-                return asyncDrawable.getBitmapWorkerTask();
-            }
-        }
-        return null;
-    }
-
 
     @Override
     public void onConversationUpdate() {
@@ -250,9 +196,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         this.binding = DataBindingUtil.setContentView(this, R.layout.activity_muc_details);
-        this.binding.mucMoreDetails.setVisibility(View.GONE);
         this.binding.changeConferenceButton.setOnClickListener(this.mChangeConferenceSettings);
-        this.binding.invite.setOnClickListener(inviteListener);
         setSupportActionBar((Toolbar) binding.toolbar);
         configureActionBar(getSupportActionBar());
         this.binding.editNickButton.setOnClickListener(v -> quickEdit(mConversation.getMucOptions().getActualNick(),
@@ -285,9 +229,18 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
         this.binding.mucEditTitle.addTextChangedListener(this);
         this.binding.mucEditSubject.addTextChangedListener(this);
         this.binding.mucEditSubject.addTextChangedListener(new StylingHelper.MessageEditorStyler(this.binding.mucEditSubject));
-        mMediaAdapter = new MediaAdapter(this,R.dimen.media_size);
+        this.mMediaAdapter = new MediaAdapter(this, R.dimen.media_size);
+        this.mUserPreviewAdapter = new UserPreviewAdapter();
         this.binding.media.setAdapter(mMediaAdapter);
+        this.binding.users.setAdapter(mUserPreviewAdapter);
         GridManager.setupLayoutManager(this, this.binding.media, R.dimen.media_size);
+        GridManager.setupLayoutManager(this, this.binding.users, R.dimen.media_size);
+        this.binding.invite.setOnClickListener(v -> inviteToConversation(mConversation));
+        this.binding.showUsers.setOnClickListener(v -> {
+            Intent intent = new Intent(this, MucUsersActivity.class);
+            intent.putExtra("uuid", mConversation.getUuid());
+            startActivity(intent);
+        });
     }
 
     @Override
@@ -434,41 +387,11 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
         return super.onCreateOptionsMenu(menu);
     }
 
-    @Override
-    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
-        Object tag = v.getTag();
-        if (tag instanceof User) {
-            getMenuInflater().inflate(R.menu.muc_details_context, menu);
-            final User user = (User) tag;
-            this.mSelectedUser = user;
-            String name;
-            final Contact contact = user.getContact();
-            if (contact != null && contact.showInContactList()) {
-                name = contact.getDisplayName();
-            } else if (user.getRealJid() != null) {
-                name = user.getRealJid().asBareJid().toString();
-            } else {
-                name = user.getName();
-            }
-            menu.setHeaderTitle(name);
-            MucDetailsContextMenuHelper.configureMucDetailsContextMenu(this, menu, mConversation, user);
-        }
-        super.onCreateContextMenu(menu, v, menuInfo);
-    }
-
-    @Override
-    public boolean onContextItemSelected(MenuItem item) {
-        if (!MucDetailsContextMenuHelper.onContextItemSelected(item, mSelectedUser, mConversation, this)) {
-            return super.onContextItemSelected(item);
-        }
-        return true;
-    }
-
     @Override
     public void onMediaLoaded(List<Attachment> attachments) {
         runOnUiThread(() -> {
             int limit = GridManager.getCurrentColumnCount(binding.media);
-            mMediaAdapter.setAttachments(attachments.subList(0, Math.min(limit,attachments.size())));
+            mMediaAdapter.setAttachments(attachments.subList(0, Math.min(limit, attachments.size())));
             binding.mediaWrapper.setVisibility(attachments.size() > 0 ? View.VISIBLE : View.GONE);
         });
 
@@ -545,7 +468,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
         this.binding.editMucNameButton.setVisibility((self.getAffiliation().ranks(MucOptions.Affiliation.OWNER) || mucOptions.canChangeSubject()) ? View.VISIBLE : View.GONE);
         this.binding.detailsAccount.setText(getString(R.string.using_account, account));
         this.binding.jid.setText(mConversation.getJid().asBareJid().toEscapedString());
-        AvatarWorkerTask.loadAvatar(mConversation,binding.yourPhoto,R.dimen.avatar_on_details_screen_size);
+        AvatarWorkerTask.loadAvatar(mConversation, binding.yourPhoto, R.dimen.avatar_on_details_screen_size);
         String roomName = mucOptions.getName();
         String subject = mucOptions.getSubject();
         final boolean hasTitle;
@@ -566,7 +489,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
             StylingHelper.format(spannable, this.binding.mucSubject.getCurrentTextColor());
             MyLinkify.addLinks(spannable, false);
             this.binding.mucSubject.setText(EmojiWrapper.transform(spannable));
-            this.binding.mucSubject.setTextAppearance(this,subject.length() > (hasTitle ? 128 : 196) ? R.style.TextAppearance_Conversations_Body1_Linkified : R.style.TextAppearance_Conversations_Subhead);
+            this.binding.mucSubject.setTextAppearance(this, subject.length() > (hasTitle ? 128 : 196) ? R.style.TextAppearance_Conversations_Body1_Linkified : R.style.TextAppearance_Conversations_Subhead);
             this.binding.mucSubject.setAutoLinkMask(0);
             this.binding.mucSubject.setVisibility(View.VISIBLE);
         } else {
@@ -574,7 +497,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
         }
         this.binding.mucYourNick.setText(mucOptions.getActualNick());
         if (mucOptions.online()) {
-            this.binding.mucMoreDetails.setVisibility(View.VISIBLE);
+            this.binding.usersWrapper.setVisibility(View.VISIBLE);
             this.binding.mucSettings.setVisibility(View.VISIBLE);
             this.binding.mucInfoMore.setVisibility(this.mAdvancedMode ? View.VISIBLE : View.GONE);
             this.binding.mucRole.setVisibility(View.VISIBLE);
@@ -595,7 +518,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
                 this.binding.changeConferenceButton.setVisibility(View.INVISIBLE);
             }
         } else {
-            this.binding.mucMoreDetails.setVisibility(View.GONE);
+            this.binding.usersWrapper.setVisibility(View.GONE);
             this.binding.mucInfoMore.setVisibility(View.GONE);
             this.binding.mucSettings.setVisibility(View.GONE);
         }
@@ -619,74 +542,40 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
             this.binding.notificationStatusText.setText(R.string.notify_only_when_highlighted);
             this.binding.notificationStatusButton.setImageResource(ic_notifications_none);
         }
-
-        final LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-        this.binding.mucMembers.removeAllViews();
-        if (inflater == null) {
-            return;
-        }
-        final ArrayList<User> users = mucOptions.getUsers();
-        Collections.sort(users);
-        for (final User user : users) {
-            ContactBinding binding = DataBindingUtil.inflate(inflater, R.layout.contact, this.binding.mucMembers, false);
-            this.setListItemBackgroundOnView(binding.getRoot());
-            binding.getRoot().setOnClickListener(view1 -> highlightInMuc(mConversation, user.getName()));
-            registerForContextMenu(binding.getRoot());
-            binding.getRoot().setTag(user);
-            if (mAdvancedMode && user.getPgpKeyId() != 0) {
-                binding.key.setVisibility(View.VISIBLE);
-                binding.key.setOnClickListener(v -> viewPgpKey(user));
-                binding.key.setText(OpenPgpUtils.convertKeyIdToHex(user.getPgpKeyId()));
-            }
-            Contact contact = user.getContact();
-            String name = user.getName();
-            if (contact != null) {
-                binding.contactDisplayName.setText(contact.getDisplayName());
-                binding.contactJid.setText((name != null ? name + " \u2022 " : "") + getStatus(user));
-            } else {
-                binding.contactDisplayName.setText(name == null ? "" : name);
-                binding.contactJid.setText(getStatus(user));
-
-            }
-            loadAvatar(user, binding.contactPhoto);
-            if (user.getRole() == MucOptions.Role.NONE) {
-                binding.contactJid.setAlpha(INACTIVE_ALPHA);
-                binding.key.setAlpha(INACTIVE_ALPHA);
-                binding.contactDisplayName.setAlpha(INACTIVE_ALPHA);
-                binding.contactPhoto.setAlpha(INACTIVE_ALPHA);
-            }
-            this.binding.mucMembers.addView(binding.getRoot());
-            if (mConversation.getMucOptions().canInvite()) {
-                this.binding.invite.setVisibility(View.VISIBLE);
+        List<User> users = mucOptions.getUsers();
+        Collections.sort(users, (a, b) -> {
+            if (b.getAffiliation().outranks(a.getAffiliation())) {
+                return 1;
+            } else if (a.getAffiliation().outranks(b.getAffiliation())) {
+                return -1;
             } else {
-                this.binding.invite.setVisibility(View.GONE);
+                if (a.getAvatar() != null && b.getAvatar() == null) {
+                    return -1;
+                } else if (a.getAvatar() == null && b.getAvatar() != null) {
+                    return 1;
+                } else {
+                    return a.getComparableName().compareToIgnoreCase(b.getComparableName());
+                }
             }
-        }
+        });
+        this.mUserPreviewAdapter.setUserList(MucOptions.sub(users, GridManager.getCurrentColumnCount(binding.users)));
+        this.binding.invite.setVisibility(mucOptions.canInvite() ? View.VISIBLE : View.GONE);
+
     }
 
-    private String getStatus(User user) {
-        if (mAdvancedMode) {
-            return getString(user.getAffiliation().getResId()) +
-                    " (" + getString(user.getRole().getResId()) + ')';
+    public static String getStatus(Context context, User user, final boolean advanced) {
+        if (advanced) {
+            return String.format("%s (%s)", context.getString(user.getAffiliation().getResId()), context.getString(user.getRole().getResId()));
         } else {
-            return getString(user.getAffiliation().getResId());
+            return context.getString(user.getAffiliation().getResId());
         }
     }
 
-    private void viewPgpKey(User user) {
-        PgpEngine pgp = xmppConnectionService.getPgpEngine();
-        if (pgp != null) {
-            PendingIntent intent = pgp.getIntentForKey(user.getPgpKeyId());
-            if (intent != null) {
-                try {
-                    startIntentSenderForResult(intent.getIntentSender(), 0, null, 0, 0, 0);
-                } catch (SendIntentException ignored) {
-
-                }
-            }
-        }
+    private String getStatus(User user) {
+        return getStatus(this, user, mAdvancedMode);
     }
 
+
     @Override
     public void onAffiliationChangedSuccessful(Jid jid) {
         refreshUi();
@@ -711,6 +600,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
     public void onRoomDestroySucceeded() {
         finish();
     }
+
     @Override
     public void onRoomDestroyFailed() {
         displayToast(getString(R.string.could_not_destroy_room));
@@ -735,28 +625,6 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
         });
     }
 
-    public void loadAvatar(User user, ImageView imageView) {
-        if (cancelPotentialWork(user, imageView)) {
-            final Bitmap bm = avatarService().get(user, getPixel(48), true);
-            if (bm != null) {
-                cancelPotentialWork(user, imageView);
-                imageView.setImageBitmap(bm);
-                imageView.setBackgroundColor(0x00000000);
-            } else {
-                String seed = user.getRealJid() != null ? user.getRealJid().asBareJid().toString() : null;
-                imageView.setBackgroundColor(UIHelper.getColorForName(seed == null ? user.getName() : seed));
-                imageView.setImageDrawable(null);
-                final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
-                final AsyncDrawable asyncDrawable = new AsyncDrawable(getResources(), null, task);
-                imageView.setImageDrawable(asyncDrawable);
-                try {
-                    task.execute(user);
-                } catch (final RejectedExecutionException ignored) {
-                }
-            }
-        }
-    }
-
     @Override
     public void beforeTextChanged(CharSequence s, int start, int count, int after) {
 
@@ -784,46 +652,4 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
         }
     }
 
-    static class AsyncDrawable extends BitmapDrawable {
-        private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
-
-        AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) {
-            super(res, bitmap);
-            bitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask);
-        }
-
-        BitmapWorkerTask getBitmapWorkerTask() {
-            return bitmapWorkerTaskReference.get();
-        }
-    }
-
-    class BitmapWorkerTask extends AsyncTask<User, Void, Bitmap> {
-        private final WeakReference<ImageView> imageViewReference;
-        private User o = null;
-
-        private BitmapWorkerTask(ImageView imageView) {
-            imageViewReference = new WeakReference<>(imageView);
-        }
-
-        @Override
-        protected Bitmap doInBackground(User... params) {
-            this.o = params[0];
-            if (imageViewReference.get() == null) {
-                return null;
-            }
-            return avatarService().get(this.o, getPixel(48), isCancelled());
-        }
-
-        @Override
-        protected void onPostExecute(Bitmap bitmap) {
-            if (bitmap != null && !isCancelled()) {
-                final ImageView imageView = imageViewReference.get();
-                if (imageView != null) {
-                    imageView.setImageBitmap(bitmap);
-                    imageView.setBackgroundColor(0x00000000);
-                }
-            }
-        }
-    }
-
 }

src/main/java/eu/siacs/conversations/ui/MucUsersActivity.java πŸ”—

@@ -0,0 +1,71 @@
+package eu.siacs.conversations.ui;
+
+import android.content.Intent;
+import android.databinding.DataBindingUtil;
+import android.os.Bundle;
+import android.support.v7.widget.Toolbar;
+import android.view.MenuItem;
+
+import java.util.ArrayList;
+import java.util.Collections;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.databinding.ActivityMucUsersBinding;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.MucOptions;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.ui.adapter.UserAdapter;
+import eu.siacs.conversations.ui.util.MucDetailsContextMenuHelper;
+
+public class MucUsersActivity extends XmppActivity implements XmppConnectionService.OnRosterUpdate {
+
+    private UserAdapter userAdapter;
+
+    private Conversation mConversation = null;
+
+    @Override
+    protected void refreshUiReal() {
+    }
+
+    @Override
+    void onBackendConnected() {
+        final Intent intent = getIntent();
+        final String uuid = intent == null ? null : intent.getStringExtra("uuid");
+        if (uuid != null) {
+            mConversation = xmppConnectionService.findConversationByUuid(uuid);
+        }
+        loadAndSubmitUsers();
+    }
+
+    private void loadAndSubmitUsers() {
+        if (mConversation != null) {
+            ArrayList<MucOptions.User> users = mConversation.getMucOptions().getUsers();
+            Collections.sort(users);
+            userAdapter.submitList(users);
+        }
+    }
+
+     @Override
+    public boolean onContextItemSelected(MenuItem item) {
+        if (!MucDetailsContextMenuHelper.onContextItemSelected(item, userAdapter.getSelectedUser(), mConversation, this)) {
+            return super.onContextItemSelected(item);
+        }
+        return true;
+    }
+
+    @Override
+    protected void onCreate(final Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        ActivityMucUsersBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_muc_users);
+        setSupportActionBar((Toolbar) binding.toolbar);
+        configureActionBar(getSupportActionBar(), true);
+        this.userAdapter = new UserAdapter(true);
+        binding.list.setAdapter(this.userAdapter);
+    }
+
+
+    @Override
+    public void onRosterUpdate() {
+        loadAndSubmitUsers();
+    }
+}

src/main/java/eu/siacs/conversations/ui/adapter/UserAdapter.java πŸ”—

@@ -0,0 +1,147 @@
+package eu.siacs.conversations.ui.adapter;
+
+import android.app.PendingIntent;
+import android.content.IntentSender;
+import android.databinding.DataBindingUtil;
+import android.support.annotation.NonNull;
+import android.support.v7.recyclerview.extensions.ListAdapter;
+import android.support.v7.util.DiffUtil;
+import android.support.v7.widget.RecyclerView;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.PopupMenu;
+
+import org.openintents.openpgp.util.OpenPgpUtils;
+
+import java.util.List;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.crypto.PgpEngine;
+import eu.siacs.conversations.databinding.ContactBinding;
+import eu.siacs.conversations.databinding.UserPreviewBinding;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.MucOptions;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.ui.ConferenceDetailsActivity;
+import eu.siacs.conversations.ui.XmppActivity;
+import eu.siacs.conversations.ui.util.AvatarWorkerTask;
+import eu.siacs.conversations.ui.util.MucDetailsContextMenuHelper;
+
+public class UserAdapter extends ListAdapter<MucOptions.User, UserAdapter.ViewHolder> implements View.OnCreateContextMenuListener {
+
+    private MucOptions.User selectedUser = null;
+
+    static final DiffUtil.ItemCallback<MucOptions.User> DIFF = new DiffUtil.ItemCallback<MucOptions.User>() {
+        @Override
+        public boolean areItemsTheSame(@NonNull MucOptions.User a, @NonNull MucOptions.User b) {
+            return a == b;
+        }
+
+        @Override
+        public boolean areContentsTheSame(@NonNull MucOptions.User a, @NonNull MucOptions.User b) {
+            return a.equals(b);
+        }
+    };
+    private final boolean advancedMode;
+
+    public UserAdapter(final boolean advancedMode) {
+        super(DIFF);
+        this.advancedMode = advancedMode;
+    }
+
+    @NonNull
+    @Override
+    public ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int position) {
+        return new ViewHolder(DataBindingUtil.inflate(LayoutInflater.from(viewGroup.getContext()), R.layout.contact, viewGroup, false));
+    }
+
+    @Override
+    public void onBindViewHolder(@NonNull ViewHolder viewHolder, int position) {
+        final MucOptions.User user = getItem(position);
+        AvatarWorkerTask.loadAvatar(user, viewHolder.binding.contactPhoto, R.dimen.avatar);
+        viewHolder.binding.getRoot().setOnClickListener(v -> {
+            final XmppActivity activity = XmppActivity.find(v);
+            if (activity != null) {
+                activity.highlightInMuc(user.getConversation(), user.getName());
+            }
+        });
+        viewHolder.binding.getRoot().setTag(user);
+        viewHolder.binding.getRoot().setOnCreateContextMenuListener(this);
+        viewHolder.binding.getRoot().setOnLongClickListener(v -> {
+            selectedUser = user;
+            return false;
+        });
+        final String name = user.getName();
+        final Contact contact = user.getContact();
+        if (contact != null) {
+            viewHolder.binding.contactDisplayName.setText(contact.getDisplayName());
+            if (name != null) {
+                viewHolder.binding.contactJid.setText(String.format("%s \u2022 %s", name, ConferenceDetailsActivity.getStatus(viewHolder.binding.getRoot().getContext(), user, advancedMode)));
+            } else {
+                viewHolder.binding.contactJid.setText(ConferenceDetailsActivity.getStatus(viewHolder.binding.getRoot().getContext(), user, advancedMode));
+            }
+        } else {
+            viewHolder.binding.contactDisplayName.setText(name == null ? "" : name);
+            viewHolder.binding.contactJid.setText(ConferenceDetailsActivity.getStatus(viewHolder.binding.getRoot().getContext(), user, advancedMode));
+        }
+        if (advancedMode && user.getPgpKeyId() != 0) {
+            viewHolder.binding.key.setVisibility(View.VISIBLE);
+            viewHolder.binding.key.setOnClickListener(v -> {
+                final XmppActivity activity = XmppActivity.find(v);
+                final XmppConnectionService service = activity == null ? null : activity.xmppConnectionService;
+                final PgpEngine pgpEngine = service == null ? null : service.getPgpEngine();
+                if (pgpEngine != null) {
+                    PendingIntent intent = pgpEngine.getIntentForKey(user.getPgpKeyId());
+                    if (intent != null) {
+                        try {
+                            activity.startIntentSenderForResult(intent.getIntentSender(), 0, null, 0, 0, 0);
+                        } catch (IntentSender.SendIntentException ignored) {
+
+                        }
+                    }
+                }
+            });
+            viewHolder.binding.key.setText(OpenPgpUtils.convertKeyIdToHex(user.getPgpKeyId()));
+        }
+
+
+    }
+
+    public MucOptions.User getSelectedUser() {
+        return selectedUser;
+    }
+
+    @Override
+    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
+        final XmppActivity activity = XmppActivity.find(v);
+        final Object tag = v.getTag();
+        if (tag instanceof MucOptions.User && activity != null) {
+            activity.getMenuInflater().inflate(R.menu.muc_details_context, menu);
+            final MucOptions.User user = (MucOptions.User) tag;
+            String name;
+            final Contact contact = user.getContact();
+            if (contact != null && contact.showInContactList()) {
+                name = contact.getDisplayName();
+            } else if (user.getRealJid() != null) {
+                name = user.getRealJid().asBareJid().toString();
+            } else {
+                name = user.getName();
+            }
+            menu.setHeaderTitle(name);
+            MucDetailsContextMenuHelper.configureMucDetailsContextMenu(activity, menu, user.getConversation(), user);
+        }
+    }
+
+    class ViewHolder extends RecyclerView.ViewHolder {
+
+        private final ContactBinding binding;
+
+        private ViewHolder(ContactBinding binding) {
+            super(binding.getRoot());
+            this.binding = binding;
+        }
+    }
+}

src/main/java/eu/siacs/conversations/ui/adapter/UserPreviewAdapter.java πŸ”—

@@ -0,0 +1,73 @@
+package eu.siacs.conversations.ui.adapter;
+
+import android.databinding.DataBindingUtil;
+import android.support.annotation.NonNull;
+import android.support.v7.recyclerview.extensions.ListAdapter;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.ViewGroup;
+import android.widget.PopupMenu;
+
+import java.util.List;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.databinding.UserPreviewBinding;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.MucOptions;
+import eu.siacs.conversations.ui.XmppActivity;
+import eu.siacs.conversations.ui.util.AvatarWorkerTask;
+import eu.siacs.conversations.ui.util.MucDetailsContextMenuHelper;
+
+public class UserPreviewAdapter extends ListAdapter<MucOptions.User,UserPreviewAdapter.ViewHolder> {
+
+    public UserPreviewAdapter() {
+        super(UserAdapter.DIFF);
+    }
+
+    @NonNull
+    @Override
+    public ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int position) {
+        return new ViewHolder(DataBindingUtil.inflate(LayoutInflater.from(viewGroup.getContext()), R.layout.user_preview, viewGroup, false));
+    }
+
+    @Override
+    public void onBindViewHolder(@NonNull ViewHolder viewHolder, int position) {
+        final MucOptions.User user = getItem(position);
+        AvatarWorkerTask.loadAvatar(user, viewHolder.binding.avatar, R.dimen.media_size);
+        viewHolder.binding.getRoot().setOnClickListener(v -> {
+            final XmppActivity activity = XmppActivity.find(v);
+            if (activity != null) {
+                activity.highlightInMuc(user.getConversation(), user.getName());
+            }
+        });
+        viewHolder.binding.getRoot().setOnLongClickListener(v -> {
+            final XmppActivity activity = XmppActivity.find(v);
+            if (activity == null) {
+                return true;
+            }
+            final PopupMenu popupMenu = new PopupMenu(activity, v);
+            popupMenu.inflate(R.menu.muc_details_context);
+            final Menu menu = popupMenu.getMenu();
+            MucDetailsContextMenuHelper.configureMucDetailsContextMenu(activity, menu, user.getConversation(), user);
+            popupMenu.setOnMenuItemClickListener(menuItem -> MucDetailsContextMenuHelper.onContextItemSelected(menuItem, user, user.getConversation(), activity));
+            popupMenu.show();
+            return true;
+        });
+    }
+
+    public void setUserList(List<MucOptions.User> users) {
+        submitList(users);
+    }
+
+
+    class ViewHolder extends RecyclerView.ViewHolder {
+
+        private final UserPreviewBinding binding;
+
+        private ViewHolder(UserPreviewBinding binding) {
+            super(binding.getRoot());
+            this.binding = binding;
+        }
+    }
+}

src/main/java/eu/siacs/conversations/ui/util/GridManager.java πŸ”—

@@ -29,7 +29,8 @@ public class GridManager {
                 }
                 final ColumnInfo columnInfo = calculateColumnCount(context, recyclerView.getMeasuredWidth(), desiredSize);
                 Log.d(Config.LOGTAG, "final count " + columnInfo.count);
-                if (recyclerView.getAdapter().getItemCount() != 0) {
+                final RecyclerView.Adapter adapter = recyclerView.getAdapter();
+                if (adapter != null && adapter.getItemCount() != 0) {
                     Log.e(Config.LOGTAG, "adapter already has items; just go with it now");
                     return;
                 }

src/main/res/layout/account_row.xml πŸ”—

@@ -5,7 +5,7 @@
     <RelativeLayout
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:background="?android:selectableItemBackground"
+        android:background="?selectableItemBackground"
         android:paddingLeft="8dp"
         android:paddingBottom="8dp"
         android:paddingTop="8dp">

src/main/res/layout/activity_muc_details.xml πŸ”—

@@ -360,8 +360,8 @@
                 </android.support.v7.widget.CardView>
 
                 <android.support.v7.widget.CardView
-                    android:id="@+id/muc_more_details"
-                    android:layout_width="match_parent"
+                    android:id="@+id/users_wrapper"
+                    android:layout_width="fill_parent"
                     android:layout_height="wrap_content"
                     android:layout_marginBottom="@dimen/activity_vertical_margin"
                     android:layout_marginLeft="@dimen/activity_horizontal_margin"
@@ -373,21 +373,23 @@
                         android:layout_height="wrap_content"
                         android:orientation="vertical">
 
-                        <LinearLayout
-                            android:id="@+id/muc_members"
+                        <android.support.v7.widget.RecyclerView
+                            android:id="@+id/users"
                             android:layout_width="match_parent"
                             android:layout_height="wrap_content"
-                            android:orientation="vertical"
-                            android:padding="@dimen/card_padding_list">
-                        </LinearLayout>
+                            android:orientation="horizontal"
+                            android:paddingEnd="@dimen/card_padding_regular"
+                            android:paddingStart="@dimen/card_padding_regular"
+                            android:paddingTop="@dimen/card_padding_regular"
+                            android:paddingBottom="@dimen/card_padding_list"
+                            android:layout_marginStart="-2dp"
+                            android:layout_marginEnd="-2dp"/>
 
                         <LinearLayout
                             android:layout_width="wrap_content"
                             android:layout_height="match_parent"
-                            android:layout_gravity="center_horizontal"
-                            android:layout_marginTop="8dp"
-                            android:orientation="horizontal">
-
+                            android:orientation="horizontal"
+                            android:layout_gravity="end">
 
                             <Button
                                 android:id="@+id/invite"
@@ -397,9 +399,19 @@
                                 android:minWidth="0dp"
                                 android:paddingLeft="16dp"
                                 android:paddingRight="16dp"
-                                android:text="@string/invite_contact"
-                                android:textColor="?attr/colorAccent"/>
+                                android:text="@string/invite"
+                                android:textColor="?attr/colorAccent" />
 
+                            <Button
+                                android:id="@+id/show_users"
+                                style="@style/Widget.Conversations.Button.Borderless"
+                                android:layout_width="wrap_content"
+                                android:layout_height="wrap_content"
+                                android:minWidth="0dp"
+                                android:paddingLeft="16dp"
+                                android:paddingRight="16dp"
+                                android:text="@string/view_users"
+                                android:textColor="?attr/colorAccent" />
                         </LinearLayout>
                     </LinearLayout>
                 </android.support.v7.widget.CardView>

src/main/res/layout/activity_muc_users.xml πŸ”—

@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+
+
+    <LinearLayout
+        android:layout_width="fill_parent"
+        android:layout_height="fill_parent"
+        android:background="?attr/color_background_primary"
+        android:orientation="vertical">
+
+        <include
+            android:id="@+id/toolbar"
+            layout="@layout/toolbar" />
+
+
+        <android.support.design.widget.CoordinatorLayout
+            android:id="@+id/coordinator"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:background="?attr/color_background_primary">
+
+            <android.support.v7.widget.RecyclerView
+                android:id="@+id/list"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:background="?attr/color_background_primary"
+                android:orientation="vertical"
+                app:layoutManager="android.support.v7.widget.LinearLayoutManager" />
+        </android.support.design.widget.CoordinatorLayout>
+
+    </LinearLayout>
+</layout>

src/main/res/layout/contact.xml πŸ”—

@@ -5,7 +5,7 @@
     <RelativeLayout
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:background="?attr/list_item_background"
+        android:background="?selectableItemBackground"
         android:padding="@dimen/list_padding">
 
         <com.makeramen.roundedimageview.RoundedImageView

src/main/res/layout/user_preview.xml πŸ”—

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android">
+    <eu.siacs.conversations.ui.widget.SquareFrameLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:padding="2dp"
+        android:background="?selectableItemBackground">
+        <ImageView
+            android:id="@+id/avatar"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:background="@color/black54"
+            android:scaleType="centerInside"/>
+    </eu.siacs.conversations.ui.widget.SquareFrameLayout>
+</layout>

src/main/res/values/strings.xml πŸ”—

@@ -51,6 +51,7 @@
     <string name="share_with">Share with…</string>
     <string name="start_conversation">Start conversation</string>
     <string name="invite_contact">Invite contact</string>
+    <string name="invite">Invite</string>
     <string name="contacts">Contacts</string>
     <string name="contact">Contact</string>
     <string name="cancel">Cancel</string>
@@ -748,6 +749,8 @@
     <string name="pref_more_notification_settings_summary">Importance, Sound, Vibrate</string>
     <string name="video_compression_channel_name">Video compression</string>
     <string name="view_media">View media</string>
+    <string name="view_users">View members</string>
+    <string name="group_chat_members">Group chat members</string>
     <string name="media_browser">Media browser</string>
     <string name="security_violation_not_attaching_file">File omitted due to security violation.</string>
     <string name="pref_video_compression">Video Quality</string>