Add pending member activity.

master
Alan Evans 2020-04-03 16:24:25 -03:00 committed by Greyson Parrelli
parent ef0f26b64c
commit 1290d0ead9
17 changed files with 699 additions and 2 deletions

View File

@ -251,6 +251,10 @@
android:windowSoftInputMode="stateVisible"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".groups.ui.pendingmemberinvites.PendingMemberInvitesActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:theme="@style/TextSecure.LightNoActionBar" />
<activity android:name=".DatabaseMigrationActivity"
android:theme="@style/NoAnimation.Theme.AppCompat.Light.DarkActionBar"
android:launchMode="singleTask"

View File

@ -150,6 +150,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.events.ReminderUpdateEvent;
import org.thoughtcrime.securesms.giph.ui.GiphyActivity;
import org.thoughtcrime.securesms.groups.ui.LeaveGroupDialog;
import org.thoughtcrime.securesms.groups.ui.pendingmemberinvites.PendingMemberInvitesActivity;
import org.thoughtcrime.securesms.insights.InsightsLauncher;
import org.thoughtcrime.securesms.invites.InviteReminderModel;
import org.thoughtcrime.securesms.invites.InviteReminderRepository;
@ -730,6 +731,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
} else {
menu.findItem(R.id.menu_distribution_conversation).setChecked(true);
}
} else if (isActiveV2Group()) {
inflater.inflate(R.menu.conversation_push_group_v2_options, menu);
} else if (isActiveGroup()) {
inflater.inflate(R.menu.conversation_push_group_options, menu);
}
@ -835,6 +838,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
case R.id.menu_distribution_broadcast: handleDistributionBroadcastEnabled(item); return true;
case R.id.menu_distribution_conversation: handleDistributionConversationEnabled(item); return true;
case R.id.menu_edit_group: handleEditPushGroup(); return true;
case R.id.menu_pending_members: handlePendingMembers(); return true;
case R.id.menu_leave: handleLeavePushGroup(); return true;
case R.id.menu_invite: handleInviteLink(); return true;
case R.id.menu_mute_notifications: handleMuteNotifications(); return true;
@ -1130,6 +1134,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
startActivityForResult(intent, GROUP_EDIT);
}
private void handlePendingMembers() {
startActivity(PendingMemberInvitesActivity.newIntent(ConversationActivity.this, recipient.get().requireGroupId().requireV2()));
}
private void handleDistributionBroadcastEnabled(MenuItem item) {
distributionType = ThreadDatabase.DistributionTypes.BROADCAST;
item.setChecked(true);
@ -2108,6 +2116,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
return record.isPresent() && record.get().isActive();
}
private boolean isActiveV2Group() {
if (!isGroupConversation()) return false;
Optional<GroupRecord> record = DatabaseFactory.getGroupDatabase(this).getGroup(getRecipient().getId());
return record.isPresent() && record.get().isActive() && record.get().isV2Group();
}
@SuppressWarnings("SimplifiableIfStatement")
private boolean isSelfConversation() {
if (!TextSecurePreferences.isPushRegistered(this)) return false;

View File

@ -0,0 +1,37 @@
package org.thoughtcrime.securesms.groups;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import com.google.protobuf.ByteString;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
import org.signal.zkgroup.util.UUIDUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
import java.util.UUID;
public final class GroupProtoUtil {
private GroupProtoUtil() {
}
@WorkerThread
public static Recipient pendingMemberToRecipient(@NonNull Context context, @NonNull DecryptedPendingMember pendingMember) {
return uuidByteStringToRecipient(context, pendingMember.getUuid());
}
@WorkerThread
public static Recipient uuidByteStringToRecipient(@NonNull Context context, @NonNull ByteString uuidByteString) {
UUID uuid = UUIDUtil.deserialize(uuidByteString.toByteArray());
if (uuid.equals(GroupsV2Operations.UNKNOWN_UUID)) {
return Recipient.UNKNOWN;
}
return Recipient.externalPush(context, uuid, null);
}
}

View File

@ -20,7 +20,7 @@ public abstract class GroupMemberEntry {
return onClick;
}
public static class FullMember extends GroupMemberEntry {
public final static class FullMember extends GroupMemberEntry {
private final Recipient member;
@ -32,4 +32,40 @@ public abstract class GroupMemberEntry {
return member;
}
}
public final static class PendingMember extends GroupMemberEntry {
private final Recipient invitee;
private final byte[] inviteeCipherText;
public PendingMember(@NonNull Recipient invitee, @NonNull byte[] inviteeCipherText) {
this.invitee = invitee;
this.inviteeCipherText = inviteeCipherText;
}
public Recipient getInvitee() {
return invitee;
}
public byte[] getInviteeCipherText() {
return inviteeCipherText;
}
}
public final static class UnknownPendingMemberCount extends GroupMemberEntry {
private Recipient inviter;
private int inviteCount;
public UnknownPendingMemberCount(@NonNull Recipient inviter, int inviteCount) {
this.inviter = inviter;
this.inviteCount = inviteCount;
}
public Recipient getInviter() {
return inviter;
}
public int getInviteCount() {
return inviteCount;
}
}
}

View File

@ -18,7 +18,9 @@ import java.util.Collection;
final class GroupMemberListAdapter extends RecyclerView.Adapter<GroupMemberListAdapter.ViewHolder> {
private static final int FULL_MEMBER = 0;
private static final int FULL_MEMBER = 0;
private static final int OWN_INVITE_PENDING = 1;
private static final int OTHER_INVITE_PENDING_COUNT = 2;
private final ArrayList<GroupMemberEntry> data = new ArrayList<>();
@ -35,6 +37,14 @@ final class GroupMemberListAdapter extends RecyclerView.Adapter<GroupMemberListA
return new FullMemberViewHolder(LayoutInflater.from(parent.getContext())
.inflate(R.layout.group_recipient_list_item,
parent, false));
case OWN_INVITE_PENDING:
return new OwnInvitePendingMemberViewHolder(LayoutInflater.from(parent.getContext())
.inflate(R.layout.group_recipient_list_item,
parent, false));
case OTHER_INVITE_PENDING_COUNT:
return new UnknownPendingMemberCountViewHolder(LayoutInflater.from(parent.getContext())
.inflate(R.layout.group_recipient_list_item,
parent, false));
default:
throw new AssertionError();
}
@ -51,6 +61,10 @@ final class GroupMemberListAdapter extends RecyclerView.Adapter<GroupMemberListA
if (groupMemberEntry instanceof GroupMemberEntry.FullMember) {
return FULL_MEMBER;
} else if (groupMemberEntry instanceof GroupMemberEntry.PendingMember) {
return OWN_INVITE_PENDING;
} else if (groupMemberEntry instanceof GroupMemberEntry.UnknownPendingMemberCount) {
return OTHER_INVITE_PENDING_COUNT;
}
throw new AssertionError();
@ -112,4 +126,41 @@ final class GroupMemberListAdapter extends RecyclerView.Adapter<GroupMemberListA
bindRecipient(fullMember.getMember());
}
}
final static class OwnInvitePendingMemberViewHolder extends ViewHolder {
OwnInvitePendingMemberViewHolder(@NonNull View itemView) {
super(itemView);
}
@Override
void bind(@NonNull GroupMemberEntry memberEntry) {
super.bind(memberEntry);
GroupMemberEntry.PendingMember pendingMember = (GroupMemberEntry.PendingMember) memberEntry;
bindRecipient(pendingMember.getInvitee());
}
}
final static class UnknownPendingMemberCountViewHolder extends ViewHolder {
UnknownPendingMemberCountViewHolder(@NonNull View itemView) {
super(itemView);
}
@Override
void bind(@NonNull GroupMemberEntry memberEntry) {
super.bind(memberEntry);
GroupMemberEntry.UnknownPendingMemberCount pendingMemberCount = (GroupMemberEntry.UnknownPendingMemberCount) memberEntry;
Recipient inviter = pendingMemberCount.getInviter();
String displayName = inviter.getDisplayName(itemView.getContext());
String displayText = context.getResources().getQuantityString(R.plurals.GroupMemberList_invited,
pendingMemberCount.getInviteCount(),
displayName, pendingMemberCount.getInviteCount());
bindImageAndText(inviter, displayText);
}
}
}

