diff --git a/app/src/main/java/org/thoughtcrime/securesms/GroupMembersDialog.java b/app/src/main/java/org/thoughtcrime/securesms/GroupMembersDialog.java index 246c7f4fa5..6112a71da4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/GroupMembersDialog.java +++ b/app/src/main/java/org/thoughtcrime/securesms/GroupMembersDialog.java @@ -1,131 +1,78 @@ package org.thoughtcrime.securesms; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; -import android.os.AsyncTask; + +import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; +import androidx.lifecycle.Lifecycle; import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry; +import org.thoughtcrime.securesms.groups.ui.GroupMemberListView; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientExporter; -import org.thoughtcrime.securesms.util.FeatureFlags; +import org.thoughtcrime.securesms.util.concurrent.SimpleTask; -import java.util.LinkedList; -import java.util.List; +import java.util.ArrayList; -public class GroupMembersDialog extends AsyncTask> { +public final class GroupMembersDialog { - private static final String TAG = GroupMembersDialog.class.getSimpleName(); + private final Context context; + private final Recipient groupRecipient; + private final Lifecycle lifecycle; - private final Recipient recipient; - private final Context context; - - public GroupMembersDialog(Context context, Recipient recipient) { - this.recipient = recipient; - this.context = context; - } - - @Override - public void onPreExecute() {} - - @Override - protected List doInBackground(Void... params) { - return DatabaseFactory.getGroupDatabase(context).getGroupMembers(recipient.requireGroupId(), true); - } - - @Override - public void onPostExecute(List members) { - GroupMembers groupMembers = new GroupMembers(members); - AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle(R.string.ConversationActivity_group_members); - builder.setIconAttribute(R.attr.group_members_dialog_icon); - builder.setCancelable(true); - builder.setItems(groupMembers.getRecipientStrings(), new GroupMembersOnClickListener(context, groupMembers)); - builder.setPositiveButton(android.R.string.ok, null); - builder.show(); + public GroupMembersDialog(@NonNull Context context, + @NonNull Recipient groupRecipient, + @NonNull Lifecycle lifecycle) + { + this.context = context; + this.groupRecipient = groupRecipient; + this.lifecycle = lifecycle; } public void display() { - executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + SimpleTask.run( + lifecycle, + () -> DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupRecipient.requireGroupId(), true), + members -> { + AlertDialog dialog = new AlertDialog.Builder(context) + .setTitle(R.string.ConversationActivity_group_members) + .setIconAttribute(R.attr.group_members_dialog_icon) + .setCancelable(true) + .setView(R.layout.dialog_group_members) + .setPositiveButton(android.R.string.ok, null) + .show(); + + GroupMemberListView memberListView = dialog.findViewById(R.id.list_members); + + ArrayList pendingMembers = new ArrayList<>(members.size()); + for (Recipient member : members) { + GroupMemberEntry.FullMember entry = new GroupMemberEntry.FullMember(member); + + entry.setOnClick(() -> contactClick(member)); + + if (member.isLocalNumber()) { + pendingMembers.add(0, entry); + } else { + pendingMembers.add(entry); + } + } + + //noinspection ConstantConditions + memberListView.setMembers(pendingMembers); + } + ); } - private static class GroupMembersOnClickListener implements DialogInterface.OnClickListener { - private final GroupMembers groupMembers; - private final Context context; + private void contactClick(@NonNull Recipient recipient) { + if (recipient.getContactUri() != null) { + Intent intent = new Intent(context, RecipientPreferenceActivity.class); + intent.putExtra(RecipientPreferenceActivity.RECIPIENT_ID, recipient.getId()); - public GroupMembersOnClickListener(Context context, GroupMembers members) { - this.context = context; - this.groupMembers = members; - } - - @Override - public void onClick(DialogInterface dialogInterface, int item) { - Recipient recipient = groupMembers.get(item); - - if (recipient.getContactUri() != null) { - Intent intent = new Intent(context, RecipientPreferenceActivity.class); - intent.putExtra(RecipientPreferenceActivity.RECIPIENT_ID, recipient.getId()); - - context.startActivity(intent); - } else { - context.startActivity(RecipientExporter.export(recipient).asAddContactIntent()); - } - } - } - - /** - * Wraps a List of Recipient (just like @class Recipients), - * but with focus on the order of the Recipients. - * So that the order of the RecipientStrings[] matches - * the internal order. - * - * @author Christoph Haefner - */ - private class GroupMembers { - private final String TAG = GroupMembers.class.getSimpleName(); - - private final LinkedList members = new LinkedList<>(); - - public GroupMembers(List recipients) { - for (Recipient recipient : recipients) { - if (recipient.isLocalNumber()) { - members.push(recipient); - } else { - members.add(recipient); - } - } - } - - public String[] getRecipientStrings() { - List recipientStrings = new LinkedList<>(); - - for (Recipient recipient : members) { - if (recipient.isLocalNumber()) { - recipientStrings.add(context.getString(R.string.GroupMembersDialog_you)); - } else { - String name = getRecipientName(recipient); - recipientStrings.add(name); - } - } - - return recipientStrings.toArray(new String[members.size()]); - } - - private String getRecipientName(Recipient recipient) { - if (FeatureFlags.profileDisplay()) return recipient.getDisplayName(context); - - String name = recipient.toShortString(context); - - if (recipient.getName(context) == null && !recipient.getProfileName().isEmpty()) { - name += " ~" + recipient.getProfileName().toString(); - } - - return name; - } - - public Recipient get(int index) { - return members.get(index); + context.startActivity(intent); + } else { + context.startActivity(RecipientExporter.export(recipient).asAddContactIntent()); } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/AvatarImageView.java b/app/src/main/java/org/thoughtcrime/securesms/components/AvatarImageView.java index 05ccb491b4..147a4f2e33 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/AvatarImageView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/AvatarImageView.java @@ -21,9 +21,11 @@ import org.thoughtcrime.securesms.color.MaterialColor; import org.thoughtcrime.securesms.contacts.avatars.ContactColors; import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto; import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto; +import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientExporter; +import org.thoughtcrime.securesms.util.AvatarUtil; import org.thoughtcrime.securesms.util.ThemeUtil; import java.util.Objects; @@ -109,6 +111,15 @@ public final class AvatarImageView extends AppCompatImageView { this.fallbackPhotoProvider = fallbackPhotoProvider; } + public void setRecipient(@NonNull Recipient recipient) { + if (recipient.isLocalNumber()) { + setAvatar(GlideApp.with(this), null, false); + AvatarUtil.loadIconIntoImageView(recipient, this); + } else { + setAvatar(GlideApp.with(this), recipient, false); + } + } + public void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient, boolean quickContactEnabled) { if (recipient != null) { RecipientContactPhoto photo = new RecipientContactPhoto(recipient); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java index 99dd033f1e..6ed2eb6e26 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -1196,7 +1196,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity } private void handleDisplayGroupRecipients() { - new GroupMembersDialog(this, getRecipient()).display(); + new GroupMembersDialog(this, getRecipient(), getLifecycle()).display(); } private void handleAddToContacts() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupMemberEntry.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupMemberEntry.java new file mode 100644 index 0000000000..e82c23d729 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupMemberEntry.java @@ -0,0 +1,35 @@ +package org.thoughtcrime.securesms.groups.ui; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.thoughtcrime.securesms.recipients.Recipient; + +public abstract class GroupMemberEntry { + + private @Nullable Runnable onClick; + + private GroupMemberEntry() { + } + + public void setOnClick(@NonNull Runnable onClick) { + this.onClick = onClick; + } + + public @Nullable Runnable getOnClick() { + return onClick; + } + + public static class FullMember extends GroupMemberEntry { + + private final Recipient member; + + public FullMember(@NonNull Recipient member) { + this.member = member; + } + + public Recipient getMember() { + return member; + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupMemberListAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupMemberListAdapter.java new file mode 100644 index 0000000000..f3631aa468 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupMemberListAdapter.java @@ -0,0 +1,115 @@ +package org.thoughtcrime.securesms.groups.ui; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.components.AvatarImageView; +import org.thoughtcrime.securesms.recipients.Recipient; + +import java.util.ArrayList; +import java.util.Collection; + +final class GroupMemberListAdapter extends RecyclerView.Adapter { + + private static final int FULL_MEMBER = 0; + + private final ArrayList data = new ArrayList<>(); + + void updateData(@NonNull Collection recipients) { + data.clear(); + data.addAll(recipients); + notifyDataSetChanged(); + } + + @Override + public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + switch (viewType) { + case FULL_MEMBER: + return new FullMemberViewHolder(LayoutInflater.from(parent.getContext()) + .inflate(R.layout.group_recipient_list_item, + parent, false)); + default: + throw new AssertionError(); + } + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + holder.bind(data.get(position)); + } + + @Override + public int getItemViewType(int position) { + GroupMemberEntry groupMemberEntry = data.get(position); + + if (groupMemberEntry instanceof GroupMemberEntry.FullMember) { + return FULL_MEMBER; + } + + throw new AssertionError(); + } + + @Override + public int getItemCount() { + return data.size(); + } + + static abstract class ViewHolder extends RecyclerView.ViewHolder { + + final Context context; + private final AvatarImageView avatar; + private final TextView recipient; + final PopupMenuView popupMenu; + + ViewHolder(@NonNull View itemView) { + super(itemView); + + context = itemView.getContext(); + avatar = itemView.findViewById(R.id.recipient_avatar); + recipient = itemView.findViewById(R.id.recipient_name); + popupMenu = itemView.findViewById(R.id.popupMenu); + } + + void bindRecipient(@NonNull Recipient recipient) { + String displayName = recipient.isLocalNumber() ? context.getString(R.string.GroupMembersDialog_you) + : recipient.getDisplayName(itemView.getContext()); + bindImageAndText(recipient, displayName); + } + + void bindImageAndText(@NonNull Recipient recipient, @NonNull String displayText) { + this.recipient.setText(displayText); + this.avatar.setRecipient(recipient); + } + + void bind(@NonNull GroupMemberEntry memberEntry) { + Runnable onClick = memberEntry.getOnClick(); + View.OnClickListener onClickListener = v -> { if (onClick != null) onClick.run(); }; + + this.avatar.setOnClickListener(onClickListener); + this.recipient.setOnClickListener(onClickListener); + } + } + + final static class FullMemberViewHolder extends ViewHolder { + + FullMemberViewHolder(@NonNull View itemView) { + super(itemView); + } + + @Override + void bind(@NonNull GroupMemberEntry memberEntry) { + super.bind(memberEntry); + + GroupMemberEntry.FullMember fullMember = (GroupMemberEntry.FullMember) memberEntry; + + bindRecipient(fullMember.getMember()); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupMemberListView.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupMemberListView.java new file mode 100644 index 0000000000..710f7bf1b0 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupMemberListView.java @@ -0,0 +1,62 @@ +package org.thoughtcrime.securesms.groups.ui; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import org.thoughtcrime.securesms.R; + +import java.util.Collection; + +public final class GroupMemberListView extends RecyclerView { + + private final GroupMemberListAdapter membersAdapter = new GroupMemberListAdapter(); + private int maxHeight; + + public GroupMemberListView(@NonNull Context context) { + super(context); + initialize(context, null); + } + + public GroupMemberListView(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + initialize(context, attrs); + } + + public GroupMemberListView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + initialize(context, attrs); + } + + private void initialize(@NonNull Context context, @Nullable AttributeSet attrs) { + setHasFixedSize(true); + setLayoutManager(new LinearLayoutManager(context)); + setAdapter(membersAdapter); + + if (attrs != null) { + TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.GroupMemberListView, 0, 0); + try { + maxHeight = typedArray.getDimensionPixelSize(R.styleable.GroupMemberListView_maxHeight, 0); + } finally { + typedArray.recycle(); + } + } + } + + public void setMembers(@NonNull Collection recipients) { + membersAdapter.updateData(recipients); + } + + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + if (maxHeight > 0) { + heightSpec = MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.AT_MOST); + } + super.onMeasure(widthSpec, heightSpec); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/PopupMenuView.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/PopupMenuView.java new file mode 100644 index 0000000000..ba695307bb --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/PopupMenuView.java @@ -0,0 +1,60 @@ +package org.thoughtcrime.securesms.groups.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.MenuInflater; +import android.view.View; + +import androidx.annotation.IdRes; +import androidx.annotation.MenuRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.PopupMenu; + +import org.thoughtcrime.securesms.R; + +public final class PopupMenuView extends View { + + private @MenuRes int menu; + private @Nullable ItemClick callback; + + public PopupMenuView(Context context) { + super(context); + init(); + } + + public PopupMenuView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + init(); + } + + public PopupMenuView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + private void init() { + setBackgroundResource(R.drawable.ic_more_vert_24); + + setOnClickListener(v -> { + if (callback != null) { + PopupMenu popup = new PopupMenu(getContext(), v); + MenuInflater inflater = popup.getMenuInflater(); + + inflater.inflate(menu, popup.getMenu()); + + popup.setOnMenuItemClickListener(item -> callback.onItemClick(item.getItemId())); + popup.show(); + } + }); + } + + public void setMenu(@MenuRes int menu, @NonNull ItemClick callback) { + this.menu = menu; + this.callback = callback; + } + + public interface ItemClick { + boolean onItemClick(@IdRes int menuItemId); + } +} diff --git a/app/src/main/res/layout/dialog_group_members.xml b/app/src/main/res/layout/dialog_group_members.xml new file mode 100644 index 0000000000..d169253bea --- /dev/null +++ b/app/src/main/res/layout/dialog_group_members.xml @@ -0,0 +1,23 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/group_recipient_list_item.xml b/app/src/main/res/layout/group_recipient_list_item.xml new file mode 100644 index 0000000000..d65ee6e808 --- /dev/null +++ b/app/src/main/res/layout/group_recipient_list_item.xml @@ -0,0 +1,45 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index bf2582c923..22fe03645d 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -286,11 +286,16 @@ + + + + + - - - - + + + + @@ -479,4 +484,8 @@ + + + +