Implement new group creation screens behind flag.
This commit is contained in:
parent
ed0825112d
commit
ccff7b1148
42 changed files with 1422 additions and 84 deletions
|
@ -483,6 +483,11 @@
|
||||||
android:theme="@style/TextSecure.LightNoActionBar"
|
android:theme="@style/TextSecure.LightNoActionBar"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
|
||||||
|
|
||||||
|
<activity android:name=".groups.ui.creategroup.CreateGroupActivity"
|
||||||
|
android:theme="@style/TextSecure.LightNoActionBar" />
|
||||||
|
|
||||||
|
<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="org.thoughtcrime.securesms.service.WebRtcCallService"/>
|
||||||
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
|
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
|
||||||
|
|
|
@ -50,6 +50,8 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActionB
|
||||||
{
|
{
|
||||||
private static final String TAG = ContactSelectionActivity.class.getSimpleName();
|
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 DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||||
|
|
||||||
|
@ -67,11 +69,11 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActionB
|
||||||
protected void onCreate(Bundle icicle, boolean ready) {
|
protected void onCreate(Bundle icicle, boolean ready) {
|
||||||
if (!getIntent().hasExtra(ContactSelectionListFragment.DISPLAY_MODE)) {
|
if (!getIntent().hasExtra(ContactSelectionListFragment.DISPLAY_MODE)) {
|
||||||
int displayMode = TextSecurePreferences.isSmsEnabled(this) ? DisplayMode.FLAG_ALL
|
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);
|
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();
|
initializeToolbar();
|
||||||
initializeResources();
|
initializeResources();
|
||||||
|
@ -90,7 +92,7 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActionB
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeToolbar() {
|
private void initializeToolbar() {
|
||||||
this.toolbar = ViewUtil.findById(this, R.id.toolbar);
|
this.toolbar = findViewById(R.id.toolbar);
|
||||||
setSupportActionBar(toolbar);
|
setSupportActionBar(toolbar);
|
||||||
|
|
||||||
assert getSupportActionBar() != null;
|
assert getSupportActionBar() != null;
|
||||||
|
|
|
@ -187,10 +187,14 @@ public final class ContactSelectionListFragment extends Fragment
|
||||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull List<SelectedContact> getSelectedContacts() {
|
public @NonNull List<SelectedContact> getSelectedContacts() {
|
||||||
return cursorRecyclerViewAdapter.getSelectedContacts();
|
return cursorRecyclerViewAdapter.getSelectedContacts();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getSelectedContactsCount() {
|
||||||
|
return cursorRecyclerViewAdapter.getSelectedContactsCount();
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isMulti() {
|
private boolean isMulti() {
|
||||||
return requireActivity().getIntent().getBooleanExtra(MULTI_SELECT, false);
|
return requireActivity().getIntent().getBooleanExtra(MULTI_SELECT, false);
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ import android.view.View;
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationActivity;
|
import org.thoughtcrime.securesms.conversation.ConversationActivity;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||||
|
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
|
@ -97,7 +98,7 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleCreateGroup() {
|
private void handleCreateGroup() {
|
||||||
startActivity(new Intent(this, GroupCreateActivity.class));
|
startActivity(CreateGroupActivity.newIntent(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleInvite() {
|
private void handleInvite() {
|
||||||
|
@ -105,10 +106,10 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean onPrepareOptionsPanel(View view, Menu menu) {
|
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||||
MenuInflater inflater = this.getMenuInflater();
|
|
||||||
menu.clear();
|
menu.clear();
|
||||||
inflater.inflate(R.menu.new_conversation_activity, menu);
|
getMenuInflater().inflate(R.menu.new_conversation_activity, menu);
|
||||||
|
|
||||||
super.onPrepareOptionsMenu(menu);
|
super.onPrepareOptionsMenu(menu);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -158,13 +158,12 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setAvatarClickHandler(final Recipient recipient, boolean quickContactEnabled) {
|
private void setAvatarClickHandler(final Recipient recipient, boolean quickContactEnabled) {
|
||||||
super.setOnClickListener(v -> {
|
|
||||||
if (quickContactEnabled) {
|
if (quickContactEnabled) {
|
||||||
getContext().startActivity(RecipientPreferenceActivity.getLaunchIntent(getContext(), recipient.getId()));
|
super.setOnClickListener(v -> getContext().startActivity(RecipientPreferenceActivity.getLaunchIntent(getContext(), recipient.getId())));
|
||||||
} else if (listener != null) {
|
} else {
|
||||||
listener.onClick(v);
|
super.setOnClickListener(listener);
|
||||||
|
setClickable(listener != null);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class RecipientContactPhoto {
|
private static class RecipientContactPhoto {
|
||||||
|
|
|
@ -85,11 +85,15 @@ public class ContactRepository {
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
public Cursor querySignalContacts(@NonNull String query) {
|
public Cursor querySignalContacts(@NonNull String query) {
|
||||||
Cursor cursor = TextUtils.isEmpty(query) ? recipientDatabase.getSignalContacts()
|
return querySignalContacts(query, true);
|
||||||
: recipientDatabase.querySignalContacts(query);
|
}
|
||||||
|
|
||||||
|
@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();
|
Recipient self = Recipient.self();
|
||||||
boolean nameMatch = self.getDisplayName(context).toLowerCase().contains(query.toLowerCase());
|
boolean nameMatch = self.getDisplayName(context).toLowerCase().contains(query.toLowerCase());
|
||||||
boolean numberMatch = self.getE164().isPresent() && self.requireE164().contains(query);
|
boolean numberMatch = self.getE164().isPresent() && self.requireE164().contains(query);
|
||||||
|
|
|
@ -250,6 +250,10 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
|
||||||
return selectedContacts.getContacts();
|
return selectedContacts.getContacts();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getSelectedContactsCount() {
|
||||||
|
return selectedContacts.size();
|
||||||
|
}
|
||||||
|
|
||||||
private CharSequence getSpannedHeaderString(int position) {
|
private CharSequence getSpannedHeaderString(int position) {
|
||||||
final String headerString = getHeaderString(position);
|
final String headerString = getHeaderString(position);
|
||||||
if (isPush(position)) {
|
if (isPush(position)) {
|
||||||
|
|
|
@ -59,7 +59,8 @@ public class ContactsCursorLoader extends CursorLoader {
|
||||||
public static final int FLAG_SMS = 1 << 1;
|
public static final int FLAG_SMS = 1 << 1;
|
||||||
public static final int FLAG_ACTIVE_GROUPS = 1 << 2;
|
public static final int FLAG_ACTIVE_GROUPS = 1 << 2;
|
||||||
public static final int FLAG_INACTIVE_GROUPS = 1 << 3;
|
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,
|
private static final String[] CONTACT_PROJECTION = new String[]{ContactRepository.ID_COLUMN,
|
||||||
|
@ -248,7 +249,7 @@ public class ContactsCursorLoader extends CursorLoader {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pushEnabled(mode)) {
|
if (pushEnabled(mode)) {
|
||||||
cursorList.add(contactRepository.querySignalContacts(filter));
|
cursorList.add(contactRepository.querySignalContacts(filter, selfEnabled(mode)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pushEnabled(mode) && smsEnabled(mode)) {
|
if (pushEnabled(mode) && smsEnabled(mode)) {
|
||||||
|
@ -329,6 +330,10 @@ public class ContactsCursorLoader extends CursorLoader {
|
||||||
return sum == 0;
|
return sum == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean selfEnabled(int mode) {
|
||||||
|
return flagSet(mode, DisplayMode.FLAG_SELF);
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean pushEnabled(int mode) {
|
private static boolean pushEnabled(int mode) {
|
||||||
return flagSet(mode, DisplayMode.FLAG_PUSH);
|
return flagSet(mode, DisplayMode.FLAG_PUSH);
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,10 @@ public final class SelectedContactSet {
|
||||||
return new ArrayList<>(contacts);
|
return new ArrayList<>(contacts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int size() {
|
||||||
|
return contacts.size();
|
||||||
|
}
|
||||||
|
|
||||||
public void clear() {
|
public void clear() {
|
||||||
contacts.clear();
|
contacts.clear();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1357,17 +1357,29 @@ public class RecipientDatabase extends Database {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public @Nullable Cursor getSignalContacts() {
|
public @Nullable Cursor getSignalContacts() {
|
||||||
|
return getSignalContacts(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public @Nullable Cursor getSignalContacts(boolean includeSelf) {
|
||||||
String selection = BLOCKED + " = ? AND " +
|
String selection = BLOCKED + " = ? AND " +
|
||||||
REGISTERED + " = ? AND " +
|
REGISTERED + " = ? AND " +
|
||||||
GROUP_ID + " IS NULL AND " +
|
GROUP_ID + " IS NULL AND " +
|
||||||
"(" + SORT_NAME + " NOT NULL OR " + USERNAME + " NOT NULL)";
|
"(" + 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;
|
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);
|
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 = TextUtils.isEmpty(query) ? "*" : query;
|
||||||
query = "%" + query + "%";
|
query = "%" + query + "%";
|
||||||
|
|
||||||
|
@ -1379,7 +1391,15 @@ public class RecipientDatabase extends Database {
|
||||||
SORT_NAME + " LIKE ? OR " +
|
SORT_NAME + " LIKE ? OR " +
|
||||||
USERNAME + " LIKE ?" +
|
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;
|
String orderBy = SORT_NAME + ", " + SYSTEM_DISPLAY_NAME + ", " + SEARCH_PROFILE_NAME + ", " + PHONE;
|
||||||
|
|
||||||
return databaseHelper.getReadableDatabase().query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy);
|
return databaseHelper.getReadableDatabase().query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy);
|
||||||
|
|
|
@ -31,7 +31,49 @@ public abstract class GroupMemberEntry {
|
||||||
@Override
|
@Override
|
||||||
public abstract int hashCode();
|
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 {
|
public final static class FullMember extends GroupMemberEntry {
|
||||||
|
|
||||||
|
@ -52,7 +94,7 @@ public abstract class GroupMemberEntry {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
boolean sameId(GroupMemberEntry newItem) {
|
boolean sameId(@NonNull GroupMemberEntry newItem) {
|
||||||
if (getClass() != newItem.getClass()) return false;
|
if (getClass() != newItem.getClass()) return false;
|
||||||
|
|
||||||
return member.getId().equals(((GroupMemberEntry.FullMember) newItem).member.getId());
|
return member.getId().equals(((GroupMemberEntry.FullMember) newItem).member.getId());
|
||||||
|
@ -97,7 +139,7 @@ public abstract class GroupMemberEntry {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
boolean sameId(GroupMemberEntry newItem) {
|
boolean sameId(@NonNull GroupMemberEntry newItem) {
|
||||||
if (getClass() != newItem.getClass()) return false;
|
if (getClass() != newItem.getClass()) return false;
|
||||||
|
|
||||||
return invitee.getId().equals(((GroupMemberEntry.PendingMember) newItem).invitee.getId());
|
return invitee.getId().equals(((GroupMemberEntry.PendingMember) newItem).invitee.getId());
|
||||||
|
@ -153,7 +195,7 @@ public abstract class GroupMemberEntry {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
boolean sameId(GroupMemberEntry newItem) {
|
boolean sameId(@NonNull GroupMemberEntry newItem) {
|
||||||
if (getClass() != newItem.getClass()) return false;
|
if (getClass() != newItem.getClass()) return false;
|
||||||
|
|
||||||
return inviter.getId().equals(((GroupMemberEntry.UnknownPendingMemberCount) newItem).inviter.getId());
|
return inviter.getId().equals(((GroupMemberEntry.UnknownPendingMemberCount) newItem).inviter.getId());
|
||||||
|
|
|
@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.util.LifecycleRecyclerAdapter;
|
import org.thoughtcrime.securesms.util.LifecycleRecyclerAdapter;
|
||||||
import org.thoughtcrime.securesms.util.LifecycleViewHolder;
|
import org.thoughtcrime.securesms.util.LifecycleViewHolder;
|
||||||
|
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
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 FULL_MEMBER = 0;
|
||||||
private static final int OWN_INVITE_PENDING = 1;
|
private static final int OWN_INVITE_PENDING = 1;
|
||||||
private static final int OTHER_INVITE_PENDING_COUNT = 2;
|
private static final int OTHER_INVITE_PENDING_COUNT = 2;
|
||||||
|
private static final int NEW_GROUP_CANDIDATE = 3;
|
||||||
|
|
||||||
private final ArrayList<GroupMemberEntry> data = new ArrayList<>();
|
private final ArrayList<GroupMemberEntry> data = new ArrayList<>();
|
||||||
|
|
||||||
@Nullable private AdminActionsListener adminActionsListener;
|
@Nullable private AdminActionsListener adminActionsListener;
|
||||||
@Nullable private RecipientClickListener recipientClickListener;
|
@Nullable private RecipientClickListener recipientClickListener;
|
||||||
|
@Nullable private RecipientLongClickListener recipientLongClickListener;
|
||||||
|
|
||||||
void updateData(@NonNull List<? extends GroupMemberEntry> recipients) {
|
void updateData(@NonNull List<? extends GroupMemberEntry> recipients) {
|
||||||
if (data.isEmpty()) {
|
if (data.isEmpty()) {
|
||||||
|
@ -49,16 +52,25 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
||||||
switch (viewType) {
|
switch (viewType) {
|
||||||
case FULL_MEMBER:
|
case FULL_MEMBER:
|
||||||
return new FullMemberViewHolder(LayoutInflater.from(parent.getContext())
|
return new FullMemberViewHolder(LayoutInflater.from(parent.getContext())
|
||||||
.inflate(R.layout.group_recipient_list_item,
|
.inflate(R.layout.group_recipient_list_item, parent, false),
|
||||||
parent, false), recipientClickListener, adminActionsListener);
|
recipientClickListener,
|
||||||
|
recipientLongClickListener,
|
||||||
|
adminActionsListener);
|
||||||
case OWN_INVITE_PENDING:
|
case OWN_INVITE_PENDING:
|
||||||
return new OwnInvitePendingMemberViewHolder(LayoutInflater.from(parent.getContext())
|
return new OwnInvitePendingMemberViewHolder(LayoutInflater.from(parent.getContext())
|
||||||
.inflate(R.layout.group_recipient_list_item,
|
.inflate(R.layout.group_recipient_list_item, parent, false),
|
||||||
parent, false), recipientClickListener, adminActionsListener);
|
recipientClickListener,
|
||||||
|
recipientLongClickListener,
|
||||||
|
adminActionsListener);
|
||||||
case OTHER_INVITE_PENDING_COUNT:
|
case OTHER_INVITE_PENDING_COUNT:
|
||||||
return new UnknownPendingMemberCountViewHolder(LayoutInflater.from(parent.getContext())
|
return new UnknownPendingMemberCountViewHolder(LayoutInflater.from(parent.getContext())
|
||||||
.inflate(R.layout.group_recipient_list_item,
|
.inflate(R.layout.group_recipient_list_item, parent, false),
|
||||||
parent, false), adminActionsListener);
|
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:
|
default:
|
||||||
throw new AssertionError();
|
throw new AssertionError();
|
||||||
}
|
}
|
||||||
|
@ -72,6 +84,10 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
||||||
this.recipientClickListener = recipientClickListener;
|
this.recipientClickListener = recipientClickListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setRecipientLongClickListener(@Nullable RecipientLongClickListener recipientLongClickListener) {
|
||||||
|
this.recipientLongClickListener = recipientLongClickListener;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||||
holder.bind(data.get(position));
|
holder.bind(data.get(position));
|
||||||
|
@ -87,6 +103,8 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
||||||
return OWN_INVITE_PENDING;
|
return OWN_INVITE_PENDING;
|
||||||
} else if (groupMemberEntry instanceof GroupMemberEntry.UnknownPendingMemberCount) {
|
} else if (groupMemberEntry instanceof GroupMemberEntry.UnknownPendingMemberCount) {
|
||||||
return OTHER_INVITE_PENDING_COUNT;
|
return OTHER_INVITE_PENDING_COUNT;
|
||||||
|
} else if (groupMemberEntry instanceof GroupMemberEntry.NewGroupCandidate) {
|
||||||
|
return NEW_GROUP_CANDIDATE;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new AssertionError();
|
throw new AssertionError();
|
||||||
|
@ -108,9 +126,11 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
||||||
final View admin;
|
final View admin;
|
||||||
@Nullable final RecipientClickListener recipientClickListener;
|
@Nullable final RecipientClickListener recipientClickListener;
|
||||||
@Nullable final AdminActionsListener adminActionsListener;
|
@Nullable final AdminActionsListener adminActionsListener;
|
||||||
|
@Nullable final RecipientLongClickListener recipientLongClickListener;
|
||||||
|
|
||||||
ViewHolder(@NonNull View itemView,
|
ViewHolder(@NonNull View itemView,
|
||||||
@Nullable RecipientClickListener recipientClickListener,
|
@Nullable RecipientClickListener recipientClickListener,
|
||||||
|
@Nullable RecipientLongClickListener recipientLongClickListener,
|
||||||
@Nullable AdminActionsListener adminActionsListener)
|
@Nullable AdminActionsListener adminActionsListener)
|
||||||
{
|
{
|
||||||
super(itemView);
|
super(itemView);
|
||||||
|
@ -123,6 +143,7 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
||||||
this.busyProgress = itemView.findViewById(R.id.menuBusyProgress);
|
this.busyProgress = itemView.findViewById(R.id.menuBusyProgress);
|
||||||
this.admin = itemView.findViewById(R.id.admin);
|
this.admin = itemView.findViewById(R.id.admin);
|
||||||
this.recipientClickListener = recipientClickListener;
|
this.recipientClickListener = recipientClickListener;
|
||||||
|
this.recipientLongClickListener = recipientLongClickListener;
|
||||||
this.adminActionsListener = adminActionsListener;
|
this.adminActionsListener = adminActionsListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,6 +170,13 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
||||||
recipientClickListener.onClick(recipient);
|
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) {
|
void bind(@NonNull GroupMemberEntry memberEntry) {
|
||||||
|
@ -179,9 +207,10 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
||||||
|
|
||||||
FullMemberViewHolder(@NonNull View itemView,
|
FullMemberViewHolder(@NonNull View itemView,
|
||||||
@Nullable RecipientClickListener recipientClickListener,
|
@Nullable RecipientClickListener recipientClickListener,
|
||||||
|
@Nullable RecipientLongClickListener recipientLongClickListener,
|
||||||
@Nullable AdminActionsListener adminActionsListener)
|
@Nullable AdminActionsListener adminActionsListener)
|
||||||
{
|
{
|
||||||
super(itemView, recipientClickListener, adminActionsListener);
|
super(itemView, recipientClickListener, recipientLongClickListener, adminActionsListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -195,14 +224,46 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
||||||
admin.setVisibility(fullMember.isAdmin() ? View.VISIBLE : View.INVISIBLE);
|
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 {
|
final static class OwnInvitePendingMemberViewHolder extends ViewHolder {
|
||||||
|
|
||||||
OwnInvitePendingMemberViewHolder(@NonNull View itemView,
|
OwnInvitePendingMemberViewHolder(@NonNull View itemView,
|
||||||
@Nullable RecipientClickListener recipientClickListener,
|
@Nullable RecipientClickListener recipientClickListener,
|
||||||
|
@Nullable RecipientLongClickListener recipientLongClickListener,
|
||||||
@Nullable AdminActionsListener adminActionsListener)
|
@Nullable AdminActionsListener adminActionsListener)
|
||||||
{
|
{
|
||||||
super(itemView, recipientClickListener, adminActionsListener);
|
super(itemView, recipientClickListener, recipientLongClickListener, adminActionsListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -231,7 +292,7 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
||||||
final static class UnknownPendingMemberCountViewHolder extends ViewHolder {
|
final static class UnknownPendingMemberCountViewHolder extends ViewHolder {
|
||||||
|
|
||||||
UnknownPendingMemberCountViewHolder(@NonNull View itemView, @Nullable AdminActionsListener adminActionsListener) {
|
UnknownPendingMemberCountViewHolder(@NonNull View itemView, @Nullable AdminActionsListener adminActionsListener) {
|
||||||
super(itemView, null, adminActionsListener);
|
super(itemView, null, null, adminActionsListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -59,6 +59,10 @@ public final class GroupMemberListView extends RecyclerView {
|
||||||
membersAdapter.setRecipientClickListener(listener);
|
membersAdapter.setRecipientClickListener(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setRecipientLongClickListener(@Nullable RecipientLongClickListener listener) {
|
||||||
|
membersAdapter.setRecipientLongClickListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
public void setMembers(@NonNull List<? extends GroupMemberEntry> recipients) {
|
public void setMembers(@NonNull List<? extends GroupMemberEntry> recipients) {
|
||||||
membersAdapter.updateData(recipients);
|
membersAdapter.updateData(recipients);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
||||||
|
}
|
|
@ -32,7 +32,7 @@ public class AvatarSelectionBottomSheetDialogFragment extends BottomSheetDialogF
|
||||||
private static final String ARG_REQUEST_CODE = "request_code";
|
private static final String ARG_REQUEST_CODE = "request_code";
|
||||||
private static final String ARG_IS_GROUP = "is_group";
|
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();
|
DialogFragment fragment = new AvatarSelectionBottomSheetDialogFragment();
|
||||||
List<SelectionOption> selectionOptions = new ArrayList<>(3);
|
List<SelectionOption> selectionOptions = new ArrayList<>(3);
|
||||||
Bundle args = new Bundle();
|
Bundle args = new Bundle();
|
||||||
|
@ -52,7 +52,7 @@ public class AvatarSelectionBottomSheetDialogFragment extends BottomSheetDialogF
|
||||||
.toArray(String[]::new);
|
.toArray(String[]::new);
|
||||||
|
|
||||||
args.putStringArray(ARG_OPTIONS, options);
|
args.putStringArray(ARG_OPTIONS, options);
|
||||||
args.putShort(ARG_REQUEST_CODE, resultCode);
|
args.putShort(ARG_REQUEST_CODE, requestCode);
|
||||||
args.putBoolean(ARG_IS_GROUP, isGroup);
|
args.putBoolean(ARG_IS_GROUP, isGroup);
|
||||||
fragment.setArguments(args);
|
fragment.setArguments(args);
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
17
app/src/main/res/drawable/contact_selection_checkbox.xml
Normal file
17
app/src/main/res/drawable/contact_selection_checkbox.xml
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
9
app/src/main/res/drawable/ic_arrow_right_24.xml
Normal file
9
app/src/main/res/drawable/ic_arrow_right_24.xml
Normal 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>
|
|
@ -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>
|
9
app/src/main/res/drawable/ic_check_outline_22.xml
Normal file
9
app/src/main/res/drawable/ic_check_outline_22.xml
Normal 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>
|
9
app/src/main/res/drawable/ic_error_outline_24.xml
Normal file
9
app/src/main/res/drawable/ic_error_outline_24.xml
Normal 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>
|
5
app/src/main/res/drawable/tinted_circle_dark.xml
Normal file
5
app/src/main/res/drawable/tinted_circle_dark.xml
Normal 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>
|
5
app/src/main/res/drawable/tinted_circle_light.xml
Normal file
5
app/src/main/res/drawable/tinted_circle_light.xml
Normal 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>
|
9
app/src/main/res/layout/add_group_details_activity.xml
Normal file
9
app/src/main/res/layout/add_group_details_activity.xml
Normal 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" />
|
94
app/src/main/res/layout/add_group_details_fragment.xml
Normal file
94
app/src/main/res/layout/add_group_details_fragment.xml
Normal 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>
|
|
@ -1,6 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<org.thoughtcrime.securesms.contacts.ContactSelectionListItem
|
<org.thoughtcrime.securesms.contacts.ContactSelectionListItem xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="@dimen/contact_selection_item_height"
|
android:layout_height="@dimen/contact_selection_item_height"
|
||||||
|
@ -11,21 +10,38 @@
|
||||||
android:paddingStart="@dimen/selection_item_header_width"
|
android:paddingStart="@dimen/selection_item_header_width"
|
||||||
android:paddingEnd="24dp">
|
android:paddingEnd="24dp">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="52dp"
|
||||||
|
android:layout_height="52dp">
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.components.AvatarImageView
|
<org.thoughtcrime.securesms.components.AvatarImageView
|
||||||
android:id="@+id/contact_photo_image"
|
android:id="@+id/contact_photo_image"
|
||||||
android:layout_width="48dp"
|
android:layout_width="48dp"
|
||||||
android:layout_height="48dp"
|
android:layout_height="48dp"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
android:foreground="@drawable/contact_photo_background"
|
android:foreground="@drawable/contact_photo_background"
|
||||||
android:cropToPadding="true"
|
android:cropToPadding="true"
|
||||||
android:contentDescription="@string/SingleContactSelectionActivity_contact_photo"
|
android:contentDescription="@string/SingleContactSelectionActivity_contact_photo"
|
||||||
tools:src="@color/blue_600"
|
tools:src="@color/blue_600"
|
||||||
tools:ignore="UnusedAttribute" />
|
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"
|
<LinearLayout android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="8dp"
|
||||||
android:layout_marginEnd="16dp"
|
android:layout_marginEnd="8dp"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.components.FromTextView
|
<org.thoughtcrime.securesms.components.FromTextView
|
||||||
|
@ -35,8 +51,7 @@
|
||||||
android:checkMark="?android:attr/listChoiceIndicatorMultiple"
|
android:checkMark="?android:attr/listChoiceIndicatorMultiple"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:ellipsize="marquee"
|
android:ellipsize="marquee"
|
||||||
style="@style/Signal.Text.Body"
|
android:textAppearance="@style/TextAppearance.Signal.Body1.Bold"
|
||||||
android:fontFamily="sans-serif-medium"
|
|
||||||
tools:text="@sample/contacts.json/data/name" />
|
tools:text="@sample/contacts.json/data/name" />
|
||||||
|
|
||||||
<LinearLayout android:id="@+id/number_container"
|
<LinearLayout android:id="@+id/number_container"
|
||||||
|
@ -50,7 +65,8 @@
|
||||||
android:textDirection="ltr"
|
android:textDirection="ltr"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:ellipsize="marquee"
|
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:textSize="14sp"
|
||||||
android:fontFamily="sans-serif-light"
|
android:fontFamily="sans-serif-light"
|
||||||
tools:text="@sample/contacts.json/data/number" />
|
tools:text="@sample/contacts.json/data/number" />
|
||||||
|
@ -61,7 +77,8 @@
|
||||||
android:paddingStart="10dip"
|
android:paddingStart="10dip"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:singleLine="true"
|
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"
|
android:fontFamily="sans-serif-light"
|
||||||
tools:text="@sample/contacts.json/data/label"
|
tools:text="@sample/contacts.json/data/label"
|
||||||
tools:ignore="RtlSymmetry" />
|
tools:ignore="RtlSymmetry" />
|
||||||
|
@ -70,11 +87,4 @@
|
||||||
|
|
||||||
</LinearLayout>
|
</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>
|
</org.thoughtcrime.securesms.contacts.ContactSelectionListItem>
|
||||||
|
|
42
app/src/main/res/layout/create_group_activity.xml
Normal file
42
app/src/main/res/layout/create_group_activity.xml
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
21
app/src/main/res/navigation/create_group.xml
Normal file
21
app/src/main/res/navigation/create_group.xml
Normal 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>
|
|
@ -139,6 +139,8 @@
|
||||||
<attr name="conversation_scroll_to_bottom_background" format="reference" />
|
<attr name="conversation_scroll_to_bottom_background" format="reference" />
|
||||||
<attr name="conversation_scroll_to_bottom_foreground_color" format="color" />
|
<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_info_icon" format="reference" />
|
||||||
<attr name="dialog_alert_icon" format="reference" />
|
<attr name="dialog_alert_icon" format="reference" />
|
||||||
<attr name="dialog_background_color" format="reference|color" />
|
<attr name="dialog_background_color" format="reference|color" />
|
||||||
|
@ -226,6 +228,7 @@
|
||||||
<attr name="custom_pref_toggle" format="string"/>
|
<attr name="custom_pref_toggle" format="string"/>
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
|
|
||||||
|
<attr name="group_candidate_item_background" format="reference" />
|
||||||
<attr name="group_members_dialog_icon" format="reference"/>
|
<attr name="group_members_dialog_icon" format="reference"/>
|
||||||
<attr name="manage_group_add_members_icon" format="reference"/>
|
<attr name="manage_group_add_members_icon" format="reference"/>
|
||||||
<attr name="manage_group_view_all_icon" format="reference"/>
|
<attr name="manage_group_view_all_icon" format="reference"/>
|
||||||
|
|
|
@ -487,6 +487,19 @@
|
||||||
<item quantity="other">Error canceling invites</item>
|
<item quantity="other">Error canceling invites</item>
|
||||||
</plurals>
|
</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 -->
|
<!-- ManageGroupActivity -->
|
||||||
<string name="ManageGroupActivity_disappearing_messages">Disappearing messages</string>
|
<string name="ManageGroupActivity_disappearing_messages">Disappearing messages</string>
|
||||||
<string name="ManageGroupActivity_pending_group_invites">Pending group invites</string>
|
<string name="ManageGroupActivity_pending_group_invites">Pending group invites</string>
|
||||||
|
|
|
@ -188,6 +188,8 @@
|
||||||
<item name="android:homeAsUpIndicator">@drawable/ic_arrow_left_24</item>
|
<item name="android:homeAsUpIndicator">@drawable/ic_arrow_left_24</item>
|
||||||
<!--<item name="android:windowContentOverlay">@drawable/compat_actionbar_shadow_background</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="kbs_splash_image">@drawable/ic_kbs_splash_light_svg</item>
|
||||||
|
|
||||||
<item name="attachment_type_selector_background">@color/white</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_title_color">@color/white</item>
|
||||||
<item name="conversation_subtitle_color">@color/transparent_white_90</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="contact_list_divider">@drawable/contact_list_divider_light</item>
|
||||||
|
|
||||||
<item name="debuglog_color_none">@color/debuglog_light_none</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="homeAsUpIndicator">@drawable/ic_arrow_left_24</item>
|
||||||
<item name="android: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="kbs_splash_image">@drawable/ic_kbs_splash_dark_svg</item>
|
||||||
|
|
||||||
<item name="attachment_type_selector_background">@color/core_grey_95</item>
|
<item name="attachment_type_selector_background">@color/core_grey_95</item>
|
||||||
|
|
Loading…
Add table
Reference in a new issue