View File

@ -0,0 +1,60 @@
package org.thoughtcrime.securesms.groups.ui.pendingmemberinvites;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
public class PendingMemberInvitesActivity extends PassphraseRequiredActionBarActivity {
private static final String GROUP_ID = "GROUP_ID";
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
public static Intent newIntent(@NonNull Context context, @NonNull GroupId.V2 groupId) {
Intent intent = new Intent(context, PendingMemberInvitesActivity.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_member_invites_activity);
if (savedInstanceState == null) {
getSupportFragmentManager().beginTransaction()
.replace(R.id.container, PendingMemberInvitesFragment.newInstance(GroupId.parse(getIntent().getStringExtra(GROUP_ID)).requireV2()))
.commitNow();
}
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
@Override
public void onResume() {
super.onResume();
dynamicTheme.onResume(this);
}
@Override
public boolean onSupportNavigateUp() {
onBackPressed();
return true;
}
}

View File

@ -0,0 +1,71 @@
package org.thoughtcrime.securesms.groups.ui.pendingmemberinvites;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
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.GroupMemberListView;
import java.util.Objects;
public class PendingMemberInvitesFragment extends Fragment {
private static final String GROUP_ID = "GROUP_ID";
private PendingMemberInvitesViewModel viewModel;
private GroupMemberListView youInvited;
private GroupMemberListView othersInvited;
private View youInvitedEmptyState;
private View othersInvitedEmptyState;
public static PendingMemberInvitesFragment newInstance(@NonNull GroupId.V2 groupId) {
PendingMemberInvitesFragment fragment = new PendingMemberInvitesFragment();
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_pending_member_invites_fragment, container, false);
youInvited = view.findViewById(R.id.members_you_invited);
othersInvited = view.findViewById(R.id.members_others_invited);
youInvitedEmptyState = view.findViewById(R.id.no_pending_from_you);
othersInvitedEmptyState = view.findViewById(R.id.no_pending_from_others);
return view;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
GroupId.V2 groupId = GroupId.parse(Objects.requireNonNull(requireArguments().getString(GROUP_ID))).requireV2();
PendingMemberInvitesViewModel.Factory factory = new PendingMemberInvitesViewModel.Factory(requireContext(), groupId);
viewModel = ViewModelProviders.of(requireActivity(), factory).get(PendingMemberInvitesViewModel.class);
viewModel.getWhoYouInvited().observe(getViewLifecycleOwner(), invitees -> {
youInvited.setMembers(invitees);
youInvitedEmptyState.setVisibility(invitees.isEmpty() ? View.VISIBLE : View.GONE);
});
viewModel.getWhoOthersInvited().observe(getViewLifecycleOwner(), invitees -> {
othersInvited.setMembers(invitees);
othersInvitedEmptyState.setVisibility(invitees.isEmpty() ? View.VISIBLE : View.GONE);
});
}
}

