Implement new group creation screens behind flag.

master
Alex Hart 2020-05-13 13:41:36 -03:00
parent ed0825112d
commit ccff7b1148
42 changed files with 1422 additions and 84 deletions

View File

@ -483,8 +483,13 @@
android:theme="@style/TextSecure.LightNoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
<activity android:name=".groups.ui.creategroup.CreateGroupActivity"
android:theme="@style/TextSecure.LightNoActionBar" />
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"/>
<activity android:name=".groups.ui.creategroup.details.AddGroupDetailsActivity"
android:theme="@style/TextSecure.LightNoActionBar" />
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"/>
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
<service android:enabled="true" android:exported="false" android:name=".service.KeyCachingService"/>
<service android:enabled="true" android:name=".service.IncomingMessageObserver$ForegroundService"/>

View File

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

View File

@ -187,10 +187,14 @@ public final class ContactSelectionListFragment extends Fragment
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}
@NonNull List<SelectedContact> getSelectedContacts() {
public @NonNull List<SelectedContact> getSelectedContacts() {
return cursorRecyclerViewAdapter.getSelectedContacts();
}
public int getSelectedContactsCount() {
return cursorRecyclerViewAdapter.getSelectedContactsCount();
}
private boolean isMulti() {
return requireActivity().getIntent().getBooleanExtra(MULTI_SELECT, false);
}

View File

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

View File

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

View File

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

View File

@ -250,6 +250,10 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
return selectedContacts.getContacts();
}
public int getSelectedContactsCount() {
return selectedContacts.size();
}
private CharSequence getSpannedHeaderString(int position) {
final String headerString = getHeaderString(position);
if (isPush(position)) {

View File

@ -59,7 +59,8 @@ public class ContactsCursorLoader extends CursorLoader {
public static final int FLAG_SMS = 1 << 1;
public static final int FLAG_ACTIVE_GROUPS = 1 << 2;
public static final int FLAG_INACTIVE_GROUPS = 1 << 3;
public static final int FLAG_ALL = FLAG_PUSH | FLAG_SMS | FLAG_ACTIVE_GROUPS | FLAG_INACTIVE_GROUPS;
public static final int FLAG_SELF = 1 << 4;
public static final int FLAG_ALL = FLAG_PUSH | FLAG_SMS | FLAG_ACTIVE_GROUPS | FLAG_INACTIVE_GROUPS | FLAG_SELF;
}
private static final String[] CONTACT_PROJECTION = new String[]{ContactRepository.ID_COLUMN,
@ -248,7 +249,7 @@ public class ContactsCursorLoader extends CursorLoader {
}
if (pushEnabled(mode)) {
cursorList.add(contactRepository.querySignalContacts(filter));
cursorList.add(contactRepository.querySignalContacts(filter, selfEnabled(mode)));
}
if (pushEnabled(mode) && smsEnabled(mode)) {
@ -329,6 +330,10 @@ public class ContactsCursorLoader extends CursorLoader {
return sum == 0;
}
private static boolean selfEnabled(int mode) {
return flagSet(mode, DisplayMode.FLAG_SELF);
}
private static boolean pushEnabled(int mode) {
return flagSet(mode, DisplayMode.FLAG_PUSH);
}

View File

@ -37,6 +37,10 @@ public final class SelectedContactSet {
return new ArrayList<>(contacts);
}
public int size() {
return contacts.size();
}
public void clear() {
contacts.clear();
}

View File

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

View File

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

View File

@ -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<GroupMemberL
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 static final int NEW_GROUP_CANDIDATE = 3;
private final ArrayList<GroupMemberEntry> 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<GroupMemberL
switch (viewType) {
case FULL_MEMBER:
return new FullMemberViewHolder(LayoutInflater.from(parent.getContext())
.inflate(R.layout.group_recipient_list_item,
parent, false), recipientClickListener, adminActionsListener);
.inflate(R.layout.group_recipient_list_item, parent, false),
recipientClickListener,
recipientLongClickListener,
adminActionsListener);
case OWN_INVITE_PENDING:
return new OwnInvitePendingMemberViewHolder(LayoutInflater.from(parent.getContext())
.inflate(R.layout.group_recipient_list_item,
parent, false), recipientClickListener, adminActionsListener);
.inflate(R.layout.group_recipient_list_item, parent, false),
recipientClickListener,
recipientLongClickListener,
adminActionsListener);
case OTHER_INVITE_PENDING_COUNT:
return new UnknownPendingMemberCountViewHolder(LayoutInflater.from(parent.getContext())
.inflate(R.layout.group_recipient_list_item,
parent, false), adminActionsListener);
.inflate(R.layout.group_recipient_list_item, parent, false),
adminActionsListener);
case NEW_GROUP_CANDIDATE:
return new NewGroupInviteeViewHolder(LayoutInflater.from(parent.getContext())
.inflate(R.layout.group_new_candidate_recipient_list_item, parent, false),
recipientClickListener,
recipientLongClickListener);
default:
throw new AssertionError();
}
@ -72,6 +84,10 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
this.recipientClickListener = recipientClickListener;
}
void setRecipientLongClickListener(@Nullable RecipientLongClickListener recipientLongClickListener) {
this.recipientLongClickListener = recipientLongClickListener;
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.bind(data.get(position));
@ -87,6 +103,8 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
return OWN_INVITE_PENDING;
} else if (groupMemberEntry instanceof GroupMemberEntry.UnknownPendingMemberCount) {
return OTHER_INVITE_PENDING_COUNT;
} else if (groupMemberEntry instanceof GroupMemberEntry.NewGroupCandidate) {
return NEW_GROUP_CANDIDATE;
}
throw new AssertionError();
@ -99,31 +117,34 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
static abstract class ViewHolder extends LifecycleViewHolder {
final Context context;
final AvatarImageView avatar;
final TextView recipient;
final PopupMenuView popupMenu;
final View popupMenuContainer;
final ProgressBar busyProgress;
final View admin;
@Nullable final RecipientClickListener recipientClickListener;
@Nullable final AdminActionsListener adminActionsListener;
final Context context;
final AvatarImageView avatar;
final TextView recipient;
final PopupMenuView popupMenu;
final View popupMenuContainer;
final ProgressBar busyProgress;
final View admin;
@Nullable final RecipientClickListener recipientClickListener;
@Nullable final AdminActionsListener adminActionsListener;
@Nullable final RecipientLongClickListener recipientLongClickListener;
ViewHolder(@NonNull View itemView,
@Nullable RecipientClickListener recipientClickListener,
@Nullable RecipientLongClickListener recipientLongClickListener,
@Nullable AdminActionsListener adminActionsListener)
{
super(itemView);
this.context = itemView.getContext();
this.avatar = itemView.findViewById(R.id.recipient_avatar);
this.recipient = itemView.findViewById(R.id.recipient_name);
this.popupMenu = itemView.findViewById(R.id.popupMenu);
this.popupMenuContainer = itemView.findViewById(R.id.popupMenuProgressContainer);
this.busyProgress = itemView.findViewById(R.id.menuBusyProgress);
this.admin = itemView.findViewById(R.id.admin);
this.recipientClickListener = recipientClickListener;
this.adminActionsListener = adminActionsListener;
this.context = itemView.getContext();
this.avatar = itemView.findViewById(R.id.recipient_avatar);
this.recipient = itemView.findViewById(R.id.recipient_name);
this.popupMenu = itemView.findViewById(R.id.popupMenu);
this.popupMenuContainer = itemView.findViewById(R.id.popupMenuProgressContainer);
this.busyProgress = itemView.findViewById(R.id.menuBusyProgress);
this.admin = itemView.findViewById(R.id.admin);
this.recipientClickListener = recipientClickListener;
this.recipientLongClickListener = recipientLongClickListener;
this.adminActionsListener = adminActionsListener;
}
void bindRecipient(@NonNull Recipient recipient) {
@ -149,6 +170,13 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
recipientClickListener.onClick(recipient);
}
});
this.itemView.setOnLongClickListener(v -> {
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<GroupMemberL
FullMemberViewHolder(@NonNull View itemView,
@Nullable RecipientClickListener recipientClickListener,
@Nullable RecipientLongClickListener recipientLongClickListener,
@Nullable AdminActionsListener adminActionsListener)
{
super(itemView, recipientClickListener, adminActionsListener);
super(itemView, recipientClickListener, recipientLongClickListener, adminActionsListener);
}
@Override
@ -195,14 +224,46 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
admin.setVisibility(fullMember.isAdmin() ? View.VISIBLE : View.INVISIBLE);
}
}
final static class NewGroupInviteeViewHolder extends ViewHolder {
private final View smsContact;
private final View smsWarning;
NewGroupInviteeViewHolder(@NonNull View itemView,
@Nullable RecipientClickListener recipientClickListener,
@Nullable RecipientLongClickListener recipientLongClickListener)
{
super(itemView, recipientClickListener, recipientLongClickListener, null);
smsContact = itemView.findViewById(R.id.sms_contact);
smsWarning = itemView.findViewById(R.id.sms_warning);
}
@Override
void bind(@NonNull GroupMemberEntry memberEntry) {
GroupMemberEntry.NewGroupCandidate newGroupCandidate = (GroupMemberEntry.NewGroupCandidate) memberEntry;
bindRecipient(newGroupCandidate.getMember());
bindRecipientClick(newGroupCandidate.getMember());
itemView.setSelected(false);
newGroupCandidate.isSelected().observe(this, itemView::setSelected);
int smsWarningVisibility = newGroupCandidate.getMember().isRegistered() ? View.GONE : View.VISIBLE;
smsContact.setVisibility(smsWarningVisibility);
smsWarning.setVisibility(smsWarningVisibility);
}
}
final static class OwnInvitePendingMemberViewHolder extends ViewHolder {
OwnInvitePendingMemberViewHolder(@NonNull View itemView,
@Nullable RecipientClickListener recipientClickListener,
@Nullable RecipientLongClickListener recipientLongClickListener,
@Nullable AdminActionsListener adminActionsListener)
{
super(itemView, recipientClickListener, adminActionsListener);
super(itemView, recipientClickListener, recipientLongClickListener, adminActionsListener);
}
@Override
@ -231,7 +292,7 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
final static class UnknownPendingMemberCountViewHolder extends ViewHolder {
UnknownPendingMemberCountViewHolder(@NonNull View itemView, @Nullable AdminActionsListener adminActionsListener) {
super(itemView, null, adminActionsListener);
super(itemView, null, null, adminActionsListener);
}
@Override

View File

@ -59,6 +59,10 @@ public final class GroupMemberListView extends RecyclerView {
membersAdapter.setRecipientClickListener(listener);
}
public void setRecipientLongClickListener(@Nullable RecipientLongClickListener listener) {
membersAdapter.setRecipientLongClickListener(listener);
}
public void setMembers(@NonNull List<? extends GroupMemberEntry> recipients) {
membersAdapter.updateData(recipients);
}

View File

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

View File

@ -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> recipientId, String number) {
if (contactsFragment.getSelectedContactsCount() >= MINIMUM_GROUP_SIZE) {
enableNext();
}
}
@Override
public void onContactDeselected(Optional<RecipientId> 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);
}
}

