Manage group links behind feature flag.

master
Alan Evans 2020-08-26 15:59:34 -03:00
parent 860f06ec9e
commit bfed03b7b5
51 changed files with 2177 additions and 80 deletions

View File

@ -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"/>

View File

@ -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());
}
}

View File

@ -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;

View File

@ -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
}
}

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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);
}
}
}

View File

@ -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) {

View File

@ -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;
}
}

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.groups.ui.pendingmemberinvites;
package org.thoughtcrime.securesms.groups.ui.invitesandrequests.invited;
import android.content.Context;

View File

@ -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;

View File

@ -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;

View File

@ -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));
}
}
}

View File

@ -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();
}
}

View File

@ -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));
}
}
}

View File

@ -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));
}
});
}
}

View File

@ -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());
}
}

View File

@ -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()));

View File

@ -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,

View File

@ -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) {

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}
});
}
}

View File

@ -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;
}
}

View File

@ -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));
}
}
}

View File

@ -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) {

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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"

View File

@ -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>

View File

@ -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"

View File

@ -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>

View File

@ -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" />

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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" />

View File

@ -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 &amp; 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 &amp; 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 cant 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>

View File

@ -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>

View File

@ -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());
}

View File

@ -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,