View File

@ -0,0 +1,85 @@
package org.thoughtcrime.securesms.groups.ui.pendingmemberinvites;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
import org.thoughtcrime.securesms.logging.Log;
import java.util.ArrayList;
import java.util.List;
public class PendingMemberInvitesViewModel extends ViewModel {
private static final String TAG = Log.tag(PendingMemberInvitesViewModel.class);
private final Context context;
private final GroupId groupId;
private final PendingMemberRepository pendingMemberRepository;
private final MutableLiveData<List<GroupMemberEntry.PendingMember>> whoYouInvited = new MutableLiveData<>();
private final MutableLiveData<List<GroupMemberEntry.UnknownPendingMemberCount>> whoOthersInvited = new MutableLiveData<>();
PendingMemberInvitesViewModel(@NonNull Context context,
@NonNull GroupId.V2 groupId,
@NonNull PendingMemberRepository pendingMemberRepository)
{
this.context = context;
this.groupId = groupId;
this.pendingMemberRepository = pendingMemberRepository;
pendingMemberRepository.getInvitees(groupId, this::setMembers);
}
public LiveData<List<GroupMemberEntry.PendingMember>> getWhoYouInvited() {
return whoYouInvited;
}
public LiveData<List<GroupMemberEntry.UnknownPendingMemberCount>> getWhoOthersInvited() {
return whoOthersInvited;
}
private void setInvitees(List<GroupMemberEntry.PendingMember> byYou, List<GroupMemberEntry.UnknownPendingMemberCount> byOthers) {
whoYouInvited.postValue(byYou);
whoOthersInvited.postValue(byOthers);
}
private void setMembers(PendingMemberRepository.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()) {
byMe.add(new GroupMemberEntry.PendingMember(pendingMember.getInvitee(),
pendingMember.getInviteeCipherText()));
}
for (PendingMemberRepository.MultiplePendingMembersInvitedByAnother pendingMembers : inviteeResult.getByOthers()) {
byOthers.add(new GroupMemberEntry.UnknownPendingMemberCount(pendingMembers.getInviter(),
pendingMembers.getUuidCipherTexts().size()));
}
setInvitees(byMe, byOthers);
}
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 PendingMemberInvitesViewModel(context, groupId, new PendingMemberRepository(context.getApplicationContext()));
}
}
}