View File

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

View File

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

View File

@ -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<List<GroupMemberEntry.NewGroupCandidate>> consumer) {
SignalExecutors.BOUNDED.execute(() -> {
List<GroupMemberEntry.NewGroupCandidate> members = new ArrayList<>(recipientIds.length);
for (RecipientId id : recipientIds) {
members.add(new GroupMemberEntry.NewGroupCandidate(Recipient.resolved(id)));
}
consumer.accept(members);
});
}
void createPushGroup(@NonNull Set<RecipientId> members,
@Nullable byte[] avatar,
@Nullable String name,
boolean mms,
Consumer<GroupCreateResult> resultConsumer)
{
SignalExecutors.BOUNDED.execute(() -> {
Set<Recipient> 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));
}
});
}
}

View File

@ -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<List<GroupMemberEntry.NewGroupCandidate>> members;
private final DefaultValueLiveData<Set<RecipientId>> selected = new DefaultValueLiveData<>(new HashSet<>());
private final DefaultValueLiveData<Set<RecipientId>> deleted = new DefaultValueLiveData<>(new HashSet<>());
private final MutableLiveData<String> name = new MutableLiveData<>("");
private final MutableLiveData<byte[]> avatar = new MutableLiveData<>();
private final LiveData<Boolean> isMms;
private final SingleLiveEvent<GroupCreateResult> groupCreateResult = new SingleLiveEvent<>();
private final LiveData<Boolean> canSubmitForm = Transformations.map(name, name -> !TextUtils.isEmpty(name));
private final AddGroupDetailsRepository repository;
AddGroupDetailsViewModel(@NonNull RecipientId[] recipientIds,
@NonNull AddGroupDetailsRepository repository)
{
this.repository = repository;
MutableLiveData<List<GroupMemberEntry.NewGroupCandidate>> initialMembers = new MutableLiveData<>();
LiveData<List<GroupMemberEntry.NewGroupCandidate>> 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<List<GroupMemberEntry.NewGroupCandidate>> getMembers() {
return members;
}
@NonNull LiveData<Boolean> getCanSubmitForm() {
return canSubmitForm;
}
@NonNull LiveData<GroupCreateResult> getGroupCreateResult() {
return groupCreateResult;
}
@NonNull LiveData<byte[]> getAvatar() {
return avatar;
}
@NonNull LiveData<Boolean> 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<RecipientId> 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<RecipientId> selected = this.selected.getValue();
Set<RecipientId> deleted = this.deleted.getValue();
deleted.addAll(selected);
this.deleted.setValue(deleted);
}
void create() {
List<GroupMemberEntry.NewGroupCandidate> members = Objects.requireNonNull(this.members.getValue());
Set<RecipientId> 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<GroupMemberEntry.NewGroupCandidate> filterDeletedMembers(@NonNull List<GroupMemberEntry.NewGroupCandidate> members, @NonNull Set<RecipientId> deleted) {
return Stream.of(members)
.filterNot(member -> deleted.contains(member.getMember().getId()))
.toList();
}
private static @NonNull List<GroupMemberEntry.NewGroupCandidate> updateSelectedMembers(@NonNull List<GroupMemberEntry.NewGroupCandidate> members, @NonNull Set<RecipientId> selected) {
for (GroupMemberEntry.NewGroupCandidate member : members) {
member.setSelected(selected.contains(member.getMember().getId()));
}
return members;
}
private boolean isAnyForcedSms(@NonNull List<GroupMemberEntry.NewGroupCandidate> 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 extends ViewModel> T create(@NonNull Class<T> modelClass) {
return Objects.requireNonNull(modelClass.cast(new AddGroupDetailsViewModel(recipientIds, repository)));
}
}
}

