diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index b5d5ce3e91..0ab4c863b7 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -483,8 +483,13 @@
android:theme="@style/TextSecure.LightNoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
+
-
+
+
+
diff --git a/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionActivity.java b/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionActivity.java
index 1dd2b6176b..1f1d3d2d63 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionActivity.java
@@ -50,6 +50,8 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActionB
{
private static final String TAG = ContactSelectionActivity.class.getSimpleName();
+ public static final String EXTRA_LAYOUT_RES_ID = "layout_res_id";
+
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
@@ -67,11 +69,11 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActionB
protected void onCreate(Bundle icicle, boolean ready) {
if (!getIntent().hasExtra(ContactSelectionListFragment.DISPLAY_MODE)) {
int displayMode = TextSecurePreferences.isSmsEnabled(this) ? DisplayMode.FLAG_ALL
- : DisplayMode.FLAG_PUSH | DisplayMode.FLAG_ACTIVE_GROUPS | DisplayMode.FLAG_INACTIVE_GROUPS;
+ : DisplayMode.FLAG_PUSH | DisplayMode.FLAG_ACTIVE_GROUPS | DisplayMode.FLAG_INACTIVE_GROUPS | DisplayMode.FLAG_SELF;
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, displayMode);
}
- setContentView(R.layout.contact_selection_activity);
+ setContentView(getIntent().getIntExtra(EXTRA_LAYOUT_RES_ID, R.layout.contact_selection_activity));
initializeToolbar();
initializeResources();
@@ -90,7 +92,7 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActionB
}
private void initializeToolbar() {
- this.toolbar = ViewUtil.findById(this, R.id.toolbar);
+ this.toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
assert getSupportActionBar() != null;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java
index 2715f97348..d59b2d216d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java
@@ -187,10 +187,14 @@ public final class ContactSelectionListFragment extends Fragment
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}
- @NonNull List getSelectedContacts() {
+ public @NonNull List getSelectedContacts() {
return cursorRecyclerViewAdapter.getSelectedContacts();
}
+ public int getSelectedContactsCount() {
+ return cursorRecyclerViewAdapter.getSelectedContactsCount();
+ }
+
private boolean isMulti() {
return requireActivity().getIntent().getBooleanExtra(MULTI_SELECT, false);
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/NewConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/NewConversationActivity.java
index 5e462bf7c3..e268aeb963 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/NewConversationActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/NewConversationActivity.java
@@ -26,6 +26,7 @@ import android.view.View;
import org.thoughtcrime.securesms.conversation.ConversationActivity;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.ThreadDatabase;
+import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
@@ -97,7 +98,7 @@ public class NewConversationActivity extends ContactSelectionActivity
}
private void handleCreateGroup() {
- startActivity(new Intent(this, GroupCreateActivity.class));
+ startActivity(CreateGroupActivity.newIntent(this));
}
private void handleInvite() {
@@ -105,10 +106,10 @@ public class NewConversationActivity extends ContactSelectionActivity
}
@Override
- protected boolean onPrepareOptionsPanel(View view, Menu menu) {
- MenuInflater inflater = this.getMenuInflater();
+ public boolean onPrepareOptionsMenu(Menu menu) {
menu.clear();
- inflater.inflate(R.menu.new_conversation_activity, menu);
+ getMenuInflater().inflate(R.menu.new_conversation_activity, menu);
+
super.onPrepareOptionsMenu(menu);
return true;
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/AvatarImageView.java b/app/src/main/java/org/thoughtcrime/securesms/components/AvatarImageView.java
index c17952cf1f..5916b28a62 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/AvatarImageView.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/AvatarImageView.java
@@ -158,13 +158,12 @@ public final class AvatarImageView extends AppCompatImageView {
}
private void setAvatarClickHandler(final Recipient recipient, boolean quickContactEnabled) {
- super.setOnClickListener(v -> {
- if (quickContactEnabled) {
- getContext().startActivity(RecipientPreferenceActivity.getLaunchIntent(getContext(), recipient.getId()));
- } else if (listener != null) {
- listener.onClick(v);
- }
- });
+ if (quickContactEnabled) {
+ super.setOnClickListener(v -> getContext().startActivity(RecipientPreferenceActivity.getLaunchIntent(getContext(), recipient.getId())));
+ } else {
+ super.setOnClickListener(listener);
+ setClickable(listener != null);
+ }
}
private static class RecipientContactPhoto {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactRepository.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactRepository.java
index c7a1a83893..e3c913cc43 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactRepository.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactRepository.java
@@ -85,11 +85,15 @@ public class ContactRepository {
@WorkerThread
public Cursor querySignalContacts(@NonNull String query) {
- Cursor cursor = TextUtils.isEmpty(query) ? recipientDatabase.getSignalContacts()
- : recipientDatabase.querySignalContacts(query);
+ return querySignalContacts(query, true);
+ }
+ @WorkerThread
+ public Cursor querySignalContacts(@NonNull String query, boolean includeSelf) {
+ Cursor cursor = TextUtils.isEmpty(query) ? recipientDatabase.getSignalContacts(includeSelf)
+ : recipientDatabase.querySignalContacts(query, includeSelf);
- if (noteToSelfTitle.toLowerCase().contains(query.toLowerCase())) {
+ if (includeSelf && noteToSelfTitle.toLowerCase().contains(query.toLowerCase())) {
Recipient self = Recipient.self();
boolean nameMatch = self.getDisplayName(context).toLowerCase().contains(query.toLowerCase());
boolean numberMatch = self.getE164().isPresent() && self.requireE164().contains(query);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java
index c272a9236c..f3d3530335 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java
@@ -250,6 +250,10 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter(contacts);
}
+ public int size() {
+ return contacts.size();
+ }
+
public void clear() {
contacts.clear();
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java
index 151f96d4fc..e893d8f6fd 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java
@@ -1357,17 +1357,29 @@ public class RecipientDatabase extends Database {
}
}
public @Nullable Cursor getSignalContacts() {
+ return getSignalContacts(true);
+ }
+
+ public @Nullable Cursor getSignalContacts(boolean includeSelf) {
String selection = BLOCKED + " = ? AND " +
REGISTERED + " = ? AND " +
GROUP_ID + " IS NULL AND " +
"(" + SORT_NAME + " NOT NULL OR " + USERNAME + " NOT NULL)";
- String[] args = new String[] { "0", String.valueOf(RegisteredState.REGISTERED.getId()) };
+ String[] args;
+
+ if (includeSelf) {
+ args = new String[] { "0", String.valueOf(RegisteredState.REGISTERED.getId()) };
+ } else {
+ selection += " AND " + ID + " != ?";
+ args = new String[] { "0", String.valueOf(RegisteredState.REGISTERED.getId()), String.valueOf(Recipient.self().getId().toLong()) };
+ }
+
String orderBy = SORT_NAME + ", " + SYSTEM_DISPLAY_NAME + ", " + SEARCH_PROFILE_NAME + ", " + USERNAME + ", " + PHONE;
return databaseHelper.getReadableDatabase().query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy);
}
- public @Nullable Cursor querySignalContacts(@NonNull String query) {
+ public @Nullable Cursor querySignalContacts(@NonNull String query, boolean includeSelf) {
query = TextUtils.isEmpty(query) ? "*" : query;
query = "%" + query + "%";
@@ -1379,7 +1391,15 @@ public class RecipientDatabase extends Database {
SORT_NAME + " LIKE ? OR " +
USERNAME + " LIKE ?" +
")";
- String[] args = new String[] { "0", String.valueOf(RegisteredState.REGISTERED.getId()), query, query, query };
+ String[] args;
+
+ if (includeSelf) {
+ args = new String[]{"0", String.valueOf(RegisteredState.REGISTERED.getId()), query, query, query};
+ } else {
+ selection += " AND " + ID + " != ?";
+ args = new String[] { "0", String.valueOf(RegisteredState.REGISTERED.getId()), query, query, query, String.valueOf(Recipient.self().getId().toLong()) };
+ }
+
String orderBy = SORT_NAME + ", " + SYSTEM_DISPLAY_NAME + ", " + SEARCH_PROFILE_NAME + ", " + PHONE;
return databaseHelper.getReadableDatabase().query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupMemberEntry.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupMemberEntry.java
index 0081da68fe..24df4655a5 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupMemberEntry.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupMemberEntry.java
@@ -31,7 +31,49 @@ public abstract class GroupMemberEntry {
@Override
public abstract int hashCode();
- abstract boolean sameId(GroupMemberEntry newItem);
+ abstract boolean sameId(@NonNull GroupMemberEntry newItem);
+
+ public final static class NewGroupCandidate extends GroupMemberEntry {
+
+ private final DefaultValueLiveData isSelected = new DefaultValueLiveData<>(false);
+ private final Recipient member;
+
+ public NewGroupCandidate(@NonNull Recipient member) {
+ this.member = member;
+ }
+
+ public @NonNull Recipient getMember() {
+ return member;
+ }
+
+ public @NonNull LiveData isSelected() {
+ return isSelected;
+ }
+
+ public void setSelected(boolean isSelected) {
+ this.isSelected.postValue(isSelected);
+ }
+
+ @Override
+ boolean sameId(@NonNull GroupMemberEntry newItem) {
+ if (getClass() != newItem.getClass()) return false;
+
+ return member.getId().equals(((NewGroupCandidate) newItem).member.getId());
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (!(obj instanceof NewGroupCandidate)) return false;
+
+ NewGroupCandidate other = (NewGroupCandidate) obj;
+ return other.member.equals(member);
+ }
+
+ @Override
+ public int hashCode() {
+ return member.hashCode();
+ }
+ }
public final static class FullMember extends GroupMemberEntry {
@@ -52,7 +94,7 @@ public abstract class GroupMemberEntry {
}
@Override
- boolean sameId(GroupMemberEntry newItem) {
+ boolean sameId(@NonNull GroupMemberEntry newItem) {
if (getClass() != newItem.getClass()) return false;
return member.getId().equals(((GroupMemberEntry.FullMember) newItem).member.getId());
@@ -97,7 +139,7 @@ public abstract class GroupMemberEntry {
}
@Override
- boolean sameId(GroupMemberEntry newItem) {
+ boolean sameId(@NonNull GroupMemberEntry newItem) {
if (getClass() != newItem.getClass()) return false;
return invitee.getId().equals(((GroupMemberEntry.PendingMember) newItem).invitee.getId());
@@ -153,7 +195,7 @@ public abstract class GroupMemberEntry {
}
@Override
- boolean sameId(GroupMemberEntry newItem) {
+ boolean sameId(@NonNull GroupMemberEntry newItem) {
if (getClass() != newItem.getClass()) return false;
return inviter.getId().equals(((GroupMemberEntry.UnknownPendingMemberCount) newItem).inviter.getId());
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupMemberListAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupMemberListAdapter.java
index 06c17e8d78..3a0fa53504 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupMemberListAdapter.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupMemberListAdapter.java
@@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.LifecycleRecyclerAdapter;
import org.thoughtcrime.securesms.util.LifecycleViewHolder;
+import org.thoughtcrime.securesms.util.ThemeUtil;
import java.util.ArrayList;
import java.util.List;
@@ -26,11 +27,13 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter data = new ArrayList<>();
- @Nullable private AdminActionsListener adminActionsListener;
- @Nullable private RecipientClickListener recipientClickListener;
+ @Nullable private AdminActionsListener adminActionsListener;
+ @Nullable private RecipientClickListener recipientClickListener;
+ @Nullable private RecipientLongClickListener recipientLongClickListener;
void updateData(@NonNull List extends GroupMemberEntry> recipients) {
if (data.isEmpty()) {
@@ -49,16 +52,25 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter {
+ if (recipientLongClickListener != null && getAdapterPosition() != RecyclerView.NO_POSITION) {
+ return recipientLongClickListener.onLongClick(recipient);
+ }
+
+ return false;
+ });
}
void bind(@NonNull GroupMemberEntry memberEntry) {
@@ -179,9 +207,10 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter recipients) {
membersAdapter.updateData(recipients);
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/RecipientLongClickListener.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/RecipientLongClickListener.java
new file mode 100644
index 0000000000..b0d2a73b1a
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/RecipientLongClickListener.java
@@ -0,0 +1,9 @@
+package org.thoughtcrime.securesms.groups.ui;
+
+import androidx.annotation.NonNull;
+
+import org.thoughtcrime.securesms.recipients.Recipient;
+
+public interface RecipientLongClickListener {
+ boolean onLongClick(@NonNull Recipient recipient);
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/CreateGroupActivity.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/CreateGroupActivity.java
new file mode 100644
index 0000000000..f1aa5bdcca
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/CreateGroupActivity.java
@@ -0,0 +1,107 @@
+package org.thoughtcrime.securesms.groups.ui.creategroup;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.MenuItem;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.annimon.stream.Stream;
+
+import org.thoughtcrime.securesms.ContactSelectionActivity;
+import org.thoughtcrime.securesms.ContactSelectionListFragment;
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
+import org.thoughtcrime.securesms.groups.ui.creategroup.details.AddGroupDetailsActivity;
+import org.thoughtcrime.securesms.recipients.RecipientId;
+import org.thoughtcrime.securesms.util.TextSecurePreferences;
+import org.whispersystems.libsignal.util.guava.Optional;
+
+public class CreateGroupActivity extends ContactSelectionActivity {
+
+ private static final int MINIMUM_GROUP_SIZE = 1;
+ private static final short REQUEST_CODE_ADD_DETAILS = 17275;
+
+ private View next;
+
+ public static Intent newIntent(@NonNull Context context) {
+ Intent intent = new Intent(context, CreateGroupActivity.class);
+
+ intent.putExtra(ContactSelectionListFragment.MULTI_SELECT, true);
+ intent.putExtra(ContactSelectionListFragment.REFRESHABLE, false);
+ intent.putExtra(ContactSelectionActivity.EXTRA_LAYOUT_RES_ID, R.layout.create_group_activity);
+
+ int displayMode = TextSecurePreferences.isSmsEnabled(context) ? ContactsCursorLoader.DisplayMode.FLAG_SMS | ContactsCursorLoader.DisplayMode.FLAG_PUSH
+ : ContactsCursorLoader.DisplayMode.FLAG_PUSH;
+
+ intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, displayMode);
+
+ return intent;
+ }
+
+ @Override
+ public void onCreate(Bundle bundle, boolean ready) {
+ super.onCreate(bundle, ready);
+ assert getSupportActionBar() != null;
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+
+ next = findViewById(R.id.next);
+
+ disableNext();
+ next.setOnClickListener(v -> handleNextPressed());
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ finish();
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
+ if (requestCode == REQUEST_CODE_ADD_DETAILS && resultCode == RESULT_OK) {
+ finish();
+ } else {
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+ }
+
+ @Override
+ public void onContactSelected(Optional recipientId, String number) {
+ if (contactsFragment.getSelectedContactsCount() >= MINIMUM_GROUP_SIZE) {
+ enableNext();
+ }
+ }
+
+ @Override
+ public void onContactDeselected(Optional recipientId, String number) {
+ if (contactsFragment.getSelectedContactsCount() < MINIMUM_GROUP_SIZE) {
+ disableNext();
+ }
+ }
+
+ private void enableNext() {
+ next.setEnabled(true);
+ next.animate().alpha(1f);
+ }
+
+ private void disableNext() {
+ next.setEnabled(false);
+ next.animate().alpha(0.5f);
+ }
+
+ private void handleNextPressed() {
+ RecipientId[] ids = Stream.of(contactsFragment.getSelectedContacts())
+ .map(selectedContact -> selectedContact.getOrCreateRecipientId(this))
+ .toArray(RecipientId[]::new);
+
+ startActivityForResult(AddGroupDetailsActivity.newIntent(this, ids), REQUEST_CODE_ADD_DETAILS);
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsActivity.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsActivity.java
new file mode 100644
index 0000000000..dddb8f62c4
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsActivity.java
@@ -0,0 +1,78 @@
+package org.thoughtcrime.securesms.groups.ui.creategroup.details;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.navigation.NavGraph;
+import androidx.navigation.Navigation;
+
+import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.conversation.ConversationActivity;
+import org.thoughtcrime.securesms.database.ThreadDatabase;
+import org.thoughtcrime.securesms.recipients.RecipientId;
+import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
+import org.thoughtcrime.securesms.util.DynamicTheme;
+
+public class AddGroupDetailsActivity extends PassphraseRequiredActionBarActivity implements AddGroupDetailsFragment.Callback {
+
+ private static final String EXTRA_RECIPIENTS = "recipient_ids";
+
+ private final DynamicTheme theme = new DynamicNoActionBarTheme();
+
+ public static Intent newIntent(@NonNull Context context, @NonNull RecipientId[] recipients) {
+ Intent intent = new Intent(context, AddGroupDetailsActivity.class);
+
+ intent.putExtra(EXTRA_RECIPIENTS, recipients);
+
+ return intent;
+ }
+
+ @Override
+ protected void onCreate(@Nullable Bundle bundle, boolean ready) {
+ theme.onCreate(this);
+
+ setContentView(R.layout.add_group_details_activity);
+
+ if (bundle == null) {
+ Parcelable[] parcelables = getIntent().getParcelableArrayExtra(EXTRA_RECIPIENTS);
+ RecipientId[] ids = new RecipientId[parcelables.length];
+
+ System.arraycopy(parcelables, 0, ids, 0, parcelables.length);
+
+ AddGroupDetailsFragmentArgs arguments = new AddGroupDetailsFragmentArgs.Builder(ids).build();
+ NavGraph graph = Navigation.findNavController(this, R.id.nav_host_fragment).getGraph();
+
+ Navigation.findNavController(this, R.id.nav_host_fragment).setGraph(graph, arguments.toBundle());
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ theme.onResume(this);
+ }
+
+ @Override
+ public void onGroupCreated(@NonNull RecipientId recipientId, long threadId) {
+ Intent intent = ConversationActivity.buildIntent(this,
+ recipientId,
+ threadId,
+ ThreadDatabase.DistributionTypes.DEFAULT,
+ -1);
+
+ startActivity(intent);
+ setResult(RESULT_OK);
+ finish();
+ }
+
+ @Override
+ public void onNavigationButtonPressed() {
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsFragment.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsFragment.java
new file mode 100644
index 0000000000..3d152eca04
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsFragment.java
@@ -0,0 +1,287 @@
+package org.thoughtcrime.securesms.groups.ui.creategroup.details;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.InsetDrawable;
+import android.os.Bundle;
+import android.view.ActionMode;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.appcompat.widget.Toolbar;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.ViewModelProviders;
+import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
+
+import com.bumptech.glide.load.engine.DiskCacheStrategy;
+import com.bumptech.glide.request.target.CustomTarget;
+import com.bumptech.glide.request.transition.Transition;
+import com.dd.CircularProgressButton;
+
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.groups.ui.GroupMemberListView;
+import org.thoughtcrime.securesms.mediasend.AvatarSelectionActivity;
+import org.thoughtcrime.securesms.mediasend.AvatarSelectionBottomSheetDialogFragment;
+import org.thoughtcrime.securesms.mediasend.Media;
+import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
+import org.thoughtcrime.securesms.mms.GlideApp;
+import org.thoughtcrime.securesms.profiles.AvatarHelper;
+import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.recipients.RecipientId;
+import org.thoughtcrime.securesms.util.BitmapUtil;
+import org.thoughtcrime.securesms.util.ViewUtil;
+import org.thoughtcrime.securesms.util.text.AfterTextChanged;
+
+import java.util.Objects;
+
+public class AddGroupDetailsFragment extends Fragment {
+
+ private static final int AVATAR_PLACEHOLDER_INSET_DP = 18;
+ private static final short REQUEST_CODE_AVATAR = 27621;
+ private static final String ARG_RECIPIENT_IDS = "recipient_ids";
+
+ private CircularProgressButton create;
+ private Callback callback;
+ private AddGroupDetailsViewModel viewModel;
+ private Drawable avatarPlaceholder;
+ private EditText name;
+ private Toolbar toolbar;
+ private ActionMode actionMode;
+
+ private ActionMode.Callback recipientActionModeCallback = new ActionMode.Callback() {
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ mode.getMenuInflater().inflate(R.menu.add_group_details_fragment_context_menu, menu);
+
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ return false;
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ if (item.getItemId() == R.id.action_delete) {
+ viewModel.deleteSelected();
+ mode.finish();
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ actionMode = null;
+ viewModel.clearSelected();
+ }
+ };
+
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+
+ if (context instanceof Callback) {
+ callback = (Callback) context;
+ } else {
+ throw new ClassCastException("Parent context should implement AddGroupDetailsFragment.Callback");
+ }
+ }
+
+ public static Fragment create(@NonNull RecipientId[] recipientIds) {
+ AddGroupDetailsFragment fragment = new AddGroupDetailsFragment();
+ Bundle arguments = new Bundle();
+
+ arguments.putParcelableArray(ARG_RECIPIENT_IDS, recipientIds);
+ fragment.setArguments(arguments);
+
+ return fragment;
+ }
+
+ @Override
+ public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
+ @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState)
+ {
+ return inflater.inflate(R.layout.add_group_details_fragment, container, false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ create = view.findViewById(R.id.create);
+ name = view.findViewById(R.id.group_name);
+ toolbar = view.findViewById(R.id.toolbar);
+
+ setCreateEnabled(false, false);
+
+ GroupMemberListView members = view.findViewById(R.id.member_list);
+ ImageView avatar = view.findViewById(R.id.group_avatar);
+ View mmsWarning = view.findViewById(R.id.mms_warning);
+
+ avatarPlaceholder = VectorDrawableCompat.create(getResources(), R.drawable.ic_camera_outline_32_ultramarine, requireActivity().getTheme());
+
+ if (savedInstanceState == null) {
+ avatar.setImageDrawable(new InsetDrawable(avatarPlaceholder, ViewUtil.dpToPx(AVATAR_PLACEHOLDER_INSET_DP)));
+ }
+
+ initializeViewModel();
+
+ avatar.setOnClickListener(v -> AvatarSelectionBottomSheetDialogFragment.create(false, true, REQUEST_CODE_AVATAR, true)
+ .show(getChildFragmentManager(), "BOTTOM"));
+ members.setRecipientLongClickListener(this::handleRecipientLongClick);
+ members.setRecipientClickListener(this::handleRecipientClick);
+ name.addTextChangedListener(new AfterTextChanged(editable -> viewModel.setName(editable.toString())));
+ toolbar.setNavigationOnClickListener(unused -> callback.onNavigationButtonPressed());
+ create.setOnClickListener(v -> handleCreateClicked());
+ viewModel.getMembers().observe(getViewLifecycleOwner(), members::setMembers);
+ viewModel.getCanSubmitForm().observe(getViewLifecycleOwner(), isFormValid -> setCreateEnabled(isFormValid, true));
+ viewModel.getIsMms().observe(getViewLifecycleOwner(), isMms -> mmsWarning.setVisibility(isMms ? View.VISIBLE : View.GONE));
+ viewModel.getAvatar().observe(getViewLifecycleOwner(), avatarBytes -> {
+ if (avatarBytes == null) {
+ avatar.setImageDrawable(new InsetDrawable(avatarPlaceholder, ViewUtil.dpToPx(AVATAR_PLACEHOLDER_INSET_DP)));
+ } else {
+ GlideApp.with(this)
+ .load(avatarBytes)
+ .circleCrop()
+ .skipMemoryCache(true)
+ .diskCacheStrategy(DiskCacheStrategy.NONE)
+ .into(avatar);
+ }
+ });
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
+ if (requestCode == REQUEST_CODE_AVATAR && resultCode == Activity.RESULT_OK && data != null) {
+ final Media result = data.getParcelableExtra(AvatarSelectionActivity.EXTRA_MEDIA);
+ final DecryptableStreamUriLoader.DecryptableUri decryptableUri = new DecryptableStreamUriLoader.DecryptableUri(result.getUri());
+
+ GlideApp.with(this)
+ .asBitmap()
+ .load(decryptableUri)
+ .skipMemoryCache(true)
+ .diskCacheStrategy(DiskCacheStrategy.NONE)
+ .centerCrop()
+ .override(AvatarHelper.AVATAR_DIMENSIONS, AvatarHelper.AVATAR_DIMENSIONS)
+ .into(new CustomTarget() {
+ @Override
+ public void onResourceReady(@NonNull Bitmap resource, Transition super Bitmap> transition) {
+ viewModel.setAvatar(Objects.requireNonNull(BitmapUtil.toByteArray(resource)));
+ }
+
+ @Override
+ public void onLoadCleared(@Nullable Drawable placeholder) {
+ }
+ });
+ } else {
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+ }
+
+ private void initializeViewModel() {
+ AddGroupDetailsFragmentArgs args = AddGroupDetailsFragmentArgs.fromBundle(requireArguments());
+ AddGroupDetailsRepository repository = new AddGroupDetailsRepository(requireContext());
+ AddGroupDetailsViewModel.Factory factory = new AddGroupDetailsViewModel.Factory(args.getRecipientIds(), repository);
+
+ viewModel = ViewModelProviders.of(this, factory).get(AddGroupDetailsViewModel.class);
+
+ viewModel.getGroupCreateResult().observe(getViewLifecycleOwner(), this::handleGroupCreateResult);
+ }
+
+ private void handleCreateClicked() {
+ create.setClickable(false);
+ create.setIndeterminateProgressMode(true);
+ create.setProgress(50);
+
+ viewModel.create();
+ }
+
+ private void handleRecipientClick(@NonNull Recipient recipient) {
+ if (actionMode == null) {
+ return;
+ }
+
+ int size = viewModel.toggleSelected(recipient);
+ if (size == 0) {
+ actionMode.finish();
+ }
+ }
+
+ private boolean handleRecipientLongClick(@NonNull Recipient recipient) {
+ if (actionMode != null) {
+ return false;
+ }
+
+ actionMode = toolbar.startActionMode(recipientActionModeCallback);
+
+ if (actionMode != null) {
+ viewModel.toggleSelected(recipient);
+ return true;
+ }
+
+ return false;
+ }
+
+ private void handleGroupCreateResult(@NonNull GroupCreateResult groupCreateResult) {
+ groupCreateResult.consume(this::handleGroupCreateResultSuccess, this::handleGroupCreateResultError);
+ }
+
+ private void handleGroupCreateResultSuccess(@NonNull GroupCreateResult.Success success) {
+ callback.onGroupCreated(success.getGroupRecipient().getId(), success.getThreadId());
+ }
+
+ private void handleGroupCreateResultError(@NonNull GroupCreateResult.Error error) {
+ switch (error.getErrorType()) {
+ case ERROR_IO:
+ case ERROR_BUSY:
+ toast(R.string.AddGroupDetailsFragment__try_again_later);
+ break;
+ case ERROR_FAILED:
+ toast(R.string.AddGroupDetailsFragment__group_creation_failed);
+ break;
+ case ERROR_INVALID_NAME:
+ name.setError(getString(R.string.AddGroupDetailsFragment__this_field_is_required));
+ break;
+ case ERROR_INVALID_MEMBER_COUNT:
+ toast(R.string.AddGroupDetailsFragment__groups_require_at_least_two_members);
+ break;
+ default:
+ throw new IllegalStateException("Unexpected error: " + error.getErrorType().name());
+ }
+ }
+
+ private void toast(@StringRes int toastStringId) {
+ Toast.makeText(requireContext(), toastStringId, Toast.LENGTH_SHORT)
+ .show();
+ }
+
+ private void setCreateEnabled(boolean isEnabled, boolean animate) {
+ if (create.isEnabled() == isEnabled) {
+ return;
+ }
+
+ create.setEnabled(isEnabled);
+ create.animate()
+ .setDuration(animate ? 300 : 0)
+ .alpha(isEnabled ? 1f : 0.5f);
+ }
+
+ public interface Callback {
+ void onGroupCreated(@NonNull RecipientId recipientId, long threadId);
+ void onNavigationButtonPressed();
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsRepository.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsRepository.java
new file mode 100644
index 0000000000..5272fd6441
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsRepository.java
@@ -0,0 +1,67 @@
+package org.thoughtcrime.securesms.groups.ui.creategroup.details;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.util.Consumer;
+
+import com.annimon.stream.Stream;
+
+import org.thoughtcrime.securesms.groups.GroupChangeBusyException;
+import org.thoughtcrime.securesms.groups.GroupChangeFailedException;
+import org.thoughtcrime.securesms.groups.GroupManager;
+import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
+import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.recipients.RecipientId;
+import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+final class AddGroupDetailsRepository {
+
+ private final Context context;
+
+ AddGroupDetailsRepository(@NonNull Context context) {
+ this.context = context;
+ }
+
+ void resolveMembers(@NonNull RecipientId[] recipientIds, Consumer> consumer) {
+ SignalExecutors.BOUNDED.execute(() -> {
+ List members = new ArrayList<>(recipientIds.length);
+
+ for (RecipientId id : recipientIds) {
+ members.add(new GroupMemberEntry.NewGroupCandidate(Recipient.resolved(id)));
+ }
+
+ consumer.accept(members);
+ });
+ }
+
+ void createPushGroup(@NonNull Set members,
+ @Nullable byte[] avatar,
+ @Nullable String name,
+ boolean mms,
+ Consumer resultConsumer)
+ {
+ SignalExecutors.BOUNDED.execute(() -> {
+ Set recipients = new HashSet<>(Stream.of(members).map(Recipient::resolved).toList());
+
+ try {
+ GroupManager.GroupActionResult result = GroupManager.createGroup(context, recipients, avatar, name, mms);
+
+ resultConsumer.accept(GroupCreateResult.success(result));
+ } catch (GroupChangeBusyException e) {
+ resultConsumer.accept(GroupCreateResult.error(GroupCreateResult.Error.Type.ERROR_BUSY));
+ } catch (GroupChangeFailedException e) {
+ resultConsumer.accept(GroupCreateResult.error(GroupCreateResult.Error.Type.ERROR_FAILED));
+ } catch (IOException e) {
+ resultConsumer.accept(GroupCreateResult.error(GroupCreateResult.Error.Type.ERROR_IO));
+ }
+ });
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsViewModel.java
new file mode 100644
index 0000000000..16206c1907
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsViewModel.java
@@ -0,0 +1,165 @@
+package org.thoughtcrime.securesms.groups.ui.creategroup.details;
+
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.Transformations;
+import androidx.lifecycle.ViewModel;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.annimon.stream.Collectors;
+import com.annimon.stream.Stream;
+
+import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
+import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.recipients.RecipientId;
+import org.thoughtcrime.securesms.util.DefaultValueLiveData;
+import org.thoughtcrime.securesms.util.SingleLiveEvent;
+import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+public final class AddGroupDetailsViewModel extends ViewModel {
+
+ private final LiveData> members;
+ private final DefaultValueLiveData> selected = new DefaultValueLiveData<>(new HashSet<>());
+ private final DefaultValueLiveData> deleted = new DefaultValueLiveData<>(new HashSet<>());
+ private final MutableLiveData name = new MutableLiveData<>("");
+ private final MutableLiveData avatar = new MutableLiveData<>();
+ private final LiveData isMms;
+ private final SingleLiveEvent groupCreateResult = new SingleLiveEvent<>();
+ private final LiveData canSubmitForm = Transformations.map(name, name -> !TextUtils.isEmpty(name));
+ private final AddGroupDetailsRepository repository;
+
+ AddGroupDetailsViewModel(@NonNull RecipientId[] recipientIds,
+ @NonNull AddGroupDetailsRepository repository)
+ {
+ this.repository = repository;
+
+ MutableLiveData> initialMembers = new MutableLiveData<>();
+ LiveData> membersWithoutDeleted = LiveDataUtil.combineLatest(initialMembers,
+ deleted,
+ AddGroupDetailsViewModel::filterDeletedMembers);
+
+ members = LiveDataUtil.combineLatest(membersWithoutDeleted, selected, AddGroupDetailsViewModel::updateSelectedMembers);
+ isMms = Transformations.map(members, this::isAnyForcedSms);
+
+ repository.resolveMembers(recipientIds, initialMembers::postValue);
+ }
+
+ @NonNull LiveData> getMembers() {
+ return members;
+ }
+
+ @NonNull LiveData getCanSubmitForm() {
+ return canSubmitForm;
+ }
+
+ @NonNull LiveData getGroupCreateResult() {
+ return groupCreateResult;
+ }
+
+ @NonNull LiveData getAvatar() {
+ return avatar;
+ }
+
+ @NonNull LiveData getIsMms() {
+ return isMms;
+ }
+
+ void setAvatar(@NonNull byte[] avatar) {
+ this.avatar.setValue(avatar);
+ }
+
+ void setName(@NonNull String name) {
+ this.name.setValue(name);
+ }
+
+ int toggleSelected(@NonNull Recipient recipient) {
+ Set selected = this.selected.getValue();
+
+ if (!selected.add(recipient.getId())) {
+ selected.remove(recipient.getId());
+ }
+
+ this.selected.setValue(selected);
+
+ return selected.size();
+ }
+
+ void clearSelected() {
+ this.selected.setValue(new HashSet<>());
+ }
+
+ void deleteSelected() {
+ Set selected = this.selected.getValue();
+ Set deleted = this.deleted.getValue();
+
+ deleted.addAll(selected);
+ this.deleted.setValue(deleted);
+ }
+
+ void create() {
+ List members = Objects.requireNonNull(this.members.getValue());
+ Set memberIds = Stream.of(members).map(member -> member.getMember().getId()).collect(Collectors.toSet());
+ byte[] avatarBytes = avatar.getValue();
+ String groupName = name.getValue();
+ boolean isGroupMms = isMms.getValue() == Boolean.TRUE;
+
+ if (TextUtils.isEmpty(groupName)) {
+ groupCreateResult.postValue(GroupCreateResult.error(GroupCreateResult.Error.Type.ERROR_INVALID_NAME));
+ return;
+ }
+
+ if (memberIds.isEmpty()) {
+ groupCreateResult.postValue(GroupCreateResult.error(GroupCreateResult.Error.Type.ERROR_INVALID_MEMBER_COUNT));
+ return;
+ }
+
+ repository.createPushGroup(memberIds,
+ avatarBytes,
+ groupName,
+ isGroupMms,
+ groupCreateResult::postValue);
+ }
+
+ private static @NonNull List filterDeletedMembers(@NonNull List members, @NonNull Set deleted) {
+ return Stream.of(members)
+ .filterNot(member -> deleted.contains(member.getMember().getId()))
+ .toList();
+ }
+
+ private static @NonNull List updateSelectedMembers(@NonNull List members, @NonNull Set selected) {
+ for (GroupMemberEntry.NewGroupCandidate member : members) {
+ member.setSelected(selected.contains(member.getMember().getId()));
+ }
+
+ return members;
+ }
+
+ private boolean isAnyForcedSms(@NonNull List members) {
+ return Stream.of(members)
+ .anyMatch(member -> !member.getMember().isRegistered());
+ }
+
+ static final class Factory implements ViewModelProvider.Factory {
+
+ private final RecipientId[] recipientIds;
+ private final AddGroupDetailsRepository repository;
+
+ Factory(@NonNull RecipientId[] recipientIds, @NonNull AddGroupDetailsRepository repository) {
+ this.recipientIds = recipientIds;
+ this.repository = repository;
+ }
+
+ @Override
+ public @NonNull T create(@NonNull Class modelClass) {
+ return Objects.requireNonNull(modelClass.cast(new AddGroupDetailsViewModel(recipientIds, repository)));
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/GroupCreateResult.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/GroupCreateResult.java
new file mode 100644
index 0000000000..d95b147523
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/GroupCreateResult.java
@@ -0,0 +1,77 @@
+package org.thoughtcrime.securesms.groups.ui.creategroup.details;
+
+import androidx.annotation.NonNull;
+import androidx.core.util.Consumer;
+
+import org.thoughtcrime.securesms.groups.GroupManager;
+import org.thoughtcrime.securesms.recipients.Recipient;
+
+abstract class GroupCreateResult {
+
+ static GroupCreateResult success(@NonNull GroupManager.GroupActionResult result) {
+ return new GroupCreateResult.Success(result.getThreadId(), result.getGroupRecipient());
+ }
+
+ static GroupCreateResult error(@NonNull GroupCreateResult.Error.Type errorType) {
+ return new GroupCreateResult.Error(errorType);
+ }
+
+ private GroupCreateResult() {
+ }
+
+ static final class Success extends GroupCreateResult {
+ private final long threadId;
+ private final Recipient groupRecipient;
+
+ private Success(long threadId, @NonNull Recipient groupRecipient) {
+ this.threadId = threadId;
+ this.groupRecipient = groupRecipient;
+ }
+
+ long getThreadId() {
+ return threadId;
+ }
+
+ @NonNull Recipient getGroupRecipient() {
+ return groupRecipient;
+ }
+
+ @Override
+ void consume(@NonNull Consumer successConsumer,
+ @NonNull Consumer errorConsumer)
+ {
+ successConsumer.accept(this);
+ }
+ }
+
+ static final class Error extends GroupCreateResult {
+ private final Error.Type errorType;
+
+ private Error(Error.Type errorType) {
+ this.errorType = errorType;
+ }
+
+ @Override
+ void consume(@NonNull Consumer successConsumer,
+ @NonNull Consumer errorConsumer)
+ {
+ errorConsumer.accept(this);
+ }
+
+ public Type getErrorType() {
+ return errorType;
+ }
+
+ enum Type {
+ ERROR_IO,
+ ERROR_BUSY,
+ ERROR_FAILED,
+ ERROR_INVALID_NAME,
+ ERROR_INVALID_MEMBER_COUNT
+ }
+ }
+
+ abstract void consume(@NonNull Consumer successConsumer,
+ @NonNull Consumer errorConsumer);
+
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionBottomSheetDialogFragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionBottomSheetDialogFragment.java
index fc2c16f908..9713d61bea 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionBottomSheetDialogFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionBottomSheetDialogFragment.java
@@ -32,7 +32,7 @@ public class AvatarSelectionBottomSheetDialogFragment extends BottomSheetDialogF
private static final String ARG_REQUEST_CODE = "request_code";
private static final String ARG_IS_GROUP = "is_group";
- public static DialogFragment create(boolean includeClear, boolean includeCamera, short resultCode, boolean isGroup) {
+ public static DialogFragment create(boolean includeClear, boolean includeCamera, short requestCode, boolean isGroup) {
DialogFragment fragment = new AvatarSelectionBottomSheetDialogFragment();
List selectionOptions = new ArrayList<>(3);
Bundle args = new Bundle();
@@ -52,7 +52,7 @@ public class AvatarSelectionBottomSheetDialogFragment extends BottomSheetDialogF
.toArray(String[]::new);
args.putStringArray(ARG_OPTIONS, options);
- args.putShort(ARG_REQUEST_CODE, resultCode);
+ args.putShort(ARG_REQUEST_CODE, requestCode);
args.putBoolean(ARG_IS_GROUP, isGroup);
fragment.setArguments(args);
diff --git a/app/src/main/res/drawable-v21/group_candidate_item_background_dark.xml b/app/src/main/res/drawable-v21/group_candidate_item_background_dark.xml
new file mode 100644
index 0000000000..f191271836
--- /dev/null
+++ b/app/src/main/res/drawable-v21/group_candidate_item_background_dark.xml
@@ -0,0 +1,17 @@
+
+
+ -
+
+
+
+
+ -
+
+
-
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable-v21/group_candidate_item_background_light.xml b/app/src/main/res/drawable-v21/group_candidate_item_background_light.xml
new file mode 100644
index 0000000000..08ccc39dd6
--- /dev/null
+++ b/app/src/main/res/drawable-v21/group_candidate_item_background_light.xml
@@ -0,0 +1,17 @@
+
+
+ -
+
+
+
+
+ -
+
+
-
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/contact_selection_checkbox.xml b/app/src/main/res/drawable/contact_selection_checkbox.xml
new file mode 100644
index 0000000000..c1228f8cd9
--- /dev/null
+++ b/app/src/main/res/drawable/contact_selection_checkbox.xml
@@ -0,0 +1,17 @@
+
+
+ -
+
+
-
+
+
+
+
+
+
+
+
+ -
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/group_candidate_item_background_dark.xml b/app/src/main/res/drawable/group_candidate_item_background_dark.xml
new file mode 100644
index 0000000000..c2336004d1
--- /dev/null
+++ b/app/src/main/res/drawable/group_candidate_item_background_dark.xml
@@ -0,0 +1,9 @@
+
+
+ -
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/group_candidate_item_background_light.xml b/app/src/main/res/drawable/group_candidate_item_background_light.xml
new file mode 100644
index 0000000000..2de40201c8
--- /dev/null
+++ b/app/src/main/res/drawable/group_candidate_item_background_light.xml
@@ -0,0 +1,9 @@
+
+
+ -
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_arrow_right_24.xml b/app/src/main/res/drawable/ic_arrow_right_24.xml
new file mode 100644
index 0000000000..7b443d8450
--- /dev/null
+++ b/app/src/main/res/drawable/ic_arrow_right_24.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_camera_outline_32_ultramarine.xml b/app/src/main/res/drawable/ic_camera_outline_32_ultramarine.xml
new file mode 100644
index 0000000000..82feba1281
--- /dev/null
+++ b/app/src/main/res/drawable/ic_camera_outline_32_ultramarine.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_check_outline_22.xml b/app/src/main/res/drawable/ic_check_outline_22.xml
new file mode 100644
index 0000000000..9f2ebd3d51
--- /dev/null
+++ b/app/src/main/res/drawable/ic_check_outline_22.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_error_outline_24.xml b/app/src/main/res/drawable/ic_error_outline_24.xml
new file mode 100644
index 0000000000..db18e74c79
--- /dev/null
+++ b/app/src/main/res/drawable/ic_error_outline_24.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/tinted_circle_dark.xml b/app/src/main/res/drawable/tinted_circle_dark.xml
new file mode 100644
index 0000000000..c56381ea67
--- /dev/null
+++ b/app/src/main/res/drawable/tinted_circle_dark.xml
@@ -0,0 +1,5 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/tinted_circle_light.xml b/app/src/main/res/drawable/tinted_circle_light.xml
new file mode 100644
index 0000000000..ddb1cd660d
--- /dev/null
+++ b/app/src/main/res/drawable/tinted_circle_light.xml
@@ -0,0 +1,5 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/add_group_details_activity.xml b/app/src/main/res/layout/add_group_details_activity.xml
new file mode 100644
index 0000000000..b193a4772e
--- /dev/null
+++ b/app/src/main/res/layout/add_group_details_activity.xml
@@ -0,0 +1,9 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/add_group_details_fragment.xml b/app/src/main/res/layout/add_group_details_fragment.xml
new file mode 100644
index 0000000000..0a5a48a49f
--- /dev/null
+++ b/app/src/main/res/layout/add_group_details_fragment.xml
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/contact_selection_list_item.xml b/app/src/main/res/layout/contact_selection_list_item.xml
index f1549b3fa7..0101d54112 100644
--- a/app/src/main/res/layout/contact_selection_list_item.xml
+++ b/app/src/main/res/layout/contact_selection_list_item.xml
@@ -1,31 +1,47 @@
-
+
-
+
+
+
+
+
+
@@ -61,7 +77,8 @@
android:paddingStart="10dip"
android:ellipsize="end"
android:singleLine="true"
- android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textAppearance="@style/TextAppearance.Signal.Body2"
+ android:textColor="?attr/title_text_color_secondary"
android:fontFamily="sans-serif-light"
tools:text="@sample/contacts.json/data/label"
tools:ignore="RtlSymmetry" />
@@ -70,11 +87,4 @@
-
-
diff --git a/app/src/main/res/layout/create_group_activity.xml b/app/src/main/res/layout/create_group_activity.xml
new file mode 100644
index 0000000000..05a58f2e3b
--- /dev/null
+++ b/app/src/main/res/layout/create_group_activity.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/group_new_candidate_recipient_list_item.xml b/app/src/main/res/layout/group_new_candidate_recipient_list_item.xml
new file mode 100644
index 0000000000..7cfad1012d
--- /dev/null
+++ b/app/src/main/res/layout/group_new_candidate_recipient_list_item.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/add_group_details_fragment_context_menu.xml b/app/src/main/res/menu/add_group_details_fragment_context_menu.xml
new file mode 100644
index 0000000000..8156129fc0
--- /dev/null
+++ b/app/src/main/res/menu/add_group_details_fragment_context_menu.xml
@@ -0,0 +1,9 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/navigation/create_group.xml b/app/src/main/res/navigation/create_group.xml
new file mode 100644
index 0000000000..38ad375bea
--- /dev/null
+++ b/app/src/main/res/navigation/create_group.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
index 637d01b4f4..16186757de 100644
--- a/app/src/main/res/values/attrs.xml
+++ b/app/src/main/res/values/attrs.xml
@@ -139,6 +139,8 @@
+
+
@@ -226,6 +228,7 @@
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 265b6968d7..9c4597a6fe 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -487,6 +487,19 @@
- Error canceling invites
+
+ Name this group
+ Create
+ Members
+ Group name (required)
+ This field is required.
+ Groups require at least two members.
+ Group creation failed.
+ Try again later.
+ You\'ve selected a contact that doesn\'t support Signal groups, so this group will be MMS.
+ Remove
+ SMS contact
+
Disappearing messages
Pending group invites
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index bd995361a2..06b8626c5f 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -188,6 +188,8 @@
- @drawable/ic_arrow_left_24
+ - @drawable/group_candidate_item_background_light
+
- @drawable/ic_kbs_splash_light_svg
- @color/white
@@ -251,6 +253,8 @@
- @color/white
- @color/transparent_white_90
+ - @drawable/tinted_circle_light
+
- @drawable/contact_list_divider_light
- @color/debuglog_light_none
@@ -475,6 +479,10 @@
- @drawable/ic_arrow_left_24
- @drawable/ic_arrow_left_24
+ - @drawable/group_candidate_item_background_dark
+
+ - @drawable/tinted_circle_dark
+
- @drawable/ic_kbs_splash_dark_svg
- @color/core_grey_95