View File

@ -0,0 +1,131 @@
package org.thoughtcrime.securesms.groups.ui.pendingmemberinvites;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.core.util.Consumer;
import com.annimon.stream.Stream;
import com.google.protobuf.ByteString;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
import org.signal.zkgroup.util.UUIDUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.GroupProtoUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
final class PendingMemberRepository {
private final Context context;
private final Executor executor;
PendingMemberRepository(@NonNull Context context) {
this.context = context.getApplicationContext();
this.executor = SignalExecutors.BOUNDED;
}
public void getInvitees(GroupId.V2 groupId, @NonNull Consumer<InviteeResult> onInviteesLoaded) {
executor.execute(() -> {
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
GroupDatabase.V2GroupProperties v2GroupProperties = groupDatabase.getGroup(groupId).get().requireV2GroupProperties();
DecryptedGroup decryptedGroup = v2GroupProperties.getDecryptedGroup();
List<DecryptedPendingMember> pendingMembersList = decryptedGroup.getPendingMembersList();
List<SinglePendingMemberInvitedByYou> byMe = new ArrayList<>(pendingMembersList.size());
List<MultiplePendingMembersInvitedByAnother> byOthers = new ArrayList<>(pendingMembersList.size());
ByteString self = ByteString.copyFrom(UUIDUtil.serialize(Recipient.self().getUuid().get()));
Stream.of(pendingMembersList)
.groupBy(DecryptedPendingMember::getAddedByUuid)
.forEach(g ->
{
ByteString inviterUuid = g.getKey();
List<DecryptedPendingMember> invitedMembers = g.getValue();
if (self.equals(inviterUuid)) {
for (DecryptedPendingMember pendingMember : invitedMembers) {
Recipient invitee = GroupProtoUtil.pendingMemberToRecipient(context, pendingMember);
byte[] uuidCipherText = pendingMember.getUuidCipherText().toByteArray();
byMe.add(new SinglePendingMemberInvitedByYou(invitee, uuidCipherText));
}
} else {
Recipient inviter = GroupProtoUtil.uuidByteStringToRecipient(context, inviterUuid);
ArrayList<byte[]> uuidCipherTexts = new ArrayList<>(invitedMembers.size());
for (DecryptedPendingMember pendingMember : invitedMembers) {
uuidCipherTexts.add(pendingMember.getUuidCipherText().toByteArray());
}
byOthers.add(new MultiplePendingMembersInvitedByAnother(inviter, uuidCipherTexts));
}
}
);
onInviteesLoaded.accept(new InviteeResult(byMe, byOthers));
});
}
public static final class InviteeResult {
private final List<SinglePendingMemberInvitedByYou> byMe;
private final List<MultiplePendingMembersInvitedByAnother> byOthers;
private InviteeResult(List<SinglePendingMemberInvitedByYou> byMe,
List<MultiplePendingMembersInvitedByAnother> byOthers)
{
this.byMe = byMe;
this.byOthers = byOthers;
}
public List<SinglePendingMemberInvitedByYou> getByMe() {
return byMe;
}
public List<MultiplePendingMembersInvitedByAnother> getByOthers() {
return byOthers;
}
}
public final static class SinglePendingMemberInvitedByYou {
private final Recipient invitee;
private final byte[] inviteeCipherText;
private SinglePendingMemberInvitedByYou(@NonNull Recipient invitee, @NonNull byte[] inviteeCipherText) {
this.invitee = invitee;
this.inviteeCipherText = inviteeCipherText;
}
public Recipient getInvitee() {
return invitee;
}
public byte[] getInviteeCipherText() {
return inviteeCipherText;
}
}
public final static class MultiplePendingMembersInvitedByAnother {
private final Recipient inviter;
private final ArrayList<byte[]> uuidCipherTexts;
private MultiplePendingMembersInvitedByAnother(@NonNull Recipient inviter, @NonNull ArrayList<byte[]> uuidCipherTexts) {
this.inviter = inviter;
this.uuidCipherTexts = uuidCipherTexts;
}
public Recipient getInviter() {
return inviter;
}
public ArrayList<byte[]> getUuidCipherTexts() {
return uuidCipherTexts;
}
}
}