View File

@ -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<Success> successConsumer,
@NonNull Consumer<Error> 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<Success> successConsumer,
@NonNull Consumer<Error> 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<Success> successConsumer,
@NonNull Consumer<Error> errorConsumer);
}

View File

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

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true">
<shape android:shape="rectangle">
<solid android:color="?attr/colorControlHighlight" />
</shape>
</item>
<item android:state_selected="false">
<ripple android:color="?attr/colorControlHighlight">
<item android:id="@android:id/background">
<shape android:shape="rectangle">
<solid android:color="@color/core_grey_95" />
</shape>
</item>
</ripple>
</item>
</selector>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true">
<shape android:shape="rectangle">
<solid android:color="?attr/colorControlHighlight" />
</shape>
</item>
<item android:state_selected="false">
<ripple android:color="?attr/colorControlHighlight">
<item android:id="@android:id/background">
<shape android:shape="rectangle">
<solid android:color="@color/core_white" />
</shape>
</item>
</ripple>
</item>
</selector>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true">
<layer-list>
<item>
<shape android:shape="oval">
<solid android:color="@color/core_ultramarine" />
<stroke android:width="1dp" android:color="@color/white" />
</shape>
</item>
<item android:drawable="@drawable/ic_check_outline_22" />
</layer-list>
</item>
<item android:state_checked="false">
<color android:color="@null" />
</item>
</selector>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true">
<shape android:shape="rectangle">
<solid android:color="@color/core_ultramarine_light" />
</shape>
</item>
<item android:state_selected="false" android:drawable="@null" />
</selector>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true">
<shape android:shape="rectangle">
<solid android:color="@color/core_ultramarine" />
</shape>
</item>
<item android:state_selected="false" android:drawable="@null" />
</selector>

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="@color/white"
android:pathData="M11.409,19.47l5.471,-5.471l1.559,-1.249l-14.439,0l0,-1.5l14.439,0l-1.559,-1.249l-5.471,-5.471l1.061,-1.06l8.53,8.53l-8.53,8.53l-1.061,-1.06z"/>
</vector>

