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: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=".service.ApplicationMigrationService"/>
|
||||
|
|
|
@ -50,6 +50,8 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActionB
|
|||
{
|
||||
private static final String TAG = ContactSelectionActivity.class.getSimpleName();
|
||||
|
||||
public static final String EXTRA_LAYOUT_RES_ID = "layout_res_id";
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||
|
||||
|
@ -67,11 +69,11 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActionB
|
|||
protected void onCreate(Bundle icicle, boolean ready) {
|
||||
if (!getIntent().hasExtra(ContactSelectionListFragment.DISPLAY_MODE)) {
|
||||
int displayMode = TextSecurePreferences.isSmsEnabled(this) ? DisplayMode.FLAG_ALL
|
||||
: DisplayMode.FLAG_PUSH | DisplayMode.FLAG_ACTIVE_GROUPS | DisplayMode.FLAG_INACTIVE_GROUPS;
|
||||
: DisplayMode.FLAG_PUSH | DisplayMode.FLAG_ACTIVE_GROUPS | DisplayMode.FLAG_INACTIVE_GROUPS | DisplayMode.FLAG_SELF;
|
||||
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, displayMode);
|
||||
}
|
||||
|
||||
setContentView(R.layout.contact_selection_activity);
|
||||
setContentView(getIntent().getIntExtra(EXTRA_LAYOUT_RES_ID, R.layout.contact_selection_activity));
|
||||
|
||||
initializeToolbar();
|
||||
initializeResources();
|
||||
|
@ -90,7 +92,7 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActionB
|
|||
}
|
||||
|
||||
private void initializeToolbar() {
|
||||
this.toolbar = ViewUtil.findById(this, R.id.toolbar);
|
||||
this.toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
assert getSupportActionBar() != null;
|
||||
|
|
|
@ -187,10 +187,14 @@ public final class ContactSelectionListFragment extends Fragment
|
|||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
@NonNull List<SelectedContact> getSelectedContacts() {
|
||||
public @NonNull List<SelectedContact> getSelectedContacts() {
|
||||
return cursorRecyclerViewAdapter.getSelectedContacts();
|
||||
}
|
||||
|
||||
public int getSelectedContactsCount() {
|
||||
return cursorRecyclerViewAdapter.getSelectedContactsCount();
|
||||
}
|
||||
|
||||
private boolean isMulti() {
|
||||
return requireActivity().getIntent().getBooleanExtra(MULTI_SELECT, false);
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ import android.view.View;
|
|||
import org.thoughtcrime.securesms.conversation.ConversationActivity;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
@ -97,7 +98,7 @@ public class NewConversationActivity extends ContactSelectionActivity
|
|||
}
|
||||
|
||||
private void handleCreateGroup() {
|
||||
startActivity(new Intent(this, GroupCreateActivity.class));
|
||||
startActivity(CreateGroupActivity.newIntent(this));
|
||||
}
|
||||
|
||||
private void handleInvite() {
|
||||
|
@ -105,10 +106,10 @@ public class NewConversationActivity extends ContactSelectionActivity
|
|||
}
|
||||
|
||||
@Override
|
||||
protected boolean onPrepareOptionsPanel(View view, Menu menu) {
|
||||
MenuInflater inflater = this.getMenuInflater();
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
menu.clear();
|
||||
inflater.inflate(R.menu.new_conversation_activity, menu);
|
||||
getMenuInflater().inflate(R.menu.new_conversation_activity, menu);
|
||||
|
||||
super.onPrepareOptionsMenu(menu);
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -158,13 +158,12 @@ public final class AvatarImageView extends AppCompatImageView {
|
|||
}
|
||||
|
||||
private void setAvatarClickHandler(final Recipient recipient, boolean quickContactEnabled) {
|
||||
super.setOnClickListener(v -> {
|
||||
if (quickContactEnabled) {
|
||||
getContext().startActivity(RecipientPreferenceActivity.getLaunchIntent(getContext(), recipient.getId()));
|
||||
} else if (listener != null) {
|
||||
listener.onClick(v);
|
||||
super.setOnClickListener(v -> getContext().startActivity(RecipientPreferenceActivity.getLaunchIntent(getContext(), recipient.getId())));
|
||||
} else {
|
||||
super.setOnClickListener(listener);
|
||||
setClickable(listener != null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static class RecipientContactPhoto {
|
||||
|
|
|
@ -85,11 +85,15 @@ public class ContactRepository {
|
|||
|
||||
@WorkerThread
|
||||
public Cursor querySignalContacts(@NonNull String query) {
|
||||
Cursor cursor = TextUtils.isEmpty(query) ? recipientDatabase.getSignalContacts()
|
||||
: recipientDatabase.querySignalContacts(query);
|
||||
return querySignalContacts(query, true);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public Cursor querySignalContacts(@NonNull String query, boolean includeSelf) {
|
||||
Cursor cursor = TextUtils.isEmpty(query) ? recipientDatabase.getSignalContacts(includeSelf)
|
||||
: recipientDatabase.querySignalContacts(query, includeSelf);
|
||||
|
||||
if (noteToSelfTitle.toLowerCase().contains(query.toLowerCase())) {
|
||||
if (includeSelf && noteToSelfTitle.toLowerCase().contains(query.toLowerCase())) {
|
||||
Recipient self = Recipient.self();
|
||||
boolean nameMatch = self.getDisplayName(context).toLowerCase().contains(query.toLowerCase());
|
||||
boolean numberMatch = self.getE164().isPresent() && self.requireE164().contains(query);
|
||||
|
|
|
@ -250,6 +250,10 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
|
|||
return selectedContacts.getContacts();
|
||||
}
|
||||
|
||||
public int getSelectedContactsCount() {
|
||||
return selectedContacts.size();
|
||||
}
|
||||
|
||||
private CharSequence getSpannedHeaderString(int position) {
|
||||
final String headerString = getHeaderString(position);
|
||||
if (isPush(position)) {
|
||||
|
|
|
@ -59,7 +59,8 @@ public class ContactsCursorLoader extends CursorLoader {
|
|||
public static final int FLAG_SMS = 1 << 1;
|
||||
public static final int FLAG_ACTIVE_GROUPS = 1 << 2;
|
||||
public static final int FLAG_INACTIVE_GROUPS = 1 << 3;
|
||||
public static final int FLAG_ALL = FLAG_PUSH | FLAG_SMS | FLAG_ACTIVE_GROUPS | FLAG_INACTIVE_GROUPS;
|
||||
public static final int FLAG_SELF = 1 << 4;
|
||||
public static final int FLAG_ALL = FLAG_PUSH | FLAG_SMS | FLAG_ACTIVE_GROUPS | FLAG_INACTIVE_GROUPS | FLAG_SELF;
|
||||
}
|
||||
|
||||
private static final String[] CONTACT_PROJECTION = new String[]{ContactRepository.ID_COLUMN,
|
||||
|
@ -248,7 +249,7 @@ public class ContactsCursorLoader extends CursorLoader {
|
|||
}
|
||||
|
||||
if (pushEnabled(mode)) {
|
||||
cursorList.add(contactRepository.querySignalContacts(filter));
|
||||
cursorList.add(contactRepository.querySignalContacts(filter, selfEnabled(mode)));
|
||||
}
|
||||
|
||||
if (pushEnabled(mode) && smsEnabled(mode)) {
|
||||
|
@ -329,6 +330,10 @@ public class ContactsCursorLoader extends CursorLoader {
|
|||
return sum == 0;
|
||||
}
|
||||
|
||||
private static boolean selfEnabled(int mode) {
|
||||
return flagSet(mode, DisplayMode.FLAG_SELF);
|
||||
}
|
||||
|
||||
private static boolean pushEnabled(int mode) {
|
||||
return flagSet(mode, DisplayMode.FLAG_PUSH);
|
||||
}
|
||||
|
|
|
@ -37,6 +37,10 @@ public final class SelectedContactSet {
|
|||
return new ArrayList<>(contacts);
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return contacts.size();
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
contacts.clear();
|
||||
}
|
||||
|
|
|
@ -1357,17 +1357,29 @@ public class RecipientDatabase extends Database {
|
|||
}
|
||||
}
|
||||
public @Nullable Cursor getSignalContacts() {
|
||||
return getSignalContacts(true);
|
||||
}
|
||||
|
||||
public @Nullable Cursor getSignalContacts(boolean includeSelf) {
|
||||
String selection = BLOCKED + " = ? AND " +
|
||||
REGISTERED + " = ? AND " +
|
||||
GROUP_ID + " IS NULL AND " +
|
||||
"(" + SORT_NAME + " NOT NULL OR " + USERNAME + " NOT NULL)";
|
||||
String[] args = new String[] { "0", String.valueOf(RegisteredState.REGISTERED.getId()) };
|
||||
String[] args;
|
||||
|
||||
if (includeSelf) {
|
||||
args = new String[] { "0", String.valueOf(RegisteredState.REGISTERED.getId()) };
|
||||
} else {
|
||||
selection += " AND " + ID + " != ?";
|
||||
args = new String[] { "0", String.valueOf(RegisteredState.REGISTERED.getId()), String.valueOf(Recipient.self().getId().toLong()) };
|
||||
}
|
||||
|
||||
String orderBy = SORT_NAME + ", " + SYSTEM_DISPLAY_NAME + ", " + SEARCH_PROFILE_NAME + ", " + USERNAME + ", " + PHONE;
|
||||
|
||||
return databaseHelper.getReadableDatabase().query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy);
|
||||
}
|
||||
|
||||
public @Nullable Cursor querySignalContacts(@NonNull String query) {
|
||||
public @Nullable Cursor querySignalContacts(@NonNull String query, boolean includeSelf) {
|
||||
query = TextUtils.isEmpty(query) ? "*" : query;
|
||||
query = "%" + query + "%";
|
||||
|
||||
|
@ -1379,7 +1391,15 @@ public class RecipientDatabase extends Database {
|
|||
SORT_NAME + " LIKE ? OR " +
|
||||
USERNAME + " LIKE ?" +
|
||||
")";
|
||||
String[] args = new String[] { "0", String.valueOf(RegisteredState.REGISTERED.getId()), query, query, query };
|
||||
String[] args;
|
||||
|
||||
if (includeSelf) {
|
||||
args = new String[]{"0", String.valueOf(RegisteredState.REGISTERED.getId()), query, query, query};
|
||||
} else {
|
||||
selection += " AND " + ID + " != ?";
|
||||
args = new String[] { "0", String.valueOf(RegisteredState.REGISTERED.getId()), query, query, query, String.valueOf(Recipient.self().getId().toLong()) };
|
||||
}
|
||||
|
||||
String orderBy = SORT_NAME + ", " + SYSTEM_DISPLAY_NAME + ", " + SEARCH_PROFILE_NAME + ", " + PHONE;
|
||||
|
||||
return databaseHelper.getReadableDatabase().query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy);
|
||||
|
|
|
@ -31,7 +31,49 @@ public abstract class GroupMemberEntry {
|
|||
@Override
|
||||
public abstract int hashCode();
|
||||
|
||||
abstract boolean sameId(GroupMemberEntry newItem);
|
||||
abstract boolean sameId(@NonNull GroupMemberEntry newItem);
|
||||
|
||||
public final static class NewGroupCandidate extends GroupMemberEntry {
|
||||
|
||||
private final DefaultValueLiveData<Boolean> isSelected = new DefaultValueLiveData<>(false);
|
||||
private final Recipient member;
|
||||
|
||||
public NewGroupCandidate(@NonNull Recipient member) {
|
||||
this.member = member;
|
||||
}
|
||||
|
||||
public @NonNull Recipient getMember() {
|
||||
return member;
|
||||
}
|
||||
|
||||
public @NonNull LiveData<Boolean> isSelected() {
|
||||
return isSelected;
|
||||
}
|
||||
|
||||
public void setSelected(boolean isSelected) {
|
||||
this.isSelected.postValue(isSelected);
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean sameId(@NonNull GroupMemberEntry newItem) {
|
||||
if (getClass() != newItem.getClass()) return false;
|
||||
|
||||
return member.getId().equals(((NewGroupCandidate) newItem).member.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (!(obj instanceof NewGroupCandidate)) return false;
|
||||
|
||||
NewGroupCandidate other = (NewGroupCandidate) obj;
|
||||
return other.member.equals(member);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return member.hashCode();
|
||||
}
|
||||
}
|
||||
|
||||
public final static class FullMember extends GroupMemberEntry {
|
||||
|
||||
|
@ -52,7 +94,7 @@ public abstract class GroupMemberEntry {
|
|||
}
|
||||
|
||||
@Override
|
||||
boolean sameId(GroupMemberEntry newItem) {
|
||||
boolean sameId(@NonNull GroupMemberEntry newItem) {
|
||||
if (getClass() != newItem.getClass()) return false;
|
||||
|
||||
return member.getId().equals(((GroupMemberEntry.FullMember) newItem).member.getId());
|
||||
|
@ -97,7 +139,7 @@ public abstract class GroupMemberEntry {
|
|||
}
|
||||
|
||||
@Override
|
||||
boolean sameId(GroupMemberEntry newItem) {
|
||||
boolean sameId(@NonNull GroupMemberEntry newItem) {
|
||||
if (getClass() != newItem.getClass()) return false;
|
||||
|
||||
return invitee.getId().equals(((GroupMemberEntry.PendingMember) newItem).invitee.getId());
|
||||
|
@ -153,7 +195,7 @@ public abstract class GroupMemberEntry {
|
|||
}
|
||||
|
||||
@Override
|
||||
boolean sameId(GroupMemberEntry newItem) {
|
||||
boolean sameId(@NonNull GroupMemberEntry newItem) {
|
||||
if (getClass() != newItem.getClass()) return false;
|
||||
|
||||
return inviter.getId().equals(((GroupMemberEntry.UnknownPendingMemberCount) newItem).inviter.getId());
|
||||
|
|
|
@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.components.AvatarImageView;
|
|||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.LifecycleRecyclerAdapter;
|
||||
import org.thoughtcrime.securesms.util.LifecycleViewHolder;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
@ -26,11 +27,13 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
|||
private static final int FULL_MEMBER = 0;
|
||||
private static final int OWN_INVITE_PENDING = 1;
|
||||
private static final int OTHER_INVITE_PENDING_COUNT = 2;
|
||||
private static final int NEW_GROUP_CANDIDATE = 3;
|
||||
|
||||
private final ArrayList<GroupMemberEntry> data = new ArrayList<>();
|
||||
|
||||
@Nullable private AdminActionsListener adminActionsListener;
|
||||
@Nullable private RecipientClickListener recipientClickListener;
|
||||
@Nullable private RecipientLongClickListener recipientLongClickListener;
|
||||
|
||||
void updateData(@NonNull List<? extends GroupMemberEntry> recipients) {
|
||||
if (data.isEmpty()) {
|
||||
|
@ -49,16 +52,25 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
|||
switch (viewType) {
|
||||
case FULL_MEMBER:
|
||||
return new FullMemberViewHolder(LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.group_recipient_list_item,
|
||||
parent, false), recipientClickListener, adminActionsListener);
|
||||
.inflate(R.layout.group_recipient_list_item, parent, false),
|
||||
recipientClickListener,
|
||||
recipientLongClickListener,
|
||||
adminActionsListener);
|
||||
case OWN_INVITE_PENDING:
|
||||
return new OwnInvitePendingMemberViewHolder(LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.group_recipient_list_item,
|
||||
parent, false), recipientClickListener, adminActionsListener);
|
||||
.inflate(R.layout.group_recipient_list_item, parent, false),
|
||||
recipientClickListener,
|
||||
recipientLongClickListener,
|
||||
adminActionsListener);
|
||||
case OTHER_INVITE_PENDING_COUNT:
|
||||
return new UnknownPendingMemberCountViewHolder(LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.group_recipient_list_item,
|
||||
parent, false), adminActionsListener);
|
||||
.inflate(R.layout.group_recipient_list_item, parent, false),
|
||||
adminActionsListener);
|
||||
case NEW_GROUP_CANDIDATE:
|
||||
return new NewGroupInviteeViewHolder(LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.group_new_candidate_recipient_list_item, parent, false),
|
||||
recipientClickListener,
|
||||
recipientLongClickListener);
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
@ -72,6 +84,10 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
|||
this.recipientClickListener = recipientClickListener;
|
||||
}
|
||||
|
||||
void setRecipientLongClickListener(@Nullable RecipientLongClickListener recipientLongClickListener) {
|
||||
this.recipientLongClickListener = recipientLongClickListener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
holder.bind(data.get(position));
|
||||
|
@ -87,6 +103,8 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
|||
return OWN_INVITE_PENDING;
|
||||
} else if (groupMemberEntry instanceof GroupMemberEntry.UnknownPendingMemberCount) {
|
||||
return OTHER_INVITE_PENDING_COUNT;
|
||||
} else if (groupMemberEntry instanceof GroupMemberEntry.NewGroupCandidate) {
|
||||
return NEW_GROUP_CANDIDATE;
|
||||
}
|
||||
|
||||
throw new AssertionError();
|
||||
|
@ -108,9 +126,11 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
|||
final View admin;
|
||||
@Nullable final RecipientClickListener recipientClickListener;
|
||||
@Nullable final AdminActionsListener adminActionsListener;
|
||||
@Nullable final RecipientLongClickListener recipientLongClickListener;
|
||||
|
||||
ViewHolder(@NonNull View itemView,
|
||||
@Nullable RecipientClickListener recipientClickListener,
|
||||
@Nullable RecipientLongClickListener recipientLongClickListener,
|
||||
@Nullable AdminActionsListener adminActionsListener)
|
||||
{
|
||||
super(itemView);
|
||||
|
@ -123,6 +143,7 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
|||
this.busyProgress = itemView.findViewById(R.id.menuBusyProgress);
|
||||
this.admin = itemView.findViewById(R.id.admin);
|
||||
this.recipientClickListener = recipientClickListener;
|
||||
this.recipientLongClickListener = recipientLongClickListener;
|
||||
this.adminActionsListener = adminActionsListener;
|
||||
}
|
||||
|
||||
|
@ -149,6 +170,13 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
|||
recipientClickListener.onClick(recipient);
|
||||
}
|
||||
});
|
||||
this.itemView.setOnLongClickListener(v -> {
|
||||
if (recipientLongClickListener != null && getAdapterPosition() != RecyclerView.NO_POSITION) {
|
||||
return recipientLongClickListener.onLongClick(recipient);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
void bind(@NonNull GroupMemberEntry memberEntry) {
|
||||
|
@ -179,9 +207,10 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
|||
|
||||
FullMemberViewHolder(@NonNull View itemView,
|
||||
@Nullable RecipientClickListener recipientClickListener,
|
||||
@Nullable RecipientLongClickListener recipientLongClickListener,
|
||||
@Nullable AdminActionsListener adminActionsListener)
|
||||
{
|
||||
super(itemView, recipientClickListener, adminActionsListener);
|
||||
super(itemView, recipientClickListener, recipientLongClickListener, adminActionsListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -195,14 +224,46 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
|||
admin.setVisibility(fullMember.isAdmin() ? View.VISIBLE : View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
final static class NewGroupInviteeViewHolder extends ViewHolder {
|
||||
|
||||
private final View smsContact;
|
||||
private final View smsWarning;
|
||||
|
||||
NewGroupInviteeViewHolder(@NonNull View itemView,
|
||||
@Nullable RecipientClickListener recipientClickListener,
|
||||
@Nullable RecipientLongClickListener recipientLongClickListener)
|
||||
{
|
||||
super(itemView, recipientClickListener, recipientLongClickListener, null);
|
||||
|
||||
smsContact = itemView.findViewById(R.id.sms_contact);
|
||||
smsWarning = itemView.findViewById(R.id.sms_warning);
|
||||
}
|
||||
|
||||
@Override
|
||||
void bind(@NonNull GroupMemberEntry memberEntry) {
|
||||
GroupMemberEntry.NewGroupCandidate newGroupCandidate = (GroupMemberEntry.NewGroupCandidate) memberEntry;
|
||||
|
||||
bindRecipient(newGroupCandidate.getMember());
|
||||
bindRecipientClick(newGroupCandidate.getMember());
|
||||
|
||||
itemView.setSelected(false);
|
||||
newGroupCandidate.isSelected().observe(this, itemView::setSelected);
|
||||
|
||||
int smsWarningVisibility = newGroupCandidate.getMember().isRegistered() ? View.GONE : View.VISIBLE;
|
||||
|
||||
smsContact.setVisibility(smsWarningVisibility);
|
||||
smsWarning.setVisibility(smsWarningVisibility);
|
||||
}
|
||||
}
|
||||
|
||||
final static class OwnInvitePendingMemberViewHolder extends ViewHolder {
|
||||
|
||||
OwnInvitePendingMemberViewHolder(@NonNull View itemView,
|
||||
@Nullable RecipientClickListener recipientClickListener,
|
||||
@Nullable RecipientLongClickListener recipientLongClickListener,
|
||||
@Nullable AdminActionsListener adminActionsListener)
|
||||
{
|
||||
super(itemView, recipientClickListener, adminActionsListener);
|
||||
super(itemView, recipientClickListener, recipientLongClickListener, adminActionsListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -231,7 +292,7 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
|||
final static class UnknownPendingMemberCountViewHolder extends ViewHolder {
|
||||
|
||||
UnknownPendingMemberCountViewHolder(@NonNull View itemView, @Nullable AdminActionsListener adminActionsListener) {
|
||||
super(itemView, null, adminActionsListener);
|
||||
super(itemView, null, null, adminActionsListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -59,6 +59,10 @@ public final class GroupMemberListView extends RecyclerView {
|
|||
membersAdapter.setRecipientClickListener(listener);
|
||||
}
|
||||
|
||||
public void setRecipientLongClickListener(@Nullable RecipientLongClickListener listener) {
|
||||
membersAdapter.setRecipientLongClickListener(listener);
|
||||
}
|
||||
|
||||
public void setMembers(@NonNull List<? extends GroupMemberEntry> recipients) {
|
||||
membersAdapter.updateData(recipients);
|
||||
}
|
||||
|
|
|
@ -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_IS_GROUP = "is_group";
|
||||
|
||||
public static DialogFragment create(boolean includeClear, boolean includeCamera, short resultCode, boolean isGroup) {
|
||||
public static DialogFragment create(boolean includeClear, boolean includeCamera, short requestCode, boolean isGroup) {
|
||||
DialogFragment fragment = new AvatarSelectionBottomSheetDialogFragment();
|
||||
List<SelectionOption> selectionOptions = new ArrayList<>(3);
|
||||
Bundle args = new Bundle();
|
||||
|
@ -52,7 +52,7 @@ public class AvatarSelectionBottomSheetDialogFragment extends BottomSheetDialogF
|
|||
.toArray(String[]::new);
|
||||
|
||||
args.putStringArray(ARG_OPTIONS, options);
|
||||
args.putShort(ARG_REQUEST_CODE, resultCode);
|
||||
args.putShort(ARG_REQUEST_CODE, requestCode);
|
||||
args.putBoolean(ARG_IS_GROUP, isGroup);
|
||||
fragment.setArguments(args);
|
||||
|
||||
|
|
|
@ -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"?>
|
||||
<org.thoughtcrime.securesms.contacts.ContactSelectionListItem
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<org.thoughtcrime.securesms.contacts.ContactSelectionListItem xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/contact_selection_item_height"
|
||||
|
@ -11,21 +10,38 @@
|
|||
android:paddingStart="@dimen/selection_item_header_width"
|
||||
android:paddingEnd="24dp">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="52dp"
|
||||
android:layout_height="52dp">
|
||||
|
||||
<org.thoughtcrime.securesms.components.AvatarImageView
|
||||
android:id="@+id/contact_photo_image"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:foreground="@drawable/contact_photo_background"
|
||||
android:cropToPadding="true"
|
||||
android:contentDescription="@string/SingleContactSelectionActivity_contact_photo"
|
||||
tools:src="@color/blue_600"
|
||||
tools:ignore="UnusedAttribute" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatCheckBox
|
||||
android:id="@+id/check_box"
|
||||
android:background="@drawable/contact_selection_checkbox"
|
||||
android:button="@null"
|
||||
android:layout_width="22dp"
|
||||
android:layout_height="22dp"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:focusable="false"
|
||||
android:clickable="false" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<LinearLayout android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<org.thoughtcrime.securesms.components.FromTextView
|
||||
|
@ -35,8 +51,7 @@
|
|||
android:checkMark="?android:attr/listChoiceIndicatorMultiple"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="marquee"
|
||||
style="@style/Signal.Text.Body"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body1.Bold"
|
||||
tools:text="@sample/contacts.json/data/name" />
|
||||
|
||||
<LinearLayout android:id="@+id/number_container"
|
||||
|
@ -50,7 +65,8 @@
|
|||
android:textDirection="ltr"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="marquee"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body2"
|
||||
android:textColor="?attr/title_text_color_secondary"
|
||||
android:textSize="14sp"
|
||||
android:fontFamily="sans-serif-light"
|
||||
tools:text="@sample/contacts.json/data/number" />
|
||||
|
@ -61,7 +77,8 @@
|
|||
android:paddingStart="10dip"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body2"
|
||||
android:textColor="?attr/title_text_color_secondary"
|
||||
android:fontFamily="sans-serif-light"
|
||||
tools:text="@sample/contacts.json/data/label"
|
||||
tools:ignore="RtlSymmetry" />
|
||||
|
@ -70,11 +87,4 @@
|
|||
|
||||
</LinearLayout>
|
||||
|
||||
<CheckBox android:id="@+id/check_box"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:focusable="false"
|
||||
android:clickable="false" />
|
||||
|
||||
</org.thoughtcrime.securesms.contacts.ContactSelectionListItem>
|
||||
|
|
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_foreground_color" format="color" />
|
||||
|
||||
<attr name="tinted_circle_background" format="reference" />
|
||||
|
||||
<attr name="dialog_info_icon" format="reference" />
|
||||
<attr name="dialog_alert_icon" format="reference" />
|
||||
<attr name="dialog_background_color" format="reference|color" />
|
||||
|
@ -226,6 +228,7 @@
|
|||
<attr name="custom_pref_toggle" format="string"/>
|
||||
</declare-styleable>
|
||||
|
||||
<attr name="group_candidate_item_background" format="reference" />
|
||||
<attr name="group_members_dialog_icon" format="reference"/>
|
||||
<attr name="manage_group_add_members_icon" format="reference"/>
|
||||
<attr name="manage_group_view_all_icon" format="reference"/>
|
||||
|
|
|
@ -487,6 +487,19 @@
|
|||
<item quantity="other">Error canceling invites</item>
|
||||
</plurals>
|
||||
|
||||
<!-- AddGroupDetailsFragment -->
|
||||
<string name="AddGroupDetailsFragment__name_this_group">Name this group</string>
|
||||
<string name="AddGroupDetailsFragment__create">Create</string>
|
||||
<string name="AddGroupDetailsFragment__members">Members</string>
|
||||
<string name="AddGroupDetailsFragment__group_name_required">Group name (required)</string>
|
||||
<string name="AddGroupDetailsFragment__this_field_is_required">This field is required.</string>
|
||||
<string name="AddGroupDetailsFragment__groups_require_at_least_two_members">Groups require at least two members.</string>
|
||||
<string name="AddGroupDetailsFragment__group_creation_failed">Group creation failed.</string>
|
||||
<string name="AddGroupDetailsFragment__try_again_later">Try again later.</string>
|
||||
<string name="AddGroupDetailsFragment__youve_selected_a_contact_that_doesnt">You\'ve selected a contact that doesn\'t support Signal groups, so this group will be MMS.</string>
|
||||
<string name="AddGroupDetailsFragment__remove">Remove</string>
|
||||
<string name="AddGroupDetailsFragment__sms_contact">SMS contact</string>
|
||||
|
||||
<!-- ManageGroupActivity -->
|
||||
<string name="ManageGroupActivity_disappearing_messages">Disappearing messages</string>
|
||||
<string name="ManageGroupActivity_pending_group_invites">Pending group invites</string>
|
||||
|
|
|
@ -188,6 +188,8 @@
|
|||
<item name="android:homeAsUpIndicator">@drawable/ic_arrow_left_24</item>
|
||||
<!--<item name="android:windowContentOverlay">@drawable/compat_actionbar_shadow_background</item>-->
|
||||
|
||||
<item name="group_candidate_item_background">@drawable/group_candidate_item_background_light</item>
|
||||
|
||||
<item name="kbs_splash_image">@drawable/ic_kbs_splash_light_svg</item>
|
||||
|
||||
<item name="attachment_type_selector_background">@color/white</item>
|
||||
|
@ -251,6 +253,8 @@
|
|||
<item name="conversation_title_color">@color/white</item>
|
||||
<item name="conversation_subtitle_color">@color/transparent_white_90</item>
|
||||
|
||||
<item name="tinted_circle_background">@drawable/tinted_circle_light</item>
|
||||
|
||||
<item name="contact_list_divider">@drawable/contact_list_divider_light</item>
|
||||
|
||||
<item name="debuglog_color_none">@color/debuglog_light_none</item>
|
||||
|
@ -475,6 +479,10 @@
|
|||
<item name="homeAsUpIndicator">@drawable/ic_arrow_left_24</item>
|
||||
<item name="android:homeAsUpIndicator">@drawable/ic_arrow_left_24</item>
|
||||
|
||||
<item name="group_candidate_item_background">@drawable/group_candidate_item_background_dark</item>
|
||||
|
||||
<item name="tinted_circle_background">@drawable/tinted_circle_dark</item>
|
||||
|
||||
<item name="kbs_splash_image">@drawable/ic_kbs_splash_dark_svg</item>
|
||||
|
||||
<item name="attachment_type_selector_background">@color/core_grey_95</item>
|
||||
|
|
Loading…
Add table
Reference in a new issue