Manage group links behind feature flag.
parent
860f06ec9e
commit
bfed03b7b5
|
@ -268,6 +268,10 @@
|
|||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
|
||||
|
||||
<activity android:name=".groups.ui.invitesandrequests.ManagePendingAndRequestingMembersActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
|
||||
|
||||
<activity android:name=".groups.ui.managegroup.ManageGroupActivity"
|
||||
android:windowSoftInputMode="stateAlwaysHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
|
|
|
@ -8,6 +8,7 @@ import android.os.Build.VERSION_CODES;
|
|||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.app.ActivityOptionsCompat;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
@ -19,6 +20,8 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
|||
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageActivityHelper;
|
||||
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Base class for all activities. The vast majority of activities shouldn't extend this directly.
|
||||
* Instead, they should extend {@link PassphraseRequiredActivity} so they're protected by
|
||||
|
@ -72,9 +75,9 @@ public abstract class BaseActivity extends AppCompatActivity {
|
|||
ActivityCompat.startActivity(this, intent, bundle);
|
||||
}
|
||||
|
||||
@TargetApi(VERSION_CODES.LOLLIPOP)
|
||||
@TargetApi(21)
|
||||
protected void setStatusBarColor(int color) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
getWindow().setStatusBarColor(color);
|
||||
}
|
||||
}
|
||||
|
@ -87,4 +90,8 @@ public abstract class BaseActivity extends AppCompatActivity {
|
|||
private void logEvent(@NonNull String event) {
|
||||
Log.d(TAG, "[" + Log.tag(getClass()) + "] " + event);
|
||||
}
|
||||
|
||||
protected final @NonNull ActionBar requireSupportActionBar() {
|
||||
return Objects.requireNonNull(getSupportActionBar());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,8 @@ 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);
|
||||
ONLY_ADMINS(R.string.GroupManagement_access_level_only_admins),
|
||||
NO_ONE(R.string.GroupManagement_access_level_no_one);
|
||||
|
||||
private final @StringRes int string;
|
||||
|
||||
|
|
|
@ -247,6 +247,49 @@ public final class GroupManager {
|
|||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static void cycleGroupLinkPassword(@NonNull Context context,
|
||||
@NonNull GroupId.V2 groupId)
|
||||
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException, GroupChangeBusyException
|
||||
{
|
||||
try (GroupManagerV2.GroupEditor editor = new GroupManagerV2(context).edit(groupId.requireV2())) {
|
||||
editor.cycleGroupLinkPassword();
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static void setGroupLinkEnabledState(@NonNull Context context,
|
||||
@NonNull GroupId.V2 groupId,
|
||||
@NonNull GroupLinkState state)
|
||||
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException, GroupChangeBusyException
|
||||
{
|
||||
try (GroupManagerV2.GroupEditor editor = new GroupManagerV2(context).edit(groupId.requireV2())) {
|
||||
editor.setJoinByGroupLinkState(state);
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static void approveRequests(@NonNull Context context,
|
||||
@NonNull GroupId.V2 groupId,
|
||||
@NonNull Collection<RecipientId> recipientIds)
|
||||
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException, GroupChangeBusyException
|
||||
{
|
||||
try (GroupManagerV2.GroupEditor editor = new GroupManagerV2(context).edit(groupId.requireV2())) {
|
||||
editor.approveRequests(recipientIds);
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static void denyRequests(@NonNull Context context,
|
||||
@NonNull GroupId.V2 groupId,
|
||||
@NonNull Collection<RecipientId> recipientIds)
|
||||
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException, GroupChangeBusyException
|
||||
{
|
||||
try (GroupManagerV2.GroupEditor editor = new GroupManagerV2(context).edit(groupId.requireV2())) {
|
||||
editor.denyRequests(recipientIds);
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static @NonNull GroupActionResult addMembers(@NonNull Context context,
|
||||
@NonNull GroupId.Push groupId,
|
||||
|
@ -339,4 +382,10 @@ public final class GroupManager {
|
|||
return invitedMembers;
|
||||
}
|
||||
}
|
||||
|
||||
public enum GroupLinkState {
|
||||
DISABLED,
|
||||
ENABLED,
|
||||
ENABLED_WITH_APPROVAL
|
||||
}
|
||||
}
|
||||
|
|
|
@ -292,6 +292,17 @@ final class GroupManagerV2 {
|
|||
return commitChangeWithConflictResolution(groupOperations.createRemoveInvitationChange(new HashSet<>(uuidCipherTexts)));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@NonNull GroupManager.GroupActionResult approveRequests(@NonNull Collection<RecipientId> recipientIds)
|
||||
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
|
||||
{
|
||||
Set<UUID> uuids = Stream.of(recipientIds)
|
||||
.map(r -> Recipient.resolved(r).getUuid().get())
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
return commitChangeWithConflictResolution(groupOperations.createApproveGroupJoinRequest(uuids));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@NonNull GroupManager.GroupActionResult denyRequests(@NonNull Collection<RecipientId> recipientIds)
|
||||
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
|
||||
|
@ -402,6 +413,40 @@ final class GroupManagerV2 {
|
|||
return commitChangeWithConflictResolution(groupOperations.createAcceptInviteChange(groupCandidate.getProfileKeyCredential().get()));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public GroupManager.GroupActionResult cycleGroupLinkPassword()
|
||||
throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException
|
||||
{
|
||||
return commitChangeWithConflictResolution(groupOperations.createModifyGroupLinkPasswordChange(GroupLinkPassword.createNew().serialize()));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public GroupManager.GroupActionResult setJoinByGroupLinkState(@NonNull GroupManager.GroupLinkState state)
|
||||
throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException
|
||||
{
|
||||
AccessControl.AccessRequired access;
|
||||
|
||||
switch (state) {
|
||||
case DISABLED : access = AccessControl.AccessRequired.UNSATISFIABLE; break;
|
||||
case ENABLED : access = AccessControl.AccessRequired.ANY; break;
|
||||
case ENABLED_WITH_APPROVAL: access = AccessControl.AccessRequired.ADMINISTRATOR; break;
|
||||
default: throw new AssertionError();
|
||||
}
|
||||
|
||||
GroupChange.Actions.Builder change = groupOperations.createChangeJoinByLinkRights(access);
|
||||
|
||||
if (state != GroupManager.GroupLinkState.DISABLED) {
|
||||
DecryptedGroup group = groupDatabase.requireGroup(groupId).requireV2GroupProperties().getDecryptedGroup();
|
||||
|
||||
if (group.getInviteLinkPassword().isEmpty()) {
|
||||
Log.d(TAG, "First time enabling group links for group and password empty, generating");
|
||||
change = groupOperations.createModifyGroupLinkPasswordAndRightsChange(GroupLinkPassword.createNew().serialize(), access);
|
||||
}
|
||||
}
|
||||
|
||||
return commitChangeWithConflictResolution(change);
|
||||
}
|
||||
|
||||
private @NonNull GroupManager.GroupActionResult commitChangeWithConflictResolution(@NonNull GroupChange.Actions.Builder change)
|
||||
throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException
|
||||
{
|
||||
|
@ -480,7 +525,7 @@ final class GroupManagerV2 {
|
|||
return new GroupManager.GroupActionResult(recipientAndThread.groupRecipient, recipientAndThread.threadId, newMembersCount, newPendingMembers);
|
||||
}
|
||||
|
||||
private GroupChange commitToServer(GroupChange.Actions change)
|
||||
private @NonNull GroupChange commitToServer(@NonNull GroupChange.Actions change)
|
||||
throws GroupNotAMemberException, GroupChangeFailedException, IOException, GroupInsufficientRightsException
|
||||
{
|
||||
try {
|
||||
|
@ -948,6 +993,8 @@ final class GroupManagerV2 {
|
|||
return AccessControl.AccessRequired.MEMBER;
|
||||
case ONLY_ADMINS:
|
||||
return AccessControl.AccessRequired.ADMINISTRATOR;
|
||||
case NO_ONE:
|
||||
return AccessControl.AccessRequired.UNSATISFIABLE;
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
|
|
@ -12,17 +12,25 @@ import androidx.lifecycle.Transformations;
|
|||
import com.annimon.stream.ComparatorCompat;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.storageservice.protos.groups.AccessControl;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember;
|
||||
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.groups.v2.GroupInviteLinkUrl;
|
||||
import org.thoughtcrime.securesms.groups.v2.GroupLinkPassword;
|
||||
import org.thoughtcrime.securesms.groups.v2.GroupLinkUrlAndStatus;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
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.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
|
@ -37,30 +45,77 @@ public final class LiveGroup {
|
|||
.thenComparing(HAS_DISPLAY_NAME)
|
||||
.thenComparing(ALPHABETICAL);
|
||||
|
||||
private final GroupDatabase groupDatabase;
|
||||
private final LiveData<Recipient> recipient;
|
||||
private final LiveData<GroupDatabase.GroupRecord> groupRecord;
|
||||
private final LiveData<List<GroupMemberEntry.FullMember>> fullMembers;
|
||||
private final GroupDatabase groupDatabase;
|
||||
private final LiveData<Recipient> recipient;
|
||||
private final LiveData<GroupDatabase.GroupRecord> groupRecord;
|
||||
private final LiveData<List<GroupMemberEntry.FullMember>> fullMembers;
|
||||
private final LiveData<List<GroupMemberEntry.RequestingMember>> requestingMembers;
|
||||
private final LiveData<GroupLinkUrlAndStatus> groupLink;
|
||||
|
||||
public LiveGroup(@NonNull GroupId groupId) {
|
||||
Context context = ApplicationDependencies.getApplication();
|
||||
MutableLiveData<LiveRecipient> liveRecipient = new MutableLiveData<>();
|
||||
|
||||
this.groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||
this.recipient = Transformations.switchMap(liveRecipient, LiveRecipient::getLiveData);
|
||||
this.groupRecord = LiveDataUtil.filterNotNull(LiveDataUtil.mapAsync(recipient, groupRecipient -> groupDatabase.getGroup(groupRecipient.getId()).orNull()));
|
||||
this.fullMembers = LiveDataUtil.mapAsync(groupRecord,
|
||||
g -> Stream.of(g.getMembers())
|
||||
.map(m -> {
|
||||
Recipient recipient = Recipient.resolved(m);
|
||||
return new GroupMemberEntry.FullMember(recipient, g.isAdmin(recipient));
|
||||
})
|
||||
.sorted(MEMBER_ORDER)
|
||||
.toList());
|
||||
this.groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||
this.recipient = Transformations.switchMap(liveRecipient, LiveRecipient::getLiveData);
|
||||
this.groupRecord = LiveDataUtil.filterNotNull(LiveDataUtil.mapAsync(recipient, groupRecipient -> groupDatabase.getGroup(groupRecipient.getId()).orNull()));
|
||||
this.fullMembers = mapToFullMembers(this.groupRecord);
|
||||
this.requestingMembers = mapToRequestingMembers(this.groupRecord);
|
||||
|
||||
if (groupId.isV2()) {
|
||||
LiveData<GroupDatabase.V2GroupProperties> v2Properties = Transformations.map(this.groupRecord, GroupDatabase.GroupRecord::requireV2GroupProperties);
|
||||
this.groupLink = Transformations.map(v2Properties, g -> {
|
||||
DecryptedGroup group = g.getDecryptedGroup();
|
||||
AccessControl.AccessRequired addFromInviteLink = group.getAccessControl().getAddFromInviteLink();
|
||||
|
||||
if (group.getInviteLinkPassword().isEmpty()) {
|
||||
return GroupLinkUrlAndStatus.NONE;
|
||||
}
|
||||
|
||||
boolean enabled = addFromInviteLink == AccessControl.AccessRequired.ANY || addFromInviteLink == AccessControl.AccessRequired.ADMINISTRATOR;
|
||||
boolean adminApproval = addFromInviteLink == AccessControl.AccessRequired.ADMINISTRATOR;
|
||||
String url = GroupInviteLinkUrl.forGroup(g.getGroupMasterKey(), group)
|
||||
.getUrl();
|
||||
|
||||
return new GroupLinkUrlAndStatus(enabled, adminApproval, url);
|
||||
});
|
||||
} else {
|
||||
this.groupLink = new MutableLiveData<>(GroupLinkUrlAndStatus.NONE);
|
||||
}
|
||||
|
||||
SignalExecutors.BOUNDED.execute(() -> liveRecipient.postValue(Recipient.externalGroup(context, groupId).live()));
|
||||
}
|
||||
|
||||
protected static LiveData<List<GroupMemberEntry.FullMember>> mapToFullMembers(@NonNull LiveData<GroupDatabase.GroupRecord> groupRecord) {
|
||||
return LiveDataUtil.mapAsync(groupRecord,
|
||||
g -> Stream.of(g.getMembers())
|
||||
.map(m -> {
|
||||
Recipient recipient = Recipient.resolved(m);
|
||||
return new GroupMemberEntry.FullMember(recipient, g.isAdmin(recipient));
|
||||
})
|
||||
.sorted(MEMBER_ORDER)
|
||||
.toList());
|
||||
}
|
||||
|
||||
protected static LiveData<List<GroupMemberEntry.RequestingMember>> mapToRequestingMembers(@NonNull LiveData<GroupDatabase.GroupRecord> groupRecord) {
|
||||
return LiveDataUtil.mapAsync(groupRecord,
|
||||
g -> {
|
||||
if (!g.isV2Group()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
boolean selfAdmin = g.isAdmin(Recipient.self());
|
||||
List<DecryptedRequestingMember> requestingMembersList = g.requireV2GroupProperties().getDecryptedGroup().getRequestingMembersList();
|
||||
|
||||
return Stream.of(requestingMembersList)
|
||||
.map(requestingMember -> {
|
||||
Recipient recipient = Recipient.externalPush(ApplicationDependencies.getApplication(), UuidUtil.fromByteString(requestingMember.getUuid()), null, false);
|
||||
return new GroupMemberEntry.RequestingMember(recipient, selfAdmin);
|
||||
})
|
||||
.toList();
|
||||
});
|
||||
}
|
||||
|
||||
public LiveData<String> getTitle() {
|
||||
return LiveDataUtil.combineLatest(groupRecord, recipient, (groupRecord, recipient) -> {
|
||||
String title = groupRecord.getTitle();
|
||||
|
@ -91,6 +146,17 @@ public final class LiveGroup {
|
|||
return Transformations.map(groupRecord, g -> g.isV2Group() ? g.requireV2GroupProperties().getDecryptedGroup().getPendingMembersCount() : 0);
|
||||
}
|
||||
|
||||
public LiveData<Integer> getPendingAndRequestingMemberCount() {
|
||||
return Transformations.map(groupRecord, g -> {
|
||||
if (g.isV2Group()) {
|
||||
DecryptedGroup decryptedGroup = g.requireV2GroupProperties().getDecryptedGroup();
|
||||
|
||||
return decryptedGroup.getPendingMembersCount() + decryptedGroup.getRequestingMembersCount();
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
public LiveData<GroupAccessControl> getMembershipAdditionAccessControl() {
|
||||
return Transformations.map(groupRecord, GroupDatabase.GroupRecord::getMembershipAdditionAccessControl);
|
||||
}
|
||||
|
@ -110,6 +176,10 @@ public final class LiveGroup {
|
|||
return fullMembers;
|
||||
}
|
||||
|
||||
public LiveData<List<GroupMemberEntry.RequestingMember>> getRequestingMembers() {
|
||||
return requestingMembers;
|
||||
}
|
||||
|
||||
public LiveData<Integer> getExpireMessages() {
|
||||
return Transformations.map(recipient, Recipient::getExpireMessages);
|
||||
}
|
||||
|
@ -153,7 +223,12 @@ public final class LiveGroup {
|
|||
switch (rights) {
|
||||
case ALL_MEMBERS: return memberLevel.isInGroup();
|
||||
case ONLY_ADMINS: return memberLevel == GroupDatabase.MemberLevel.ADMINISTRATOR;
|
||||
case NO_ONE : return false;
|
||||
default: throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
public LiveData<GroupLinkUrlAndStatus> getGroupLink() {
|
||||
return groupLink;
|
||||
}
|
||||
}
|
|
@ -7,4 +7,8 @@ public interface AdminActionsListener {
|
|||
void onRevokeInvite(@NonNull GroupMemberEntry.PendingMember pendingMember);
|
||||
|
||||
void onRevokeAllInvites(@NonNull GroupMemberEntry.UnknownPendingMemberCount pendingMembers);
|
||||
|
||||
void onApproveRequest(@NonNull GroupMemberEntry.RequestingMember requestingMember);
|
||||
|
||||
void onDenyRequest(@NonNull GroupMemberEntry.RequestingMember requestingMember);
|
||||
}
|
||||
|
|
|
@ -222,4 +222,45 @@ public abstract class GroupMemberEntry {
|
|||
return hash + (cancellable ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
public final static class RequestingMember extends GroupMemberEntry {
|
||||
private final Recipient requester;
|
||||
private final boolean approvableDeniable;
|
||||
|
||||
public RequestingMember(@NonNull Recipient requester, boolean approvableDeniable) {
|
||||
this.requester = requester;
|
||||
this.approvableDeniable = approvableDeniable;
|
||||
}
|
||||
|
||||
public Recipient getRequester() {
|
||||
return requester;
|
||||
}
|
||||
|
||||
public boolean isApprovableDeniable() {
|
||||
return approvableDeniable;
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean sameId(@NonNull GroupMemberEntry newItem) {
|
||||
if (getClass() != newItem.getClass()) return false;
|
||||
|
||||
return requester.getId().equals(((GroupMemberEntry.PendingMember) newItem).invitee.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (!(obj instanceof RequestingMember)) return false;
|
||||
|
||||
RequestingMember other = (RequestingMember) obj;
|
||||
return other.requester.equals(requester) &&
|
||||
other.approvableDeniable == approvableDeniable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int hash = requester.hashCode();
|
||||
hash *= 31;
|
||||
return hash + (approvableDeniable ? 1 : 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
|||
private static final int OWN_INVITE_PENDING = 1;
|
||||
private static final int OTHER_INVITE_PENDING_COUNT = 2;
|
||||
private static final int NEW_GROUP_CANDIDATE = 3;
|
||||
private static final int REQUESTING_MEMBER = 4;
|
||||
|
||||
private final List<GroupMemberEntry> data = new ArrayList<>();
|
||||
private final Set<GroupMemberEntry> selection = new HashSet<>();
|
||||
|
@ -101,6 +102,14 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
|||
recipientClickListener,
|
||||
recipientLongClickListener,
|
||||
selectionChangeListener);
|
||||
case REQUESTING_MEMBER:
|
||||
return new RequestingMemberViewHolder(LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.group_recipient_requesting_list_item, parent, false),
|
||||
recipientClickListener,
|
||||
recipientLongClickListener,
|
||||
adminActionsListener,
|
||||
selectionChangeListener);
|
||||
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
@ -140,6 +149,8 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
|||
return OTHER_INVITE_PENDING_COUNT;
|
||||
} else if (groupMemberEntry instanceof GroupMemberEntry.NewGroupCandidate) {
|
||||
return NEW_GROUP_CANDIDATE;
|
||||
} else if (groupMemberEntry instanceof GroupMemberEntry.RequestingMember) {
|
||||
return REQUESTING_MEMBER;
|
||||
}
|
||||
|
||||
throw new AssertionError();
|
||||
|
@ -159,7 +170,7 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
|||
final PopupMenuView popupMenu;
|
||||
final View popupMenuContainer;
|
||||
final ProgressBar busyProgress;
|
||||
final View admin;
|
||||
@Nullable final View admin;
|
||||
final SelectionChangeListener selectionChangeListener;
|
||||
@Nullable final RecipientClickListener recipientClickListener;
|
||||
@Nullable final AdminActionsListener adminActionsListener;
|
||||
|
@ -225,7 +236,9 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
|||
|
||||
void bind(@NonNull GroupMemberEntry memberEntry, boolean isSelected) {
|
||||
busyProgress.setVisibility(View.GONE);
|
||||
admin.setVisibility(View.GONE);
|
||||
if (admin != null) {
|
||||
admin.setVisibility(View.GONE);
|
||||
}
|
||||
hideMenu();
|
||||
|
||||
itemView.setOnClickListener(null);
|
||||
|
@ -268,7 +281,9 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
|||
|
||||
bindRecipient(fullMember.getMember());
|
||||
bindRecipientClick(fullMember.getMember());
|
||||
admin.setVisibility(fullMember.isAdmin() ? View.VISIBLE : View.INVISIBLE);
|
||||
if (admin != null) {
|
||||
admin.setVisibility(fullMember.isAdmin() ? View.VISIBLE : View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
final static class NewGroupInviteeViewHolder extends ViewHolder {
|
||||
|
@ -379,6 +394,46 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
|||
}
|
||||
}
|
||||
|
||||
final static class RequestingMemberViewHolder extends ViewHolder {
|
||||
|
||||
private final View approveRequest;
|
||||
private final View denyRequest;
|
||||
|
||||
RequestingMemberViewHolder(@NonNull View itemView,
|
||||
@Nullable RecipientClickListener recipientClickListener,
|
||||
@Nullable RecipientLongClickListener recipientLongClickListener,
|
||||
@Nullable AdminActionsListener adminActionsListener,
|
||||
@NonNull SelectionChangeListener selectionChangeListener)
|
||||
{
|
||||
super(itemView, recipientClickListener, recipientLongClickListener, adminActionsListener, selectionChangeListener);
|
||||
|
||||
approveRequest = itemView.findViewById(R.id.request_approve);
|
||||
denyRequest = itemView.findViewById(R.id.request_deny);
|
||||
}
|
||||
|
||||
@Override
|
||||
void bind(@NonNull GroupMemberEntry memberEntry, boolean isSelected) {
|
||||
super.bind(memberEntry, isSelected);
|
||||
|
||||
GroupMemberEntry.RequestingMember requestingMember = (GroupMemberEntry.RequestingMember) memberEntry;
|
||||
|
||||
if (adminActionsListener != null && requestingMember.isApprovableDeniable()) {
|
||||
approveRequest.setVisibility(View.VISIBLE);
|
||||
denyRequest .setVisibility(View.VISIBLE);
|
||||
approveRequest.setOnClickListener(v -> adminActionsListener.onApproveRequest(requestingMember));
|
||||
denyRequest .setOnClickListener(v -> adminActionsListener.onDenyRequest (requestingMember));
|
||||
} else {
|
||||
approveRequest.setVisibility(View.GONE);
|
||||
denyRequest .setVisibility(View.GONE);
|
||||
approveRequest.setOnClickListener(null);
|
||||
denyRequest .setOnClickListener(null);
|
||||
}
|
||||
|
||||
bindRecipient(requestingMember.getRequester());
|
||||
bindRecipientClick(requestingMember.getRequester());
|
||||
}
|
||||
}
|
||||
|
||||
private final class SelectionChangeListener {
|
||||
void onSelectionChange(int position, boolean isChecked) {
|
||||
if (selectable) {
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
package org.thoughtcrime.securesms.groups.ui.invitesandrequests;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.google.android.material.tabs.TabLayoutMediator;
|
||||
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.ui.invitesandrequests.invited.PendingMemberInvitesFragment;
|
||||
import org.thoughtcrime.securesms.groups.ui.invitesandrequests.requesting.RequestingMembersFragment;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
|
||||
public class ManagePendingAndRequestingMembersActivity extends PassphraseRequiredActivity {
|
||||
|
||||
private static final String GROUP_ID = "GROUP_ID";
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||
|
||||
public static Intent newIntent(@NonNull Context context, @NonNull GroupId.V2 groupId) {
|
||||
Intent intent = new Intent(context, ManagePendingAndRequestingMembersActivity.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_pending_and_requesting_member_activity);
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
GroupId.V2 groupId = GroupId.parseOrThrow(getIntent().getStringExtra(GROUP_ID)).requireV2();
|
||||
|
||||
ViewPager2 viewPager = findViewById(R.id.pending_and_requesting_pager);
|
||||
TabLayout tabLayout = findViewById(R.id.pending_and_requesting_tabs);
|
||||
|
||||
viewPager.setAdapter(new ViewPagerAdapter(this, groupId));
|
||||
|
||||
new TabLayoutMediator(tabLayout, viewPager,
|
||||
(tab, position) -> {
|
||||
switch (position) {
|
||||
case 0 : tab.setText(R.string.PendingMembersActivity_requests); break;
|
||||
case 1 : tab.setText(R.string.PendingMembersActivity_invites); break;
|
||||
default: throw new AssertionError();
|
||||
}
|
||||
}
|
||||
).attach();
|
||||
}
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
requireSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
|
||||
private static class ViewPagerAdapter extends FragmentStateAdapter {
|
||||
|
||||
private final GroupId.V2 groupId;
|
||||
|
||||
public ViewPagerAdapter(@NonNull FragmentActivity fragmentActivity,
|
||||
@NonNull GroupId.V2 groupId)
|
||||
{
|
||||
super(fragmentActivity);
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Fragment createFragment(int position) {
|
||||
switch (position) {
|
||||
case 0 : return RequestingMembersFragment.newInstance(groupId);
|
||||
case 1 : return PendingMemberInvitesFragment.newInstance(groupId);
|
||||
default: throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSupportNavigateUp() {
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package org.thoughtcrime.securesms.groups.ui.pendingmemberinvites;
|
||||
package org.thoughtcrime.securesms.groups.ui.invitesandrequests.invited;
|
||||
|
||||
import android.content.Context;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package org.thoughtcrime.securesms.groups.ui.pendingmemberinvites;
|
||||
package org.thoughtcrime.securesms.groups.ui.invitesandrequests.invited;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
|
@ -15,6 +15,8 @@ import org.thoughtcrime.securesms.groups.GroupId;
|
|||
import org.thoughtcrime.securesms.groups.ui.AdminActionsListener;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberListView;
|
||||
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
|
@ -47,6 +49,10 @@ public class PendingMemberInvitesFragment extends Fragment {
|
|||
youInvitedEmptyState = view.findViewById(R.id.no_pending_from_you);
|
||||
othersInvitedEmptyState = view.findViewById(R.id.no_pending_from_others);
|
||||
|
||||
youInvited.setRecipientClickListener(recipient ->
|
||||
RecipientBottomSheetDialogFragment.create(recipient.getId(), null)
|
||||
.show(requireActivity().getSupportFragmentManager(), BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG));
|
||||
|
||||
youInvited.setAdminActionsListener(new AdminActionsListener() {
|
||||
|
||||
@Override
|
||||
|
@ -56,7 +62,17 @@ public class PendingMemberInvitesFragment extends Fragment {
|
|||
|
||||
@Override
|
||||
public void onRevokeAllInvites(@NonNull GroupMemberEntry.UnknownPendingMemberCount pendingMembers) {
|
||||
throw new AssertionError();
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApproveRequest(@NonNull GroupMemberEntry.RequestingMember requestingMember) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDenyRequest(@NonNull GroupMemberEntry.RequestingMember requestingMember) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -64,13 +80,23 @@ public class PendingMemberInvitesFragment extends Fragment {
|
|||
|
||||
@Override
|
||||
public void onRevokeInvite(@NonNull GroupMemberEntry.PendingMember pendingMember) {
|
||||
throw new AssertionError();
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRevokeAllInvites(@NonNull GroupMemberEntry.UnknownPendingMemberCount pendingMembers) {
|
||||
viewModel.revokeInvitesFor(pendingMembers);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApproveRequest(@NonNull GroupMemberEntry.RequestingMember requestingMember) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDenyRequest(@NonNull GroupMemberEntry.RequestingMember requestingMember) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
});
|
||||
|
||||
return view;
|
|
@ -1,4 +1,4 @@
|
|||
package org.thoughtcrime.securesms.groups.ui.pendingmemberinvites;
|
||||
package org.thoughtcrime.securesms.groups.ui.invitesandrequests.invited;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
|
@ -33,15 +33,15 @@ import java.util.concurrent.Executor;
|
|||
/**
|
||||
* Repository for modifying the pending members on a single group.
|
||||
*/
|
||||
final class PendingMemberRepository {
|
||||
final class PendingMemberInvitesRepository {
|
||||
|
||||
private static final String TAG = Log.tag(PendingMemberRepository.class);
|
||||
private static final String TAG = Log.tag(PendingMemberInvitesRepository.class);
|
||||
|
||||
private final Context context;
|
||||
private final GroupId.V2 groupId;
|
||||
private final Executor executor;
|
||||
|
||||
PendingMemberRepository(@NonNull Context context, @NonNull GroupId.V2 groupId) {
|
||||
PendingMemberInvitesRepository(@NonNull Context context, @NonNull GroupId.V2 groupId) {
|
||||
this.context = context.getApplicationContext();
|
||||
this.executor = SignalExecutors.BOUNDED;
|
||||
this.groupId = groupId;
|
|
@ -1,4 +1,4 @@
|
|||
package org.thoughtcrime.securesms.groups.ui.pendingmemberinvites;
|
||||
package org.thoughtcrime.securesms.groups.ui.invitesandrequests.invited;
|
||||
|
||||
import android.content.Context;
|
||||
import android.widget.Toast;
|
||||
|
@ -23,12 +23,12 @@ import java.util.List;
|
|||
public class PendingMemberInvitesViewModel extends ViewModel {
|
||||
|
||||
private final Context context;
|
||||
private final PendingMemberRepository pendingMemberRepository;
|
||||
private final PendingMemberInvitesRepository pendingMemberRepository;
|
||||
private final DefaultValueLiveData<List<GroupMemberEntry.PendingMember>> whoYouInvited = new DefaultValueLiveData<>(Collections.emptyList());
|
||||
private final DefaultValueLiveData<List<GroupMemberEntry.UnknownPendingMemberCount>> whoOthersInvited = new DefaultValueLiveData<>(Collections.emptyList());
|
||||
|
||||
private PendingMemberInvitesViewModel(@NonNull Context context,
|
||||
@NonNull PendingMemberRepository pendingMemberRepository)
|
||||
@NonNull PendingMemberInvitesRepository pendingMemberRepository)
|
||||
{
|
||||
this.context = context;
|
||||
this.pendingMemberRepository = pendingMemberRepository;
|
||||
|
@ -49,17 +49,17 @@ public class PendingMemberInvitesViewModel extends ViewModel {
|
|||
whoOthersInvited.postValue(byOthers);
|
||||
}
|
||||
|
||||
private void setMembers(PendingMemberRepository.InviteeResult inviteeResult) {
|
||||
private void setMembers(PendingMemberInvitesRepository.InviteeResult inviteeResult) {
|
||||
List<GroupMemberEntry.PendingMember> byMe = new ArrayList<>(inviteeResult.getByMe().size());
|
||||
List<GroupMemberEntry.UnknownPendingMemberCount> byOthers = new ArrayList<>(inviteeResult.getByOthers().size());
|
||||
|
||||
for (PendingMemberRepository.SinglePendingMemberInvitedByYou pendingMember : inviteeResult.getByMe()) {
|
||||
for (PendingMemberInvitesRepository.SinglePendingMemberInvitedByYou pendingMember : inviteeResult.getByMe()) {
|
||||
byMe.add(new GroupMemberEntry.PendingMember(pendingMember.getInvitee(),
|
||||
pendingMember.getInviteeCipherText(),
|
||||
inviteeResult.isCanRevokeInvites()));
|
||||
}
|
||||
|
||||
for (PendingMemberRepository.MultiplePendingMembersInvitedByAnother pendingMembers : inviteeResult.getByOthers()) {
|
||||
for (PendingMemberInvitesRepository.MultiplePendingMembersInvitedByAnother pendingMembers : inviteeResult.getByOthers()) {
|
||||
byOthers.add(new GroupMemberEntry.UnknownPendingMemberCount(pendingMembers.getInviter(),
|
||||
pendingMembers.getUuidCipherTexts(),
|
||||
inviteeResult.isCanRevokeInvites()));
|
||||
|
@ -148,7 +148,7 @@ public class PendingMemberInvitesViewModel extends ViewModel {
|
|||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
//noinspection unchecked
|
||||
return (T) new PendingMemberInvitesViewModel(context, new PendingMemberRepository(context.getApplicationContext(), groupId));
|
||||
return (T) new PendingMemberInvitesViewModel(context, new PendingMemberInvitesRepository(context.getApplicationContext(), groupId));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
package org.thoughtcrime.securesms.groups.ui.invitesandrequests.requesting;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
final class RequestConfirmationDialog {
|
||||
|
||||
private RequestConfirmationDialog() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirms that you want to approve or deny a request to join the group depending on
|
||||
* {@param approve}.
|
||||
*/
|
||||
static AlertDialog show(@NonNull Context context,
|
||||
@NonNull Recipient requester,
|
||||
boolean approve,
|
||||
@NonNull Runnable onApproveOrDeny)
|
||||
{
|
||||
if (approve) {
|
||||
return showRequestApproveConfirmationDialog(context, requester, onApproveOrDeny);
|
||||
} else {
|
||||
return showRequestDenyConfirmationDialog(context, requester, onApproveOrDeny);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirms that you want to approve a request to join the group.
|
||||
*/
|
||||
private static AlertDialog showRequestApproveConfirmationDialog(@NonNull Context context,
|
||||
@NonNull Recipient requester,
|
||||
@NonNull Runnable onApprove)
|
||||
{
|
||||
return new AlertDialog.Builder(context)
|
||||
.setMessage(context.getString(R.string.RequestConfirmationDialog_add_s_to_the_group,
|
||||
requester.getDisplayName(context)))
|
||||
.setPositiveButton(R.string.RequestConfirmationDialog_add, (dialog, which) -> onApprove.run())
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirms that you want to deny a request to join the group.
|
||||
*/
|
||||
private static AlertDialog showRequestDenyConfirmationDialog(@NonNull Context context,
|
||||
@NonNull Recipient requester,
|
||||
@NonNull Runnable onDeny)
|
||||
{
|
||||
return new AlertDialog.Builder(context)
|
||||
.setMessage(context.getString(R.string.RequestConfirmationDialog_deny_request_from_s,
|
||||
requester.getDisplayName(context)))
|
||||
.setPositiveButton(R.string.RequestConfirmationDialog_deny, (dialog, which) -> onDeny.run())
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
package org.thoughtcrime.securesms.groups.ui.invitesandrequests.requesting;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.LiveGroup;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupErrors;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.AsynchronousCallback;
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class RequestingMemberInvitesViewModel extends ViewModel {
|
||||
|
||||
private final Context context;
|
||||
private final RequestingMemberRepository requestingMemberRepository;
|
||||
private final MutableLiveData<String> toasts;
|
||||
private final LiveData<List<GroupMemberEntry.RequestingMember>> requesting;
|
||||
|
||||
private RequestingMemberInvitesViewModel(@NonNull Context context,
|
||||
@NonNull GroupId.V2 groupId,
|
||||
@NonNull RequestingMemberRepository requestingMemberRepository)
|
||||
{
|
||||
this.context = context;
|
||||
this.requestingMemberRepository = requestingMemberRepository;
|
||||
this.requesting = new LiveGroup(groupId).getRequestingMembers();
|
||||
this.toasts = new SingleLiveEvent<>();
|
||||
}
|
||||
|
||||
LiveData<List<GroupMemberEntry.RequestingMember>> getRequesting() {
|
||||
return requesting;
|
||||
}
|
||||
|
||||
LiveData<String> getToasts() {
|
||||
return toasts;
|
||||
}
|
||||
|
||||
void approveRequestFor(@NonNull GroupMemberEntry.RequestingMember requestingMember) {
|
||||
approveOrDeny(requestingMember, true);
|
||||
}
|
||||
|
||||
void denyRequestFor(@NonNull GroupMemberEntry.RequestingMember requestingMember) {
|
||||
approveOrDeny(requestingMember, false);
|
||||
}
|
||||
|
||||
private void approveOrDeny(@NonNull GroupMemberEntry.RequestingMember requestingMember, boolean approve) {
|
||||
RequestConfirmationDialog.show(context, requestingMember.getRequester(), approve, () -> {
|
||||
Set<RecipientId> memberAsSet = Collections.singleton(requestingMember.getRequester().getId());
|
||||
|
||||
if (approve) {
|
||||
requestingMember.setBusy(true);
|
||||
requestingMemberRepository.approveRequests(memberAsSet, new AsynchronousCallback.WorkerThread<Void, GroupChangeFailureReason>() {
|
||||
@Override
|
||||
public void onComplete(@Nullable Void result) {
|
||||
requestingMember.setBusy(false);
|
||||
toasts.postValue(context.getString(R.string.RequestingMembersFragment_added_s, requestingMember.getRequester().getDisplayName(context)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(@Nullable GroupChangeFailureReason error) {
|
||||
requestingMember.setBusy(false);
|
||||
toasts.postValue(context.getString(GroupErrors.getUserDisplayMessage(error)));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
requestingMember.setBusy(true);
|
||||
requestingMemberRepository.denyRequests(memberAsSet, new AsynchronousCallback.WorkerThread<Void, GroupChangeFailureReason>() {
|
||||
@Override
|
||||
public void onComplete(@Nullable Void result) {
|
||||
requestingMember.setBusy(false);
|
||||
toasts.postValue(context.getString(R.string.RequestingMembersFragment_denied_s, requestingMember.getRequester().getDisplayName(context)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(@Nullable GroupChangeFailureReason error) {
|
||||
requestingMember.setBusy(false);
|
||||
toasts.postValue(context.getString(GroupErrors.getUserDisplayMessage(error)));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static class Factory implements ViewModelProvider.Factory {
|
||||
|
||||
private final Context context;
|
||||
private final GroupId.V2 groupId;
|
||||
|
||||
public Factory(@NonNull Context context, @NonNull GroupId.V2 groupId) {
|
||||
this.context = context;
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
//noinspection unchecked
|
||||
return (T) new RequestingMemberInvitesViewModel(context, groupId, new RequestingMemberRepository(context.getApplicationContext(), groupId));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
package org.thoughtcrime.securesms.groups.ui.invitesandrequests.requesting;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.groups.GroupChangeException;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.GroupManager;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.AsynchronousCallback;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* Repository for modifying the requesting members on a single group.
|
||||
*/
|
||||
final class RequestingMemberRepository {
|
||||
|
||||
private static final String TAG = Log.tag(RequestingMemberRepository.class);
|
||||
|
||||
private final Context context;
|
||||
private final GroupId.V2 groupId;
|
||||
|
||||
RequestingMemberRepository(@NonNull Context context, @NonNull GroupId.V2 groupId) {
|
||||
this.context = context.getApplicationContext();
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
void approveRequests(@NonNull Collection<RecipientId> recipientIds,
|
||||
@NonNull AsynchronousCallback.WorkerThread<Void, GroupChangeFailureReason> callback)
|
||||
{
|
||||
SignalExecutors.UNBOUNDED.execute(() -> {
|
||||
try {
|
||||
GroupManager.approveRequests(context, groupId, recipientIds);
|
||||
callback.onComplete(null);
|
||||
} catch (GroupChangeException | IOException e) {
|
||||
Log.w(TAG, e);
|
||||
callback.onError(GroupChangeFailureReason.fromException(e));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void denyRequests(@NonNull Collection<RecipientId> recipientIds,
|
||||
@NonNull AsynchronousCallback.WorkerThread<Void, GroupChangeFailureReason> callback)
|
||||
{
|
||||
SignalExecutors.UNBOUNDED.execute(() -> {
|
||||
try {
|
||||
GroupManager.denyRequests(context, groupId, recipientIds);
|
||||
callback.onComplete(null);
|
||||
} catch (GroupChangeException | IOException e) {
|
||||
Log.w(TAG, e);
|
||||
callback.onError(GroupChangeFailureReason.fromException(e));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
package org.thoughtcrime.securesms.groups.ui.invitesandrequests.requesting;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.ui.AdminActionsListener;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberListView;
|
||||
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Lists and allows approval/denial of people requesting access to the group.
|
||||
*/
|
||||
public class RequestingMembersFragment extends Fragment {
|
||||
|
||||
private static final String GROUP_ID = "GROUP_ID";
|
||||
|
||||
private RequestingMemberInvitesViewModel viewModel;
|
||||
private GroupMemberListView requestingMembers;
|
||||
private View noRequestingMessage;
|
||||
private View requestingExplanation;
|
||||
|
||||
public static RequestingMembersFragment newInstance(@NonNull GroupId.V2 groupId) {
|
||||
RequestingMembersFragment fragment = new RequestingMembersFragment();
|
||||
Bundle args = new Bundle();
|
||||
|
||||
args.putString(GROUP_ID, groupId.toString());
|
||||
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_requesting_member_fragment, container, false);
|
||||
|
||||
requestingMembers = view.findViewById(R.id.requesting_members);
|
||||
noRequestingMessage = view.findViewById(R.id.no_requesting);
|
||||
requestingExplanation = view.findViewById(R.id.requesting_members_explain);
|
||||
|
||||
requestingMembers.setRecipientClickListener(recipient ->
|
||||
RecipientBottomSheetDialogFragment.create(recipient.getId(), null)
|
||||
.show(requireActivity().getSupportFragmentManager(), BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG));
|
||||
|
||||
requestingMembers.setAdminActionsListener(new AdminActionsListener() {
|
||||
|
||||
@Override
|
||||
public void onRevokeInvite(@NonNull GroupMemberEntry.PendingMember pendingMember) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRevokeAllInvites(@NonNull GroupMemberEntry.UnknownPendingMemberCount pendingMembers) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApproveRequest(@NonNull GroupMemberEntry.RequestingMember requestingMember) {
|
||||
viewModel.approveRequestFor(requestingMember);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDenyRequest(@NonNull GroupMemberEntry.RequestingMember requestingMember) {
|
||||
viewModel.denyRequestFor(requestingMember);
|
||||
}
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
GroupId.V2 groupId = GroupId.parseOrThrow(Objects.requireNonNull(requireArguments().getString(GROUP_ID))).requireV2();
|
||||
|
||||
RequestingMemberInvitesViewModel.Factory factory = new RequestingMemberInvitesViewModel.Factory(requireContext(), groupId);
|
||||
|
||||
viewModel = ViewModelProviders.of(requireActivity(), factory).get(RequestingMemberInvitesViewModel.class);
|
||||
|
||||
viewModel.getRequesting().observe(getViewLifecycleOwner(), requesting -> {
|
||||
requestingMembers.setMembers(requesting);
|
||||
noRequestingMessage.setVisibility(requesting.isEmpty() ? View.VISIBLE: View.GONE);
|
||||
requestingExplanation.setVisibility(requesting.isEmpty() ? View.GONE : View.VISIBLE);
|
||||
});
|
||||
|
||||
viewModel.getToasts().observe(getViewLifecycleOwner(), toast -> Toast.makeText(requireContext(), toast, Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
}
|
|
@ -37,6 +37,7 @@ import org.thoughtcrime.securesms.contacts.avatars.FallbackPhoto80dp;
|
|||
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.invitesandrequests.ManagePendingAndRequestingMembersActivity;
|
||||
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupInviteSentDialog;
|
||||
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupRightsDialog;
|
||||
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupsLearnMoreBottomSheetDialogFragment;
|
||||
|
@ -50,6 +51,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
|||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.recipients.ui.notifications.CustomNotificationsDialogFragment;
|
||||
import org.thoughtcrime.securesms.recipients.ui.sharablegrouplink.ShareableGroupLinkDialogFragment;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.LifecycleCursorWrapper;
|
||||
|
@ -64,13 +66,16 @@ public class ManageGroupFragment extends LoggingFragment {
|
|||
|
||||
private static final String TAG = Log.tag(ManageGroupFragment.class);
|
||||
|
||||
private static final int RETURN_FROM_MEDIA = 33114;
|
||||
private static final int PICK_CONTACT = 61341;
|
||||
private static final int RETURN_FROM_MEDIA = 33114;
|
||||
private static final int PICK_CONTACT = 61341;
|
||||
public static final String DIALOG_TAG = "DIALOG";
|
||||
|
||||
private ManageGroupViewModel viewModel;
|
||||
private GroupMemberListView groupMemberList;
|
||||
private View pendingMembersRow;
|
||||
private TextView pendingMembersCount;
|
||||
private View pendingAndRequestingRow;
|
||||
private TextView pendingAndRequestingCount;
|
||||
private Toolbar toolbar;
|
||||
private TextView groupName;
|
||||
private LearnMoreTextView groupV1Indicator;
|
||||
|
@ -81,6 +86,7 @@ public class ManageGroupFragment extends LoggingFragment {
|
|||
private View groupMediaCard;
|
||||
private View accessControlCard;
|
||||
private View pendingMembersCard;
|
||||
private View groupLinkCard;
|
||||
private ManageGroupViewModel.CursorFactory cursorFactory;
|
||||
private View sharedMediaRow;
|
||||
private View editGroupAccessRow;
|
||||
|
@ -103,6 +109,8 @@ public class ManageGroupFragment extends LoggingFragment {
|
|||
private View mentionsRow;
|
||||
private TextView mentionsValue;
|
||||
private View toggleAllMembers;
|
||||
private View groupLinkRow;
|
||||
private TextView groupLinkButton;
|
||||
|
||||
private final Recipient.FallbackPhotoProvider fallbackPhotoProvider = new Recipient.FallbackPhotoProvider() {
|
||||
@Override
|
||||
|
@ -137,10 +145,13 @@ public class ManageGroupFragment extends LoggingFragment {
|
|||
groupMemberList = view.findViewById(R.id.group_members);
|
||||
pendingMembersRow = view.findViewById(R.id.pending_members_row);
|
||||
pendingMembersCount = view.findViewById(R.id.pending_members_count);
|
||||
pendingAndRequestingRow = view.findViewById(R.id.pending_and_requesting_members_row);
|
||||
pendingAndRequestingCount = view.findViewById(R.id.pending_and_requesting_members_count);
|
||||
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);
|
||||
groupLinkCard = view.findViewById(R.id.group_link_card);
|
||||
sharedMediaRow = view.findViewById(R.id.shared_media_row);
|
||||
editGroupAccessRow = view.findViewById(R.id.edit_group_access_row);
|
||||
editGroupAccessValue = view.findViewById(R.id.edit_group_access_value);
|
||||
|
@ -162,6 +173,8 @@ public class ManageGroupFragment extends LoggingFragment {
|
|||
mentionsRow = view.findViewById(R.id.group_mentions_row);
|
||||
mentionsValue = view.findViewById(R.id.group_mentions_value);
|
||||
toggleAllMembers = view.findViewById(R.id.toggle_all_members);
|
||||
groupLinkRow = view.findViewById(R.id.group_link_row);
|
||||
groupLinkButton = view.findViewById(R.id.group_link_button);
|
||||
|
||||
groupV1Indicator.setOnLinkClickListener(v -> GroupsLearnMoreBottomSheetDialogFragment.show(requireFragmentManager()));
|
||||
groupV1Indicator.setLearnMoreVisible(true);
|
||||
|
@ -193,18 +206,34 @@ public class ManageGroupFragment extends LoggingFragment {
|
|||
}
|
||||
});
|
||||
|
||||
viewModel.getPendingMemberCount().observe(getViewLifecycleOwner(),
|
||||
pendingInviteCount -> {
|
||||
pendingMembersRow.setOnClickListener(v -> {
|
||||
FragmentActivity activity = requireActivity();
|
||||
activity.startActivity(PendingMemberInvitesActivity.newIntent(activity, groupId.requireV2()));
|
||||
if (FeatureFlags.groupsV2manageGroupLinks()) {
|
||||
viewModel.getPendingAndRequestingCount().observe(getViewLifecycleOwner(),
|
||||
pendingAndRequestingCount -> {
|
||||
pendingAndRequestingRow.setOnClickListener(v -> {
|
||||
FragmentActivity activity = requireActivity();
|
||||
activity.startActivity(ManagePendingAndRequestingMembersActivity.newIntent(activity, groupId.requireV2()));
|
||||
});
|
||||
if (pendingAndRequestingCount == 0) {
|
||||
this.pendingAndRequestingCount.setVisibility(View.GONE);
|
||||
} else {
|
||||
this.pendingAndRequestingCount.setText(String.format(Locale.getDefault(), "%d", pendingAndRequestingCount));
|
||||
this.pendingAndRequestingCount.setVisibility(View.VISIBLE);
|
||||
}
|
||||
});
|
||||
if (pendingInviteCount == 0) {
|
||||
pendingMembersCount.setText(R.string.ManageGroupActivity_none);
|
||||
} else {
|
||||
pendingMembersCount.setText(getResources().getQuantityString(R.plurals.ManageGroupActivity_invited, pendingInviteCount, pendingInviteCount));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
viewModel.getPendingMemberCount().observe(getViewLifecycleOwner(),
|
||||
pendingInviteCount -> {
|
||||
pendingMembersRow.setOnClickListener(v -> {
|
||||
FragmentActivity activity = requireActivity();
|
||||
activity.startActivity(PendingMemberInvitesActivity.newIntent(activity, groupId.requireV2()));
|
||||
});
|
||||
if (pendingInviteCount == 0) {
|
||||
pendingMembersCount.setText(R.string.ManageGroupActivity_none);
|
||||
} else {
|
||||
pendingMembersCount.setText(getResources().getQuantityString(R.plurals.ManageGroupActivity_invited, pendingInviteCount, pendingInviteCount));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
avatar.setFallbackPhotoProvider(fallbackPhotoProvider);
|
||||
|
||||
|
@ -230,9 +259,15 @@ public class ManageGroupFragment extends LoggingFragment {
|
|||
AvatarPreviewActivity.createTransitionBundle(activity, avatar));
|
||||
});
|
||||
customNotificationsRow.setOnClickListener(v -> CustomNotificationsDialogFragment.create(groupRecipient.getId())
|
||||
.show(requireFragmentManager(), "CUSTOM_NOTIFICATIONS"));
|
||||
.show(requireFragmentManager(), DIALOG_TAG));
|
||||
});
|
||||
|
||||
if (groupId.isV2()) {
|
||||
groupLinkRow.setOnClickListener(v -> ShareableGroupLinkDialogFragment.create(groupId.requireV2())
|
||||
.show(requireFragmentManager(), DIALOG_TAG));
|
||||
viewModel.getGroupLinkOn().observe(getViewLifecycleOwner(), linkEnabled -> groupLinkButton.setText(booleanToOnOff(linkEnabled)));
|
||||
}
|
||||
|
||||
viewModel.getGroupViewState().observe(getViewLifecycleOwner(), vs -> {
|
||||
if (vs == null) return;
|
||||
sharedMediaRow.setOnClickListener(v -> startActivity(MediaOverviewActivity.forThread(context, vs.getThreadId())));
|
||||
|
@ -245,7 +280,8 @@ public class ManageGroupFragment extends LoggingFragment {
|
|||
ViewCompat.getLayoutDirection(threadPhotoRailView) == ViewCompat.LAYOUT_DIRECTION_LTR),
|
||||
RETURN_FROM_MEDIA));
|
||||
|
||||
pendingMembersCard.setVisibility(vs.getGroupRecipient().requireGroupId().isV2() ? View.VISIBLE : View.GONE);
|
||||
pendingMembersCard.setVisibility(!FeatureFlags.groupsV2manageGroupLinks() && vs.getGroupRecipient().requireGroupId().isV2() ? View.VISIBLE : View.GONE);
|
||||
groupLinkCard .setVisibility( FeatureFlags.groupsV2manageGroupLinks() && vs.getGroupRecipient().requireGroupId().isV2() ? View.VISIBLE : View.GONE);
|
||||
});
|
||||
|
||||
leaveGroup.setVisibility(groupId.isPush() ? View.VISIBLE : View.GONE);
|
||||
|
@ -324,8 +360,7 @@ public class ManageGroupFragment extends LoggingFragment {
|
|||
|
||||
if (NotificationChannels.supported()) {
|
||||
viewModel.hasCustomNotifications().observe(getViewLifecycleOwner(), hasCustomNotifications -> {
|
||||
customNotificationsButton.setText(hasCustomNotifications ? R.string.ManageGroupActivity_on
|
||||
: R.string.ManageGroupActivity_off);
|
||||
customNotificationsButton.setText(booleanToOnOff(hasCustomNotifications));
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -343,6 +378,11 @@ public class ManageGroupFragment extends LoggingFragment {
|
|||
});
|
||||
}
|
||||
|
||||
private static int booleanToOnOff(boolean isOn) {
|
||||
return isOn ? R.string.ManageGroupActivity_on
|
||||
: R.string.ManageGroupActivity_off;
|
||||
}
|
||||
|
||||
public boolean onMenuItemSelected(@NonNull MenuItem item) {
|
||||
if (item.getItemId() == R.id.action_edit) {
|
||||
startActivity(EditProfileActivity.getIntentForGroupProfile(requireActivity(), getGroupId().requirePush()));
|
||||
|
|
|
@ -33,6 +33,7 @@ import org.thoughtcrime.securesms.groups.ui.GroupErrors;
|
|||
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
|
||||
import org.thoughtcrime.securesms.groups.ui.addmembers.AddMembersActivity;
|
||||
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupMentionSettingDialog;
|
||||
import org.thoughtcrime.securesms.groups.v2.GroupLinkUrlAndStatus;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
@ -63,6 +64,7 @@ public class ManageGroupViewModel extends ViewModel {
|
|||
private final LiveData<Boolean> canAddMembers;
|
||||
private final LiveData<List<GroupMemberEntry.FullMember>> members;
|
||||
private final LiveData<Integer> pendingMemberCount;
|
||||
private final LiveData<Integer> pendingAndRequestingCount;
|
||||
private final LiveData<String> disappearingMessageTimer;
|
||||
private final LiveData<String> memberCountSummary;
|
||||
private final LiveData<String> fullMemberCountSummary;
|
||||
|
@ -78,6 +80,7 @@ public class ManageGroupViewModel extends ViewModel {
|
|||
private final LiveData<Boolean> canBlockGroup;
|
||||
private final LiveData<Boolean> showLegacyIndicator;
|
||||
private final LiveData<String> mentionSetting;
|
||||
private final LiveData<Boolean> groupLinkOn;
|
||||
|
||||
private ManageGroupViewModel(@NonNull Context context, @NonNull ManageGroupRepository manageGroupRepository) {
|
||||
this.context = context;
|
||||
|
@ -99,6 +102,7 @@ public class ManageGroupViewModel extends ViewModel {
|
|||
memberListCollapseState,
|
||||
ManageGroupViewModel::filterMemberList);
|
||||
this.pendingMemberCount = liveGroup.getPendingMemberCount();
|
||||
this.pendingAndRequestingCount = liveGroup.getPendingAndRequestingMemberCount();
|
||||
this.showLegacyIndicator = new MutableLiveData<>(groupId.isV1() && FeatureFlags.groupsV2create());
|
||||
this.memberCountSummary = LiveDataUtil.combineLatest(liveGroup.getMembershipCountDescription(context.getResources()),
|
||||
this.showLegacyIndicator,
|
||||
|
@ -119,6 +123,7 @@ public class ManageGroupViewModel extends ViewModel {
|
|||
this.canBlockGroup = Transformations.map(this.groupRecipient, recipient -> !recipient.isBlocked());
|
||||
this.mentionSetting = Transformations.distinctUntilChanged(Transformations.map(this.groupRecipient,
|
||||
recipient -> MentionUtil.getMentionSettingDisplayValue(context, recipient.getMentionSetting())));
|
||||
this.groupLinkOn = Transformations.map(liveGroup.getGroupLink(), GroupLinkUrlAndStatus::isEnabled);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
|
@ -136,6 +141,10 @@ public class ManageGroupViewModel extends ViewModel {
|
|||
return pendingMemberCount;
|
||||
}
|
||||
|
||||
LiveData<Integer> getPendingAndRequestingCount() {
|
||||
return pendingAndRequestingCount;
|
||||
}
|
||||
|
||||
LiveData<String> getMemberCountSummary() {
|
||||
return memberCountSummary;
|
||||
}
|
||||
|
@ -216,6 +225,10 @@ public class ManageGroupViewModel extends ViewModel {
|
|||
return mentionSetting;
|
||||
}
|
||||
|
||||
LiveData<Boolean> getGroupLinkOn() {
|
||||
return groupLinkOn;
|
||||
}
|
||||
|
||||
void handleExpirationSelection() {
|
||||
manageGroupRepository.getRecipient(groupRecipient ->
|
||||
ExpirationDialog.show(context,
|
||||
|
|
|
@ -10,9 +10,16 @@ import androidx.appcompat.widget.Toolbar;
|
|||
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.ui.invitesandrequests.ManagePendingAndRequestingMembersActivity;
|
||||
import org.thoughtcrime.securesms.groups.ui.invitesandrequests.invited.PendingMemberInvitesFragment;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
|
||||
/**
|
||||
* @deprecated With group links FF, this activity is replaced with {@link ManagePendingAndRequestingMembersActivity}.
|
||||
*/
|
||||
@Deprecated
|
||||
public class PendingMemberInvitesActivity extends PassphraseRequiredActivity {
|
||||
|
||||
private static final String GROUP_ID = "GROUP_ID";
|
||||
|
@ -33,6 +40,11 @@ public class PendingMemberInvitesActivity extends PassphraseRequiredActivity {
|
|||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState, boolean ready) {
|
||||
super.onCreate(savedInstanceState, ready);
|
||||
|
||||
if (FeatureFlags.groupsV2manageGroupLinks()) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
setContentView(R.layout.group_pending_member_invites_activity);
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
package org.thoughtcrime.securesms.groups.v2;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public final class GroupLinkUrlAndStatus {
|
||||
|
||||
public static final GroupLinkUrlAndStatus NONE = new GroupLinkUrlAndStatus(false, false, "");
|
||||
|
||||
private final boolean enabled;
|
||||
private final boolean requiresApproval;
|
||||
private final String url;
|
||||
|
||||
public GroupLinkUrlAndStatus(boolean enabled,
|
||||
boolean requiresApproval,
|
||||
@NonNull String url)
|
||||
{
|
||||
this.enabled = enabled;
|
||||
this.requiresApproval = requiresApproval;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public boolean isRequiresApproval() {
|
||||
return requiresApproval;
|
||||
}
|
||||
|
||||
public @NonNull String getUrl() {
|
||||
return url;
|
||||
}
|
||||
}
|
|
@ -5,6 +5,8 @@ import androidx.annotation.Nullable;
|
|||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedMember;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||
|
@ -163,12 +165,16 @@ public final class MessageGroupContext {
|
|||
}
|
||||
|
||||
public @NonNull List<UUID> getAllActivePendingAndRemovedMembers() {
|
||||
LinkedList<UUID> memberUuids = new LinkedList<>();
|
||||
LinkedList<UUID> memberUuids = new LinkedList<>();
|
||||
DecryptedGroup groupState = decryptedGroupV2Context.getGroupState();
|
||||
DecryptedGroupChange groupChange = decryptedGroupV2Context.getChange();
|
||||
|
||||
memberUuids.addAll(DecryptedGroupUtil.membersToUuidList(decryptedGroupV2Context.getGroupState().getMembersList()));
|
||||
memberUuids.addAll(DecryptedGroupUtil.pendingToUuidList(decryptedGroupV2Context.getGroupState().getPendingMembersList()));
|
||||
memberUuids.addAll(DecryptedGroupUtil.removedMembersUuidList(decryptedGroupV2Context.getChange()));
|
||||
memberUuids.addAll(DecryptedGroupUtil.removedPendingMembersUuidList(decryptedGroupV2Context.getChange()));
|
||||
memberUuids.addAll(DecryptedGroupUtil.membersToUuidList(groupState.getMembersList()));
|
||||
memberUuids.addAll(DecryptedGroupUtil.pendingToUuidList(groupState.getPendingMembersList()));
|
||||
|
||||
memberUuids.addAll(DecryptedGroupUtil.removedMembersUuidList(groupChange));
|
||||
memberUuids.addAll(DecryptedGroupUtil.removedPendingMembersUuidList(groupChange));
|
||||
memberUuids.addAll(DecryptedGroupUtil.removedRequestingMembersUuidList(groupChange));
|
||||
|
||||
return UuidUtil.filterKnown(memberUuids);
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ package org.thoughtcrime.securesms.qr;
|
|||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
|
@ -15,6 +17,10 @@ public class QrCode {
|
|||
public static final String TAG = QrCode.class.getSimpleName();
|
||||
|
||||
public static @NonNull Bitmap create(String data) {
|
||||
return create(data, Color.BLACK);
|
||||
}
|
||||
|
||||
public static @NonNull Bitmap create(String data, @ColorInt int foregroundColor) {
|
||||
try {
|
||||
BitMatrix result = new QRCodeWriter().encode(data, BarcodeFormat.QR_CODE, 512, 512);
|
||||
Bitmap bitmap = Bitmap.createBitmap(result.getWidth(), result.getHeight(), Bitmap.Config.ARGB_8888);
|
||||
|
@ -22,7 +28,7 @@ public class QrCode {
|
|||
for (int y = 0; y < result.getHeight(); y++) {
|
||||
for (int x = 0; x < result.getWidth(); x++) {
|
||||
if (result.get(x, y)) {
|
||||
bitmap.setPixel(x, y, Color.BLACK);
|
||||
bitmap.setPixel(x, y, foregroundColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
package org.thoughtcrime.securesms.recipients.ui.sharablegrouplink;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.ShareCompat;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.LiveGroup;
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class GroupLinkBottomSheetDialogFragment extends BottomSheetDialogFragment {
|
||||
|
||||
public static final String ARG_GROUP_ID = "group_id";
|
||||
|
||||
public static void show(@NonNull FragmentManager manager, @NonNull GroupId.V2 groupId) {
|
||||
GroupLinkBottomSheetDialogFragment fragment = new GroupLinkBottomSheetDialogFragment();
|
||||
Bundle args = new Bundle();
|
||||
|
||||
args.putString(ARG_GROUP_ID, groupId.toString());
|
||||
|
||||
fragment.setArguments(args);
|
||||
fragment.show(manager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
setStyle(DialogFragment.STYLE_NORMAL,
|
||||
ThemeUtil.isDarkTheme(requireContext()) ? R.style.Theme_Signal_RoundedBottomSheet
|
||||
: R.style.Theme_Signal_RoundedBottomSheet_Light);
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.group_link_share_bottom_sheet, container, false);
|
||||
|
||||
View shareViaSignalButton = view.findViewById(R.id.group_link_bottom_sheet_share_via_signal_button);
|
||||
View copyButton = view.findViewById(R.id.group_link_bottom_sheet_copy_button);
|
||||
View viewQrButton = view.findViewById(R.id.group_link_bottom_sheet_qr_code_button);
|
||||
View shareBySystemButton = view.findViewById(R.id.group_link_bottom_sheet_share_via_system_button);
|
||||
|
||||
GroupId.V2 groupId = GroupId.parseOrThrow(Objects.requireNonNull(requireArguments().getString(ARG_GROUP_ID))).requireV2();
|
||||
|
||||
LiveGroup liveGroup = new LiveGroup(groupId);
|
||||
|
||||
liveGroup.getGroupLink().observe(getViewLifecycleOwner(), groupLink -> {
|
||||
if (!groupLink.isEnabled()) {
|
||||
Toast.makeText(requireContext(), R.string.GroupLinkBottomSheet_the_link_is_not_currently_active, Toast.LENGTH_SHORT).show();
|
||||
dismiss();
|
||||
return;
|
||||
}
|
||||
|
||||
shareViaSignalButton.setOnClickListener(v -> dismiss()); // Todo [Alan] GV2 Add share within signal
|
||||
shareViaSignalButton.setVisibility(View.GONE);
|
||||
|
||||
copyButton.setOnClickListener(v -> {
|
||||
Context context = requireContext();
|
||||
Util.copyToClipboard(context, groupLink.getUrl());
|
||||
Toast.makeText(context, R.string.GroupLinkBottomSheet_copied_to_clipboard, Toast.LENGTH_SHORT).show();
|
||||
dismiss();
|
||||
});
|
||||
|
||||
viewQrButton.setOnClickListener(v -> dismiss()); // Todo [Alan] GV2 Add share QR within signal
|
||||
viewQrButton.setVisibility(View.GONE);
|
||||
|
||||
shareBySystemButton.setOnClickListener(v -> {
|
||||
ShareCompat.IntentBuilder.from(requireActivity())
|
||||
.setType("text/plain")
|
||||
.setText(groupLink.getUrl())
|
||||
.startChooser();
|
||||
|
||||
dismiss();
|
||||
});
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void show(@NonNull FragmentManager manager, @Nullable String tag) {
|
||||
BottomSheetUtil.show(manager, tag, this);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
package org.thoughtcrime.securesms.recipients.ui.sharablegrouplink;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.SwitchCompat;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
||||
|
||||
public final class ShareableGroupLinkDialogFragment extends DialogFragment {
|
||||
|
||||
private static final String ARG_GROUP_ID = "group_id";
|
||||
|
||||
private ShareableGroupLinkViewModel viewModel;
|
||||
private GroupId.V2 groupId;
|
||||
private SimpleProgressDialog.DismissibleDialog dialog;
|
||||
|
||||
public static DialogFragment create(@NonNull GroupId.V2 groupId) {
|
||||
DialogFragment fragment = new ShareableGroupLinkDialogFragment();
|
||||
Bundle args = new Bundle();
|
||||
|
||||
args.putString(ARG_GROUP_ID, groupId.toString());
|
||||
fragment.setArguments(args);
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setStyle(STYLE_NO_FRAME, ThemeUtil.isDarkTheme(requireActivity()) ? R.style.TextSecure_DarkTheme
|
||||
: R.style.TextSecure_LightTheme);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
|
||||
@Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState)
|
||||
{
|
||||
return inflater.inflate(R.layout.shareable_group_link_dialog_fragment, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
initializeViewModel();
|
||||
initializeViews(view);
|
||||
}
|
||||
|
||||
private void initializeViewModel() {
|
||||
//noinspection ConstantConditions
|
||||
groupId = GroupId.parseOrThrow(requireArguments().getString(ARG_GROUP_ID)).requireV2();
|
||||
|
||||
ShareableGroupLinkRepository repository = new ShareableGroupLinkRepository(requireContext(), groupId);
|
||||
ShareableGroupLinkViewModel.Factory factory = new ShareableGroupLinkViewModel.Factory(groupId, repository);
|
||||
|
||||
viewModel = ViewModelProviders.of(this, factory).get(ShareableGroupLinkViewModel.class);
|
||||
}
|
||||
|
||||
private void initializeViews(@NonNull View view) {
|
||||
SwitchCompat shareableGroupLinkSwitch = view.findViewById(R.id.shareable_group_link_enable_switch);
|
||||
TextView shareableGroupLinkDisplay = view.findViewById(R.id.shareable_group_link_display);
|
||||
SwitchCompat approveNewMembersSwitch = view.findViewById(R.id.shareable_group_link_approve_new_members_switch);
|
||||
View shareableGroupLinkRow = view.findViewById(R.id.shareable_group_link_row);
|
||||
View shareRow = view.findViewById(R.id.shareable_group_link_share_row);
|
||||
View resetLinkRow = view.findViewById(R.id.shareable_group_link_reset_link_row);
|
||||
View approveNewMembersRow = view.findViewById(R.id.shareable_group_link_approve_new_members_row);
|
||||
|
||||
Toolbar toolbar = view.findViewById(R.id.shareable_group_link_toolbar);
|
||||
|
||||
toolbar.setNavigationOnClickListener(v -> dismissAllowingStateLoss());
|
||||
|
||||
viewModel.getGroupLink().observe(getViewLifecycleOwner(), groupLink -> {
|
||||
shareableGroupLinkSwitch.setChecked(groupLink.isEnabled());
|
||||
approveNewMembersSwitch.setChecked(groupLink.isRequiresApproval());
|
||||
shareableGroupLinkDisplay.setText(groupLink.getUrl());
|
||||
});
|
||||
|
||||
shareRow.setOnClickListener(v -> GroupLinkBottomSheetDialogFragment.show(requireFragmentManager(), groupId));
|
||||
|
||||
shareableGroupLinkRow.setOnClickListener(v -> viewModel.onToggleGroupLink(requireContext()));
|
||||
approveNewMembersRow.setOnClickListener(v -> viewModel.onToggleApproveMembers(requireContext()));
|
||||
resetLinkRow.setOnClickListener(v -> viewModel.onResetLink(requireContext()));
|
||||
|
||||
viewModel.getToasts().observe(getViewLifecycleOwner(), t -> Toast.makeText(requireContext(), t, Toast.LENGTH_SHORT).show());
|
||||
|
||||
viewModel.getBusy().observe(getViewLifecycleOwner(), busy -> {
|
||||
if (busy) {
|
||||
if (dialog == null) {
|
||||
dialog = SimpleProgressDialog.showDelayed(requireContext());
|
||||
}
|
||||
} else {
|
||||
if (dialog != null) {
|
||||
dialog.dismiss();
|
||||
dialog = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
package org.thoughtcrime.securesms.recipients.ui.sharablegrouplink;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.signal.storageservice.protos.groups.AccessControl;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.groups.GroupChangeBusyException;
|
||||
import org.thoughtcrime.securesms.groups.GroupChangeFailedException;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.GroupInsufficientRightsException;
|
||||
import org.thoughtcrime.securesms.groups.GroupManager;
|
||||
import org.thoughtcrime.securesms.groups.GroupNotAMemberException;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
|
||||
import org.thoughtcrime.securesms.util.AsynchronousCallback;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
final class ShareableGroupLinkRepository {
|
||||
|
||||
private final Context context;
|
||||
private final GroupId.V2 groupId;
|
||||
|
||||
ShareableGroupLinkRepository(@NonNull Context context, @NonNull GroupId.V2 groupId) {
|
||||
this.context = context;
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
void cycleGroupLinkPassword(@NonNull AsynchronousCallback.WorkerThread<Void, GroupChangeFailureReason> callback) {
|
||||
SignalExecutors.UNBOUNDED.execute(() -> {
|
||||
try {
|
||||
GroupManager.cycleGroupLinkPassword(context, groupId);
|
||||
callback.onComplete(null);
|
||||
} catch (GroupNotAMemberException | GroupChangeFailedException | GroupInsufficientRightsException | IOException | GroupChangeBusyException e) {
|
||||
callback.onError(GroupChangeFailureReason.fromException(e));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void toggleGroupLinkEnabled(@NonNull AsynchronousCallback.WorkerThread<Void, GroupChangeFailureReason> callback) {
|
||||
setGroupLinkEnabledState(toggleGroupLinkState(true, false), callback);
|
||||
}
|
||||
|
||||
void toggleGroupLinkApprovalRequired(@NonNull AsynchronousCallback.WorkerThread<Void, GroupChangeFailureReason> callback) {
|
||||
setGroupLinkEnabledState(toggleGroupLinkState(false, true), callback);
|
||||
}
|
||||
|
||||
private void setGroupLinkEnabledState(@NonNull GroupManager.GroupLinkState state,
|
||||
@NonNull AsynchronousCallback.WorkerThread<Void, GroupChangeFailureReason> callback)
|
||||
{
|
||||
SignalExecutors.UNBOUNDED.execute(() -> {
|
||||
try {
|
||||
GroupManager.setGroupLinkEnabledState(context, groupId, state);
|
||||
callback.onComplete(null);
|
||||
} catch (GroupNotAMemberException | GroupChangeFailedException | GroupInsufficientRightsException | IOException | GroupChangeBusyException e) {
|
||||
callback.onError(GroupChangeFailureReason.fromException(e));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private GroupManager.GroupLinkState toggleGroupLinkState(boolean toggleEnabled, boolean toggleApprovalNeeded) {
|
||||
AccessControl.AccessRequired currentState = DatabaseFactory.getGroupDatabase(context)
|
||||
.getGroup(groupId)
|
||||
.get()
|
||||
.requireV2GroupProperties()
|
||||
.getDecryptedGroup()
|
||||
.getAccessControl()
|
||||
.getAddFromInviteLink();
|
||||
|
||||
boolean enabled;
|
||||
boolean approvalNeeded;
|
||||
|
||||
switch (currentState) {
|
||||
case UNKNOWN:
|
||||
case UNSATISFIABLE:
|
||||
case UNRECOGNIZED:
|
||||
case MEMBER:
|
||||
enabled = false;
|
||||
approvalNeeded = false;
|
||||
break;
|
||||
case ANY:
|
||||
enabled = true;
|
||||
approvalNeeded = false;
|
||||
break;
|
||||
case ADMINISTRATOR:
|
||||
enabled = true;
|
||||
approvalNeeded = true;
|
||||
break;
|
||||
default: throw new AssertionError();
|
||||
}
|
||||
|
||||
if (toggleApprovalNeeded) {
|
||||
approvalNeeded = !approvalNeeded;
|
||||
}
|
||||
|
||||
if (toggleEnabled) {
|
||||
enabled = !enabled;
|
||||
if (enabled) approvalNeeded = true;
|
||||
}
|
||||
|
||||
if (approvalNeeded && enabled) {
|
||||
return GroupManager.GroupLinkState.ENABLED_WITH_APPROVAL;
|
||||
} else {
|
||||
if (enabled) {
|
||||
return GroupManager.GroupLinkState.ENABLED;
|
||||
}
|
||||
}
|
||||
|
||||
return GroupManager.GroupLinkState.DISABLED;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
package org.thoughtcrime.securesms.recipients.ui.sharablegrouplink;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.LiveGroup;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupErrors;
|
||||
import org.thoughtcrime.securesms.groups.v2.GroupLinkUrlAndStatus;
|
||||
import org.thoughtcrime.securesms.util.AsynchronousCallback;
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||
|
||||
final class ShareableGroupLinkViewModel extends ViewModel {
|
||||
|
||||
private final ShareableGroupLinkRepository repository;
|
||||
private final LiveData<GroupLinkUrlAndStatus> groupLink;
|
||||
private final SingleLiveEvent<String> toasts;
|
||||
private final SingleLiveEvent<Boolean> busy;
|
||||
|
||||
private ShareableGroupLinkViewModel(@NonNull GroupId.V2 groupId, @NonNull ShareableGroupLinkRepository repository) {
|
||||
this.repository = repository;
|
||||
this.groupLink = new LiveGroup(groupId).getGroupLink();
|
||||
this.toasts = new SingleLiveEvent<>();
|
||||
this.busy = new SingleLiveEvent<>();
|
||||
}
|
||||
|
||||
LiveData<GroupLinkUrlAndStatus> getGroupLink() {
|
||||
return groupLink;
|
||||
}
|
||||
|
||||
LiveData<String> getToasts() {
|
||||
return toasts;
|
||||
}
|
||||
|
||||
LiveData<Boolean> getBusy() {
|
||||
return busy;
|
||||
}
|
||||
|
||||
void onToggleGroupLink(@NonNull Context context) {
|
||||
busy.setValue(true);
|
||||
repository.toggleGroupLinkEnabled(new AsynchronousCallback.WorkerThread<Void, GroupChangeFailureReason>() {
|
||||
@Override
|
||||
public void onComplete(@Nullable Void result) {
|
||||
busy.postValue(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(@Nullable GroupChangeFailureReason error) {
|
||||
busy.postValue(false);
|
||||
toasts.postValue(context.getString(GroupErrors.getUserDisplayMessage(error)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void onToggleApproveMembers(@NonNull Context context) {
|
||||
busy.setValue(true);
|
||||
repository.toggleGroupLinkApprovalRequired(new AsynchronousCallback.WorkerThread<Void, GroupChangeFailureReason>() {
|
||||
@Override
|
||||
public void onComplete(@Nullable Void result) {
|
||||
busy.postValue(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(@Nullable GroupChangeFailureReason error) {
|
||||
busy.postValue(false);
|
||||
toasts.postValue(context.getString(GroupErrors.getUserDisplayMessage(error)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void onResetLink(@NonNull Context context) {
|
||||
busy.setValue(true);
|
||||
repository.cycleGroupLinkPassword(new AsynchronousCallback.WorkerThread<Void, GroupChangeFailureReason>() {
|
||||
@Override
|
||||
public void onComplete(@Nullable Void result) {
|
||||
busy.postValue(false);
|
||||
toasts.postValue(context.getString(R.string.ShareableGroupLinkDialogFragment__group_link_reset));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(@Nullable GroupChangeFailureReason error) {
|
||||
busy.postValue(false);
|
||||
toasts.postValue(context.getString(GroupErrors.getUserDisplayMessage(error)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static final class Factory implements ViewModelProvider.Factory {
|
||||
|
||||
private final GroupId.V2 groupId;
|
||||
private final ShareableGroupLinkRepository repository;
|
||||
|
||||
public Factory(@NonNull GroupId.V2 groupId, @NonNull ShareableGroupLinkRepository repository) {
|
||||
this.groupId = groupId;
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
//noinspection ConstantConditions
|
||||
return modelClass.cast(new ShareableGroupLinkViewModel(groupId, repository));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -57,6 +57,7 @@ public final class FeatureFlags {
|
|||
private static final String GROUPS_V2 = "android.groupsv2.3";
|
||||
private static final String GROUPS_V2_CREATE = "android.groupsv2.create.3";
|
||||
private static final String GROUPS_V2_JOIN_VERSION = "android.groupsv2.joinVersion";
|
||||
private static final String GROUPS_V2_LINKS_VERSION = "android.groupsv2.manageGroupLinksVersion";
|
||||
private static final String GROUPS_V2_CAPACITY = "global.groupsv2.maxGroupSize";
|
||||
private static final String CDS = "android.cds.4";
|
||||
private static final String INTERNAL_USER = "android.internalUser";
|
||||
|
@ -208,6 +209,11 @@ public final class FeatureFlags {
|
|||
!SignalStore.internalValues().gv2DoNotCreateGv2Groups();
|
||||
}
|
||||
|
||||
/** Allow creation and managing of group links. */
|
||||
public static boolean groupsV2manageGroupLinks() {
|
||||
return groupsV2() && getVersionFlag(GROUPS_V2_LINKS_VERSION) == VersionFlag.ON;
|
||||
}
|
||||
|
||||
private static boolean groupsV2LatestFlag() {
|
||||
return getBoolean(GROUPS_V2, false);
|
||||
}
|
||||
|
@ -231,11 +237,12 @@ public final class FeatureFlags {
|
|||
* You must still check GV2 capabilities to respect linked devices.
|
||||
*/
|
||||
public static GroupJoinStatus clientLocalGroupJoinStatus() {
|
||||
int groupJoinVersion = getInteger(GROUPS_V2_JOIN_VERSION, 0);
|
||||
|
||||
if (groupJoinVersion == 0) return GroupJoinStatus.COMING_SOON;
|
||||
else if (groupJoinVersion > BuildConfig.CANONICAL_VERSION_CODE) return GroupJoinStatus.UPDATE_TO_JOIN;
|
||||
else return GroupJoinStatus.LOCAL_CAN_JOIN;
|
||||
switch (getVersionFlag(GROUPS_V2_JOIN_VERSION)) {
|
||||
case ON_IN_FUTURE_VERSION: return GroupJoinStatus.UPDATE_TO_JOIN;
|
||||
case ON : return GroupJoinStatus.LOCAL_CAN_JOIN;
|
||||
case OFF :
|
||||
default : return GroupJoinStatus.COMING_SOON;
|
||||
}
|
||||
}
|
||||
|
||||
public enum GroupJoinStatus {
|
||||
|
@ -385,6 +392,31 @@ public final class FeatureFlags {
|
|||
return changes;
|
||||
}
|
||||
|
||||
private static @NonNull VersionFlag getVersionFlag(@NonNull String key) {
|
||||
int versionFromKey = getInteger(key, 0);
|
||||
|
||||
if (versionFromKey == 0) {
|
||||
return VersionFlag.OFF;
|
||||
}
|
||||
|
||||
if (BuildConfig.CANONICAL_VERSION_CODE >= versionFromKey) {
|
||||
return VersionFlag.ON;
|
||||
} else {
|
||||
return VersionFlag.ON_IN_FUTURE_VERSION;
|
||||
}
|
||||
}
|
||||
|
||||
private enum VersionFlag {
|
||||
/** The flag is no set */
|
||||
OFF,
|
||||
|
||||
/** The flag is set on for a version higher than the current client version */
|
||||
ON_IN_FUTURE_VERSION,
|
||||
|
||||
/** The flag is set on for this version or earlier */
|
||||
ON
|
||||
}
|
||||
|
||||
private static boolean getBoolean(@NonNull String key, boolean defaultValue) {
|
||||
Boolean forced = (Boolean) FORCED_VALUES.get(key);
|
||||
if (forced != null) {
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="@color/core_ultramarine" />
|
||||
</shape>
|
|
@ -0,0 +1,12 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="28dp"
|
||||
android:height="28dp"
|
||||
android:viewportWidth="28"
|
||||
android:viewportHeight="28">
|
||||
<path
|
||||
android:fillColor="?icon_tint"
|
||||
android:pathData="M14,2.5A11.5,11.5 0,1 1,2.5 14,11.6 11.6,0 0,1 14,2.5M14,1A13,13 0,1 0,27 14,13 13,0 0,0 14,1Z" />
|
||||
<path
|
||||
android:fillColor="?icon_tint"
|
||||
android:pathData="M12,19.1l-4.5,-4.6l1,-1l3.5,3.4l7.5,-7.4l1,1l-8.5,8.6z" />
|
||||
</vector>
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:autoMirrored="true"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?icon_tint"
|
||||
android:pathData="M15.95,19.5A3,3 0,0 1,13 22H6a3,3 0,0 1,-3 -3V9A3,3 0,0 1,6 6h0.5V7.5H6A1.5,1.5 0,0 0,4.5 9V19A1.5,1.5 0,0 0,6 20.5h7a1.5,1.5 0,0 0,1.408 -1ZM18,3.5H11A1.5,1.5 0,0 0,9.5 5V15A1.5,1.5 0,0 0,11 16.5h7A1.5,1.5 0,0 0,19.5 15V5A1.5,1.5 0,0 0,18 3.5M18,2a3,3 0,0 1,3 3V15a3,3 0,0 1,-3 3H11a3,3 0,0 1,-3 -3V5a3,3 0,0 1,3 -3Z" />
|
||||
</vector>
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:autoMirrored="true"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?icon_tint"
|
||||
android:pathData="M21,5V15a3,3 0,0 1,-3 3H11a3,3 0,0 1,-3 -3V5a3,3 0,0 1,3 -3h7A3,3 0,0 1,21 5ZM11,19.5A4.505,4.505 0,0 1,6.5 15V6H6A3,3 0,0 0,3 9V19a3,3 0,0 0,3 3h7a3,3 0,0 0,2.95 -2.5Z" />
|
||||
</vector>
|
|
@ -0,0 +1,12 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="28dp"
|
||||
android:height="28dp"
|
||||
android:viewportWidth="28"
|
||||
android:viewportHeight="28">
|
||||
<path
|
||||
android:fillColor="?icon_tint"
|
||||
android:pathData="M14,2.5A11.5,11.5 0,1 1,2.5 14,11.6 11.6,0 0,1 14,2.5M14,1A13,13 0,1 0,27 14,13 13,0 0,0 14,1Z" />
|
||||
<path
|
||||
android:fillColor="?icon_tint"
|
||||
android:pathData="M19,10l-1,-1l-4,3.9l-4,-3.9l-1,1l3.9,4l-3.9,4l1,1l4,-3.9l4,3.9l1,-1l-3.9,-4l3.9,-4z" />
|
||||
</vector>
|
|
@ -0,0 +1,36 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?icon_tint"
|
||||
android:pathData="M3,3V8.2H8.2V3ZM6.8,6.8H4.5V4.5H6.8Z" />
|
||||
<path
|
||||
android:fillColor="?icon_tint"
|
||||
android:pathData="M3,15.8L3,21L8.2,21L8.2,15.8ZM6.8,19.5L4.5,19.5L4.5,17.3L6.8,17.3Z" />
|
||||
<path
|
||||
android:fillColor="?icon_tint"
|
||||
android:pathData="M15.8,3L15.8,8.2L21,8.2L21,3ZM19.5,6.8L17.3,6.8L17.3,4.5h2.2Z" />
|
||||
<path
|
||||
android:fillColor="?icon_tint"
|
||||
android:pathData="M19.5,9.8v4.4H15.8v1.5H21V9.8Z" />
|
||||
<path
|
||||
android:fillColor="?icon_tint"
|
||||
android:pathData="M15.8,17.2V21h1.5V18.7h2.2V21H21V17.2Z" />
|
||||
<path
|
||||
android:fillColor="?icon_tint"
|
||||
android:pathData="M9.8,3V4.5h2.9V8.2h1.5V3Z" />
|
||||
<path
|
||||
android:fillColor="?icon_tint"
|
||||
android:pathData="M12.7,9.8v3H9.8v4.5h2.9V21h1.5V15.7H11.3V14.2h2.9V11.3h1.6v1.5h1.4v-3Z" />
|
||||
<path
|
||||
android:fillColor="?icon_tint"
|
||||
android:pathData="M11.3,18.8H9.8V21h1.5Z" />
|
||||
<path
|
||||
android:fillColor="?icon_tint"
|
||||
android:pathData="M8.2,12.8H6v1.4H8.2Z" />
|
||||
<path
|
||||
android:fillColor="?icon_tint"
|
||||
android:pathData="M9.8,6V9.8H3v4.5H4.5v-3h6.8V6Z" />
|
||||
</vector>
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?icon_tint"
|
||||
android:pathData="M12,18.8A6.9,6.9 0,0 1,5.2 12,6.9 6.9,0 0,1 12,5.2a6.5,6.5 0,0 1,4.2 1.6L14,9l6.2,0.8 -0.7,-6.3L17.3,5.7A7.8,7.8 0,0 0,12 3.8a8.2,8.2 0,0 0,0 16.4,8.3 8.3,0 0,0 7.9,-5.7H18.3A7,7 0,0 1,12 18.8Z" />
|
||||
</vector>
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?icon_tint"
|
||||
android:pathData="M18,16.2a3,3 0,0 0,-1.9 0.8L8.6,12.8a2.4,2.4 0,0 0,0.2 -0.8c0,-0.2 -0.1,-0.5 -0.1,-0.7L16.1,7a2.7,2.7 0,0 0,1.9 0.8A2.9,2.9 0,0 0,20.8 5a2.8,2.8 0,1 0,-5.6 0c0,0.2 0.1,0.5 0.1,0.7L7.9,10A2.7,2.7 0,0 0,6 9.2,2.9 2.9,0 0,0 3.2,12 2.9,2.9 0,0 0,6 14.8a2.9,2.9 0,0 0,1.8 -0.7l7.5,4.3c0,0.2 -0.1,0.4 -0.1,0.6A2.8,2.8 0,1 0,18 16.2Z" />
|
||||
</vector>
|
|
@ -22,9 +22,8 @@
|
|||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/CustomNotificationsDialogFragment__messages"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body2"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body2.Bold"
|
||||
android:textColor="?attr/colorAccent"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/custom_notifications_toolbar" />
|
||||
|
@ -39,7 +38,6 @@
|
|||
android:orientation="horizontal"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:text="@string/CustomNotificationsDialogFragment__notification_sound"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/custom_notifications_message_section_header">
|
||||
|
||||
|
@ -77,7 +75,6 @@
|
|||
android:orientation="horizontal"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:text="@string/CustomNotificationsDialogFragment__notification_sound"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/custom_notifications_row">
|
||||
|
||||
|
@ -116,7 +113,6 @@
|
|||
android:orientation="horizontal"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:text="@string/CustomNotificationsDialogFragment__notification_sound"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/custom_notifications_sound_row">
|
||||
|
||||
|
@ -159,9 +155,8 @@
|
|||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="36dp"
|
||||
android:text="@string/CustomNotificationsDialogFragment__call_settings"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body2"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body2.Bold"
|
||||
android:textColor="?attr/colorAccent"
|
||||
android:textStyle="bold"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
@ -178,7 +173,6 @@
|
|||
android:orientation="horizontal"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:text="@string/CustomNotificationsDialogFragment__notification_sound"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/custom_notifications_call_settings_section_header"
|
||||
|
@ -215,7 +209,6 @@
|
|||
android:orientation="horizontal"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:text="@string/CustomNotificationsDialogFragment__notification_sound"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/custom_notifications_ringtone_row"
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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"
|
||||
tools:theme="@style/Theme.Signal.RoundedBottomSheet.Light">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<Button
|
||||
android:id="@+id/group_link_bottom_sheet_share_via_signal_button"
|
||||
style="@style/Widget.Signal.Button.TextButton.Drawable"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="20dp"
|
||||
android:text="@string/GroupLinkBottomSheet_share_via_signal"
|
||||
app:drawableStartCompat="?message_icon" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/group_link_bottom_sheet_copy_button"
|
||||
style="@style/Widget.Signal.Button.TextButton.Drawable"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="20dp"
|
||||
android:text="@string/GroupLinkBottomSheet_copy"
|
||||
app:drawableStartCompat="?copy_icon" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/group_link_bottom_sheet_qr_code_button"
|
||||
style="@style/Widget.Signal.Button.TextButton.Drawable"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="20dp"
|
||||
android:text="@string/GroupLinkBottomSheet_qr_code"
|
||||
app:drawableStartCompat="?qr_icon" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/group_link_bottom_sheet_share_via_system_button"
|
||||
style="@style/Widget.Signal.Button.TextButton.Drawable"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="20dp"
|
||||
android:text="@string/GroupLinkBottomSheet_share"
|
||||
app:drawableStartCompat="?share_icon" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -509,13 +509,103 @@
|
|||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/group_membership_card"
|
||||
android:id="@+id/group_link_card"
|
||||
style="@style/Widget.Signal.CardView.PreferenceRow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/group_manage_fragment_card_vertical_padding"
|
||||
app:layout_constraintTop_toBottomOf="@id/group_access_control_card">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/pending_and_requesting_members_row"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/group_manage_fragment_row_height"
|
||||
android:background="?selectableItemBackground"
|
||||
android:baselineAligned="false"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:paddingEnd="@dimen/group_manage_fragment_row_horizontal_padding">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_vertical"
|
||||
android:text="@string/ManageGroupActivity_member_requests_and_invites"
|
||||
android:textAppearance="@style/Signal.Text.Body" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pending_and_requesting_members_count"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:background="@drawable/circle_ultramarine"
|
||||
android:gravity="center"
|
||||
android:minWidth="22dp"
|
||||
android:minHeight="22dp"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="4dp"
|
||||
android:textAppearance="@style/Signal.Text.Caption"
|
||||
android:textColor="@color/core_white"
|
||||
tools:text="3" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/group_link_row"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/group_manage_fragment_row_height"
|
||||
android:background="?selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical|start"
|
||||
android:paddingStart="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:paddingEnd="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:text="@string/ManageGroupActivity_sharable_group_link"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/Signal.Text.Body" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/group_link_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_vertical|end"
|
||||
android:paddingStart="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:paddingEnd="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textColor="@color/ultramarine_text_button"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/group_custom_notifications"
|
||||
app:layout_constraintTop_toBottomOf="@id/group_mute_notifications_switch"
|
||||
tools:text="Off" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/group_membership_card"
|
||||
style="@style/Widget.Signal.CardView.PreferenceRow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/group_manage_fragment_card_vertical_padding"
|
||||
app:layout_constraintTop_toBottomOf="@id/group_link_card">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
<?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="org.thoughtcrime.securesms.groups.ui.invitesandrequests.ManagePendingAndRequestingMembersActivity">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/toolbar_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/windowBackground"
|
||||
app:elevation="0dp">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?actionBarSize"
|
||||
android:background="@android:color/transparent"
|
||||
app:title="@string/PendingMemberInvitesActivity_pending_group_invites"
|
||||
app:titleTextColor="?title_text_color_primary" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/pending_and_requesting_tabs"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/pending_and_requesting_pager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</LinearLayout>
|
|
@ -7,7 +7,7 @@
|
|||
android:background="?pref_divider"
|
||||
android:fillViewport="true"
|
||||
android:paddingTop="0dp"
|
||||
tools:context="org.thoughtcrime.securesms.groups.ui.pendingmemberinvites.PendingMemberInvitesFragment">
|
||||
tools:context="org.thoughtcrime.securesms.groups.ui.invitesandrequests.invited.PendingMemberInvitesFragment">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/pendingmemberinvites"
|
||||
|
@ -31,7 +31,7 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/PendingMembersActivity_people_you_invited" />
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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="64dp"
|
||||
android:background="?selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<org.thoughtcrime.securesms.components.AvatarImageView
|
||||
android:id="@+id/recipient_avatar"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginStart="16dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatCheckBox
|
||||
android:id="@+id/recipient_selected"
|
||||
android:layout_width="22dp"
|
||||
android:layout_height="22dp"
|
||||
android:background="?attr/contact_selection_checkbox_background"
|
||||
android:button="@null"
|
||||
android:clickable="false"
|
||||
android:focusable="false"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/recipient_avatar"
|
||||
app:layout_constraintEnd_toEndOf="@+id/recipient_avatar"
|
||||
tools:checked="true" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/recipient_name"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:gravity="start|center_vertical"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/recipient_avatar"
|
||||
app:layout_constraintEnd_toStartOf="@+id/request_deny"
|
||||
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" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/request_approve"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:color/transparent"
|
||||
android:contentDescription="@string/GroupRecipientListItem_approve_description"
|
||||
android:padding="10dp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/recipient_avatar"
|
||||
app:layout_constraintEnd_toStartOf="@+id/popupMenuProgressContainer"
|
||||
app:layout_constraintTop_toTopOf="@+id/recipient_avatar"
|
||||
app:srcCompat="@drawable/ic_check_28_tinted" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/request_deny"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:color/transparent"
|
||||
android:contentDescription="@string/GroupRecipientListItem_deny_description"
|
||||
android:padding="10dp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/recipient_avatar"
|
||||
app:layout_constraintEnd_toStartOf="@+id/request_approve"
|
||||
app:layout_constraintTop_toTopOf="@+id/recipient_avatar"
|
||||
app:srcCompat="@drawable/ic_deny_28_tinted" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/popupMenuProgressContainer"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:visibility="visible">
|
||||
|
||||
<org.thoughtcrime.securesms.groups.ui.PopupMenuView
|
||||
android:id="@+id/popupMenu"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:visibility="gone"
|
||||
app:background_tint="?title_text_color_primary"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/menuBusyProgress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,76 @@
|
|||
<?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="?pref_divider"
|
||||
android:fillViewport="true"
|
||||
android:paddingTop="0dp"
|
||||
tools:context="org.thoughtcrime.securesms.groups.ui.invitesandrequests.invited.PendingMemberInvitesFragment">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/cardView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardBackgroundColor="?android:windowBackground"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
style="@style/TextAppearance.Signal.Subtitle2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/RequestingMembersFragment_pending_member_requests" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/no_requesting"
|
||||
style="@style/TextAppearance.Signal.Body2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:text="@string/RequestingMembersFragment_no_member_requests_to_show"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<org.thoughtcrime.securesms.groups.ui.GroupMemberListView
|
||||
android:id="@+id/requesting_members"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:listitem="@layout/group_recipient_requesting_list_item"
|
||||
tools:maxHeight="192dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/requesting_members_explain"
|
||||
style="@style/TextAppearance.Signal.Caption"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/RequestingMembersFragment_explanation" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
|
@ -0,0 +1,203 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/shareable_group_link_toolbar"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:navigationIcon="@drawable/ic_arrow_left_24"
|
||||
app:title="@string/ShareableGroupLinkDialogFragment__shareable_group_link" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/shareable_group_link_manage_and_share_section_header"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/ShareableGroupLinkDialogFragment__manage_and_share"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body2.Bold"
|
||||
android:textColor="?attr/colorAccent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/shareable_group_link_toolbar" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/shareable_group_link_row"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:background="?selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/shareable_group_link_manage_and_share_section_header">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/shareable_group_link_enable_label"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical|start"
|
||||
android:text="@string/ShareableGroupLinkDialogFragment__group_link"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body2" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/shareable_group_link_enable_switch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:clickable="false"
|
||||
app:layout_constraintBottom_toBottomOf="@id/shareable_group_link_enable_label"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/shareable_group_link_enable_label"
|
||||
app:layout_constraintTop_toTopOf="@id/shareable_group_link_enable_label" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/shareable_group_link_display_row"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/shareable_group_link_row">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/shareable_group_link_display"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_weight="1"
|
||||
android:enabled="false"
|
||||
android:gravity="center_vertical|start"
|
||||
android:text="@string/ShareableGroupLinkDialogFragment__share"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body2"
|
||||
tools:text="https://signal.org/7836478267" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/shareable_group_link_share_row"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:background="?selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="36dp"
|
||||
android:paddingEnd="12dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/shareable_group_link_display_row">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/shareable_group_link_share_label"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="51dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_weight="1"
|
||||
android:drawablePadding="8dp"
|
||||
android:enabled="false"
|
||||
android:gravity="center_vertical|start"
|
||||
android:text="@string/ShareableGroupLinkDialogFragment__share"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body2"
|
||||
app:drawableStartCompat="?share_icon" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/shareable_group_link_reset_link_row"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:background="?selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="36dp"
|
||||
android:paddingEnd="12dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/shareable_group_link_share_row">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/shareable_group_link_reset_link_label"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:layout_weight="1"
|
||||
android:drawablePadding="8dp"
|
||||
android:enabled="false"
|
||||
android:gravity="center_vertical|start"
|
||||
android:text="@string/ShareableGroupLinkDialogFragment__reset_link"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body2"
|
||||
app:drawableStartCompat="?reset_link_icon" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/shareable_group_link_member_requests_section_header"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="36dp"
|
||||
android:text="@string/ShareableGroupLinkDialogFragment__member_requests"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body2.Bold"
|
||||
android:textColor="?attr/colorAccent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/shareable_group_link_reset_link_row" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/shareable_group_link_approve_new_members_row"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:background="?selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/shareable_group_link_member_requests_section_header">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical|start"
|
||||
android:text="@string/ShareableGroupLinkDialogFragment__approve_new_members"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body2" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/shareable_group_link_approve_new_members_switch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:clickable="false"
|
||||
app:layout_constraintBottom_toBottomOf="@id/shareable_group_link_enable_label"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/shareable_group_link_enable_label"
|
||||
app:layout_constraintTop_toTopOf="@id/shareable_group_link_enable_label" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -190,6 +190,11 @@
|
|||
<attr name="invite_edit_text_background" format="reference" />
|
||||
<attr name="invite_share_icon" format="reference" />
|
||||
|
||||
<attr name="share_icon" format="reference" />
|
||||
<attr name="copy_icon" format="reference" />
|
||||
<attr name="qr_icon" format="reference" />
|
||||
<attr name="reset_link_icon" format="reference" />
|
||||
|
||||
<attr name="linkpreview_background_color" format="color" />
|
||||
<attr name="linkpreview_primary_text_color" format="color" />
|
||||
<attr name="linkpreview_secondary_text_color" format="color" />
|
||||
|
|
|
@ -469,6 +469,7 @@
|
|||
<string name="GroupManagement_access_level_anyone">Anyone</string>
|
||||
<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_no_one">No one</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>
|
||||
|
@ -505,6 +506,8 @@
|
|||
|
||||
<!-- PendingMembersActivity -->
|
||||
<string name="PendingMemberInvitesActivity_pending_group_invites">Pending group invites</string>
|
||||
<string name="PendingMembersActivity_requests">Requests</string>
|
||||
<string name="PendingMembersActivity_invites">Invites</string>
|
||||
<string name="PendingMembersActivity_people_you_invited">People you invited</string>
|
||||
<string name="PendingMembersActivity_you_have_no_pending_invites">You have no pending invites.</string>
|
||||
<string name="PendingMembersActivity_invites_by_other_group_members">Invites by other group members</string>
|
||||
|
@ -521,6 +524,13 @@
|
|||
<item quantity="one">Error revoking invite</item>
|
||||
<item quantity="other">Error revoking invites</item>
|
||||
</plurals>
|
||||
|
||||
<!-- RequestingMembersFragment -->
|
||||
<string name="RequestingMembersFragment_pending_member_requests">Pending member requests</string>
|
||||
<string name="RequestingMembersFragment_no_member_requests_to_show">No member requests to show.</string>
|
||||
<string name="RequestingMembersFragment_explanation">People on this list are attempting to join this group via the sharable group link.</string>
|
||||
<string name="RequestingMembersFragment_added_s">Added "%1$s"</string>
|
||||
<string name="RequestingMembersFragment_denied_s">Denied "%1$s"</string>
|
||||
|
||||
<!-- AddMembersActivity -->
|
||||
<string name="AddMembersActivity__done">Done</string>
|
||||
|
@ -559,10 +569,12 @@
|
|||
<!-- ManageGroupActivity -->
|
||||
<string name="ManageGroupActivity_disappearing_messages">Disappearing messages</string>
|
||||
<string name="ManageGroupActivity_pending_group_invites">Pending group invites</string>
|
||||
<string name="ManageGroupActivity_member_requests_and_invites">Member requests & invites</string>
|
||||
<string name="ManageGroupActivity_add_members">Add members</string>
|
||||
<string name="ManageGroupActivity_edit_group_info">Edit group info</string>
|
||||
<string name="ManageGroupActivity_choose_who_can_edit_the_group_name_avatar_and_disappearing_messages">Choose who can edit the group name, avatar, and disappearing messages.</string>
|
||||
<string name="ManageGroupActivity_choose_who_can_add_or_invite_new_members">Choose who can add or invite new members.</string>
|
||||
<string name="ManageGroupActivity_sharable_group_link">Sharable group link</string>
|
||||
<string name="ManageGroupActivity_block_group">Block group</string>
|
||||
<string name="ManageGroupActivity_unblock_group">Unblock group</string>
|
||||
<string name="ManageGroupActivity_leave_group">Leave group</string>
|
||||
|
@ -647,6 +659,24 @@
|
|||
<string name="CustomNotificationsDialogFragment__enabled">Enabled</string>
|
||||
<string name="CustomNotificationsDialogFragment__disabled">Disabled</string>
|
||||
<string name="CustomNotificationsDialogFragment__default">Default</string>
|
||||
|
||||
<!-- ShareableGroupLinkDialogFragment -->
|
||||
<string name="ShareableGroupLinkDialogFragment__shareable_group_link">Shareable group link</string>
|
||||
<string name="ShareableGroupLinkDialogFragment__manage_and_share">Manage & share</string>
|
||||
<string name="ShareableGroupLinkDialogFragment__group_link">Group link</string>
|
||||
<string name="ShareableGroupLinkDialogFragment__share">Share</string>
|
||||
<string name="ShareableGroupLinkDialogFragment__reset_link">Reset link</string>
|
||||
<string name="ShareableGroupLinkDialogFragment__member_requests">Member requests</string>
|
||||
<string name="ShareableGroupLinkDialogFragment__approve_new_members">Approve new members</string>
|
||||
<string name="ShareableGroupLinkDialogFragment__enabled">Enabled</string>
|
||||
<string name="ShareableGroupLinkDialogFragment__disabled">Disabled</string>
|
||||
<string name="ShareableGroupLinkDialogFragment__default">Default</string>
|
||||
<string name="ShareableGroupLinkDialogFragment__group_link_reset">Group link reset</string>
|
||||
|
||||
<!-- GroupLinkShareQrDialogFragment -->
|
||||
<string name="GroupLinkShareQrDialogFragment__qr_code">QR code</string>
|
||||
<string name="GroupLinkShareQrDialogFragment__people_who_scan_this_code_will">People who scan this code will be able to join your group. Admins will still need to approve new members if you have that setting turned on.</string>
|
||||
<string name="GroupLinkShareQrDialogFragment__share_code">Share code</string>
|
||||
|
||||
<!-- GV2 Invite Revoke confirmation dialog -->
|
||||
<string name="InviteRevokeConfirmationDialog_revoke_own_single_invite">Do you want to revoke the invite you sent to %1$s?</string>
|
||||
|
@ -679,6 +709,12 @@
|
|||
<string name="GroupJoinUpdateRequiredBottomSheetDialogFragment_update_signal">Update Signal</string>
|
||||
<string name="GroupJoinUpdateRequiredBottomSheetDialogFragment_group_link_is_not_valid">Group link is not valid</string>
|
||||
|
||||
<!-- GV2 Request confirmation dialog -->
|
||||
<string name="RequestConfirmationDialog_add_s_to_the_group">Add “%1$s” to the group?</string>
|
||||
<string name="RequestConfirmationDialog_deny_request_from_s">Deny request from “%1$s”?</string>
|
||||
<string name="RequestConfirmationDialog_add">Add</string>
|
||||
<string name="RequestConfirmationDialog_deny">Deny</string>
|
||||
|
||||
<!-- CropImageActivity -->
|
||||
<string name="CropImageActivity_group_avatar">Group avatar</string>
|
||||
<string name="CropImageActivity_profile_avatar">Avatar</string>
|
||||
|
@ -2546,6 +2582,8 @@
|
|||
<string name="RecipientBottomSheet_copied_to_clipboard">Copied to clipboard</string>
|
||||
|
||||
<string name="GroupRecipientListItem_admin">Admin</string>
|
||||
<string name="GroupRecipientListItem_approve_description">Approve</string>
|
||||
<string name="GroupRecipientListItem_deny_description">Deny</string>
|
||||
|
||||
|
||||
<!-- GroupsLearnMoreBottomSheetDialogFragment -->
|
||||
|
@ -2556,6 +2594,15 @@
|
|||
<string name="GroupsLearnMore_paragraph_2">Legacy Groups can’t be converted into New Groups, but you can create a New Group with the same members.</string>
|
||||
<string name="GroupsLearnMore_paragraph_3">To create a New Group, all members should update to the latest version of Signal.</string>
|
||||
|
||||
|
||||
<!-- GroupLinkBottomSheetDialogFragment -->
|
||||
<string name="GroupLinkBottomSheet_share_via_signal">Share via Signal</string>
|
||||
<string name="GroupLinkBottomSheet_copy">Copy</string>
|
||||
<string name="GroupLinkBottomSheet_qr_code">QR Code</string>
|
||||
<string name="GroupLinkBottomSheet_share">Share</string>
|
||||
<string name="GroupLinkBottomSheet_copied_to_clipboard">Copied to clipboard</string>
|
||||
<string name="GroupLinkBottomSheet_the_link_is_not_currently_active">The link is not currently active</string>
|
||||
|
||||
<!-- EOF -->
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -444,6 +444,11 @@
|
|||
<item name="invite_edit_text_background">@drawable/invite_edit_text_background_light</item>
|
||||
<item name="invite_share_icon">@drawable/ic_share_outline_24</item>
|
||||
|
||||
<item name="share_icon">@drawable/ic_share_outline_24_tinted</item>
|
||||
<item name="copy_icon">@drawable/ic_copy_outline_24_tinted</item>
|
||||
<item name="qr_icon">@drawable/ic_qrcode_24_tinted</item>
|
||||
<item name="reset_link_icon">@drawable/ic_reset_24_tinted</item>
|
||||
|
||||
<item name="shared_contact_details_header_background">@color/grey_100</item>
|
||||
<item name="shared_contact_details_titlebar">@color/grey_400</item>
|
||||
<item name="shared_contact_item_button_color">@color/core_grey_02</item>
|
||||
|
@ -489,6 +494,11 @@
|
|||
<item name="invite_edit_text_background">@drawable/invite_edit_text_background_dark</item>
|
||||
<item name="invite_share_icon">@drawable/ic_share_solid_24_dark</item>
|
||||
|
||||
<item name="share_icon">@drawable/ic_share_solid_24_tinted</item>
|
||||
<item name="copy_icon">@drawable/ic_copy_solid_24_tinted</item>
|
||||
<item name="qr_icon">@drawable/ic_qrcode_24_tinted</item>
|
||||
<item name="reset_link_icon">@drawable/ic_reset_24_tinted</item>
|
||||
|
||||
<item name="title_text_color_primary">@color/core_grey_05</item>
|
||||
<item name="title_text_color_secondary">@color/core_grey_25</item>
|
||||
|
||||
|
|
|
@ -111,6 +111,24 @@ public final class DecryptedGroupUtil {
|
|||
return uuidList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will not return any non-decryptable member UUIDs.
|
||||
*/
|
||||
public static ArrayList<UUID> removedRequestingMembersUuidList(DecryptedGroupChange groupChange) {
|
||||
List<ByteString> deleteRequestingMembers = groupChange.getDeleteRequestingMembersList();
|
||||
ArrayList<UUID> uuidList = new ArrayList<>(deleteRequestingMembers.size());
|
||||
|
||||
for (ByteString member : deleteRequestingMembers) {
|
||||
UUID uuid = toUuid(member);
|
||||
|
||||
if(!UuidUtil.UNKNOWN_UUID.equals(uuid)) {
|
||||
uuidList.add(uuid);
|
||||
}
|
||||
}
|
||||
|
||||
return uuidList;
|
||||
}
|
||||
|
||||
public static UUID toUuid(DecryptedMember member) {
|
||||
return toUuid(member.getUuid());
|
||||
}
|
||||
|
|
|
@ -1632,7 +1632,6 @@ public class PushServiceSocket {
|
|||
.readTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
|
||||
.build();
|
||||
|
||||
// Log.d(TAG, "Opening URL: " + String.format("%s%s", connectionHolder.getUrl(), path));
|
||||
Log.d(TAG, "Opening URL: <REDACTED>");
|
||||
|
||||
Request.Builder request = new Request.Builder().url(connectionHolder.getUrl() + path);
|
||||
|
@ -1960,8 +1959,13 @@ public class PushServiceSocket {
|
|||
public GroupChange patchGroupsV2Group(GroupChange.Actions groupChange, String authorization, Optional<byte[]> groupLinkPassword)
|
||||
throws NonSuccessfulResponseCodeException, PushNetworkException, InvalidProtocolBufferException
|
||||
{
|
||||
String path = groupLinkPassword.transform(p -> String.format(GROUPSV2_GROUP_PASSWORD, Base64UrlSafe.encodeBytesWithoutPadding(p)))
|
||||
.or(GROUPSV2_GROUP);
|
||||
String path;
|
||||
|
||||
if (groupLinkPassword.isPresent()) {
|
||||
path = String.format(GROUPSV2_GROUP_PASSWORD, Base64UrlSafe.encodeBytesWithoutPadding(groupLinkPassword.get()));
|
||||
} else {
|
||||
path = GROUPSV2_GROUP;
|
||||
}
|
||||
|
||||
ResponseBody response = makeStorageRequest(authorization,
|
||||
path,
|
||||
|
|
Loading…
Reference in New Issue