View File

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:fillColor="@color/core_ultramarine"
android:pathData="M16,10.5a6.25,6.25 0,1 1,-6.25 6.25A6.25,6.25 0,0 1,16 10.5M16,9a7.75,7.75 0,1 0,7.75 7.75A7.75,7.75 0,0 0,16 9Z"/>
<path
android:fillColor="@color/core_ultramarine"
android:pathData="M18.59,4.5A1.52,1.52 0,0 1,19.75 5L21.8,7.5H25A3.5,3.5 0,0 1,28.5 11V24A3.5,3.5 0,0 1,25 27.5H7A3.5,3.5 0,0 1,3.5 24V11A3.5,3.5 0,0 1,7 7.5h3.2L12.25,5a1.52,1.52 0,0 1,1.16 -0.54h5.18m0,-1.5H13.41A3,3 0,0 0,11.1 4.08L9.5,6H7a5,5 0,0 0,-5 5V24a5,5 0,0 0,5 5H25a5,5 0,0 0,5 -5V11a5,5 0,0 0,-5 -5H22.5L20.9,4.08A3,3 0,0 0,18.59 3Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<path
android:pathData="M15.773,7.338L16.834,8.4L9.232,16L5.171,11.939L6.232,10.879L9.232,13.879L15.773,7.338Z"
android:fillColor="@color/white"/>
</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="?attr/icon_tint"
android:pathData="M10.75,6h2.5l-0.5,7.5h-1.5ZM12,2.5A9.5,9.5 0,1 0,21.5 12,9.511 9.511,0 0,0 12,2.5M12,1A11,11 0,1 1,1 12,11 11,0 0,1 12,1ZM13.5,16.5A1.5,1.5 0,1 0,12 18,1.5 1.5,0 0,0 13.5,16.5Z"/>
</vector>

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_grey_80" />
</shape>

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_grey_05" />
</shape>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/create_group" />

