New group management screen.
This commit is contained in:
parent
e0502c24e1
commit
723639d928
30 changed files with 1621 additions and 175 deletions
|
@ -257,6 +257,9 @@
|
|||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:theme="@style/TextSecure.LightNoActionBar" />
|
||||
|
||||
<activity android:name=".groups.ui.managegroup.ManageGroupActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".DatabaseMigrationActivity"
|
||||
android:theme="@style/NoAnimation.Theme.AppCompat.Light.DarkActionBar"
|
||||
android:launchMode="singleTask"
|
||||
|
|
|
@ -1,44 +1,27 @@
|
|||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
|
||||
import org.thoughtcrime.securesms.groups.LiveGroup;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberListView;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientExporter;
|
||||
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public final class GroupMembersDialog {
|
||||
|
||||
private final FragmentActivity fragmentActivity;
|
||||
private final Recipient groupRecipient;
|
||||
private final Lifecycle lifecycle;
|
||||
|
||||
public GroupMembersDialog(@NonNull FragmentActivity activity,
|
||||
@NonNull Recipient groupRecipient,
|
||||
@NonNull Lifecycle lifecycle)
|
||||
@NonNull Recipient groupRecipient)
|
||||
{
|
||||
this.fragmentActivity = activity;
|
||||
this.groupRecipient = groupRecipient;
|
||||
this.lifecycle = lifecycle;
|
||||
}
|
||||
|
||||
public void display() {
|
||||
SimpleTask.run(
|
||||
lifecycle,
|
||||
() -> DatabaseFactory.getGroupDatabase(fragmentActivity).getGroupMembers(groupRecipient.requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_INCLUDING_SELF),
|
||||
members -> {
|
||||
AlertDialog dialog = new AlertDialog.Builder(fragmentActivity)
|
||||
.setTitle(R.string.ConversationActivity_group_members)
|
||||
.setIconAttribute(R.attr.group_members_dialog_icon)
|
||||
|
@ -49,26 +32,17 @@ public final class GroupMembersDialog {
|
|||
|
||||
GroupMemberListView memberListView = dialog.findViewById(R.id.list_members);
|
||||
|
||||
ArrayList<GroupMemberEntry.FullMember> pendingMembers = new ArrayList<>(members.size());
|
||||
for (Recipient member : members) {
|
||||
GroupMemberEntry.FullMember entry = new GroupMemberEntry.FullMember(member);
|
||||
|
||||
entry.setOnClick(() -> {
|
||||
dialog.dismiss();
|
||||
contactClick(member);
|
||||
});
|
||||
|
||||
if (member.isLocalNumber()) {
|
||||
pendingMembers.add(0, entry);
|
||||
} else {
|
||||
pendingMembers.add(entry);
|
||||
}
|
||||
}
|
||||
LiveGroup liveGroup = new LiveGroup(groupRecipient.requireGroupId());
|
||||
|
||||
//noinspection ConstantConditions
|
||||
memberListView.setMembers(pendingMembers);
|
||||
}
|
||||
);
|
||||
liveGroup.getFullMembers().observe(fragmentActivity, memberListView::setMembers);
|
||||
|
||||
dialog.setOnDismissListener(d -> liveGroup.removeObservers(fragmentActivity));
|
||||
|
||||
memberListView.setRecipientClickListener(recipient -> {
|
||||
dialog.dismiss();
|
||||
contactClick(recipient);
|
||||
});
|
||||
}
|
||||
|
||||
private void contactClick(@NonNull Recipient recipient) {
|
||||
|
|
|
@ -148,7 +148,11 @@ import org.thoughtcrime.securesms.database.model.StickerRecord;
|
|||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.events.ReminderUpdateEvent;
|
||||
import org.thoughtcrime.securesms.giph.ui.GiphyActivity;
|
||||
import org.thoughtcrime.securesms.groups.GroupChangeFailedException;
|
||||
import org.thoughtcrime.securesms.groups.GroupInsufficientRightsException;
|
||||
import org.thoughtcrime.securesms.groups.GroupManager;
|
||||
import org.thoughtcrime.securesms.groups.ui.LeaveGroupDialog;
|
||||
import org.thoughtcrime.securesms.groups.ui.managegroup.ManageGroupActivity;
|
||||
import org.thoughtcrime.securesms.groups.ui.pendingmemberinvites.PendingMemberInvitesActivity;
|
||||
import org.thoughtcrime.securesms.insights.InsightsLauncher;
|
||||
import org.thoughtcrime.securesms.invites.InviteReminderModel;
|
||||
|
@ -846,6 +850,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||
case R.id.menu_distribution_broadcast: handleDistributionBroadcastEnabled(item); return true;
|
||||
case R.id.menu_distribution_conversation: handleDistributionConversationEnabled(item); return true;
|
||||
case R.id.menu_edit_group: handleEditPushGroup(); return true;
|
||||
case R.id.menu_manage_group: handleManagePushGroup(); return true;
|
||||
case R.id.menu_pending_members: handlePendingMembers(); return true;
|
||||
case R.id.menu_leave: handleLeavePushGroup(); return true;
|
||||
case R.id.menu_invite: handleInviteLink(); return true;
|
||||
|
@ -924,29 +929,43 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||
//////// Event Handlers
|
||||
|
||||
private void handleSelectMessageExpiration() {
|
||||
if (isPushGroupConversation() && !isActiveGroup()) {
|
||||
boolean activeGroup = isActiveGroup();
|
||||
|
||||
if (isPushGroupConversation() && !activeGroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
//noinspection CodeBlock2Expr
|
||||
ExpirationDialog.show(this, recipient.get().getExpireMessages(), expirationTime -> {
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
ExpirationDialog.show(this, recipient.get().getExpireMessages(),
|
||||
expirationTime ->
|
||||
SimpleTask.run(
|
||||
getLifecycle(),
|
||||
() -> {
|
||||
if (activeGroup) {
|
||||
try {
|
||||
GroupManager.updateGroupTimer(ConversationActivity.this, getRecipient().requireGroupId().requirePush(), expirationTime);
|
||||
} catch (GroupInsufficientRightsException e) {
|
||||
Log.w(TAG, e);
|
||||
return ConversationActivity.this.getString(R.string.ManageGroupActivity_you_dont_have_the_rights_to_do_this);
|
||||
} catch (GroupChangeFailedException e) {
|
||||
Log.w(TAG, e);
|
||||
return ConversationActivity.this.getString(R.string.ManageGroupActivity_failed_to_update_the_group);
|
||||
}
|
||||
} else {
|
||||
DatabaseFactory.getRecipientDatabase(ConversationActivity.this).setExpireMessages(recipient.getId(), expirationTime);
|
||||
OutgoingExpirationUpdateMessage outgoingMessage = new OutgoingExpirationUpdateMessage(getRecipient(), System.currentTimeMillis(), expirationTime * 1000L);
|
||||
MessageSender.send(ConversationActivity.this, outgoingMessage, threadId, false, null);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void result) {
|
||||
return null;
|
||||
},
|
||||
(errorString) -> {
|
||||
if (errorString != null) {
|
||||
Toast.makeText(ConversationActivity.this, errorString, Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
invalidateOptionsMenu();
|
||||
if (fragment != null) fragment.setLastSeen(0);
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private void handleMuteNotifications() {
|
||||
|
@ -1129,6 +1148,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||
startActivityForResult(intent, GROUP_EDIT);
|
||||
}
|
||||
|
||||
private void handleManagePushGroup() {
|
||||
startActivityForResult(ManageGroupActivity.newIntent(ConversationActivity.this, recipient.get().requireGroupId()), GROUP_EDIT);
|
||||
}
|
||||
|
||||
private void handlePendingMembers() {
|
||||
startActivity(PendingMemberInvitesActivity.newIntent(ConversationActivity.this, recipient.get().requireGroupId().requireV2()));
|
||||
}
|
||||
|
@ -1182,7 +1205,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||
}
|
||||
|
||||
private void handleDisplayGroupRecipients() {
|
||||
new GroupMembersDialog(this, getRecipient(), getLifecycle()).display();
|
||||
new GroupMembersDialog(this, getRecipient()).display();
|
||||
}
|
||||
|
||||
private void handleAddToContacts() {
|
||||
|
|
|
@ -15,12 +15,14 @@ import com.google.protobuf.InvalidProtocolBufferException;
|
|||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.signal.storageservice.protos.groups.AccessControl;
|
||||
import org.signal.storageservice.protos.groups.Member;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedMember;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.groups.GroupAccessControl;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
@ -651,6 +653,34 @@ public final class GroupDatabase extends Database {
|
|||
public boolean isAdmin(@NonNull Recipient recipient) {
|
||||
return isV2Group() && requireV2GroupProperties().isAdmin(recipient);
|
||||
}
|
||||
|
||||
/**
|
||||
* Who is allowed to add to the membership of this group.
|
||||
*/
|
||||
public GroupAccessControl getMembershipAdditionAccessControl() {
|
||||
if (isV2Group()) {
|
||||
if (requireV2GroupProperties().getDecryptedGroup().getAccessControl().getMembers() == AccessControl.AccessRequired.MEMBER) {
|
||||
return GroupAccessControl.ALL_MEMBERS;
|
||||
}
|
||||
return GroupAccessControl.ONLY_ADMINS;
|
||||
} else {
|
||||
return GroupAccessControl.ALL_MEMBERS;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Who is allowed to modify the attributes of this group, name/avatar/timer etc.
|
||||
*/
|
||||
public GroupAccessControl getAttributesAccessControl() {
|
||||
if (isV2Group()) {
|
||||
if (requireV2GroupProperties().getDecryptedGroup().getAccessControl().getAttributes() == AccessControl.AccessRequired.MEMBER) {
|
||||
return GroupAccessControl.ALL_MEMBERS;
|
||||
}
|
||||
return GroupAccessControl.ONLY_ADMINS;
|
||||
} else {
|
||||
return GroupAccessControl.ALL_MEMBERS;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class V2GroupProperties {
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
package org.thoughtcrime.securesms.groups;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
public enum GroupAccessControl {
|
||||
ALL_MEMBERS(R.string.GroupManagement_access_level_all_members),
|
||||
ONLY_ADMINS(R.string.GroupManagement_access_level_only_admins);
|
||||
|
||||
private final @StringRes int string;
|
||||
|
||||
GroupAccessControl(@StringRes int string) {
|
||||
this.string = string;
|
||||
}
|
||||
|
||||
public @StringRes int getString() {
|
||||
return string;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package org.thoughtcrime.securesms.groups;
|
||||
|
||||
public final class GroupChangeFailedException extends Exception {
|
||||
|
||||
GroupChangeFailedException(Throwable throwable) {
|
||||
super(throwable);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package org.thoughtcrime.securesms.groups;
|
||||
|
||||
public final class GroupInsufficientRightsException extends Exception {
|
||||
|
||||
GroupInsufficientRightsException(Throwable throwable) {
|
||||
super(throwable);
|
||||
}
|
||||
}
|
|
@ -12,6 +12,7 @@ import org.signal.zkgroup.groups.UuidCiphertext;
|
|||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
|
||||
import org.whispersystems.signalservice.api.util.InvalidNumberException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -58,6 +59,17 @@ public final class GroupManager {
|
|||
return V1GroupManager.leaveGroup(context, groupId.requireV1());
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static void updateGroupTimer(@NonNull Context context, @NonNull GroupId.Push groupId, int expirationTime)
|
||||
throws GroupChangeFailedException, GroupInsufficientRightsException
|
||||
{
|
||||
if (groupId.isV2()) {
|
||||
throw new GroupChangeFailedException(new AssertionError("NYI")); // TODO: GV2 allow timer change
|
||||
} else {
|
||||
V1GroupManager.updateGroupTimer(context, groupId.requireV1(), expirationTime);
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static void cancelInvites(@NonNull Context context,
|
||||
@NonNull GroupId.V2 groupId,
|
||||
|
@ -67,6 +79,24 @@ public final class GroupManager {
|
|||
throw new AssertionError("NYI"); // TODO: GV2 allow invite cancellation
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static void applyMembershipAdditionRightsChange(@NonNull Context context,
|
||||
@NonNull GroupId.V2 groupId,
|
||||
@NonNull GroupAccessControl newRights)
|
||||
throws GroupChangeFailedException, GroupInsufficientRightsException
|
||||
{
|
||||
throw new GroupChangeFailedException(new AssertionError("NYI")); // TODO: GV2 allow membership addition rights change
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static void applyAttributesRightsChange(@NonNull Context context,
|
||||
@NonNull GroupId.V2 groupId,
|
||||
@NonNull GroupAccessControl newRights)
|
||||
throws GroupChangeFailedException, GroupInsufficientRightsException
|
||||
{
|
||||
throw new GroupChangeFailedException(new AssertionError("NYI")); // TODO: GV2 allow attributes rights change
|
||||
}
|
||||
|
||||
public static class GroupActionResult {
|
||||
private final Recipient groupRecipient;
|
||||
private final long threadId;
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
package org.thoughtcrime.securesms.groups;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MediatorLiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
|
||||
import com.annimon.stream.ComparatorCompat;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
public final class LiveGroup extends MediatorLiveData<GroupDatabase.GroupRecord> {
|
||||
|
||||
private static final Comparator<GroupMemberEntry.FullMember> LOCAL_FIRST = (m1, m2) -> Boolean.compare(m2.getMember().isLocalNumber(), m1.getMember().isLocalNumber());
|
||||
private static final Comparator<GroupMemberEntry.FullMember> ADMIN_FIRST = (m1, m2) -> Boolean.compare(m2.isAdmin(), m1.isAdmin());
|
||||
private static final Comparator<? super GroupMemberEntry.FullMember> MEMBER_ORDER = ComparatorCompat.chain(LOCAL_FIRST)
|
||||
.thenComparing(ADMIN_FIRST);
|
||||
|
||||
private final GroupDatabase groupDatabase;
|
||||
private final LiveData<Recipient> recipient;
|
||||
|
||||
public LiveGroup(@NonNull GroupId groupId) {
|
||||
Context context = ApplicationDependencies.getApplication();
|
||||
|
||||
this.groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||
|
||||
recipient = Recipient.externalGroup(context, groupId).live().getLiveData();
|
||||
|
||||
addSource(recipient, this::refresh);
|
||||
}
|
||||
|
||||
private void refresh(@NonNull Recipient groupRecipient) {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
Optional<GroupDatabase.GroupRecord> group = groupDatabase.getGroup(groupRecipient.getId());
|
||||
if (group.isPresent()) {
|
||||
postValue(group.get());
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public LiveData<String> getTitle() {
|
||||
return Transformations.map(this, GroupDatabase.GroupRecord::getTitle);
|
||||
}
|
||||
|
||||
public LiveData<Boolean> isSelfAdmin() {
|
||||
return Transformations.map(this, g -> g.isAdmin(Recipient.self()));
|
||||
}
|
||||
|
||||
public LiveData<Boolean> getRecipientIsAdmin(@NonNull RecipientId recipientId) {
|
||||
return LiveDataUtil.mapAsync(this, g -> g.isAdmin(Recipient.resolved(recipientId)));
|
||||
}
|
||||
|
||||
public LiveData<Integer> getPendingMemberCount() {
|
||||
return Transformations.map(this, g -> g.isV2Group() ? g.requireV2GroupProperties().getDecryptedGroup().getPendingMembersCount() : 0);
|
||||
}
|
||||
|
||||
public LiveData<GroupAccessControl> getMembershipAdditionAccessControl() {
|
||||
return Transformations.map(this, GroupDatabase.GroupRecord::getMembershipAdditionAccessControl);
|
||||
}
|
||||
|
||||
public LiveData<GroupAccessControl> getAttributesAccessControl() {
|
||||
return Transformations.map(this, GroupDatabase.GroupRecord::getAttributesAccessControl);
|
||||
}
|
||||
|
||||
public LiveData<List<GroupMemberEntry.FullMember>> getFullMembers() {
|
||||
return LiveDataUtil.mapAsync(this,
|
||||
g -> Stream.of(g.getMembers())
|
||||
.map(m -> {
|
||||
Recipient recipient = Recipient.resolved(m);
|
||||
return new GroupMemberEntry.FullMember(recipient, g.isAdmin(recipient));
|
||||
})
|
||||
.sorted(MEMBER_ORDER)
|
||||
.toList());
|
||||
}
|
||||
|
||||
public LiveData<Integer> getExpireMessages() {
|
||||
return Transformations.map(recipient, Recipient::getExpireMessages);
|
||||
}
|
||||
|
||||
public LiveData<Boolean> selfCanEditGroupAttributes() {
|
||||
return LiveDataUtil.combineLatest(isSelfAdmin(),
|
||||
getAttributesAccessControl(),
|
||||
(admin, rights) -> {
|
||||
switch (rights) {
|
||||
case ALL_MEMBERS:
|
||||
return true;
|
||||
case ONLY_ADMINS:
|
||||
return admin;
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public LiveData<String> getMembershipCountDescription(@NonNull Resources resources) {
|
||||
return LiveDataUtil.combineLatest(getFullMembers(),
|
||||
getPendingMemberCount(),
|
||||
(fullMembers, invitedCount) -> getMembershipDescription(resources, invitedCount, fullMembers.size()));
|
||||
}
|
||||
|
||||
private static String getMembershipDescription(@NonNull Resources resources, int invitedCount, int fullMemberCount) {
|
||||
return invitedCount > 0 ? resources.getQuantityString(R.plurals.MessageRequestProfileView_members_and_invited, fullMemberCount,
|
||||
fullMemberCount, invitedCount)
|
||||
: resources.getQuantityString(R.plurals.MessageRequestProfileView_members, fullMemberCount,
|
||||
fullMemberCount);
|
||||
}
|
||||
}
|
|
@ -15,12 +15,14 @@ import org.thoughtcrime.securesms.attachments.UriAttachment;
|
|||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.GroupManager.GroupActionResult;
|
||||
import org.thoughtcrime.securesms.jobs.LeaveGroupJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
|
@ -175,4 +177,16 @@ final class V1GroupManager {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
static void updateGroupTimer(@NonNull Context context, @NonNull GroupId.V1 groupId, int expirationTime) {
|
||||
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||
Recipient recipient = Recipient.externalGroup(context, groupId);
|
||||
long threadId = threadDatabase.getThreadIdFor(recipient);
|
||||
|
||||
recipientDatabase.setExpireMessages(recipient.getId(), expirationTime);
|
||||
OutgoingExpirationUpdateMessage outgoingMessage = new OutgoingExpirationUpdateMessage(recipient, System.currentTimeMillis(), expirationTime * 1000L);
|
||||
MessageSender.send(context, outgoingMessage, threadId, false, null);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,19 +13,10 @@ import java.util.Collection;
|
|||
public abstract class GroupMemberEntry {
|
||||
|
||||
private final DefaultValueLiveData<Boolean> busy = new DefaultValueLiveData<>(false);
|
||||
@Nullable private Runnable onClick;
|
||||
|
||||
private GroupMemberEntry() {
|
||||
}
|
||||
|
||||
public void setOnClick(@NonNull Runnable onClick) {
|
||||
this.onClick = onClick;
|
||||
}
|
||||
|
||||
public @Nullable Runnable getOnClick() {
|
||||
return onClick;
|
||||
}
|
||||
|
||||
public LiveData<Boolean> getBusy() {
|
||||
return busy;
|
||||
}
|
||||
|
@ -34,17 +25,52 @@ public abstract class GroupMemberEntry {
|
|||
this.busy.postValue(busy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public abstract boolean equals(@Nullable Object obj);
|
||||
|
||||
@Override
|
||||
public abstract int hashCode();
|
||||
|
||||
abstract boolean sameId(GroupMemberEntry newItem);
|
||||
|
||||
public final static class FullMember extends GroupMemberEntry {
|
||||
|
||||
private final Recipient member;
|
||||
private final boolean isAdmin;
|
||||
|
||||
public FullMember(@NonNull Recipient member) {
|
||||
public FullMember(@NonNull Recipient member, boolean isAdmin) {
|
||||
this.member = member;
|
||||
this.isAdmin = isAdmin;
|
||||
}
|
||||
|
||||
public Recipient getMember() {
|
||||
return member;
|
||||
}
|
||||
|
||||
public boolean isAdmin() {
|
||||
return isAdmin;
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean sameId(GroupMemberEntry newItem) {
|
||||
if (getClass() != newItem.getClass()) return false;
|
||||
|
||||
return member.getId().equals(((GroupMemberEntry.FullMember) newItem).member.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (!(obj instanceof FullMember)) return false;
|
||||
|
||||
FullMember other = (FullMember) obj;
|
||||
return other.member.equals(member) &&
|
||||
other.isAdmin == isAdmin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return member.hashCode() * 31 + (isAdmin ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
public final static class PendingMember extends GroupMemberEntry {
|
||||
|
@ -69,6 +95,32 @@ public abstract class GroupMemberEntry {
|
|||
public boolean isCancellable() {
|
||||
return cancellable;
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean sameId(GroupMemberEntry newItem) {
|
||||
if (getClass() != newItem.getClass()) return false;
|
||||
|
||||
return invitee.getId().equals(((GroupMemberEntry.PendingMember) newItem).invitee.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (!(obj instanceof PendingMember)) return false;
|
||||
|
||||
PendingMember other = (PendingMember) obj;
|
||||
return other.invitee.equals(invitee) &&
|
||||
other.inviteeCipherText.equals(inviteeCipherText) &&
|
||||
other.cancellable == cancellable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int hash = invitee.hashCode();
|
||||
hash *= 31;
|
||||
hash += inviteeCipherText.hashCode();
|
||||
hash *= 31;
|
||||
return hash + (cancellable ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
public final static class UnknownPendingMemberCount extends GroupMemberEntry {
|
||||
|
@ -99,5 +151,31 @@ public abstract class GroupMemberEntry {
|
|||
public boolean isCancellable() {
|
||||
return cancellable;
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean sameId(GroupMemberEntry newItem) {
|
||||
if (getClass() != newItem.getClass()) return false;
|
||||
|
||||
return inviter.getId().equals(((GroupMemberEntry.UnknownPendingMemberCount) newItem).inviter.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (!(obj instanceof UnknownPendingMemberCount)) return false;
|
||||
|
||||
UnknownPendingMemberCount other = (UnknownPendingMemberCount) obj;
|
||||
return other.inviter.equals(inviter) &&
|
||||
other.ciphertexts.equals(ciphertexts) &&
|
||||
other.cancellable == cancellable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int hash = inviter.hashCode();
|
||||
hash *= 31;
|
||||
hash += ciphertexts.hashCode();
|
||||
hash *= 31;
|
||||
return hash + (cancellable ? 1 : 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import android.widget.TextView;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
|
@ -17,7 +18,7 @@ import org.thoughtcrime.securesms.util.LifecycleRecyclerAdapter;
|
|||
import org.thoughtcrime.securesms.util.LifecycleViewHolder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberListAdapter.ViewHolder> {
|
||||
|
||||
|
@ -28,11 +29,18 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
|||
private final ArrayList<GroupMemberEntry> data = new ArrayList<>();
|
||||
|
||||
@Nullable private AdminActionsListener adminActionsListener;
|
||||
@Nullable private RecipientClickListener recipientClickListener;
|
||||
|
||||
void updateData(@NonNull Collection<? extends GroupMemberEntry> recipients) {
|
||||
data.clear();
|
||||
void updateData(@NonNull List<? extends GroupMemberEntry> recipients) {
|
||||
if (data.isEmpty()) {
|
||||
data.addAll(recipients);
|
||||
notifyDataSetChanged();
|
||||
} else {
|
||||
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallback(data, recipients));
|
||||
data.clear();
|
||||
data.addAll(recipients);
|
||||
diffResult.dispatchUpdatesTo(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -41,11 +49,11 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
|||
case FULL_MEMBER:
|
||||
return new FullMemberViewHolder(LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.group_recipient_list_item,
|
||||
parent, false), adminActionsListener);
|
||||
parent, false), recipientClickListener, adminActionsListener);
|
||||
case OWN_INVITE_PENDING:
|
||||
return new OwnInvitePendingMemberViewHolder(LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.group_recipient_list_item,
|
||||
parent, false), adminActionsListener);
|
||||
parent, false), recipientClickListener, adminActionsListener);
|
||||
case OTHER_INVITE_PENDING_COUNT:
|
||||
return new UnknownPendingMemberCountViewHolder(LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.group_recipient_list_item,
|
||||
|
@ -59,6 +67,10 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
|||
this.adminActionsListener = adminActionsListener;
|
||||
}
|
||||
|
||||
void setRecipientClickListener(@Nullable RecipientClickListener recipientClickListener) {
|
||||
this.recipientClickListener = recipientClickListener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
holder.bind(data.get(position));
|
||||
|
@ -87,14 +99,19 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
|||
static abstract class ViewHolder extends LifecycleViewHolder {
|
||||
|
||||
final Context context;
|
||||
private final AvatarImageView avatar;
|
||||
private final TextView recipient;
|
||||
final AvatarImageView avatar;
|
||||
final TextView recipient;
|
||||
final PopupMenuView popupMenu;
|
||||
final View popupMenuContainer;
|
||||
final ProgressBar busyProgress;
|
||||
final View admin;
|
||||
@Nullable final RecipientClickListener recipientClickListener;
|
||||
@Nullable final AdminActionsListener adminActionsListener;
|
||||
|
||||
ViewHolder(@NonNull View itemView, @Nullable AdminActionsListener adminActionsListener) {
|
||||
ViewHolder(@NonNull View itemView,
|
||||
@Nullable RecipientClickListener recipientClickListener,
|
||||
@Nullable AdminActionsListener adminActionsListener)
|
||||
{
|
||||
super(itemView);
|
||||
|
||||
this.context = itemView.getContext();
|
||||
|
@ -103,6 +120,8 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
|||
this.popupMenu = itemView.findViewById(R.id.popupMenu);
|
||||
this.popupMenuContainer = itemView.findViewById(R.id.popupMenuProgressContainer);
|
||||
this.busyProgress = itemView.findViewById(R.id.menuBusyProgress);
|
||||
this.admin = itemView.findViewById(R.id.admin);
|
||||
this.recipientClickListener = recipientClickListener;
|
||||
this.adminActionsListener = adminActionsListener;
|
||||
}
|
||||
|
||||
|
@ -117,15 +136,23 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
|||
this.avatar.setRecipient(recipient);
|
||||
}
|
||||
|
||||
void bindRecipientClick(@NonNull Recipient recipient) {
|
||||
View.OnClickListener onClickListener = v -> {
|
||||
if (recipientClickListener != null) {
|
||||
recipientClickListener.onClick(recipient);
|
||||
}
|
||||
};
|
||||
this.avatar.setOnClickListener(onClickListener);
|
||||
this.recipient.setOnClickListener(onClickListener);
|
||||
}
|
||||
|
||||
void bind(@NonNull GroupMemberEntry memberEntry) {
|
||||
busyProgress.setVisibility(View.GONE);
|
||||
admin.setVisibility(View.GONE);
|
||||
hideMenu();
|
||||
|
||||
Runnable onClick = memberEntry.getOnClick();
|
||||
View.OnClickListener onClickListener = v -> { if (onClick != null) onClick.run(); };
|
||||
|
||||
avatar.setOnClickListener(onClickListener);
|
||||
recipient.setOnClickListener(onClickListener);
|
||||
avatar.setOnClickListener(null);
|
||||
recipient.setOnClickListener(null);
|
||||
|
||||
memberEntry.getBusy().observe(this, busy -> {
|
||||
busyProgress.setVisibility(busy ? View.VISIBLE : View.GONE);
|
||||
|
@ -146,8 +173,11 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
|||
|
||||
final static class FullMemberViewHolder extends ViewHolder {
|
||||
|
||||
FullMemberViewHolder(@NonNull View itemView, @Nullable AdminActionsListener adminActionsListener) {
|
||||
super(itemView, adminActionsListener);
|
||||
FullMemberViewHolder(@NonNull View itemView,
|
||||
@Nullable RecipientClickListener recipientClickListener,
|
||||
@Nullable AdminActionsListener adminActionsListener)
|
||||
{
|
||||
super(itemView, recipientClickListener, adminActionsListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -157,13 +187,18 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
|||
GroupMemberEntry.FullMember fullMember = (GroupMemberEntry.FullMember) memberEntry;
|
||||
|
||||
bindRecipient(fullMember.getMember());
|
||||
bindRecipientClick(fullMember.getMember());
|
||||
admin.setVisibility(fullMember.isAdmin() ? View.VISIBLE : View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
final static class OwnInvitePendingMemberViewHolder extends ViewHolder {
|
||||
|
||||
OwnInvitePendingMemberViewHolder(@NonNull View itemView, @Nullable AdminActionsListener adminActionsListener) {
|
||||
super(itemView, adminActionsListener);
|
||||
OwnInvitePendingMemberViewHolder(@NonNull View itemView,
|
||||
@Nullable RecipientClickListener recipientClickListener,
|
||||
@Nullable AdminActionsListener adminActionsListener)
|
||||
{
|
||||
super(itemView, recipientClickListener, adminActionsListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -173,6 +208,7 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
|||
GroupMemberEntry.PendingMember pendingMember = (GroupMemberEntry.PendingMember) memberEntry;
|
||||
|
||||
bindRecipient(pendingMember.getInvitee());
|
||||
bindRecipientClick(pendingMember.getInvitee());
|
||||
|
||||
if (pendingMember.isCancellable() && adminActionsListener != null) {
|
||||
popupMenu.setMenu(R.menu.own_invite_pending_menu,
|
||||
|
@ -191,7 +227,7 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
|||
final static class UnknownPendingMemberCountViewHolder extends ViewHolder {
|
||||
|
||||
UnknownPendingMemberCountViewHolder(@NonNull View itemView, @Nullable AdminActionsListener adminActionsListener) {
|
||||
super(itemView, adminActionsListener);
|
||||
super(itemView, null, adminActionsListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -228,4 +264,40 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final static class DiffCallback extends DiffUtil.Callback {
|
||||
private final List<? extends GroupMemberEntry> oldData;
|
||||
private final List<? extends GroupMemberEntry> newData;
|
||||
|
||||
DiffCallback(List<? extends GroupMemberEntry> oldData, List<? extends GroupMemberEntry> newData) {
|
||||
this.oldData = oldData;
|
||||
this.newData = newData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOldListSize() {
|
||||
return oldData.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNewListSize() {
|
||||
return newData.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
|
||||
GroupMemberEntry oldItem = oldData.get(oldItemPosition);
|
||||
GroupMemberEntry newItem = newData.get(newItemPosition);
|
||||
|
||||
return oldItem.sameId(newItem);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
|
||||
GroupMemberEntry oldItem = oldData.get(oldItemPosition);
|
||||
GroupMemberEntry newItem = newData.get(newItemPosition);
|
||||
|
||||
return oldItem.equals(newItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import androidx.recyclerview.widget.RecyclerView;
|
|||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
public final class GroupMemberListView extends RecyclerView {
|
||||
|
||||
|
@ -52,7 +52,11 @@ public final class GroupMemberListView extends RecyclerView {
|
|||
membersAdapter.setAdminActionsListener(adminActionsListener);
|
||||
}
|
||||
|
||||
public void setMembers(@NonNull Collection<? extends GroupMemberEntry> recipients) {
|
||||
public void setRecipientClickListener(@Nullable RecipientClickListener listener) {
|
||||
membersAdapter.setRecipientClickListener(listener);
|
||||
}
|
||||
|
||||
public void setMembers(@NonNull List<? extends GroupMemberEntry> recipients) {
|
||||
membersAdapter.updateData(recipients);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package org.thoughtcrime.securesms.groups.ui;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
public interface RecipientClickListener {
|
||||
void onClick(@NonNull Recipient recipient);
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
package org.thoughtcrime.securesms.groups.ui.managegroup;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
|
||||
public class ManageGroupActivity extends PassphraseRequiredActionBarActivity {
|
||||
|
||||
private static final String GROUP_ID = "GROUP_ID";
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||
|
||||
public static Intent newIntent(@NonNull Context context, @NonNull GroupId groupId) {
|
||||
Intent intent = new Intent(context, ManageGroupActivity.class);
|
||||
intent.putExtra(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.group_manage_activity);
|
||||
if (savedInstanceState == null) {
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.container, ManageGroupFragment.newInstance(getIntent().getStringExtra(GROUP_ID)))
|
||||
.commitNow();
|
||||
}
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
//noinspection ConstantConditions
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSupportNavigateUp() {
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,207 @@
|
|||
package org.thoughtcrime.securesms.groups.ui.managegroup;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
import org.thoughtcrime.securesms.MediaPreviewActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.components.ThreadPhotoRailView;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberListView;
|
||||
import org.thoughtcrime.securesms.groups.ui.LeaveGroupDialog;
|
||||
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupRightsDialog;
|
||||
import org.thoughtcrime.securesms.groups.ui.pendingmemberinvites.PendingMemberInvitesActivity;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class ManageGroupFragment extends Fragment {
|
||||
private static final String GROUP_ID = "GROUP_ID";
|
||||
|
||||
private static final String TAG = Log.tag(ManageGroupFragment.class);
|
||||
|
||||
private static final int RETURN_FROM_MEDIA = 33114;
|
||||
|
||||
private ManageGroupViewModel viewModel;
|
||||
private GroupMemberListView groupMemberList;
|
||||
private View listPending;
|
||||
private TextView groupTitle;
|
||||
private TextView memberCount;
|
||||
private AvatarImageView avatar;
|
||||
private ThreadPhotoRailView threadPhotoRailView;
|
||||
private View groupMediaCard;
|
||||
private View accessControlCard;
|
||||
private View pendingMembersCard;
|
||||
private ManageGroupViewModel.CursorFactory cursorFactory;
|
||||
private View photoRailLabel;
|
||||
private Button editGroupAccessValue;
|
||||
private Button editGroupMembershipValue;
|
||||
private Button disappearingMessages;
|
||||
private Button blockGroup;
|
||||
private Button leaveGroup;
|
||||
|
||||
static ManageGroupFragment newInstance(@NonNull String groupId) {
|
||||
ManageGroupFragment fragment = new ManageGroupFragment();
|
||||
Bundle args = new Bundle();
|
||||
|
||||
args.putString(GROUP_ID, groupId);
|
||||
fragment.setArguments(args);
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
|
||||
@Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState)
|
||||
{
|
||||
View view = inflater.inflate(R.layout.group_manage_fragment, container, false);
|
||||
|
||||
avatar = view.findViewById(R.id.group_avatar);
|
||||
groupTitle = view.findViewById(R.id.group_title);
|
||||
memberCount = view.findViewById(R.id.member_count);
|
||||
groupMemberList = view.findViewById(R.id.group_members);
|
||||
listPending = view.findViewById(R.id.listPending);
|
||||
threadPhotoRailView = view.findViewById(R.id.recent_photos);
|
||||
groupMediaCard = view.findViewById(R.id.group_media_card);
|
||||
accessControlCard = view.findViewById(R.id.group_access_control_card);
|
||||
pendingMembersCard = view.findViewById(R.id.group_pending_card);
|
||||
photoRailLabel = view.findViewById(R.id.rail_label);
|
||||
editGroupAccessValue = view.findViewById(R.id.edit_group_access_value);
|
||||
editGroupMembershipValue = view.findViewById(R.id.edit_group_membership_value);
|
||||
disappearingMessages = view.findViewById(R.id.disappearing_messages);
|
||||
blockGroup = view.findViewById(R.id.blockGroup);
|
||||
leaveGroup = view.findViewById(R.id.leaveGroup);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
Context context = requireContext();
|
||||
GroupId.Push groupId = GroupId.parseOrThrow(Objects.requireNonNull(requireArguments().getString(GROUP_ID))).requirePush();
|
||||
ManageGroupViewModel.Factory factory = new ManageGroupViewModel.Factory(context, groupId);
|
||||
|
||||
viewModel = ViewModelProviders.of(requireActivity(), factory).get(ManageGroupViewModel.class);
|
||||
|
||||
viewModel.getMembers().observe(getViewLifecycleOwner(), members -> groupMemberList.setMembers(members));
|
||||
|
||||
viewModel.getPendingMemberCount().observe(getViewLifecycleOwner(),
|
||||
members -> {
|
||||
if (members > 0) {
|
||||
listPending.setEnabled(true);
|
||||
listPending.setOnClickListener(v -> {
|
||||
FragmentActivity activity = requireActivity();
|
||||
activity.startActivity(PendingMemberInvitesActivity.newIntent(activity, groupId.requireV2()));
|
||||
});
|
||||
} else {
|
||||
listPending.setEnabled(false);
|
||||
listPending.setOnClickListener(null);
|
||||
}
|
||||
});
|
||||
|
||||
viewModel.getTitle().observe(getViewLifecycleOwner(), groupTitle::setText);
|
||||
viewModel.getMemberCountSummary().observe(getViewLifecycleOwner(), memberCount::setText);
|
||||
|
||||
viewModel.getGroupViewState().observe(getViewLifecycleOwner(), vs -> {
|
||||
if (vs == null) return;
|
||||
photoRailLabel.setOnClickListener(v -> startActivity(MediaOverviewActivity.forThread(context, vs.getThreadId())));
|
||||
avatar.setRecipient(vs.getGroupRecipient());
|
||||
|
||||
setMediaCursorFactory(vs.getMediaCursorFactory());
|
||||
|
||||
threadPhotoRailView.setListener(mediaRecord ->
|
||||
startActivityForResult(MediaPreviewActivity.intentFromMediaRecord(context,
|
||||
mediaRecord,
|
||||
ViewCompat.getLayoutDirection(threadPhotoRailView) == ViewCompat.LAYOUT_DIRECTION_LTR),
|
||||
RETURN_FROM_MEDIA));
|
||||
|
||||
accessControlCard.setVisibility(vs.getGroupRecipient().requireGroupId().isV2() ? View.VISIBLE : View.GONE);
|
||||
pendingMembersCard.setVisibility(vs.getGroupRecipient().requireGroupId().isV2() ? View.VISIBLE : View.GONE);
|
||||
});
|
||||
|
||||
leaveGroup.setVisibility(groupId.isPush() ? View.VISIBLE : View.GONE);
|
||||
leaveGroup.setOnClickListener(v -> LeaveGroupDialog.handleLeavePushGroup(context,
|
||||
getLifecycle(),
|
||||
groupId.requirePush(),
|
||||
null));
|
||||
|
||||
viewModel.getDisappearingMessageTimer().observe(getViewLifecycleOwner(), string -> disappearingMessages.setText(string));
|
||||
|
||||
disappearingMessages.setOnClickListener(v -> viewModel.handleExpirationSelection());
|
||||
blockGroup.setOnClickListener(v -> viewModel.blockAndLeave(requireActivity()));
|
||||
|
||||
viewModel.getMembershipRights().observe(getViewLifecycleOwner(), r -> {
|
||||
if (r != null) {
|
||||
editGroupMembershipValue.setText(r.getString());
|
||||
editGroupMembershipValue.setOnClickListener(v -> new GroupRightsDialog(context, GroupRightsDialog.Type.MEMBERSHIP, r, (from, to) -> viewModel.applyMembershipRightsChange(to)).show());
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
viewModel.getEditGroupAttributesRights().observe(getViewLifecycleOwner(), r -> {
|
||||
if (r != null) {
|
||||
editGroupAccessValue.setText(r.getString());
|
||||
editGroupAccessValue.setOnClickListener(v -> new GroupRightsDialog(context, GroupRightsDialog.Type.ATTRIBUTES, r, (from, to) -> viewModel.applyAttributesRightsChange(to)).show());
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
viewModel.getIsAdmin().observe(getViewLifecycleOwner(), admin -> {
|
||||
editGroupMembershipValue.setEnabled(admin);
|
||||
editGroupAccessValue.setEnabled(admin);
|
||||
});
|
||||
|
||||
viewModel.getCanEditGroupAttributes().observe(getViewLifecycleOwner(), canEdit -> disappearingMessages.setEnabled(canEdit));
|
||||
|
||||
groupMemberList.setRecipientClickListener(recipient -> RecipientBottomSheetDialogFragment.create(recipient.getId(), groupId).show(requireFragmentManager(), "BOTTOM"));
|
||||
}
|
||||
|
||||
private void setMediaCursorFactory(@Nullable ManageGroupViewModel.CursorFactory cursorFactory) {
|
||||
if (this.cursorFactory != cursorFactory) {
|
||||
this.cursorFactory = cursorFactory;
|
||||
applyMediaCursorFactory();
|
||||
}
|
||||
}
|
||||
|
||||
private void applyMediaCursorFactory() {
|
||||
Context context = getContext();
|
||||
if (context == null) return;
|
||||
if (this.cursorFactory != null) {
|
||||
Cursor cursor = this.cursorFactory.create();
|
||||
threadPhotoRailView.setCursor(GlideApp.with(context), cursor);
|
||||
groupMediaCard.setVisibility(cursor.getCount() > 0 ? View.VISIBLE : View.GONE);
|
||||
} else {
|
||||
threadPhotoRailView.setCursor(GlideApp.with(context), null);
|
||||
groupMediaCard.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (requestCode == RETURN_FROM_MEDIA) {
|
||||
applyMediaCursorFactory();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
package org.thoughtcrime.securesms.groups.ui.managegroup;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.core.util.Consumer;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.groups.GroupAccessControl;
|
||||
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.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
final class ManageGroupRepository {
|
||||
|
||||
private static final String TAG = Log.tag(ManageGroupRepository.class);
|
||||
|
||||
private final Context context;
|
||||
private final GroupId groupId;
|
||||
private final ExecutorService executor;
|
||||
|
||||
ManageGroupRepository(@NonNull Context context, @NonNull GroupId groupId) {
|
||||
this.context = context;
|
||||
this.executor = SignalExecutors.BOUNDED;
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
public GroupId getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
void getGroupState(@NonNull Consumer<GroupStateResult> onGroupStateLoaded) {
|
||||
executor.execute(() -> onGroupStateLoaded.accept(getGroupState()));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private GroupStateResult getGroupState() {
|
||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||
Recipient groupRecipient = Recipient.externalGroup(context, groupId);
|
||||
long threadId = threadDatabase.getThreadIdFor(groupRecipient);
|
||||
|
||||
return new GroupStateResult(threadId, groupRecipient);
|
||||
}
|
||||
|
||||
void setExpiration(int newExpirationTime, @NonNull Error error) {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
try {
|
||||
GroupManager.updateGroupTimer(context, groupId.requirePush(), newExpirationTime);
|
||||
} catch (GroupInsufficientRightsException e) {
|
||||
Log.w(TAG, e);
|
||||
error.onError(FailureReason.NO_RIGHTS);
|
||||
} catch (GroupChangeFailedException e) {
|
||||
Log.w(TAG, e);
|
||||
error.onError(FailureReason.OTHER);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void applyMembershipRightsChange(@NonNull GroupAccessControl newRights, @NonNull Error error) {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
try {
|
||||
GroupManager.applyMembershipAdditionRightsChange(context, groupId.requireV2(), newRights);
|
||||
} catch (GroupInsufficientRightsException e) {
|
||||
Log.w(TAG, e);
|
||||
error.onError(FailureReason.NO_RIGHTS);
|
||||
} catch (GroupChangeFailedException e) {
|
||||
Log.w(TAG, e);
|
||||
error.onError(FailureReason.OTHER);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void applyAttributesRightsChange(@NonNull GroupAccessControl newRights, @NonNull Error error) {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
try {
|
||||
GroupManager.applyAttributesRightsChange(context, groupId.requireV2(), newRights);
|
||||
} catch (GroupInsufficientRightsException e) {
|
||||
Log.w(TAG, e);
|
||||
error.onError(FailureReason.NO_RIGHTS);
|
||||
} catch (GroupChangeFailedException e) {
|
||||
Log.w(TAG, e);
|
||||
error.onError(FailureReason.OTHER);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void getRecipient(@NonNull Consumer<Recipient> recipientCallback) {
|
||||
SimpleTask.run(SignalExecutors.BOUNDED,
|
||||
() -> Recipient.externalGroup(context, groupId),
|
||||
recipientCallback::accept);
|
||||
}
|
||||
|
||||
static final class GroupStateResult {
|
||||
|
||||
private final long threadId;
|
||||
private final Recipient recipient;
|
||||
|
||||
private GroupStateResult(long threadId,
|
||||
Recipient recipient)
|
||||
{
|
||||
this.threadId = threadId;
|
||||
this.recipient = recipient;
|
||||
}
|
||||
|
||||
long getThreadId() {
|
||||
return threadId;
|
||||
}
|
||||
|
||||
Recipient getRecipient() {
|
||||
return recipient;
|
||||
}
|
||||
}
|
||||
|
||||
public enum FailureReason {
|
||||
NO_RIGHTS(R.string.ManageGroupActivity_you_dont_have_the_rights_to_do_this),
|
||||
OTHER(R.string.ManageGroupActivity_failed_to_update_the_group);
|
||||
|
||||
private final @StringRes int toastMessage;
|
||||
|
||||
FailureReason(@StringRes int toastMessage) {
|
||||
this.toastMessage = toastMessage;
|
||||
}
|
||||
|
||||
public @StringRes int getToastMessage() {
|
||||
return toastMessage;
|
||||
}
|
||||
}
|
||||
|
||||
public interface Error {
|
||||
void onError(@NonNull FailureReason failureReason);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,185 @@
|
|||
package org.thoughtcrime.securesms.groups.ui.managegroup;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.thoughtcrime.securesms.BlockUnblockDialog;
|
||||
import org.thoughtcrime.securesms.ExpirationDialog;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase;
|
||||
import org.thoughtcrime.securesms.database.loaders.MediaLoader;
|
||||
import org.thoughtcrime.securesms.database.loaders.ThreadMediaLoader;
|
||||
import org.thoughtcrime.securesms.groups.GroupAccessControl;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.LiveGroup;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ManageGroupViewModel extends ViewModel {
|
||||
|
||||
private final Context context;
|
||||
private final ManageGroupRepository manageGroupRepository;
|
||||
private final LiveData<String> title;
|
||||
private final LiveData<Boolean> isAdmin;
|
||||
private final LiveData<Boolean> canEditGroupAttributes;
|
||||
private final LiveData<List<GroupMemberEntry.FullMember>> members;
|
||||
private final LiveData<Integer> pendingMemberCount;
|
||||
private final LiveData<String> disappearingMessageTimer;
|
||||
private final LiveData<String> memberCountSummary;
|
||||
private final LiveData<GroupAccessControl> editMembershipRights;
|
||||
private final LiveData<GroupAccessControl> editGroupAttributesRights;
|
||||
private final MutableLiveData<GroupViewState> groupViewState = new MutableLiveData<>(null);
|
||||
|
||||
private ManageGroupViewModel(@NonNull Context context, @NonNull ManageGroupRepository manageGroupRepository) {
|
||||
this.context = context;
|
||||
this.manageGroupRepository = manageGroupRepository;
|
||||
|
||||
manageGroupRepository.getGroupState(this::groupStateLoaded);
|
||||
|
||||
LiveGroup liveGroup = new LiveGroup(manageGroupRepository.getGroupId());
|
||||
|
||||
this.title = liveGroup.getTitle();
|
||||
this.isAdmin = liveGroup.isSelfAdmin();
|
||||
this.members = liveGroup.getFullMembers();
|
||||
this.pendingMemberCount = liveGroup.getPendingMemberCount();
|
||||
this.memberCountSummary = liveGroup.getMembershipCountDescription(context.getResources());
|
||||
this.editMembershipRights = liveGroup.getMembershipAdditionAccessControl();
|
||||
this.editGroupAttributesRights = liveGroup.getAttributesAccessControl();
|
||||
this.disappearingMessageTimer = Transformations.map(liveGroup.getExpireMessages(), expiration -> ExpirationUtil.getExpirationDisplayValue(context, expiration));
|
||||
this.canEditGroupAttributes = liveGroup.selfCanEditGroupAttributes();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void groupStateLoaded(@NonNull ManageGroupRepository.GroupStateResult groupStateResult) {
|
||||
groupViewState.postValue(new GroupViewState(groupStateResult.getThreadId(),
|
||||
groupStateResult.getRecipient(),
|
||||
() -> new ThreadMediaLoader(context, groupStateResult.getThreadId(), MediaLoader.MediaType.GALLERY, MediaDatabase.Sorting.Newest).getCursor()));
|
||||
}
|
||||
|
||||
LiveData<List<GroupMemberEntry.FullMember>> getMembers() {
|
||||
return members;
|
||||
}
|
||||
|
||||
LiveData<Integer> getPendingMemberCount() {
|
||||
return pendingMemberCount;
|
||||
}
|
||||
|
||||
LiveData<String> getMemberCountSummary() {
|
||||
return memberCountSummary;
|
||||
}
|
||||
|
||||
LiveData<GroupViewState> getGroupViewState() {
|
||||
return groupViewState;
|
||||
}
|
||||
|
||||
LiveData<String> getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
LiveData<GroupAccessControl> getMembershipRights() {
|
||||
return editMembershipRights;
|
||||
}
|
||||
|
||||
LiveData<GroupAccessControl> getEditGroupAttributesRights() {
|
||||
return editGroupAttributesRights;
|
||||
}
|
||||
|
||||
LiveData<Boolean> getIsAdmin() {
|
||||
return isAdmin;
|
||||
}
|
||||
|
||||
LiveData<Boolean> getCanEditGroupAttributes() {
|
||||
return canEditGroupAttributes;
|
||||
}
|
||||
|
||||
LiveData<String> getDisappearingMessageTimer() {
|
||||
return disappearingMessageTimer;
|
||||
}
|
||||
|
||||
void handleExpirationSelection() {
|
||||
manageGroupRepository.getRecipient(groupRecipient ->
|
||||
ExpirationDialog.show(context,
|
||||
groupRecipient.getExpireMessages(),
|
||||
expirationTime -> manageGroupRepository.setExpiration(expirationTime, this::showErrorToast)));
|
||||
}
|
||||
|
||||
void applyMembershipRightsChange(@NonNull GroupAccessControl newRights) {
|
||||
manageGroupRepository.applyMembershipRightsChange(newRights, this::showErrorToast);
|
||||
}
|
||||
|
||||
void applyAttributesRightsChange(@NonNull GroupAccessControl newRights) {
|
||||
manageGroupRepository.applyAttributesRightsChange(newRights, this::showErrorToast);
|
||||
}
|
||||
|
||||
void blockAndLeave(@NonNull FragmentActivity activity) {
|
||||
manageGroupRepository.getRecipient(recipient -> BlockUnblockDialog.showBlockFor(activity, activity.getLifecycle(), recipient,
|
||||
() -> RecipientUtil.block(context, recipient)));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void showErrorToast(@NonNull ManageGroupRepository.FailureReason e) {
|
||||
Util.runOnMain(() -> Toast.makeText(context, e.getToastMessage(), Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
|
||||
static final class GroupViewState {
|
||||
private final long threadId;
|
||||
@NonNull private final Recipient groupRecipient;
|
||||
@NonNull private final CursorFactory mediaCursorFactory;
|
||||
|
||||
private GroupViewState(long threadId,
|
||||
@NonNull Recipient groupRecipient,
|
||||
@NonNull CursorFactory mediaCursorFactory)
|
||||
{
|
||||
this.threadId = threadId;
|
||||
this.groupRecipient = groupRecipient;
|
||||
this.mediaCursorFactory = mediaCursorFactory;
|
||||
}
|
||||
|
||||
long getThreadId() {
|
||||
return threadId;
|
||||
}
|
||||
|
||||
@NonNull Recipient getGroupRecipient() {
|
||||
return groupRecipient;
|
||||
}
|
||||
|
||||
@NonNull CursorFactory getMediaCursorFactory() {
|
||||
return mediaCursorFactory;
|
||||
}
|
||||
}
|
||||
|
||||
interface CursorFactory {
|
||||
Cursor create();
|
||||
}
|
||||
|
||||
public static class Factory implements ViewModelProvider.Factory {
|
||||
private final Context context;
|
||||
private final GroupId.Push groupId;
|
||||
|
||||
public Factory(@NonNull Context context, @NonNull GroupId.Push groupId) {
|
||||
this.context = context;
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
//noinspection unchecked
|
||||
return (T) new ManageGroupViewModel(context, new ManageGroupRepository(context.getApplicationContext(), groupId));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
package org.thoughtcrime.securesms.groups.ui.managegroup.dialogs;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.ArrayRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.groups.GroupAccessControl;
|
||||
|
||||
public final class GroupRightsDialog {
|
||||
|
||||
private final AlertDialog.Builder builder;
|
||||
|
||||
@NonNull private GroupAccessControl rights;
|
||||
|
||||
public GroupRightsDialog(@NonNull Context context,
|
||||
@NonNull Type type,
|
||||
@NonNull GroupAccessControl currentRights,
|
||||
@NonNull GroupRightsDialog.OnChange onChange)
|
||||
{
|
||||
rights = currentRights;
|
||||
|
||||
builder = new AlertDialog.Builder(context)
|
||||
.setTitle(type.message)
|
||||
.setSingleChoiceItems(type.choices, currentRights.ordinal(), (dialog, which) -> rights = GroupAccessControl.values()[which])
|
||||
.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
|
||||
})
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
GroupAccessControl newGroupAccessControl = rights;
|
||||
|
||||
if (newGroupAccessControl != currentRights) {
|
||||
onChange.changed(currentRights, newGroupAccessControl);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void show() {
|
||||
builder.show();
|
||||
}
|
||||
|
||||
public interface OnChange {
|
||||
void changed(@NonNull GroupAccessControl from, @NonNull GroupAccessControl to);
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
MEMBERSHIP(R.string.GroupManagement_choose_who_can_add_or_invite_new_members,
|
||||
R.array.GroupManagement_edit_group_membership_choices),
|
||||
|
||||
ATTRIBUTES(R.string.GroupManagement_choose_who_can_change_the_group_name_and_photo,
|
||||
R.array.GroupManagement_edit_group_info_choices);
|
||||
|
||||
@StringRes private final int message;
|
||||
@ArrayRes private final int choices;
|
||||
|
||||
Type(@StringRes int message, @ArrayRes int choices) {
|
||||
this.message = message;
|
||||
this.choices = choices;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,7 +6,6 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
@ -16,7 +15,6 @@ import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
|||
|
||||
final class RecipientDialogRepository {
|
||||
|
||||
@NonNull private final GroupDatabase groupDatabase;
|
||||
@NonNull private final Context context;
|
||||
@NonNull private final RecipientId recipientId;
|
||||
@Nullable private final GroupId groupId;
|
||||
|
@ -26,34 +24,20 @@ final class RecipientDialogRepository {
|
|||
@Nullable GroupId groupId)
|
||||
{
|
||||
this.context = context;
|
||||
this.groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||
this.recipientId = recipientId;
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
@NonNull RecipientId getRecipientId() {
|
||||
@NonNull
|
||||
RecipientId getRecipientId() {
|
||||
return recipientId;
|
||||
}
|
||||
|
||||
@Nullable GroupId getGroupId() {
|
||||
@Nullable
|
||||
GroupId getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
void isAdminOfGroup(@NonNull RecipientId recipientId, @NonNull AdminCallback callback) {
|
||||
SimpleTask.run(SignalExecutors.BOUNDED,
|
||||
() -> {
|
||||
if (groupId != null) {
|
||||
Recipient recipient = Recipient.resolved(recipientId);
|
||||
return groupDatabase.getGroup(groupId)
|
||||
.transform(g -> g.isAdmin(recipient))
|
||||
.or(false);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
callback::isAdmin);
|
||||
}
|
||||
|
||||
void getIdentity(@NonNull IdentityCallback callback) {
|
||||
SimpleTask.run(SignalExecutors.BOUNDED,
|
||||
() -> DatabaseFactory.getIdentityDatabase(context)
|
||||
|
@ -62,16 +46,12 @@ final class RecipientDialogRepository {
|
|||
callback::remoteIdentity);
|
||||
}
|
||||
|
||||
public void getRecipient(@NonNull RecipientCallback recipientCallback) {
|
||||
void getRecipient(@NonNull RecipientCallback recipientCallback) {
|
||||
SimpleTask.run(SignalExecutors.BOUNDED,
|
||||
() -> Recipient.resolved(recipientId),
|
||||
recipientCallback::onRecipient);
|
||||
}
|
||||
|
||||
interface AdminCallback {
|
||||
void isAdmin(boolean admin);
|
||||
}
|
||||
|
||||
interface IdentityCallback {
|
||||
void remoteIdentity(@Nullable IdentityDatabase.IdentityRecord identityRecord);
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ import androidx.annotation.Nullable;
|
|||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
|
@ -17,12 +16,12 @@ import org.thoughtcrime.securesms.RecipientPreferenceActivity;
|
|||
import org.thoughtcrime.securesms.VerifyIdentityActivity;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.LiveGroup;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.DefaultValueLiveData;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataPair;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
|
||||
final class RecipientDialogViewModel extends ViewModel {
|
||||
|
||||
|
@ -39,23 +38,22 @@ final class RecipientDialogViewModel extends ViewModel {
|
|||
this.recipientDialogRepository = recipientDialogRepository;
|
||||
this.identity = new MutableLiveData<>();
|
||||
|
||||
MutableLiveData<Boolean> localIsAdmin = new DefaultValueLiveData<>(false);
|
||||
MutableLiveData<Boolean> recipientIsAdmin = new DefaultValueLiveData<>(false);
|
||||
boolean recipientIsSelf = recipientDialogRepository.getRecipientId().equals(Recipient.self().getId());
|
||||
|
||||
if (recipientDialogRepository.getGroupId() != null && recipientDialogRepository.getGroupId().isV2()) {
|
||||
recipientDialogRepository.isAdminOfGroup(Recipient.self().getId(), localIsAdmin::setValue);
|
||||
recipientDialogRepository.isAdminOfGroup(recipientDialogRepository.getRecipientId(), recipientIsAdmin::setValue);
|
||||
}
|
||||
if (recipientDialogRepository.getGroupId() != null && recipientDialogRepository.getGroupId().isV2() && !recipientIsSelf) {
|
||||
LiveGroup source = new LiveGroup(recipientDialogRepository.getGroupId());
|
||||
|
||||
adminActionStatus = Transformations.map(new LiveDataPair<>(localIsAdmin, recipientIsAdmin, false, false),
|
||||
pair -> {
|
||||
boolean localAdmin = pair.first();
|
||||
boolean recipientAdmin = pair.second();
|
||||
LiveData<Boolean> localIsAdmin = source.isSelfAdmin();
|
||||
LiveData<Boolean> recipientIsAdmin = source.getRecipientIsAdmin(recipientDialogRepository.getRecipientId());
|
||||
|
||||
return new AdminActionStatus(localAdmin,
|
||||
adminActionStatus = LiveDataUtil.combineLatest(localIsAdmin, recipientIsAdmin,
|
||||
(localAdmin, recipientAdmin) ->
|
||||
new AdminActionStatus(localAdmin,
|
||||
localAdmin && !recipientAdmin,
|
||||
localAdmin && recipientAdmin);
|
||||
});
|
||||
localAdmin && recipientAdmin));
|
||||
} else {
|
||||
adminActionStatus = new MutableLiveData<>(new AdminActionStatus(false, false, false));
|
||||
}
|
||||
|
||||
recipient = Recipient.live(recipientDialogRepository.getRecipientId()).getLiveData();
|
||||
|
||||
|
|
|
@ -4,11 +4,43 @@ import androidx.annotation.NonNull;
|
|||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MediatorLiveData;
|
||||
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.whispersystems.libsignal.util.guava.Function;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
public final class LiveDataUtil {
|
||||
|
||||
private LiveDataUtil() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the {@param backgroundFunction} on {@link SignalExecutors#BOUNDED}.
|
||||
* <p>
|
||||
* The background function order is run serially, albeit possibly across multiple threads.
|
||||
* <p>
|
||||
* The background function may not run for all {@param source} updates. Later updates taking priority.
|
||||
*/
|
||||
public static <A, B> LiveData<B> mapAsync(@NonNull LiveData<A> source, @NonNull Function<A, B> backgroundFunction) {
|
||||
return mapAsync(SignalExecutors.BOUNDED, source, backgroundFunction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the {@param backgroundFunction} on the supplied {@param executor}.
|
||||
* <p>
|
||||
* Regardless of the executor supplied, the background function is run serially.
|
||||
* <p>
|
||||
* The background function may not run for all {@param source} updates. Later updates taking priority.
|
||||
*/
|
||||
public static <A, B> LiveData<B> mapAsync(@NonNull Executor executor, @NonNull LiveData<A> source, @NonNull Function<A, B> backgroundFunction) {
|
||||
MediatorLiveData<B> outputLiveData = new MediatorLiveData<>();
|
||||
Executor liveDataExecutor = new SerialLiveDataExecutor(executor);
|
||||
|
||||
outputLiveData.addSource(source, currentValue -> liveDataExecutor.execute(() -> outputLiveData.postValue(backgroundFunction.apply(currentValue))));
|
||||
|
||||
return outputLiveData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Once there is non-null data on both input {@link LiveData}, the {@link Combine} function is run
|
||||
* and produces a live data of the combined data.
|
||||
|
@ -64,4 +96,42 @@ public final class LiveDataUtil {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executor decorator that runs serially but enqueues just the latest task, dropping any pending task.
|
||||
* <p>
|
||||
* Based on SerialExecutor https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/Executor.html
|
||||
* but modified to represent a queue of size one which is replaced by the latest call to {@link #execute(Runnable)}.
|
||||
*/
|
||||
private static final class SerialLiveDataExecutor implements Executor {
|
||||
private final Executor executor;
|
||||
private Runnable next;
|
||||
private Runnable active;
|
||||
|
||||
SerialLiveDataExecutor(@NonNull Executor executor) {
|
||||
this.executor = executor;
|
||||
}
|
||||
|
||||
public synchronized void execute(@NonNull Runnable command) {
|
||||
next = () -> {
|
||||
try {
|
||||
command.run();
|
||||
} finally {
|
||||
scheduleNext();
|
||||
}
|
||||
};
|
||||
|
||||
if (active == null) {
|
||||
scheduleNext();
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void scheduleNext() {
|
||||
active = next;
|
||||
next = null;
|
||||
if (active != null) {
|
||||
executor.execute(active);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
5
app/src/main/res/color/ultramarine_text_button.xml
Normal file
5
app/src/main/res/color/ultramarine_text_button.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="@color/core_ultramarine" android:state_enabled="true" />
|
||||
<item android:color="?attr/title_text_color_secondary" />
|
||||
</selector>
|
24
app/src/main/res/layout/group_manage_activity.xml
Normal file
24
app/src/main/res/layout/group_manage_activity.xml
Normal file
|
@ -0,0 +1,24 @@
|
|||
<?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"
|
||||
tools:context=".groups.ui.managegroup.ManageGroupActivity">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:theme="@style/ThemeOverlay.AppCompat.ActionBar"
|
||||
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
|
||||
app:title=" "
|
||||
app:titleTextColor="?attr/title_text_color_primary" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</LinearLayout>
|
248
app/src/main/res/layout/group_manage_fragment.xml
Normal file
248
app/src/main/res/layout/group_manage_fragment.xml
Normal file
|
@ -0,0 +1,248 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.core.widget.NestedScrollView 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:background="?attr/pending_member_background"
|
||||
android:fillViewport="true"
|
||||
tools:context=".groups.ui.managegroup.ManageGroupFragment">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/group_title_card"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardBackgroundColor="?android:attr/windowBackground"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="16dp">
|
||||
|
||||
<org.thoughtcrime.securesms.components.AvatarImageView
|
||||
android:id="@+id/group_avatar"
|
||||
android:layout_width="96dp"
|
||||
android:layout_height="96dp"
|
||||
android:layout_gravity="center_horizontal" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/group_title"
|
||||
style="@style/TextAppearance.Signal.Title2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="14dp"
|
||||
tools:text="Parkdale Run Crew" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/member_count"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:textSize="14sp"
|
||||
tools:text="12 members (4 invited)" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/group_disappearing_messages_card"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
app:cardBackgroundColor="?android:attr/windowBackground"
|
||||
app:layout_constraintTop_toBottomOf="@id/group_title_card">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Signal.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:text="@string/ManageGroupActivity_disappearing_messages" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/disappearing_messages"
|
||||
style="@style/Widget.Signal.Button.TextButton.Ultramarine"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="textEnd"
|
||||
tools:text="Off" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/group_media_card"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:visibility="gone"
|
||||
app:cardBackgroundColor="?android:attr/windowBackground"
|
||||
app:layout_constraintTop_toBottomOf="@id/group_disappearing_messages_card"
|
||||
tools:visibility="visible">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<Button
|
||||
android:id="@+id/rail_label"
|
||||
style="@style/Widget.Signal.Button.TextButton.Ultramarine"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:focusableInTouchMode="true"
|
||||
android:padding="16dp"
|
||||
android:text="@string/recipient_preference_activity__shared_media" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.ThreadPhotoRailView
|
||||
android:id="@+id/recent_photos"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="90dp"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/group_access_control_card"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:visibility="gone"
|
||||
app:cardBackgroundColor="?android:attr/windowBackground"
|
||||
app:layout_constraintTop_toBottomOf="@id/group_media_card"
|
||||
tools:visibility="visible">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<Button
|
||||
android:id="@+id/edit_group_membership_title"
|
||||
style="@style/Widget.Signal.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/ManageGroupActivity_who_can_edit_group_membership" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/edit_group_membership_value"
|
||||
style="@style/Widget.Signal.Button.TextButton.Ultramarine"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="textEnd"
|
||||
tools:text="Only admin" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<Button
|
||||
android:id="@+id/edit_group_access_title"
|
||||
style="@style/Widget.Signal.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/ManageGroupActivity_who_can_edit_group_info" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/edit_group_access_value"
|
||||
style="@style/Widget.Signal.Button.TextButton.Ultramarine"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="textEnd"
|
||||
tools:text="All members" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/group_membership_card"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
app:cardBackgroundColor="?android:attr/windowBackground"
|
||||
app:layout_constraintTop_toBottomOf="@id/group_access_control_card">
|
||||
|
||||
<org.thoughtcrime.securesms.groups.ui.GroupMemberListView
|
||||
android:id="@+id/group_members"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:maxHeight="280dp"
|
||||
tools:listitem="@layout/group_recipient_list_item" />
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/group_pending_card"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
app:cardBackgroundColor="?android:attr/windowBackground"
|
||||
app:layout_constraintTop_toBottomOf="@id/group_membership_card">
|
||||
|
||||
<Button
|
||||
android:id="@+id/listPending"
|
||||
style="@style/Widget.Signal.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/ManageGroupActivity_pending_group_invites" />
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/group_block_and_leave_card"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
app:cardBackgroundColor="?android:attr/windowBackground"
|
||||
app:layout_constraintTop_toBottomOf="@id/group_pending_card">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<Button
|
||||
android:id="@+id/blockGroup"
|
||||
style="@style/Widget.Signal.Button.TextButton.Red"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/ManageGroupActivity_block_group" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/leaveGroup"
|
||||
style="@style/Widget.Signal.Button.TextButton.Red"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/ManageGroupActivity_leave_group" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
|
@ -22,15 +22,31 @@
|
|||
android:layout_marginEnd="8dp"
|
||||
android:gravity="start|center_vertical"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="?title_text_color_primary"
|
||||
android:textSize="14sp"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body2"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/recipient_avatar"
|
||||
app:layout_constraintEnd_toStartOf="@+id/popupMenuProgressContainer"
|
||||
app:layout_constraintEnd_toStartOf="@+id/admin"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toEndOf="@+id/recipient_avatar"
|
||||
app:layout_constraintTop_toTopOf="@+id/recipient_avatar"
|
||||
tools:text="@tools:sample/full_names" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/admin"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:gravity="start|center_vertical"
|
||||
android:text="@string/GroupRecipientListItem_admin"
|
||||
android:textColor="?attr/title_text_color_secondary"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body2"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/recipient_name"
|
||||
app:layout_constraintEnd_toStartOf="@+id/popupMenuProgressContainer"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toEndOf="@+id/recipient_name"
|
||||
app:layout_constraintTop_toTopOf="@+id/recipient_name"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/popupMenuProgressContainer"
|
||||
android:layout_width="24dp"
|
||||
|
|
|
@ -17,6 +17,17 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/admin_action_busy"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/recipient_avatar"
|
||||
app:layout_constraintEnd_toEndOf="@+id/recipient_avatar"
|
||||
app:layout_constraintStart_toStartOf="@+id/recipient_avatar"
|
||||
app:layout_constraintTop_toTopOf="@+id/recipient_avatar"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/full_name"
|
||||
style="@style/TextAppearance.Signal.Body1.Bold"
|
||||
|
@ -53,7 +64,7 @@
|
|||
|
||||
<Button
|
||||
android:id="@+id/message_button"
|
||||
style="@style/Signal.Button.TextButton.Drawable"
|
||||
style="@style/Widget.Signal.Button.TextButton.Drawable"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
|
@ -63,7 +74,7 @@
|
|||
|
||||
<Button
|
||||
android:id="@+id/secure_call_button"
|
||||
style="@style/Signal.Button.TextButton.Drawable"
|
||||
style="@style/Widget.Signal.Button.TextButton.Drawable"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
|
@ -72,7 +83,7 @@
|
|||
|
||||
<Button
|
||||
android:id="@+id/block_button"
|
||||
style="@style/Signal.Button.TextButton.Drawable"
|
||||
style="@style/Widget.Signal.Button.TextButton.Drawable"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
|
@ -83,7 +94,7 @@
|
|||
|
||||
<Button
|
||||
android:id="@+id/unblock_button"
|
||||
style="@style/Signal.Button.TextButton.Drawable"
|
||||
style="@style/Widget.Signal.Button.TextButton.Drawable"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
|
@ -94,7 +105,7 @@
|
|||
|
||||
<Button
|
||||
android:id="@+id/view_safety_number_button"
|
||||
style="@style/Signal.Button.TextButton.Drawable"
|
||||
style="@style/Widget.Signal.Button.TextButton.Drawable"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
|
@ -103,7 +114,7 @@
|
|||
|
||||
<Button
|
||||
android:id="@+id/make_group_admin_button"
|
||||
style="@style/Signal.Button.TextButton.Drawable"
|
||||
style="@style/Widget.Signal.Button.TextButton.Drawable"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
|
@ -114,7 +125,7 @@
|
|||
|
||||
<Button
|
||||
android:id="@+id/remove_group_admin_button"
|
||||
style="@style/Signal.Button.TextButton.Drawable"
|
||||
style="@style/Widget.Signal.Button.TextButton.Drawable"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
|
@ -122,10 +133,9 @@
|
|||
android:visibility="gone"
|
||||
app:drawableStartCompat="?attr/recipient_make_admin_icon"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/remove_from_group_button"
|
||||
style="@style/Signal.Button.TextButton.Drawable"
|
||||
style="@style/Widget.Signal.Button.TextButton.Drawable"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<!-- TODO GV2 Remove old one when GV2 can replace all functions -->
|
||||
<item android:id="@+id/menu_edit_group"
|
||||
android:title="@string/conversation__menu_edit_group"
|
||||
app:showAsAction="collapseActionView" />
|
||||
|
||||
<item android:id="@+id/menu_manage_group"
|
||||
android:title="@string/conversation__menu_manage_group"
|
||||
app:showAsAction="collapseActionView" />
|
||||
|
||||
<item android:id="@+id/menu_leave"
|
||||
android:title="@string/conversation__menu_leave_group"
|
||||
app:showAsAction="collapseActionView"/>
|
||||
|
|
|
@ -429,6 +429,7 @@
|
|||
<!-- GroupCreateActivity -->
|
||||
<string name="GroupCreateActivity_actionbar_title">New group</string>
|
||||
<string name="GroupCreateActivity_actionbar_edit_title">Edit group</string>
|
||||
<string name="GroupCreateActivity_actionbar_manage_title">Manage group</string>
|
||||
<string name="GroupCreateActivity_group_name_hint">Group name</string>
|
||||
<string name="GroupCreateActivity_actionbar_mms_title">New MMS group</string>
|
||||
<string name="GroupCreateActivity_contacts_dont_support_push">You have selected a contact that doesn\'t support Signal groups, so this group will be MMS.</string>
|
||||
|
@ -457,6 +458,14 @@
|
|||
<string name="GroupManagement_access_level_all_members">All members</string>
|
||||
<string name="GroupManagement_access_level_only_admins">Only admins</string>
|
||||
<string name="GroupManagement_access_level_unknown" translatable="false">Unknown</string>
|
||||
<array name="GroupManagement_edit_group_membership_choices">
|
||||
<item>@string/GroupManagement_access_level_all_members</item>
|
||||
<item>@string/GroupManagement_access_level_only_admins</item>
|
||||
</array>
|
||||
<array name="GroupManagement_edit_group_info_choices">
|
||||
<item>@string/GroupManagement_access_level_all_members</item>
|
||||
<item>@string/GroupManagement_access_level_only_admins</item>
|
||||
</array>
|
||||
|
||||
<!-- PendingMembersActivity -->
|
||||
<string name="PendingMemberInvitesActivity_pending_group_invites">Pending group invites</string>
|
||||
|
@ -477,6 +486,20 @@
|
|||
<item quantity="other">Error canceling invites</item>
|
||||
</plurals>
|
||||
|
||||
<!-- ManageGroupActivity -->
|
||||
<string name="ManageGroupActivity_disappearing_messages">Disappearing messages</string>
|
||||
<string name="ManageGroupActivity_pending_group_invites">Pending group invites</string>
|
||||
<string name="ManageGroupActivity_who_can_edit_group_membership">Who can edit group membership</string>
|
||||
<string name="ManageGroupActivity_who_can_edit_group_info">Who can edit group info</string>
|
||||
<string name="ManageGroupActivity_block_group">Block group</string>
|
||||
<string name="ManageGroupActivity_leave_group">Leave group</string>
|
||||
|
||||
<string name="ManageGroupActivity_you_dont_have_the_rights_to_do_this">You don\'t have the rights to do this</string>
|
||||
<string name="ManageGroupActivity_failed_to_update_the_group">Failed to update the group</string>
|
||||
|
||||
<string name="GroupManagement_choose_who_can_add_or_invite_new_members">Choose who can add or invite new members</string>
|
||||
<string name="GroupManagement_choose_who_can_change_the_group_name_and_photo">Choose who can change the group name and photo</string>
|
||||
|
||||
<plurals name="GroupMemberList_invited">
|
||||
<item quantity="one">%1$s invited 1 person</item>
|
||||
<item quantity="other">%1$s invited %2$d people</item>
|
||||
|
@ -1836,6 +1859,7 @@
|
|||
<!-- conversation -->
|
||||
<string name="conversation__menu_add_attachment">Add attachment</string>
|
||||
<string name="conversation__menu_edit_group">Edit group</string>
|
||||
<string name="conversation__menu_manage_group">Manage group</string>
|
||||
<string name="conversation__menu_leave_group">Leave group</string>
|
||||
<string name="conversation__menu_view_all_media">All media</string>
|
||||
<string name="conversation__menu_conversation_settings">Conversation settings</string>
|
||||
|
@ -2167,6 +2191,9 @@
|
|||
<string name="RecipientBottomSheet_make_group_admin">Make group admin</string>
|
||||
<string name="RecipientBottomSheet_remove_as_admin">Remove as admin</string>
|
||||
<string name="RecipientBottomSheet_remove_from_group">Remove from group</string>
|
||||
|
||||
<string name="GroupRecipientListItem_admin">Admin</string>
|
||||
|
||||
<!-- EOF -->
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -415,15 +415,23 @@
|
|||
</attr>
|
||||
</declare-styleable>
|
||||
|
||||
<style name="Signal.Button.TextButton" parent="Widget.AppCompat.Button.Borderless">
|
||||
<style name="Widget.Signal.Button.TextButton" parent="Widget.AppCompat.Button.Borderless">
|
||||
<item name="android:textAppearance">@style/TextAppearance.Signal.Body1</item>
|
||||
</style>
|
||||
|
||||
<style name="Signal.Button.TextButton.Drawable">
|
||||
<style name="Widget.Signal.Button.TextButton.Drawable">
|
||||
<item name="android:textAlignment">viewStart</item>
|
||||
<item name="android:drawablePadding">16dp</item>
|
||||
</style>
|
||||
|
||||
<style name="Widget.Signal.Button.TextButton.Red" >
|
||||
<item name="android:textColor">@color/core_red</item>
|
||||
</style>
|
||||
|
||||
<style name="Widget.Signal.Button.TextButton.Ultramarine" >
|
||||
<item name="android:textColor">@color/ultramarine_text_button</item>
|
||||
</style>
|
||||
|
||||
<style name="Widget.Signal.Button.CalleeDialog" parent="Widget.AppCompat.Button">
|
||||
<item name="android:textColor">@color/core_ultramarine</item>
|
||||
<item name="android:background">@drawable/callee_dialog_button_background</item>
|
||||
|
|
Loading…
Add table
Reference in a new issue