View File

@ -0,0 +1,32 @@
<?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.pendingmemberinvites.PendingMemberInvitesActivity">
<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>
<FrameLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

View File

@ -0,0 +1,121 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/pending_member_background"
android:fillViewport="true"
tools:context="org.thoughtcrime.securesms.groups.ui.pendingmemberinvites.PendingMemberInvitesFragment">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/pendingmemberinvites"
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:attr/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="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp"
android:text="@string/PendingMembersActivity_people_you_invited" />
<TextView
android:id="@+id/no_pending_from_you"
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/PendingMembersActivity_you_have_no_pending_invites"
android:visibility="gone"
tools:visibility="visible"
android:textColor="?pending_member_empty_text_color" />
<org.thoughtcrime.securesms.groups.ui.GroupMemberListView
android:id="@+id/members_you_invited"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:listitem="@layout/group_recipient_list_item"
tools:maxHeight="192dp" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:cardBackgroundColor="?android:attr/windowBackground"
app:layout_constraintTop_toBottomOf="@+id/cardView">
<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="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp"
android:text="@string/PendingMembersActivity_invites_by_other_group_members" />
<TextView
android:id="@+id/no_pending_from_others"
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="8dp"
android:text="@string/PendingMembersActivity_no_pending_invites_by_other_group_members"
android:visibility="gone"
tools:visibility="visible"
android:textColor="?pending_member_empty_text_color" />
<org.thoughtcrime.securesms.groups.ui.GroupMemberListView
android:id="@+id/members_others_invited"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:listitem="@layout/group_recipient_list_item"
tools:maxHeight="192dp" />
<TextView
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/PendingMembersActivity_missing_detail_explanation" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>

View File

@ -29,6 +29,7 @@
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:background="#00ffffff"
android:contentDescription="@string/GroupCreateActivity_remove_member_description"
android:src="@drawable/ic_menu_remove_holo_light" />
</RelativeLayout>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/menu_edit_group"
android:title="@string/conversation__menu_edit_group"
app:showAsAction="collapseActionView" />
<item android:id="@+id/menu_leave"
android:title="@string/conversation__menu_leave_group"
app:showAsAction="collapseActionView"/>
<item android:id="@+id/menu_pending_members"
android:title="@string/conversation__menu_pending_members"
app:showAsAction="collapseActionView"/>
</menu>