View File

@ -0,0 +1,94 @@
<?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/toolbar"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
app:layout_constraintTop_toTopOf="parent"
app:navigationIcon="@drawable/ic_arrow_left_24"
app:title="@string/AddGroupDetailsFragment__name_this_group"
app:titleTextAppearance="@style/TextAppearance.Signal.Body1.Bold" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/group_avatar"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:background="?attr/tinted_circle_background"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar" />
<org.thoughtcrime.securesms.components.emoji.EmojiEditText
android:id="@+id/group_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginEnd="16dp"
android:background="@null"
android:hint="@string/AddGroupDetailsFragment__group_name_required"
android:maxLength="34"
android:maxLines="1"
app:layout_constraintBottom_toBottomOf="@id/group_avatar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/group_avatar"
app:layout_constraintTop_toTopOf="@id/group_avatar" />
<TextView
android:id="@+id/mms_warning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@color/core_ultramarine"
android:paddingStart="16dp"
android:paddingTop="8dp"
android:paddingEnd="16dp"
android:paddingBottom="8dp"
android:text="@string/AddGroupDetailsFragment__youve_selected_a_contact_that_doesnt"
android:textAppearance="@style/TextAppearance.Signal.Body2"
android:textColor="@color/white"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/member_list_header"
app:layout_constraintTop_toBottomOf="@id/group_avatar"
tools:visibility="visible" />
<TextView
android:id="@+id/member_list_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="12dp"
android:text="@string/AddGroupDetailsFragment__members"
android:textAppearance="@style/TextAppearance.Signal.Subtitle2"
android:textColor="?attr/title_text_color_secondary"
app:layout_constraintTop_toBottomOf="@id/mms_warning"
app:layout_goneMarginTop="30dp" />
<org.thoughtcrime.securesms.groups.ui.GroupMemberListView
android:id="@+id/member_list"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/member_list_header" />
<com.dd.CircularProgressButton
android:id="@+id/create"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:textColor="@color/white"
app:cpb_colorIndicator="@color/white"
app:cpb_colorProgress="@color/core_ultramarine"
app:cpb_cornerRadius="28dp"
app:cpb_selectorIdle="@drawable/progress_button_state"
app:cpb_textIdle="@string/AddGroupDetailsFragment__create"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,31 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.contacts.ContactSelectionListItem
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="@dimen/contact_selection_item_height"
android:orientation="horizontal"
android:gravity="center_vertical"
android:focusable="true"
android:background="@drawable/conversation_item_background"
android:paddingStart="@dimen/selection_item_header_width"
android:paddingEnd="24dp">
<org.thoughtcrime.securesms.contacts.ContactSelectionListItem xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="@dimen/contact_selection_item_height"
android:orientation="horizontal"
android:gravity="center_vertical"
android:focusable="true"
android:background="@drawable/conversation_item_background"
android:paddingStart="@dimen/selection_item_header_width"
android:paddingEnd="24dp">
<org.thoughtcrime.securesms.components.AvatarImageView
<FrameLayout
android:layout_width="52dp"
android:layout_height="52dp">
<org.thoughtcrime.securesms.components.AvatarImageView
android:id="@+id/contact_photo_image"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center_vertical"
android:foreground="@drawable/contact_photo_background"
android:cropToPadding="true"
android:contentDescription="@string/SingleContactSelectionActivity_contact_photo"
tools:src="@color/blue_600"
tools:ignore="UnusedAttribute" />
<androidx.appcompat.widget.AppCompatCheckBox
android:id="@+id/check_box"
android:background="@drawable/contact_selection_checkbox"
android:button="@null"
android:layout_width="22dp"
android:layout_height="22dp"
android:layout_gravity="bottom|end"
android:focusable="false"
android:clickable="false" />
</FrameLayout>
<LinearLayout android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:orientation="vertical">
<org.thoughtcrime.securesms.components.FromTextView
@ -35,8 +51,7 @@
android:checkMark="?android:attr/listChoiceIndicatorMultiple"
android:singleLine="true"
android:ellipsize="marquee"
style="@style/Signal.Text.Body"
android:fontFamily="sans-serif-medium"
android:textAppearance="@style/TextAppearance.Signal.Body1.Bold"
tools:text="@sample/contacts.json/data/name" />
<LinearLayout android:id="@+id/number_container"
@ -50,7 +65,8 @@
android:textDirection="ltr"
android:singleLine="true"
android:ellipsize="marquee"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textAppearance="@style/TextAppearance.Signal.Body2"
android:textColor="?attr/title_text_color_secondary"
android:textSize="14sp"
android:fontFamily="sans-serif-light"
tools:text="@sample/contacts.json/data/number" />
@ -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 @@
</LinearLayout>
<CheckBox android:id="@+id/check_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:focusable="false"
android:clickable="false" />
</org.thoughtcrime.securesms.contacts.ContactSelectionListItem>

