mirror of
https://github.com/oxen-io/session-android.git
synced 2025-06-08 18:38:35 +00:00
Prevent last admin from leaving without selecting new admin.
This commit is contained in:
parent
b10fc6a0b0
commit
ae2b6e4d7a
@ -504,6 +504,9 @@
|
|||||||
<activity android:name=".groups.ui.creategroup.details.AddGroupDetailsActivity"
|
<activity android:name=".groups.ui.creategroup.details.AddGroupDetailsActivity"
|
||||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
|
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
|
||||||
|
|
||||||
|
<activity android:name=".groups.ui.chooseadmin.ChooseNewAdminActivity"
|
||||||
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
|
||||||
|
|
||||||
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"/>
|
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"/>
|
||||||
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
|
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
|
||||||
<service android:enabled="true" android:exported="false" android:name=".service.KeyCachingService"/>
|
<service android:enabled="true" android:exported="false" android:name=".service.KeyCachingService"/>
|
||||||
|
@ -1203,10 +1203,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LeaveGroupDialog.handleLeavePushGroup(ConversationActivity.this,
|
LeaveGroupDialog.handleLeavePushGroup(this, getRecipient().requireGroupId().requirePush(), this::finish);
|
||||||
getLifecycle(),
|
|
||||||
getRecipient().requireGroupId().requirePush(),
|
|
||||||
null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleManageGroup() {
|
private void handleManageGroup() {
|
||||||
|
@ -139,6 +139,16 @@ public final class GroupManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
public static void addMemberAdminsAndLeaveGroup(@NonNull Context context, @NonNull GroupId.V2 groupId, @NonNull Collection<RecipientId> newAdmins)
|
||||||
|
throws GroupChangeBusyException, GroupChangeFailedException, IOException, GroupInsufficientRightsException, GroupNotAMemberException
|
||||||
|
{
|
||||||
|
try (GroupManagerV2.GroupEditor edit = new GroupManagerV2(context).edit(groupId.requireV2())) {
|
||||||
|
edit.addMemberAdminsAndLeaveGroup(newAdmins);
|
||||||
|
Log.i(TAG, "Left group " + groupId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
public static void ejectFromGroup(@NonNull Context context, @NonNull GroupId.V2 groupId, @NonNull Recipient recipient)
|
public static void ejectFromGroup(@NonNull Context context, @NonNull GroupId.V2 groupId, @NonNull Recipient recipient)
|
||||||
throws GroupChangeBusyException, GroupChangeFailedException, GroupInsufficientRightsException, GroupNotAMemberException, IOException
|
throws GroupChangeBusyException, GroupChangeFailedException, GroupInsufficientRightsException, GroupNotAMemberException, IOException
|
||||||
|
@ -6,6 +6,7 @@ import androidx.annotation.NonNull;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.WorkerThread;
|
import androidx.annotation.WorkerThread;
|
||||||
|
|
||||||
|
import com.annimon.stream.Stream;
|
||||||
import com.google.protobuf.InvalidProtocolBufferException;
|
import com.google.protobuf.InvalidProtocolBufferException;
|
||||||
|
|
||||||
import org.signal.storageservice.protos.groups.AccessControl;
|
import org.signal.storageservice.protos.groups.AccessControl;
|
||||||
@ -296,6 +297,17 @@ final class GroupManagerV2 {
|
|||||||
return commitChangeWithConflictResolution(groupOperations.createRemoveMembersChange(Collections.singleton(recipient.getUuid().get())));
|
return commitChangeWithConflictResolution(groupOperations.createRemoveMembersChange(Collections.singleton(recipient.getUuid().get())));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
@NonNull GroupManager.GroupActionResult addMemberAdminsAndLeaveGroup(Collection<RecipientId> newAdmins)
|
||||||
|
throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException
|
||||||
|
{
|
||||||
|
Recipient self = Recipient.self();
|
||||||
|
List<UUID> newAdminRecipients = Stream.of(newAdmins).map(id -> Recipient.resolved(id).getUuid().get()).toList();
|
||||||
|
|
||||||
|
return commitChangeWithConflictResolution(groupOperations.createLeaveAndPromoteMembersToAdmin(self.getUuid().get(),
|
||||||
|
newAdminRecipients));
|
||||||
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
@Nullable GroupManager.GroupActionResult updateSelfProfileKeyInGroup()
|
@Nullable GroupManager.GroupActionResult updateSelfProfileKeyInGroup()
|
||||||
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
|
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
|
||||||
|
@ -40,6 +40,7 @@ public final class LiveGroup {
|
|||||||
private final GroupDatabase groupDatabase;
|
private final GroupDatabase groupDatabase;
|
||||||
private final LiveData<Recipient> recipient;
|
private final LiveData<Recipient> recipient;
|
||||||
private final LiveData<GroupDatabase.GroupRecord> groupRecord;
|
private final LiveData<GroupDatabase.GroupRecord> groupRecord;
|
||||||
|
private final LiveData<List<GroupMemberEntry.FullMember>> fullMembers;
|
||||||
|
|
||||||
public LiveGroup(@NonNull GroupId groupId) {
|
public LiveGroup(@NonNull GroupId groupId) {
|
||||||
Context context = ApplicationDependencies.getApplication();
|
Context context = ApplicationDependencies.getApplication();
|
||||||
@ -47,7 +48,15 @@ public final class LiveGroup {
|
|||||||
|
|
||||||
this.groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
this.groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||||
this.recipient = Transformations.switchMap(liveRecipient, LiveRecipient::getLiveData);
|
this.recipient = Transformations.switchMap(liveRecipient, LiveRecipient::getLiveData);
|
||||||
this.groupRecord = LiveDataUtil.filterNotNull(LiveDataUtil.mapAsync(recipient, groupRecipient-> groupDatabase.getGroup(groupRecipient.getId()).orNull()));
|
this.groupRecord = LiveDataUtil.filterNotNull(LiveDataUtil.mapAsync(recipient, groupRecipient -> groupDatabase.getGroup(groupRecipient.getId()).orNull()));
|
||||||
|
this.fullMembers = LiveDataUtil.mapAsync(groupRecord,
|
||||||
|
g -> Stream.of(g.getMembers())
|
||||||
|
.map(m -> {
|
||||||
|
Recipient recipient = Recipient.resolved(m);
|
||||||
|
return new GroupMemberEntry.FullMember(recipient, g.isAdmin(recipient));
|
||||||
|
})
|
||||||
|
.sorted(MEMBER_ORDER)
|
||||||
|
.toList());
|
||||||
|
|
||||||
SignalExecutors.BOUNDED.execute(() -> liveRecipient.postValue(Recipient.externalGroup(context, groupId).live()));
|
SignalExecutors.BOUNDED.execute(() -> liveRecipient.postValue(Recipient.externalGroup(context, groupId).live()));
|
||||||
}
|
}
|
||||||
@ -90,17 +99,17 @@ public final class LiveGroup {
|
|||||||
return Transformations.map(groupRecord, GroupDatabase.GroupRecord::getAttributesAccessControl);
|
return Transformations.map(groupRecord, GroupDatabase.GroupRecord::getAttributesAccessControl);
|
||||||
}
|
}
|
||||||
|
|
||||||
public LiveData<List<GroupMemberEntry.FullMember>> getFullMembers() {
|
public LiveData<List<GroupMemberEntry.FullMember>> getNonAdminFullMembers() {
|
||||||
return LiveDataUtil.mapAsync(groupRecord,
|
return Transformations.map(fullMembers,
|
||||||
g -> Stream.of(g.getMembers())
|
members -> Stream.of(members)
|
||||||
.map(m -> {
|
.filterNot(GroupMemberEntry.FullMember::isAdmin)
|
||||||
Recipient recipient = Recipient.resolved(m);
|
|
||||||
return new GroupMemberEntry.FullMember(recipient, g.isAdmin(recipient));
|
|
||||||
})
|
|
||||||
.sorted(MEMBER_ORDER)
|
|
||||||
.toList());
|
.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LiveData<List<GroupMemberEntry.FullMember>> getFullMembers() {
|
||||||
|
return fullMembers;
|
||||||
|
}
|
||||||
|
|
||||||
public LiveData<Integer> getExpireMessages() {
|
public LiveData<Integer> getExpireMessages() {
|
||||||
return Transformations.map(recipient, Recipient::getExpireMessages);
|
return Transformations.map(recipient, Recipient::getExpireMessages);
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import android.content.Context;
|
|||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.CheckBox;
|
||||||
import android.widget.ProgressBar;
|
import android.widget.ProgressBar;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
@ -17,10 +18,11 @@ import org.thoughtcrime.securesms.components.AvatarImageView;
|
|||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.util.LifecycleRecyclerAdapter;
|
import org.thoughtcrime.securesms.util.LifecycleRecyclerAdapter;
|
||||||
import org.thoughtcrime.securesms.util.LifecycleViewHolder;
|
import org.thoughtcrime.securesms.util.LifecycleViewHolder;
|
||||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberListAdapter.ViewHolder> {
|
final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberListAdapter.ViewHolder> {
|
||||||
|
|
||||||
@ -29,11 +31,20 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
|||||||
private static final int OTHER_INVITE_PENDING_COUNT = 2;
|
private static final int OTHER_INVITE_PENDING_COUNT = 2;
|
||||||
private static final int NEW_GROUP_CANDIDATE = 3;
|
private static final int NEW_GROUP_CANDIDATE = 3;
|
||||||
|
|
||||||
private final ArrayList<GroupMemberEntry> data = new ArrayList<>();
|
private final List<GroupMemberEntry> data = new ArrayList<>();
|
||||||
|
private final Set<GroupMemberEntry> selection = new HashSet<>();
|
||||||
|
private final SelectionChangeListener selectionChangeListener = new SelectionChangeListener();
|
||||||
|
|
||||||
|
private final boolean selectable;
|
||||||
|
|
||||||
@Nullable private AdminActionsListener adminActionsListener;
|
@Nullable private AdminActionsListener adminActionsListener;
|
||||||
@Nullable private RecipientClickListener recipientClickListener;
|
@Nullable private RecipientClickListener recipientClickListener;
|
||||||
@Nullable private RecipientLongClickListener recipientLongClickListener;
|
@Nullable private RecipientLongClickListener recipientLongClickListener;
|
||||||
|
@Nullable private RecipientSelectionChangeListener recipientSelectionChangeListener;
|
||||||
|
|
||||||
|
GroupMemberListAdapter(boolean selectable) {
|
||||||
|
this.selectable = selectable;
|
||||||
|
}
|
||||||
|
|
||||||
void updateData(@NonNull List<? extends GroupMemberEntry> recipients) {
|
void updateData(@NonNull List<? extends GroupMemberEntry> recipients) {
|
||||||
if (data.isEmpty()) {
|
if (data.isEmpty()) {
|
||||||
@ -43,6 +54,21 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
|||||||
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallback(data, recipients));
|
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallback(data, recipients));
|
||||||
data.clear();
|
data.clear();
|
||||||
data.addAll(recipients);
|
data.addAll(recipients);
|
||||||
|
|
||||||
|
if (!selection.isEmpty()) {
|
||||||
|
Set<GroupMemberEntry> newSelection = new HashSet<>();
|
||||||
|
for (GroupMemberEntry entry : recipients) {
|
||||||
|
if (selection.contains(entry)) {
|
||||||
|
newSelection.add(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selection.clear();
|
||||||
|
selection.addAll(newSelection);
|
||||||
|
if (recipientSelectionChangeListener != null) {
|
||||||
|
recipientSelectionChangeListener.onSelectionChanged(selection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
diffResult.dispatchUpdatesTo(this);
|
diffResult.dispatchUpdatesTo(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -55,22 +81,26 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
|||||||
.inflate(R.layout.group_recipient_list_item, parent, false),
|
.inflate(R.layout.group_recipient_list_item, parent, false),
|
||||||
recipientClickListener,
|
recipientClickListener,
|
||||||
recipientLongClickListener,
|
recipientLongClickListener,
|
||||||
adminActionsListener);
|
adminActionsListener,
|
||||||
|
selectionChangeListener);
|
||||||
case OWN_INVITE_PENDING:
|
case OWN_INVITE_PENDING:
|
||||||
return new OwnInvitePendingMemberViewHolder(LayoutInflater.from(parent.getContext())
|
return new OwnInvitePendingMemberViewHolder(LayoutInflater.from(parent.getContext())
|
||||||
.inflate(R.layout.group_recipient_list_item, parent, false),
|
.inflate(R.layout.group_recipient_list_item, parent, false),
|
||||||
recipientClickListener,
|
recipientClickListener,
|
||||||
recipientLongClickListener,
|
recipientLongClickListener,
|
||||||
adminActionsListener);
|
adminActionsListener,
|
||||||
|
selectionChangeListener);
|
||||||
case OTHER_INVITE_PENDING_COUNT:
|
case OTHER_INVITE_PENDING_COUNT:
|
||||||
return new UnknownPendingMemberCountViewHolder(LayoutInflater.from(parent.getContext())
|
return new UnknownPendingMemberCountViewHolder(LayoutInflater.from(parent.getContext())
|
||||||
.inflate(R.layout.group_recipient_list_item, parent, false),
|
.inflate(R.layout.group_recipient_list_item, parent, false),
|
||||||
adminActionsListener);
|
adminActionsListener,
|
||||||
|
selectionChangeListener);
|
||||||
case NEW_GROUP_CANDIDATE:
|
case NEW_GROUP_CANDIDATE:
|
||||||
return new NewGroupInviteeViewHolder(LayoutInflater.from(parent.getContext())
|
return new NewGroupInviteeViewHolder(LayoutInflater.from(parent.getContext())
|
||||||
.inflate(R.layout.group_new_candidate_recipient_list_item, parent, false),
|
.inflate(R.layout.group_new_candidate_recipient_list_item, parent, false),
|
||||||
recipientClickListener,
|
recipientClickListener,
|
||||||
recipientLongClickListener);
|
recipientLongClickListener,
|
||||||
|
selectionChangeListener);
|
||||||
default:
|
default:
|
||||||
throw new AssertionError();
|
throw new AssertionError();
|
||||||
}
|
}
|
||||||
@ -88,9 +118,14 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
|||||||
this.recipientLongClickListener = recipientLongClickListener;
|
this.recipientLongClickListener = recipientLongClickListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setRecipientSelectionChangeListener(@Nullable RecipientSelectionChangeListener recipientSelectionChangeListener) {
|
||||||
|
this.recipientSelectionChangeListener = recipientSelectionChangeListener;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||||
holder.bind(data.get(position));
|
GroupMemberEntry entry = data.get(position);
|
||||||
|
holder.bind(entry, selection.contains(entry));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -120,10 +155,12 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
|||||||
final Context context;
|
final Context context;
|
||||||
final AvatarImageView avatar;
|
final AvatarImageView avatar;
|
||||||
final TextView recipient;
|
final TextView recipient;
|
||||||
|
final CheckBox selected;
|
||||||
final PopupMenuView popupMenu;
|
final PopupMenuView popupMenu;
|
||||||
final View popupMenuContainer;
|
final View popupMenuContainer;
|
||||||
final ProgressBar busyProgress;
|
final ProgressBar busyProgress;
|
||||||
final View admin;
|
final View admin;
|
||||||
|
final SelectionChangeListener selectionChangeListener;
|
||||||
@Nullable final RecipientClickListener recipientClickListener;
|
@Nullable final RecipientClickListener recipientClickListener;
|
||||||
@Nullable final AdminActionsListener adminActionsListener;
|
@Nullable final AdminActionsListener adminActionsListener;
|
||||||
@Nullable final RecipientLongClickListener recipientLongClickListener;
|
@Nullable final RecipientLongClickListener recipientLongClickListener;
|
||||||
@ -131,13 +168,15 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
|||||||
ViewHolder(@NonNull View itemView,
|
ViewHolder(@NonNull View itemView,
|
||||||
@Nullable RecipientClickListener recipientClickListener,
|
@Nullable RecipientClickListener recipientClickListener,
|
||||||
@Nullable RecipientLongClickListener recipientLongClickListener,
|
@Nullable RecipientLongClickListener recipientLongClickListener,
|
||||||
@Nullable AdminActionsListener adminActionsListener)
|
@Nullable AdminActionsListener adminActionsListener,
|
||||||
|
@NonNull SelectionChangeListener selectionChangeListener)
|
||||||
{
|
{
|
||||||
super(itemView);
|
super(itemView);
|
||||||
|
|
||||||
this.context = itemView.getContext();
|
this.context = itemView.getContext();
|
||||||
this.avatar = itemView.findViewById(R.id.recipient_avatar);
|
this.avatar = itemView.findViewById(R.id.recipient_avatar);
|
||||||
this.recipient = itemView.findViewById(R.id.recipient_name);
|
this.recipient = itemView.findViewById(R.id.recipient_name);
|
||||||
|
this.selected = itemView.findViewById(R.id.recipient_selected);
|
||||||
this.popupMenu = itemView.findViewById(R.id.popupMenu);
|
this.popupMenu = itemView.findViewById(R.id.popupMenu);
|
||||||
this.popupMenuContainer = itemView.findViewById(R.id.popupMenuProgressContainer);
|
this.popupMenuContainer = itemView.findViewById(R.id.popupMenuProgressContainer);
|
||||||
this.busyProgress = itemView.findViewById(R.id.menuBusyProgress);
|
this.busyProgress = itemView.findViewById(R.id.menuBusyProgress);
|
||||||
@ -145,6 +184,7 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
|||||||
this.recipientClickListener = recipientClickListener;
|
this.recipientClickListener = recipientClickListener;
|
||||||
this.recipientLongClickListener = recipientLongClickListener;
|
this.recipientLongClickListener = recipientLongClickListener;
|
||||||
this.adminActionsListener = adminActionsListener;
|
this.adminActionsListener = adminActionsListener;
|
||||||
|
this.selectionChangeListener = selectionChangeListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
void bindRecipient(@NonNull Recipient recipient) {
|
void bindRecipient(@NonNull Recipient recipient) {
|
||||||
@ -166,20 +206,22 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
|||||||
|
|
||||||
this.itemView.setEnabled(true);
|
this.itemView.setEnabled(true);
|
||||||
this.itemView.setOnClickListener(v -> {
|
this.itemView.setOnClickListener(v -> {
|
||||||
if (recipientClickListener != null && getAdapterPosition() != RecyclerView.NO_POSITION) {
|
if (getAdapterPosition() != RecyclerView.NO_POSITION) {
|
||||||
|
if (recipientClickListener != null) {
|
||||||
recipientClickListener.onClick(recipient);
|
recipientClickListener.onClick(recipient);
|
||||||
}
|
}
|
||||||
|
selectionChangeListener.onSelectionChange(getAdapterPosition(), !selected.isChecked());
|
||||||
|
}
|
||||||
});
|
});
|
||||||
this.itemView.setOnLongClickListener(v -> {
|
this.itemView.setOnLongClickListener(v -> {
|
||||||
if (recipientLongClickListener != null && getAdapterPosition() != RecyclerView.NO_POSITION) {
|
if (recipientLongClickListener != null && getAdapterPosition() != RecyclerView.NO_POSITION) {
|
||||||
return recipientLongClickListener.onLongClick(recipient);
|
return recipientLongClickListener.onLongClick(recipient);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void bind(@NonNull GroupMemberEntry memberEntry) {
|
void bind(@NonNull GroupMemberEntry memberEntry, boolean isSelected) {
|
||||||
busyProgress.setVisibility(View.GONE);
|
busyProgress.setVisibility(View.GONE);
|
||||||
admin.setVisibility(View.GONE);
|
admin.setVisibility(View.GONE);
|
||||||
hideMenu();
|
hideMenu();
|
||||||
@ -190,6 +232,8 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
|||||||
busyProgress.setVisibility(busy ? View.VISIBLE : View.GONE);
|
busyProgress.setVisibility(busy ? View.VISIBLE : View.GONE);
|
||||||
popupMenu.setVisibility(busy ? View.GONE : View.VISIBLE);
|
popupMenu.setVisibility(busy ? View.GONE : View.VISIBLE);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
selected.setChecked(isSelected);
|
||||||
}
|
}
|
||||||
|
|
||||||
void hideMenu() {
|
void hideMenu() {
|
||||||
@ -208,14 +252,15 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
|||||||
FullMemberViewHolder(@NonNull View itemView,
|
FullMemberViewHolder(@NonNull View itemView,
|
||||||
@Nullable RecipientClickListener recipientClickListener,
|
@Nullable RecipientClickListener recipientClickListener,
|
||||||
@Nullable RecipientLongClickListener recipientLongClickListener,
|
@Nullable RecipientLongClickListener recipientLongClickListener,
|
||||||
@Nullable AdminActionsListener adminActionsListener)
|
@Nullable AdminActionsListener adminActionsListener,
|
||||||
|
@NonNull SelectionChangeListener selectionChangeListener)
|
||||||
{
|
{
|
||||||
super(itemView, recipientClickListener, recipientLongClickListener, adminActionsListener);
|
super(itemView, recipientClickListener, recipientLongClickListener, adminActionsListener, selectionChangeListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
void bind(@NonNull GroupMemberEntry memberEntry) {
|
void bind(@NonNull GroupMemberEntry memberEntry, boolean isSelected) {
|
||||||
super.bind(memberEntry);
|
super.bind(memberEntry, isSelected);
|
||||||
|
|
||||||
GroupMemberEntry.FullMember fullMember = (GroupMemberEntry.FullMember) memberEntry;
|
GroupMemberEntry.FullMember fullMember = (GroupMemberEntry.FullMember) memberEntry;
|
||||||
|
|
||||||
@ -231,16 +276,17 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
|||||||
|
|
||||||
NewGroupInviteeViewHolder(@NonNull View itemView,
|
NewGroupInviteeViewHolder(@NonNull View itemView,
|
||||||
@Nullable RecipientClickListener recipientClickListener,
|
@Nullable RecipientClickListener recipientClickListener,
|
||||||
@Nullable RecipientLongClickListener recipientLongClickListener)
|
@Nullable RecipientLongClickListener recipientLongClickListener,
|
||||||
|
@NonNull SelectionChangeListener selectionChangeListener)
|
||||||
{
|
{
|
||||||
super(itemView, recipientClickListener, recipientLongClickListener, null);
|
super(itemView, recipientClickListener, recipientLongClickListener, null, selectionChangeListener);
|
||||||
|
|
||||||
smsContact = itemView.findViewById(R.id.sms_contact);
|
smsContact = itemView.findViewById(R.id.sms_contact);
|
||||||
smsWarning = itemView.findViewById(R.id.sms_warning);
|
smsWarning = itemView.findViewById(R.id.sms_warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
void bind(@NonNull GroupMemberEntry memberEntry) {
|
void bind(@NonNull GroupMemberEntry memberEntry, boolean isSelected) {
|
||||||
GroupMemberEntry.NewGroupCandidate newGroupCandidate = (GroupMemberEntry.NewGroupCandidate) memberEntry;
|
GroupMemberEntry.NewGroupCandidate newGroupCandidate = (GroupMemberEntry.NewGroupCandidate) memberEntry;
|
||||||
|
|
||||||
bindRecipient(newGroupCandidate.getMember());
|
bindRecipient(newGroupCandidate.getMember());
|
||||||
@ -258,14 +304,15 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
|||||||
OwnInvitePendingMemberViewHolder(@NonNull View itemView,
|
OwnInvitePendingMemberViewHolder(@NonNull View itemView,
|
||||||
@Nullable RecipientClickListener recipientClickListener,
|
@Nullable RecipientClickListener recipientClickListener,
|
||||||
@Nullable RecipientLongClickListener recipientLongClickListener,
|
@Nullable RecipientLongClickListener recipientLongClickListener,
|
||||||
@Nullable AdminActionsListener adminActionsListener)
|
@Nullable AdminActionsListener adminActionsListener,
|
||||||
|
@NonNull SelectionChangeListener selectionChangeListener)
|
||||||
{
|
{
|
||||||
super(itemView, recipientClickListener, recipientLongClickListener, adminActionsListener);
|
super(itemView, recipientClickListener, recipientLongClickListener, adminActionsListener, selectionChangeListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
void bind(@NonNull GroupMemberEntry memberEntry) {
|
void bind(@NonNull GroupMemberEntry memberEntry, boolean isSelected) {
|
||||||
super.bind(memberEntry);
|
super.bind(memberEntry, isSelected);
|
||||||
|
|
||||||
GroupMemberEntry.PendingMember pendingMember = (GroupMemberEntry.PendingMember) memberEntry;
|
GroupMemberEntry.PendingMember pendingMember = (GroupMemberEntry.PendingMember) memberEntry;
|
||||||
|
|
||||||
@ -288,13 +335,16 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
|||||||
|
|
||||||
final static class UnknownPendingMemberCountViewHolder extends ViewHolder {
|
final static class UnknownPendingMemberCountViewHolder extends ViewHolder {
|
||||||
|
|
||||||
UnknownPendingMemberCountViewHolder(@NonNull View itemView, @Nullable AdminActionsListener adminActionsListener) {
|
UnknownPendingMemberCountViewHolder(@NonNull View itemView,
|
||||||
super(itemView, null, null, adminActionsListener);
|
@Nullable AdminActionsListener adminActionsListener,
|
||||||
|
@NonNull SelectionChangeListener selectionChangeListener)
|
||||||
|
{
|
||||||
|
super(itemView, null, null, adminActionsListener, selectionChangeListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
void bind(@NonNull GroupMemberEntry memberEntry) {
|
void bind(@NonNull GroupMemberEntry memberEntry, boolean isSelected) {
|
||||||
super.bind(memberEntry);
|
super.bind(memberEntry, isSelected);
|
||||||
GroupMemberEntry.UnknownPendingMemberCount pendingMembers = (GroupMemberEntry.UnknownPendingMemberCount) memberEntry;
|
GroupMemberEntry.UnknownPendingMemberCount pendingMembers = (GroupMemberEntry.UnknownPendingMemberCount) memberEntry;
|
||||||
|
|
||||||
Recipient inviter = pendingMembers.getInviter();
|
Recipient inviter = pendingMembers.getInviter();
|
||||||
@ -327,6 +377,23 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final class SelectionChangeListener {
|
||||||
|
void onSelectionChange(int position, boolean isChecked) {
|
||||||
|
if (selectable) {
|
||||||
|
if (isChecked) {
|
||||||
|
selection.add(data.get(position));
|
||||||
|
} else {
|
||||||
|
selection.remove(data.get(position));
|
||||||
|
}
|
||||||
|
notifyItemChanged(position);
|
||||||
|
|
||||||
|
if (recipientSelectionChangeListener != null) {
|
||||||
|
recipientSelectionChangeListener.onSelectionChanged(selection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private final static class DiffCallback extends DiffUtil.Callback {
|
private final static class DiffCallback extends DiffUtil.Callback {
|
||||||
private final List<? extends GroupMemberEntry> oldData;
|
private final List<? extends GroupMemberEntry> oldData;
|
||||||
private final List<? extends GroupMemberEntry> newData;
|
private final List<? extends GroupMemberEntry> newData;
|
||||||
|
@ -15,7 +15,7 @@ import java.util.List;
|
|||||||
|
|
||||||
public final class GroupMemberListView extends RecyclerView {
|
public final class GroupMemberListView extends RecyclerView {
|
||||||
|
|
||||||
private final GroupMemberListAdapter membersAdapter = new GroupMemberListAdapter();
|
private GroupMemberListAdapter membersAdapter;
|
||||||
private int maxHeight;
|
private int maxHeight;
|
||||||
|
|
||||||
public GroupMemberListView(@NonNull Context context) {
|
public GroupMemberListView(@NonNull Context context) {
|
||||||
@ -38,17 +38,20 @@ public final class GroupMemberListView extends RecyclerView {
|
|||||||
setHasFixedSize(true);
|
setHasFixedSize(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
setLayoutManager(new LinearLayoutManager(context));
|
boolean selectable = false;
|
||||||
setAdapter(membersAdapter);
|
|
||||||
|
|
||||||
if (attrs != null) {
|
if (attrs != null) {
|
||||||
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.GroupMemberListView, 0, 0);
|
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.GroupMemberListView, 0, 0);
|
||||||
try {
|
try {
|
||||||
maxHeight = typedArray.getDimensionPixelSize(R.styleable.GroupMemberListView_maxHeight, 0);
|
maxHeight = typedArray.getDimensionPixelSize(R.styleable.GroupMemberListView_maxHeight, 0);
|
||||||
|
selectable = typedArray.getBoolean(R.styleable.GroupMemberListView_selectable, false);
|
||||||
} finally {
|
} finally {
|
||||||
typedArray.recycle();
|
typedArray.recycle();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
membersAdapter = new GroupMemberListAdapter(selectable);
|
||||||
|
setLayoutManager(new LinearLayoutManager(context));
|
||||||
|
setAdapter(membersAdapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAdminActionsListener(@Nullable AdminActionsListener adminActionsListener) {
|
public void setAdminActionsListener(@Nullable AdminActionsListener adminActionsListener) {
|
||||||
@ -63,6 +66,10 @@ public final class GroupMemberListView extends RecyclerView {
|
|||||||
membersAdapter.setRecipientLongClickListener(listener);
|
membersAdapter.setRecipientLongClickListener(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setRecipientSelectionChangeListener(@Nullable RecipientSelectionChangeListener listener) {
|
||||||
|
membersAdapter.setRecipientSelectionChangeListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
public void setMembers(@NonNull List<? extends GroupMemberEntry> recipients) {
|
public void setMembers(@NonNull List<? extends GroupMemberEntry> recipients) {
|
||||||
membersAdapter.updateData(recipients);
|
membersAdapter.updateData(recipients);
|
||||||
}
|
}
|
||||||
|
@ -1,60 +1,115 @@
|
|||||||
package org.thoughtcrime.securesms.groups.ui;
|
package org.thoughtcrime.securesms.groups.ui;
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.lifecycle.Lifecycle;
|
import androidx.fragment.app.FragmentActivity;
|
||||||
|
|
||||||
|
import com.annimon.stream.Stream;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
|
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||||
import org.thoughtcrime.securesms.groups.GroupChangeBusyException;
|
import org.thoughtcrime.securesms.groups.GroupChangeBusyException;
|
||||||
import org.thoughtcrime.securesms.groups.GroupChangeFailedException;
|
import org.thoughtcrime.securesms.groups.GroupChangeFailedException;
|
||||||
import org.thoughtcrime.securesms.groups.GroupId;
|
import org.thoughtcrime.securesms.groups.GroupId;
|
||||||
import org.thoughtcrime.securesms.groups.GroupManager;
|
import org.thoughtcrime.securesms.groups.GroupManager;
|
||||||
|
import org.thoughtcrime.securesms.groups.ui.chooseadmin.ChooseNewAdminActivity;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public final class LeaveGroupDialog {
|
public final class LeaveGroupDialog {
|
||||||
|
|
||||||
private static final String TAG = Log.tag(LeaveGroupDialog.class);
|
private static final String TAG = Log.tag(LeaveGroupDialog.class);
|
||||||
|
|
||||||
private LeaveGroupDialog() {
|
@NonNull private final FragmentActivity activity;
|
||||||
|
@NonNull private final GroupId.Push groupId;
|
||||||
|
@Nullable private final Runnable onSuccess;
|
||||||
|
|
||||||
|
public static void handleLeavePushGroup(@NonNull FragmentActivity activity,
|
||||||
|
@NonNull GroupId.Push groupId,
|
||||||
|
@Nullable Runnable onSuccess) {
|
||||||
|
new LeaveGroupDialog(activity, groupId, onSuccess).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void handleLeavePushGroup(@NonNull Context context,
|
private LeaveGroupDialog(@NonNull FragmentActivity activity,
|
||||||
@NonNull Lifecycle lifecycle,
|
|
||||||
@NonNull GroupId.Push groupId,
|
@NonNull GroupId.Push groupId,
|
||||||
@Nullable Runnable onSuccess)
|
@Nullable Runnable onSuccess) {
|
||||||
{
|
this.activity = activity;
|
||||||
new AlertDialog.Builder(context)
|
this.groupId = groupId;
|
||||||
.setTitle(context.getString(R.string.ConversationActivity_leave_group))
|
this.onSuccess = onSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void show() {
|
||||||
|
if (!groupId.isV2()) {
|
||||||
|
showLeaveDialog();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SimpleTask.run(activity.getLifecycle(), () -> {
|
||||||
|
GroupDatabase.V2GroupProperties groupProperties = DatabaseFactory.getGroupDatabase(activity)
|
||||||
|
.getGroup(groupId)
|
||||||
|
.transform(GroupDatabase.GroupRecord::requireV2GroupProperties)
|
||||||
|
.orNull();
|
||||||
|
|
||||||
|
if (groupProperties != null && groupProperties.isAdmin(Recipient.self())) {
|
||||||
|
List<Recipient> otherMemberRecipients = groupProperties.getMemberRecipients(GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
|
||||||
|
long otherAdminsCount = Stream.of(otherMemberRecipients).filter(groupProperties::isAdmin).count();
|
||||||
|
|
||||||
|
return otherAdminsCount == 0 && !otherMemberRecipients.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}, mustSelectNewAdmin -> {
|
||||||
|
if (mustSelectNewAdmin) {
|
||||||
|
showSelectNewAdminDialog();
|
||||||
|
} else {
|
||||||
|
showLeaveDialog();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showSelectNewAdminDialog() {
|
||||||
|
new AlertDialog.Builder(activity)
|
||||||
|
.setTitle(R.string.ConversationActivity_choose_new_admin)
|
||||||
|
.setMessage(R.string.ConversationActivity_before_you_leave_you_must_choose_at_least_one_new_admin_for_this_group)
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.setPositiveButton(R.string.ConversationActivity_choose_admin, (d,w) -> activity.startActivity(ChooseNewAdminActivity.createIntent(activity, groupId.requireV2())))
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showLeaveDialog() {
|
||||||
|
new AlertDialog.Builder(activity)
|
||||||
|
.setTitle(R.string.ConversationActivity_leave_group)
|
||||||
.setIconAttribute(R.attr.dialog_info_icon)
|
.setIconAttribute(R.attr.dialog_info_icon)
|
||||||
.setCancelable(true)
|
.setCancelable(true)
|
||||||
.setMessage(context.getString(R.string.ConversationActivity_are_you_sure_you_want_to_leave_this_group))
|
.setMessage(R.string.ConversationActivity_are_you_sure_you_want_to_leave_this_group)
|
||||||
.setPositiveButton(R.string.yes, (dialog, which) ->
|
.setPositiveButton(R.string.yes, (dialog, which) -> SimpleTask.run(activity.getLifecycle(), this::leaveGroup, this::handleLeaveGroupResult))
|
||||||
SimpleTask.run(
|
.setNegativeButton(R.string.no, null)
|
||||||
lifecycle,
|
.show();
|
||||||
() -> {
|
}
|
||||||
|
|
||||||
|
private boolean leaveGroup() {
|
||||||
try {
|
try {
|
||||||
GroupManager.leaveGroup(context, groupId);
|
GroupManager.leaveGroup(activity, groupId);
|
||||||
return true;
|
return true;
|
||||||
} catch (GroupChangeFailedException | GroupChangeBusyException | IOException e) {
|
} catch (GroupChangeFailedException | GroupChangeBusyException | IOException e) {
|
||||||
Log.w(TAG, e);
|
Log.w(TAG, e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
(success) -> {
|
|
||||||
|
private void handleLeaveGroupResult(boolean success) {
|
||||||
if (success) {
|
if (success) {
|
||||||
if (onSuccess != null) onSuccess.run();
|
if (onSuccess != null) onSuccess.run();
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(context, R.string.ConversationActivity_error_leaving_group, Toast.LENGTH_LONG).show();
|
Toast.makeText(activity, R.string.ConversationActivity_error_leaving_group, Toast.LENGTH_LONG).show();
|
||||||
}
|
}
|
||||||
}))
|
|
||||||
.setNegativeButton(R.string.no, null)
|
|
||||||
.show();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
package org.thoughtcrime.securesms.groups.ui;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public interface RecipientSelectionChangeListener {
|
||||||
|
void onSelectionChanged(@NonNull Set<GroupMemberEntry> selection);
|
||||||
|
}
|
@ -0,0 +1,124 @@
|
|||||||
|
package org.thoughtcrime.securesms.groups.ui.chooseadmin;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.widget.Toolbar;
|
||||||
|
import androidx.lifecycle.ViewModelProviders;
|
||||||
|
|
||||||
|
import com.annimon.stream.Collectors;
|
||||||
|
import com.annimon.stream.Stream;
|
||||||
|
import com.dd.CircularProgressButton;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.MainActivity;
|
||||||
|
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
||||||
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.groups.BadGroupIdException;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupId;
|
||||||
|
import org.thoughtcrime.securesms.groups.ui.GroupErrors;
|
||||||
|
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
|
||||||
|
import org.thoughtcrime.securesms.groups.ui.GroupMemberListView;
|
||||||
|
import org.thoughtcrime.securesms.groups.ui.chooseadmin.ChooseNewAdminRepository.UpdateResult;
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
|
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||||
|
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public final class ChooseNewAdminActivity extends PassphraseRequiredActivity {
|
||||||
|
|
||||||
|
private static final String EXTRA_GROUP_ID = "group_id";
|
||||||
|
|
||||||
|
private ChooseNewAdminViewModel viewModel;
|
||||||
|
private GroupMemberListView groupList;
|
||||||
|
private CircularProgressButton done;
|
||||||
|
private GroupId.V2 groupId;
|
||||||
|
|
||||||
|
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||||
|
|
||||||
|
public static Intent createIntent(@NonNull Context context, @NonNull GroupId.V2 groupId) {
|
||||||
|
Intent intent = new Intent(context, ChooseNewAdminActivity.class);
|
||||||
|
intent.putExtra(EXTRA_GROUP_ID, groupId.toString());
|
||||||
|
return intent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPreCreate() {
|
||||||
|
dynamicTheme.onCreate(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState, boolean ready) {
|
||||||
|
super.onCreate(savedInstanceState, ready);
|
||||||
|
setContentView(R.layout.choose_new_admin_activity);
|
||||||
|
|
||||||
|
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||||
|
setSupportActionBar(toolbar);
|
||||||
|
//noinspection ConstantConditions
|
||||||
|
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
groupId = GroupId.parse(Objects.requireNonNull(getIntent().getStringExtra(EXTRA_GROUP_ID))).requireV2();
|
||||||
|
} catch (BadGroupIdException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
groupList = findViewById(R.id.choose_new_admin_group_list);
|
||||||
|
done = findViewById(R.id.choose_new_admin_done);
|
||||||
|
done.setIndeterminateProgressMode(true);
|
||||||
|
|
||||||
|
initializeViewModel();
|
||||||
|
|
||||||
|
groupList.setRecipientSelectionChangeListener(selection -> viewModel.setSelection(Stream.of(selection)
|
||||||
|
.select(GroupMemberEntry.FullMember.class)
|
||||||
|
.collect(Collectors.toSet())));
|
||||||
|
|
||||||
|
done.setOnClickListener(v -> {
|
||||||
|
done.setClickable(false);
|
||||||
|
done.setProgress(50);
|
||||||
|
viewModel.updateAdminsAndLeave(this::handleUpdateAndLeaveResult);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
dynamicTheme.onResume(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
|
if (item.getItemId() == android.R.id.home) {
|
||||||
|
finish();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.onOptionsItemSelected(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeViewModel() {
|
||||||
|
viewModel = ViewModelProviders.of(this, new ChooseNewAdminViewModel.Factory(groupId)).get(ChooseNewAdminViewModel.class);
|
||||||
|
|
||||||
|
viewModel.getNonAdminFullMembers().observe(this, groupList::setMembers);
|
||||||
|
viewModel.getSelection().observe(this, selection -> done.setVisibility(selection.isEmpty() ? View.GONE : View.VISIBLE));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleUpdateAndLeaveResult(@NonNull UpdateResult updateResult) {
|
||||||
|
if (updateResult.isSuccess()) {
|
||||||
|
String title = Recipient.externalGroup(this, groupId).getDisplayName(this);
|
||||||
|
Toast.makeText(this, getString(R.string.ChooseNewAdminActivity_you_left, title), Toast.LENGTH_LONG).show();
|
||||||
|
startActivity(new Intent(this, MainActivity.class));
|
||||||
|
finish();
|
||||||
|
} else {
|
||||||
|
done.setClickable(true);
|
||||||
|
done.setProgress(0);
|
||||||
|
//noinspection ConstantConditions
|
||||||
|
Toast.makeText(this, GroupErrors.getUserDisplayMessage(updateResult.getFailureReason()), Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
package org.thoughtcrime.securesms.groups.ui.chooseadmin;
|
||||||
|
|
||||||
|
import android.app.Application;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.WorkerThread;
|
||||||
|
import androidx.core.util.Consumer;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupChangeBusyException;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupChangeFailedException;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupId;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupInsufficientRightsException;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupManager;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupNotAMemberException;
|
||||||
|
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class ChooseNewAdminRepository {
|
||||||
|
private Application context;
|
||||||
|
|
||||||
|
ChooseNewAdminRepository(@NonNull Application context) {
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
@NonNull UpdateResult updateAdminsAndLeave(@NonNull GroupId.V2 groupId, @NonNull List<RecipientId> newAdminIds) {
|
||||||
|
try {
|
||||||
|
GroupManager.addMemberAdminsAndLeaveGroup(context, groupId, newAdminIds);
|
||||||
|
return new UpdateResult();
|
||||||
|
} catch (GroupInsufficientRightsException e) {
|
||||||
|
return new UpdateResult(GroupChangeFailureReason.NO_RIGHTS);
|
||||||
|
} catch (GroupNotAMemberException e) {
|
||||||
|
return new UpdateResult(GroupChangeFailureReason.NOT_A_MEMBER);
|
||||||
|
} catch (GroupChangeFailedException | GroupChangeBusyException | IOException e) {
|
||||||
|
return new UpdateResult(GroupChangeFailureReason.OTHER);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class UpdateResult {
|
||||||
|
final @Nullable GroupChangeFailureReason failureReason;
|
||||||
|
|
||||||
|
UpdateResult() {
|
||||||
|
this(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateResult(@Nullable GroupChangeFailureReason failureReason) {
|
||||||
|
this.failureReason = failureReason;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isSuccess() {
|
||||||
|
return failureReason == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable GroupChangeFailureReason getFailureReason() {
|
||||||
|
return failureReason;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,76 @@
|
|||||||
|
package org.thoughtcrime.securesms.groups.ui.chooseadmin;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.StringRes;
|
||||||
|
import androidx.core.util.Consumer;
|
||||||
|
import androidx.lifecycle.LiveData;
|
||||||
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
import androidx.lifecycle.Transformations;
|
||||||
|
import androidx.lifecycle.ViewModel;
|
||||||
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
|
||||||
|
import com.annimon.stream.Stream;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupId;
|
||||||
|
import org.thoughtcrime.securesms.groups.LiveGroup;
|
||||||
|
import org.thoughtcrime.securesms.groups.ui.GroupErrors;
|
||||||
|
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
|
||||||
|
import org.thoughtcrime.securesms.groups.ui.chooseadmin.ChooseNewAdminRepository.UpdateResult;
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
|
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||||
|
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
final class ChooseNewAdminViewModel extends ViewModel {
|
||||||
|
|
||||||
|
private final GroupId.V2 groupId;
|
||||||
|
private final ChooseNewAdminRepository repository;
|
||||||
|
private final LiveGroup liveGroup;
|
||||||
|
private final MutableLiveData<Set<GroupMemberEntry.FullMember>> selection;
|
||||||
|
|
||||||
|
public ChooseNewAdminViewModel(@NonNull GroupId.V2 groupId, @NonNull ChooseNewAdminRepository repository) {
|
||||||
|
this.groupId = groupId;
|
||||||
|
this.repository = repository;
|
||||||
|
|
||||||
|
liveGroup = new LiveGroup(groupId);
|
||||||
|
selection = new MutableLiveData<>(Collections.emptySet());
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull LiveData<List<GroupMemberEntry.FullMember>> getNonAdminFullMembers() {
|
||||||
|
return liveGroup.getNonAdminFullMembers();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull LiveData<Set<GroupMemberEntry.FullMember>> getSelection() {
|
||||||
|
return selection;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setSelection(@NonNull Set<GroupMemberEntry.FullMember> selection) {
|
||||||
|
this.selection.setValue(selection);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateAdminsAndLeave(@NonNull Consumer<UpdateResult> consumer) {
|
||||||
|
//noinspection ConstantConditions
|
||||||
|
List<RecipientId> recipientIds = Stream.of(selection.getValue()).map(entry -> entry.getMember().getId()).toList();
|
||||||
|
SimpleTask.run(() -> repository.updateAdminsAndLeave(groupId, recipientIds), consumer::accept);
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class Factory implements ViewModelProvider.Factory {
|
||||||
|
|
||||||
|
private final GroupId.V2 groupId;
|
||||||
|
|
||||||
|
Factory(@NonNull GroupId.V2 groupId) {
|
||||||
|
this.groupId = groupId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||||
|
//noinspection ConstantConditions
|
||||||
|
return modelClass.cast(new ChooseNewAdminViewModel(groupId, new ChooseNewAdminRepository(ApplicationDependencies.getApplication())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -24,6 +24,7 @@ import com.google.android.material.snackbar.Snackbar;
|
|||||||
|
|
||||||
import org.thoughtcrime.securesms.AvatarPreviewActivity;
|
import org.thoughtcrime.securesms.AvatarPreviewActivity;
|
||||||
import org.thoughtcrime.securesms.LoggingFragment;
|
import org.thoughtcrime.securesms.LoggingFragment;
|
||||||
|
import org.thoughtcrime.securesms.MainActivity;
|
||||||
import org.thoughtcrime.securesms.MediaPreviewActivity;
|
import org.thoughtcrime.securesms.MediaPreviewActivity;
|
||||||
import org.thoughtcrime.securesms.MuteDialog;
|
import org.thoughtcrime.securesms.MuteDialog;
|
||||||
import org.thoughtcrime.securesms.PushContactSelectionActivity;
|
import org.thoughtcrime.securesms.PushContactSelectionActivity;
|
||||||
@ -234,10 +235,7 @@ public class ManageGroupFragment extends LoggingFragment {
|
|||||||
});
|
});
|
||||||
|
|
||||||
leaveGroup.setVisibility(groupId.isPush() ? View.VISIBLE : View.GONE);
|
leaveGroup.setVisibility(groupId.isPush() ? View.VISIBLE : View.GONE);
|
||||||
leaveGroup.setOnClickListener(v -> LeaveGroupDialog.handleLeavePushGroup(context,
|
leaveGroup.setOnClickListener(v -> LeaveGroupDialog.handleLeavePushGroup(requireActivity(), groupId.requirePush(), () -> startActivity(new Intent(requireActivity(), MainActivity.class))));
|
||||||
getLifecycle(),
|
|
||||||
groupId.requirePush(),
|
|
||||||
null));
|
|
||||||
|
|
||||||
viewModel.getDisappearingMessageTimer().observe(getViewLifecycleOwner(), string -> disappearingMessages.setText(string));
|
viewModel.getDisappearingMessageTimer().observe(getViewLifecycleOwner(), string -> disappearingMessages.setText(string));
|
||||||
|
|
||||||
|
57
app/src/main/res/layout/choose_new_admin_activity.xml
Normal file
57
app/src/main/res/layout/choose_new_admin_activity.xml
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:id="@+id/toolbar_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?android:attr/windowBackground"
|
||||||
|
app:elevation="0dp">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.Toolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?actionBarSize"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
app:title="@string/ChooseNewAdminActivity_choose_new_admin"
|
||||||
|
app:titleTextColor="?title_text_color_primary" />
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<FrameLayout android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.groups.ui.GroupMemberListView
|
||||||
|
android:id="@+id/choose_new_admin_group_list"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:selectable="true"
|
||||||
|
tools:listitem="@layout/group_recipient_list_item" />
|
||||||
|
|
||||||
|
<com.dd.CircularProgressButton
|
||||||
|
android:id="@+id/choose_new_admin_done"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom|end"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
app:cpb_colorIndicator="@color/white"
|
||||||
|
app:cpb_colorProgress="?colorAccent"
|
||||||
|
app:cpb_cornerRadius="28dp"
|
||||||
|
app:cpb_selectorIdle="?attr/circular_progress_button_state"
|
||||||
|
app:cpb_textIdle="@string/ChooseNewAdminActivity_done"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
@ -15,7 +15,20 @@
|
|||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:src="@tools:sample/avatars"/>
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatCheckBox
|
||||||
|
android:id="@+id/recipient_selected"
|
||||||
|
android:background="?attr/contact_selection_checkbox_background"
|
||||||
|
android:button="@null"
|
||||||
|
android:layout_width="22dp"
|
||||||
|
android:layout_height="22dp"
|
||||||
|
android:focusable="false"
|
||||||
|
android:clickable="false"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/recipient_avatar"
|
||||||
|
app:layout_constraintEnd_toEndOf="@+id/recipient_avatar"
|
||||||
|
tools:checked="true"/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/recipient_name"
|
android:id="@+id/recipient_name"
|
||||||
|
@ -529,6 +529,7 @@
|
|||||||
|
|
||||||
<declare-styleable name="GroupMemberListView">
|
<declare-styleable name="GroupMemberListView">
|
||||||
<attr name="maxHeight" />
|
<attr name="maxHeight" />
|
||||||
|
<attr name="selectable" format="boolean" />
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
|
|
||||||
<declare-styleable name="WebRtcAudioOutputToggleButtonState">
|
<declare-styleable name="WebRtcAudioOutputToggleButtonState">
|
||||||
|
@ -227,6 +227,9 @@
|
|||||||
<string name="ConversationActivity_this_device_does_not_appear_to_support_dial_actions">This device does not appear to support dial actions.</string>
|
<string name="ConversationActivity_this_device_does_not_appear_to_support_dial_actions">This device does not appear to support dial actions.</string>
|
||||||
<string name="ConversationActivity_leave_group">Leave group?</string>
|
<string name="ConversationActivity_leave_group">Leave group?</string>
|
||||||
<string name="ConversationActivity_are_you_sure_you_want_to_leave_this_group">Are you sure you want to leave this group?</string>
|
<string name="ConversationActivity_are_you_sure_you_want_to_leave_this_group">Are you sure you want to leave this group?</string>
|
||||||
|
<string name="ConversationActivity_choose_new_admin">Choose new admin</string>
|
||||||
|
<string name="ConversationActivity_before_you_leave_you_must_choose_at_least_one_new_admin_for_this_group">Before you leave, you must choose at least one new admin for this group.</string>
|
||||||
|
<string name="ConversationActivity_choose_admin">Choose admin</string>
|
||||||
<string name="ConversationActivity_transport_insecure_sms">Insecure SMS</string>
|
<string name="ConversationActivity_transport_insecure_sms">Insecure SMS</string>
|
||||||
<string name="ConversationActivity_transport_insecure_mms">Insecure MMS</string>
|
<string name="ConversationActivity_transport_insecure_mms">Insecure MMS</string>
|
||||||
<string name="ConversationActivity_transport_signal">Signal</string>
|
<string name="ConversationActivity_transport_signal">Signal</string>
|
||||||
@ -442,6 +445,11 @@
|
|||||||
<string name="AddToGroupActivity_add_to_group">Add to group</string>
|
<string name="AddToGroupActivity_add_to_group">Add to group</string>
|
||||||
<string name="AddToGroupActivity_add_to_groups">Add to groups</string>
|
<string name="AddToGroupActivity_add_to_groups">Add to groups</string>
|
||||||
|
|
||||||
|
<!-- ChooseNewAdminActivity -->
|
||||||
|
<string name="ChooseNewAdminActivity_choose_new_admin">Choose new admin</string>
|
||||||
|
<string name="ChooseNewAdminActivity_done">Done</string>
|
||||||
|
<string name="ChooseNewAdminActivity_you_left">You left \"%1$s.\"</string>
|
||||||
|
|
||||||
<!-- GroupShareProfileView -->
|
<!-- GroupShareProfileView -->
|
||||||
<string name="GroupShareProfileView_share_your_profile_name_and_photo_with_this_group">Share your profile name and photo with this group?</string>
|
<string name="GroupShareProfileView_share_your_profile_name_and_photo_with_this_group">Share your profile name and photo with this group?</string>
|
||||||
<string name="GroupShareProfileView_do_you_want_to_make_your_profile_name_and_photo_visible_to_all_current_and_future_members_of_this_group">Do you want to make your profile name and photo visible to all current and future members of this group?</string>
|
<string name="GroupShareProfileView_do_you_want_to_make_your_profile_name_and_photo_visible_to_all_current_and_future_members_of_this_group">Do you want to make your profile name and photo visible to all current and future members of this group?</string>
|
||||||
|
@ -207,6 +207,18 @@ public final class GroupsV2Operations {
|
|||||||
return actions;
|
return actions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public GroupChange.Actions.Builder createLeaveAndPromoteMembersToAdmin(UUID self, List<UUID> membersToMakeAdmin) {
|
||||||
|
GroupChange.Actions.Builder actions = createRemoveMembersChange(Collections.singleton(self));
|
||||||
|
|
||||||
|
for (UUID member : membersToMakeAdmin) {
|
||||||
|
actions.addModifyMemberRoles(GroupChange.Actions.ModifyMemberRoleAction.newBuilder()
|
||||||
|
.setUserId(encryptUuid(member))
|
||||||
|
.setRole(Member.Role.ADMINISTRATOR));
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
public GroupChange.Actions.Builder createModifyGroupTimerChange(int timerDurationSeconds) {
|
public GroupChange.Actions.Builder createModifyGroupTimerChange(int timerDurationSeconds) {
|
||||||
return GroupChange.Actions
|
return GroupChange.Actions
|
||||||
.newBuilder()
|
.newBuilder()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user