View File

@ -271,6 +271,9 @@
<attr name="megaphone_reactions_shade" format="color"/>
<attr name="megaphone_reactions_close_tint" format="color"/>
<attr name="pending_member_background" format="color" />
<attr name="pending_member_empty_text_color" format="color" />
<attr name="debuglog_color_none" format="color" />
<attr name="debuglog_color_verbose" format="color" />
<attr name="debuglog_color_debug" format="color" />

View File

@ -432,6 +432,7 @@
<string name="GroupCreateActivity_cannot_add_non_push_to_existing_group">Couldn\'t add %1$s because they\'re not a Signal user.</string>
<string name="GroupCreateActivity_loading_group_details">Loading group details…</string>
<string name="GroupCreateActivity_youre_already_in_the_group">You\'re already in the group.</string>
<string name="GroupCreateActivity_remove_member_description">Remove member</string>
<!-- GroupShareProfileView -->
<string name="GroupShareProfileView_share_your_profile_name_and_photo_with_this_group">Share your profile name and photo with this group?</string>
@ -441,6 +442,19 @@
<!-- GroupMembersDialog -->
<string name="GroupMembersDialog_you">You</string>
<!-- PendingMembersActivity -->
<string name="PendingMemberInvitesActivity_pending_group_invites">Pending group 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>
<string name="PendingMembersActivity_no_pending_invites_by_other_group_members">No pending invites by other group members.</string>
<string name="PendingMembersActivity_missing_detail_explanation">Details of people invited by other group members are not shown. If invitees choose to join, their information will be shared with the group at that time. They will not see any messages in the group until they join.</string>
<plurals name="GroupMemberList_invited">
<item quantity="one">%1$s invited 1 person</item>
<item quantity="other">%1$s invited %2$d people</item>
</plurals>
<!-- CropImageActivity -->
<string name="CropImageActivity_group_avatar">Group avatar</string>
<string name="CropImageActivity_profile_avatar">Avatar</string>
@ -1674,6 +1688,7 @@
<string name="conversation__menu_view_all_media">All media</string>
<string name="conversation__menu_conversation_settings">Conversation settings</string>
<string name="conversation__menu_add_shortcut">Add to home screen</string>
<string name="conversation__menu_pending_members">Pending members</string>
<!-- conversation_popup -->
<string name="conversation_popup__menu_expand_popup">Expand popup</string>

View File

@ -97,6 +97,15 @@
<item name="android:textStyle">bold</item>
</style>
<style name="TextAppearance.Signal.Body2" parent="@style/TextAppearance.MaterialComponents.Body2">
</style>
<style name="TextAppearance.Signal.Caption" parent="@style/TextAppearance.MaterialComponents.Caption">
</style>
<style name="TextAppearance.Signal.Subtitle2" parent="@style/TextAppearance.MaterialComponents.Subtitle2">
</style>
<style name="Signal.Text.MessageRequest.Title" parent="Base.TextAppearance.AppCompat.Title">
<item name="android:textSize">20sp</item>
<item name="android:textColor">?message_request_text_color_primary</item>

View File

@ -414,6 +414,11 @@
<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>
<item name="pending_member_background">@color/core_grey_02</item>
<item name="pending_member_empty_text_color">@color/core_grey_60</item>
<item name="colorControlNormal">@color/core_grey_90</item>
</style>
<style name="TextSecure.DarkTheme" parent="@style/TextSecure.BaseDarkTheme">
@ -684,6 +689,11 @@
<item name="shared_contact_details_header_background">@color/grey_800</item>
<item name="shared_contact_details_titlebar">@color/grey_900</item>
<item name="shared_contact_item_button_color">@color/core_grey_85</item>
<item name="pending_member_background">@color/core_grey_80</item>
<item name="pending_member_empty_text_color">@color/core_grey_25</item>
<item name="colorControlNormal">@color/core_white</item>
</style>
<style name="RationaleDialogLight" parent="Theme.AppCompat.Light.Dialog.Alert">