View File

@ -0,0 +1,42 @@
<?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"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_gravity="center"
android:orientation="vertical">
<org.thoughtcrime.securesms.components.ContactFilterToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:elevation="4dp"
android:minHeight="?attr/actionBarSize"
android:theme="?attr/actionBarStyle"
app:contentInsetStartWithNavigation="0dp"
app:layout_constraintTop_toTopOf="parent"
app:navigationIcon="@drawable/ic_arrow_left_24" />
<fragment
android:id="@+id/contact_selection_list_fragment"
android:name="org.thoughtcrime.securesms.ContactSelectionListFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/next"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:tint="@color/core_white"
app:backgroundTint="@color/core_ultramarine"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:srcCompat="@drawable/ic_arrow_right_24" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,66 @@
<?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="?attr/group_candidate_item_background"
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" />
<TextView
android:id="@+id/recipient_name"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="8dp"
android:gravity="start|center_vertical"
app:layout_goneMarginEnd="16dp"
android:textAlignment="viewStart"
android:textAppearance="@style/TextAppearance.Signal.Body1"
app:layout_constraintBottom_toTopOf="@+id/sms_contact"
app:layout_constraintEnd_toStartOf="@+id/sms_warning"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@+id/recipient_avatar"
app:layout_constraintTop_toTopOf="@+id/recipient_avatar"
tools:text="@tools:sample/full_names" />
<TextView
android:id="@+id/sms_contact"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="8dp"
android:gravity="start|center_vertical"
android:text="@string/AddGroupDetailsFragment__sms_contact"
android:textAlignment="viewStart"
android:textAppearance="@style/TextAppearance.Signal.Body2"
android:textColor="?attr/title_text_color_secondary"
app:layout_constraintBottom_toBottomOf="@id/recipient_avatar"
app:layout_constraintEnd_toEndOf="@+id/recipient_name"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@+id/recipient_avatar"
app:layout_constraintTop_toBottomOf="@+id/recipient_name" />
<ImageView
android:id="@+id/sms_warning"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:scaleType="centerInside"
app:layout_constraintBottom_toBottomOf="@id/recipient_name"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/recipient_name"
app:layout_constraintTop_toTopOf="@id/recipient_name"
app:srcCompat="@drawable/ic_error_outline_24" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,9 @@
<?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/action_delete"
android:icon="?attr/menu_trash_icon"
android:title="@string/AddGroupDetailsFragment__remove"
app:showAsAction="always" />
</menu>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation 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:id="@+id/create_group"
app:startDestination="@id/addGroupDetailsFragment">
<fragment
android:id="@+id/addGroupDetailsFragment"
android:name="org.thoughtcrime.securesms.groups.ui.creategroup.details.AddGroupDetailsFragment"
android:label="add_group_details_fragment"
tools:layout="@layout/add_group_details_fragment">
<argument
android:name="recipient_ids"
app:argType="org.thoughtcrime.securesms.recipients.RecipientId[]"
app:nullable="false" />
</fragment>
</navigation>

View File

@ -139,6 +139,8 @@
<attr name="conversation_scroll_to_bottom_background" format="reference" />
<attr name="conversation_scroll_to_bottom_foreground_color" format="color" />
<attr name="tinted_circle_background" format="reference" />
<attr name="dialog_info_icon" format="reference" />
<attr name="dialog_alert_icon" format="reference" />
<attr name="dialog_background_color" format="reference|color" />
@ -226,6 +228,7 @@
<attr name="custom_pref_toggle" format="string"/>
</declare-styleable>
<attr name="group_candidate_item_background" format="reference" />
<attr name="group_members_dialog_icon" format="reference"/>
<attr name="manage_group_add_members_icon" format="reference"/>
<attr name="manage_group_view_all_icon" format="reference"/>

View File

@ -487,6 +487,19 @@
<item quantity="other">Error canceling invites</item>
</plurals>
<!-- AddGroupDetailsFragment -->
<string name="AddGroupDetailsFragment__name_this_group">Name this group</string>
<string name="AddGroupDetailsFragment__create">Create</string>
<string name="AddGroupDetailsFragment__members">Members</string>
<string name="AddGroupDetailsFragment__group_name_required">Group name (required)</string>
<string name="AddGroupDetailsFragment__this_field_is_required">This field is required.</string>
<string name="AddGroupDetailsFragment__groups_require_at_least_two_members">Groups require at least two members.</string>
<string name="AddGroupDetailsFragment__group_creation_failed">Group creation failed.</string>
<string name="AddGroupDetailsFragment__try_again_later">Try again later.</string>
<string name="AddGroupDetailsFragment__youve_selected_a_contact_that_doesnt">You\'ve selected a contact that doesn\'t support Signal groups, so this group will be MMS.</string>
<string name="AddGroupDetailsFragment__remove">Remove</string>
<string name="AddGroupDetailsFragment__sms_contact">SMS contact</string>
<!-- ManageGroupActivity -->
<string name="ManageGroupActivity_disappearing_messages">Disappearing messages</string>
<string name="ManageGroupActivity_pending_group_invites">Pending group invites</string>

View File

@ -188,6 +188,8 @@
<item name="android:homeAsUpIndicator">@drawable/ic_arrow_left_24</item>
<!--<item name="android:windowContentOverlay">@drawable/compat_actionbar_shadow_background</item>-->
<item name="group_candidate_item_background">@drawable/group_candidate_item_background_light</item>
<item name="kbs_splash_image">@drawable/ic_kbs_splash_light_svg</item>
<item name="attachment_type_selector_background">@color/white</item>
@ -251,6 +253,8 @@
<item name="conversation_title_color">@color/white</item>
<item name="conversation_subtitle_color">@color/transparent_white_90</item>
<item name="tinted_circle_background">@drawable/tinted_circle_light</item>
<item name="contact_list_divider">@drawable/contact_list_divider_light</item>
<item name="debuglog_color_none">@color/debuglog_light_none</item>
@ -475,6 +479,10 @@
<item name="homeAsUpIndicator">@drawable/ic_arrow_left_24</item>
<item name="android:homeAsUpIndicator">@drawable/ic_arrow_left_24</item>
<item name="group_candidate_item_background">@drawable/group_candidate_item_background_dark</item>
<item name="tinted_circle_background">@drawable/tinted_circle_dark</item>
<item name="kbs_splash_image">@drawable/ic_kbs_splash_dark_svg</item>
<item name="attachment_type_selector_background">@color/core_grey_95</item>