Implement new Add Members UI.
parent
707e238e5c
commit
4a455ff958
|
@ -492,6 +492,9 @@
|
|||
<activity android:name=".groups.ui.creategroup.CreateGroupActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar" />
|
||||
|
||||
<activity android:name=".groups.ui.addmembers.AddMembersActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar" />
|
||||
|
||||
<activity android:name=".groups.ui.creategroup.details.AddGroupDetailsActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar" />
|
||||
|
||||
|
|
|
@ -51,6 +51,8 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
|||
import androidx.transition.AutoTransition;
|
||||
import androidx.transition.TransitionManager;
|
||||
|
||||
import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.android.material.chip.ChipGroup;
|
||||
import com.pnikosis.materialishprogress.ProgressWheel;
|
||||
|
||||
|
@ -83,6 +85,7 @@ import java.io.IOException;
|
|||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Fragment for selecting a one or more contacts from a list.
|
||||
|
@ -101,11 +104,12 @@ public final class ContactSelectionListFragment extends Fragment
|
|||
|
||||
public static final int NO_LIMIT = Integer.MAX_VALUE;
|
||||
|
||||
public static final String DISPLAY_MODE = "display_mode";
|
||||
public static final String MULTI_SELECT = "multi_select";
|
||||
public static final String REFRESHABLE = "refreshable";
|
||||
public static final String RECENTS = "recents";
|
||||
public static final String SELECTION_LIMIT = "selection_limit";
|
||||
public static final String DISPLAY_MODE = "display_mode";
|
||||
public static final String MULTI_SELECT = "multi_select";
|
||||
public static final String REFRESHABLE = "refreshable";
|
||||
public static final String RECENTS = "recents";
|
||||
public static final String TOTAL_CAPACITY = "total_capacity";
|
||||
public static final String CURRENT_SELECTION = "current_selection";
|
||||
|
||||
private ConstraintLayout constraintLayout;
|
||||
private TextView emptyText;
|
||||
|
@ -129,6 +133,7 @@ public final class ContactSelectionListFragment extends Fragment
|
|||
@Nullable private ScrollCallback scrollCallback;
|
||||
private GlideRequests glideRequests;
|
||||
private int selectionLimit;
|
||||
private Set<RecipientId> currentSelection;
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
|
@ -205,16 +210,17 @@ public final class ContactSelectionListFragment extends Fragment
|
|||
|
||||
swipeRefresh.setEnabled(requireActivity().getIntent().getBooleanExtra(REFRESHABLE, true));
|
||||
|
||||
selectionLimit = requireActivity().getIntent().getIntExtra(SELECTION_LIMIT, NO_LIMIT);
|
||||
selectionLimit = requireActivity().getIntent().getIntExtra(TOTAL_CAPACITY, NO_LIMIT);
|
||||
currentSelection = getCurrentSelection();
|
||||
|
||||
updateGroupLimit(getChipCount());
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
private void updateGroupLimit(int childCount) {
|
||||
private void updateGroupLimit(int chipCount) {
|
||||
if (selectionLimit != NO_LIMIT) {
|
||||
groupLimit.setText(String.format(Locale.getDefault(), "%d/%d", childCount, selectionLimit));
|
||||
groupLimit.setText(String.format(Locale.getDefault(), "%d/%d", currentSelection.size() + chipCount, selectionLimit));
|
||||
groupLimit.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
groupLimit.setVisibility(View.GONE);
|
||||
|
@ -242,6 +248,13 @@ public final class ContactSelectionListFragment extends Fragment
|
|||
return cursorRecyclerViewAdapter.getSelectedContactsCount();
|
||||
}
|
||||
|
||||
private Set<RecipientId> getCurrentSelection() {
|
||||
List<RecipientId> currentSelection = requireActivity().getIntent().getParcelableArrayListExtra(CURRENT_SELECTION);
|
||||
|
||||
return currentSelection == null ? Collections.emptySet()
|
||||
: Collections.unmodifiableSet(Stream.of(currentSelection).collect(Collectors.toSet()));
|
||||
}
|
||||
|
||||
private boolean isMulti() {
|
||||
return requireActivity().getIntent().getBooleanExtra(MULTI_SELECT, false);
|
||||
}
|
||||
|
@ -253,7 +266,8 @@ public final class ContactSelectionListFragment extends Fragment
|
|||
glideRequests,
|
||||
null,
|
||||
new ListClickListener(),
|
||||
isMulti());
|
||||
isMulti(),
|
||||
currentSelection);
|
||||
|
||||
RecyclerViewConcatenateAdapterStickyHeader concatenateAdapter = new RecyclerViewConcatenateAdapterStickyHeader();
|
||||
|
||||
|
@ -546,7 +560,13 @@ public final class ContactSelectionListFragment extends Fragment
|
|||
chip.setText(recipient.getShortDisplayName(requireContext()));
|
||||
chip.setContact(selectedContact);
|
||||
chip.setCloseIconVisible(true);
|
||||
chip.setOnCloseIconClickListener(view -> markContactUnselected(selectedContact));
|
||||
chip.setOnCloseIconClickListener(view -> {
|
||||
markContactUnselected(selectedContact);
|
||||
|
||||
if (onContactSelectedListener != null) {
|
||||
onContactSelectedListener.onContactDeselected(Optional.of(recipient.getId()), recipient.getE164().orNull());
|
||||
}
|
||||
});
|
||||
|
||||
chipGroup.getLayoutTransition().addTransitionListener(new LayoutTransition.TransitionListener() {
|
||||
@Override
|
||||
|
|
|
@ -45,16 +45,24 @@ public class PushContactSelectionActivity extends ContactSelectionActivity {
|
|||
getIntent().putExtra(ContactSelectionListFragment.MULTI_SELECT, true);
|
||||
super.onCreate(icicle, ready);
|
||||
|
||||
initializeToolbar();
|
||||
}
|
||||
|
||||
protected void initializeToolbar() {
|
||||
getToolbar().setNavigationIcon(R.drawable.ic_check_24);
|
||||
getToolbar().setNavigationOnClickListener(v -> {
|
||||
Intent resultIntent = getIntent();
|
||||
List<SelectedContact> selectedContacts = contactsFragment.getSelectedContacts();
|
||||
List<RecipientId> recipients = Stream.of(selectedContacts).map(sc -> sc.getOrCreateRecipientId(this)).toList();
|
||||
|
||||
resultIntent.putParcelableArrayListExtra(KEY_SELECTED_RECIPIENTS, new ArrayList<>(recipients));
|
||||
|
||||
setResult(RESULT_OK, resultIntent);
|
||||
finish();
|
||||
onFinishedSelection();
|
||||
});
|
||||
}
|
||||
|
||||
protected final void onFinishedSelection() {
|
||||
Intent resultIntent = getIntent();
|
||||
List<SelectedContact> selectedContacts = contactsFragment.getSelectedContacts();
|
||||
List<RecipientId> recipients = Stream.of(selectedContacts).map(sc -> sc.getOrCreateRecipientId(this)).toList();
|
||||
|
||||
resultIntent.putParcelableArrayListExtra(KEY_SELECTED_RECIPIENTS, new ArrayList<>(recipients));
|
||||
|
||||
setResult(RESULT_OK, resultIntent);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,6 +45,7 @@ import org.thoughtcrime.securesms.util.Util;
|
|||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* List adapter to display all contacts and their related information
|
||||
|
@ -67,10 +68,11 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
|
|||
public static final int PAYLOAD_SELECTION_CHANGE = 1;
|
||||
|
||||
private final boolean multiSelect;
|
||||
private final LayoutInflater li;
|
||||
private final LayoutInflater layoutInflater;
|
||||
private final TypedArray drawables;
|
||||
private final ItemClickListener clickListener;
|
||||
private final GlideRequests glideRequests;
|
||||
private final Set<RecipientId> currentContacts;
|
||||
|
||||
private final SelectedContactSet selectedContacts = new SelectedContactSet();
|
||||
|
||||
|
@ -102,6 +104,7 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
|
|||
public abstract void bind(@NonNull GlideRequests glideRequests, @Nullable RecipientId recipientId, int type, String name, String number, String label, int color, boolean multiSelect);
|
||||
public abstract void unbind(@NonNull GlideRequests glideRequests);
|
||||
public abstract void setChecked(boolean checked);
|
||||
public abstract void setEnabled(boolean enabled);
|
||||
}
|
||||
|
||||
public static class ContactViewHolder extends ViewHolder {
|
||||
|
@ -131,6 +134,11 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
|
|||
public void setChecked(boolean checked) {
|
||||
getView().setChecked(checked);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabled(boolean enabled) {
|
||||
getView().setEnabled(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
public static class DividerViewHolder extends ViewHolder {
|
||||
|
@ -152,6 +160,9 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
|
|||
|
||||
@Override
|
||||
public void setChecked(boolean checked) {}
|
||||
|
||||
@Override
|
||||
public void setEnabled(boolean enabled) {}
|
||||
}
|
||||
|
||||
static class HeaderViewHolder extends RecyclerView.ViewHolder {
|
||||
|
@ -164,14 +175,16 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
|
|||
@NonNull GlideRequests glideRequests,
|
||||
@Nullable Cursor cursor,
|
||||
@Nullable ItemClickListener clickListener,
|
||||
boolean multiSelect)
|
||||
boolean multiSelect,
|
||||
@NonNull Set<RecipientId> currentContacts)
|
||||
{
|
||||
super(context, cursor);
|
||||
this.li = LayoutInflater.from(context);
|
||||
this.glideRequests = glideRequests;
|
||||
this.drawables = context.obtainStyledAttributes(STYLE_ATTRIBUTES);
|
||||
this.multiSelect = multiSelect;
|
||||
this.clickListener = clickListener;
|
||||
this.layoutInflater = LayoutInflater.from(context);
|
||||
this.glideRequests = glideRequests;
|
||||
this.drawables = context.obtainStyledAttributes(STYLE_ATTRIBUTES);
|
||||
this.multiSelect = multiSelect;
|
||||
this.clickListener = clickListener;
|
||||
this.currentContacts = currentContacts;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -188,9 +201,9 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
|
|||
@Override
|
||||
public ViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
|
||||
if (viewType == VIEW_TYPE_CONTACT) {
|
||||
return new ContactViewHolder(li.inflate(R.layout.contact_selection_list_item, parent, false), clickListener);
|
||||
return new ContactViewHolder(layoutInflater.inflate(R.layout.contact_selection_list_item, parent, false), clickListener);
|
||||
} else {
|
||||
return new DividerViewHolder(li.inflate(R.layout.contact_selection_list_divider, parent, false));
|
||||
return new DividerViewHolder(layoutInflater.inflate(R.layout.contact_selection_list_divider, parent, false));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -211,8 +224,12 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
|
|||
|
||||
viewHolder.unbind(glideRequests);
|
||||
viewHolder.bind(glideRequests, id, contactType, name, number, labelText, color, multiSelect);
|
||||
viewHolder.setEnabled(true);
|
||||
|
||||
if (numberType == ContactRepository.NEW_USERNAME_TYPE) {
|
||||
if (currentContacts.contains(id)) {
|
||||
viewHolder.setChecked(true);
|
||||
viewHolder.setEnabled(false);
|
||||
} else if (numberType == ContactRepository.NEW_USERNAME_TYPE) {
|
||||
viewHolder.setChecked(selectedContacts.contains(SelectedContact.forUsername(id, number)));
|
||||
} else {
|
||||
viewHolder.setChecked(selectedContacts.contains(SelectedContact.forPhone(id, number)));
|
||||
|
@ -230,7 +247,12 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
|
|||
int numberType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactRepository.NUMBER_TYPE_COLUMN));
|
||||
String number = cursor.getString(cursor.getColumnIndexOrThrow(ContactRepository.NUMBER_COLUMN));
|
||||
|
||||
if (numberType == ContactRepository.NEW_USERNAME_TYPE) {
|
||||
viewHolder.setEnabled(true);
|
||||
|
||||
if (currentContacts.contains(id)) {
|
||||
viewHolder.setChecked(true);
|
||||
viewHolder.setEnabled(false);
|
||||
} else if (numberType == ContactRepository.NEW_USERNAME_TYPE) {
|
||||
viewHolder.setChecked(selectedContacts.contains(SelectedContact.forUsername(id, number)));
|
||||
} else {
|
||||
viewHolder.setChecked(selectedContacts.contains(SelectedContact.forPhone(id, number)));
|
||||
|
|
|
@ -98,6 +98,12 @@ public class ContactSelectionListItem extends LinearLayout implements RecipientF
|
|||
this.checkBox.setChecked(selected);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabled(boolean enabled) {
|
||||
super.setEnabled(enabled);
|
||||
this.checkBox.setEnabled(enabled);
|
||||
}
|
||||
|
||||
public void unbind(GlideRequests glideRequests) {
|
||||
if (recipient != null) {
|
||||
recipient.removeForeverObserver(this);
|
||||
|
|
|
@ -247,7 +247,7 @@ public final class GroupManager {
|
|||
} else {
|
||||
GroupDatabase.GroupRecord groupRecord = DatabaseFactory.getGroupDatabase(context).requireGroup(groupId);
|
||||
List<RecipientId> members = groupRecord.getMembers();
|
||||
byte[] avatar = Util.readFully(AvatarHelper.getAvatar(context, groupRecord.getRecipientId()));
|
||||
byte[] avatar = groupRecord.hasAvatar() ? Util.readFully(AvatarHelper.getAvatar(context, groupRecord.getRecipientId())) : null;
|
||||
Set<RecipientId> addresses = new HashSet<>(members);
|
||||
|
||||
addresses.addAll(newMembers);
|
||||
|
|
|
@ -17,6 +17,7 @@ import org.signal.zkgroup.groups.GroupMasterKey;
|
|||
import org.signal.zkgroup.util.UUIDUtil;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||
|
@ -88,6 +89,18 @@ public final class GroupProtoUtil {
|
|||
return Recipient.externalPush(context, uuid, null);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static @NonNull RecipientId uuidByteStringToRecipientId(@NonNull ByteString uuidByteString) {
|
||||
UUID uuid = UUIDUtil.deserialize(uuidByteString.toByteArray());
|
||||
|
||||
if (uuid.equals(GroupsV2Operations.UNKNOWN_UUID)) {
|
||||
return RecipientId.UNKNOWN;
|
||||
}
|
||||
|
||||
return RecipientId.from(uuid, null);
|
||||
}
|
||||
|
||||
|
||||
public static boolean isMember(@NonNull UUID uuid, @NonNull List<DecryptedMember> membersList) {
|
||||
ByteString uuidBytes = UuidUtil.toByteString(uuid);
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
package org.thoughtcrime.securesms.groups.ui;
|
||||
|
||||
public interface AddMembersResultCallback {
|
||||
void onMembersAdded(int numberOfMembersAdded);
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
package org.thoughtcrime.securesms.groups.ui.addmembers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
import org.thoughtcrime.securesms.ContactSelectionActivity;
|
||||
import org.thoughtcrime.securesms.PushContactSelectionActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
public class AddMembersActivity extends PushContactSelectionActivity {
|
||||
|
||||
public static final String GROUP_ID = "group_id";
|
||||
|
||||
private View done;
|
||||
private AlertDialog alert;
|
||||
private AddMembersViewModel viewModel;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle icicle, boolean ready) {
|
||||
getIntent().putExtra(ContactSelectionActivity.EXTRA_LAYOUT_RES_ID, R.layout.add_members_activity);
|
||||
super.onCreate(icicle, ready);
|
||||
|
||||
AddMembersViewModel.Factory factory = new AddMembersViewModel.Factory(getGroupId());
|
||||
|
||||
done = findViewById(R.id.done);
|
||||
alert = buildConfirmationAlertDialog();
|
||||
viewModel = ViewModelProviders.of(this, factory)
|
||||
.get(AddMembersViewModel.class);
|
||||
|
||||
viewModel.getAddMemberDialogState().observe(this, state -> AddMembersActivity.updateAlertMessage(alert, state));
|
||||
|
||||
//noinspection CodeBlock2Expr
|
||||
done.setOnClickListener(v -> {
|
||||
viewModel.setDialogStateForSelectedContacts(contactsFragment.getSelectedContacts());
|
||||
alert.show();
|
||||
});
|
||||
|
||||
disableDone();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initializeToolbar() {
|
||||
getToolbar().setNavigationIcon(R.drawable.ic_arrow_left_24);
|
||||
getToolbar().setNavigationOnClickListener(v -> {
|
||||
setResult(RESULT_CANCELED);
|
||||
finish();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
if (contactsFragment.hasQueryFilter()) {
|
||||
getToolbar().clear();
|
||||
}
|
||||
|
||||
if (contactsFragment.getSelectedContactsCount() >= 1) {
|
||||
enableDone();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactDeselected(Optional<RecipientId> recipientId, String number) {
|
||||
if (contactsFragment.hasQueryFilter()) {
|
||||
getToolbar().clear();
|
||||
}
|
||||
|
||||
if (contactsFragment.getSelectedContactsCount() < 1) {
|
||||
disableDone();
|
||||
}
|
||||
}
|
||||
|
||||
private void enableDone() {
|
||||
done.setEnabled(true);
|
||||
done.animate().alpha(1f);
|
||||
}
|
||||
|
||||
private void disableDone() {
|
||||
done.setEnabled(false);
|
||||
done.animate().alpha(0.5f);
|
||||
}
|
||||
|
||||
private GroupId getGroupId() {
|
||||
return GroupId.parseOrThrow(getIntent().getStringExtra(GROUP_ID));
|
||||
}
|
||||
|
||||
private AlertDialog buildConfirmationAlertDialog() {
|
||||
return new AlertDialog.Builder(this)
|
||||
.setMessage(" ")
|
||||
.setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.cancel())
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
onFinishedSelection();
|
||||
})
|
||||
.setCancelable(true)
|
||||
.create();
|
||||
}
|
||||
|
||||
private static void updateAlertMessage(@NonNull AlertDialog alertDialog, @NonNull AddMembersViewModel.AddMemberDialogMessageState state) {
|
||||
Context context = alertDialog.getContext();
|
||||
Recipient recipient = Util.firstNonNull(state.getRecipient(), Recipient.UNKNOWN);
|
||||
|
||||
alertDialog.setMessage(context.getResources().getQuantityString(R.plurals.AddMembersActivity__add_d_members_to_s, state.getSelectionCount(),
|
||||
recipient.getDisplayName(context), state.getGroupTitle(), state.getSelectionCount()));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package org.thoughtcrime.securesms.groups.ui.addmembers;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.util.Consumer;
|
||||
|
||||
import org.thoughtcrime.securesms.contacts.SelectedContact;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
|
||||
class AddMembersRepository {
|
||||
|
||||
private final Context context;
|
||||
|
||||
AddMembersRepository() {
|
||||
this.context = ApplicationDependencies.getApplication();
|
||||
}
|
||||
|
||||
void getOrCreateRecipientId(@NonNull SelectedContact selectedContact, @NonNull Consumer<RecipientId> consumer) {
|
||||
SignalExecutors.BOUNDED.execute(() -> consumer.accept(selectedContact.getOrCreateRecipientId(context)));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
package org.thoughtcrime.securesms.groups.ui.addmembers;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.contacts.SelectedContact;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.LiveGroup;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.DefaultValueLiveData;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Preconditions;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class AddMembersViewModel extends ViewModel {
|
||||
|
||||
private final AddMembersRepository repository;
|
||||
private final LiveData<AddMemberDialogMessageState> addMemberDialogState;
|
||||
private final MutableLiveData<AddMemberDialogMessageStatePartial> partialState;
|
||||
|
||||
private AddMembersViewModel(@NonNull GroupId groupId) {
|
||||
repository = new AddMembersRepository();
|
||||
partialState = new MutableLiveData<>();
|
||||
addMemberDialogState = LiveDataUtil.combineLatest(Transformations.map(new LiveGroup(groupId).getTitle(), AddMembersViewModel::titleOrDefault),
|
||||
Transformations.switchMap(partialState, AddMembersViewModel::getStateWithoutGroupTitle),
|
||||
AddMembersViewModel::getStateWithGroupTitle);
|
||||
}
|
||||
|
||||
LiveData<AddMemberDialogMessageState> getAddMemberDialogState() {
|
||||
return addMemberDialogState;
|
||||
}
|
||||
|
||||
void setDialogStateForSelectedContacts(@NonNull List<SelectedContact> selectedContacts) {
|
||||
if (selectedContacts.size() == 1) {
|
||||
setDialogStateForSingleRecipient(selectedContacts.get(0));
|
||||
} else {
|
||||
setDialogStateForMultipleRecipients(selectedContacts.size());
|
||||
}
|
||||
}
|
||||
|
||||
private void setDialogStateForSingleRecipient(@NonNull SelectedContact selectedContact) {
|
||||
//noinspection CodeBlock2Expr
|
||||
repository.getOrCreateRecipientId(selectedContact, recipientId -> {
|
||||
partialState.postValue(new AddMemberDialogMessageStatePartial(recipientId));
|
||||
});
|
||||
}
|
||||
|
||||
private void setDialogStateForMultipleRecipients(int recipientCount) {
|
||||
partialState.setValue(new AddMemberDialogMessageStatePartial(recipientCount));
|
||||
}
|
||||
|
||||
private static LiveData<AddMemberDialogMessageState> getStateWithoutGroupTitle(@NonNull AddMemberDialogMessageStatePartial partialState) {
|
||||
if (partialState.recipientId != null) {
|
||||
return Transformations.map(Recipient.live(partialState.recipientId).getLiveData(), r -> new AddMemberDialogMessageState(r, ""));
|
||||
} else {
|
||||
return new DefaultValueLiveData<>(new AddMemberDialogMessageState(partialState.memberCount, ""));
|
||||
}
|
||||
}
|
||||
|
||||
private static AddMemberDialogMessageState getStateWithGroupTitle(@NonNull String title, @NonNull AddMemberDialogMessageState stateWithoutTitle) {
|
||||
return new AddMemberDialogMessageState(stateWithoutTitle.recipient, stateWithoutTitle.selectionCount, title);
|
||||
}
|
||||
|
||||
private static @NonNull String titleOrDefault(@Nullable String title) {
|
||||
return TextUtils.isEmpty(title) ? ApplicationDependencies.getApplication().getString(R.string.Recipient_unknown)
|
||||
: Objects.requireNonNull(title);
|
||||
}
|
||||
|
||||
private static final class AddMemberDialogMessageStatePartial {
|
||||
private final RecipientId recipientId;
|
||||
private final int memberCount;
|
||||
|
||||
private AddMemberDialogMessageStatePartial(@NonNull RecipientId recipientId) {
|
||||
this.recipientId = recipientId;
|
||||
this.memberCount = 1;
|
||||
}
|
||||
|
||||
private AddMemberDialogMessageStatePartial(int memberCount) {
|
||||
Preconditions.checkArgument(memberCount > 1);
|
||||
this.memberCount = memberCount;
|
||||
this.recipientId = null;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class AddMemberDialogMessageState {
|
||||
private final Recipient recipient;
|
||||
private final String groupTitle;
|
||||
private final int selectionCount;
|
||||
|
||||
private AddMemberDialogMessageState(@NonNull Recipient recipient, @NonNull String groupTitle) {
|
||||
this(recipient, 1, groupTitle);
|
||||
}
|
||||
|
||||
private AddMemberDialogMessageState(int selectionCount, @NonNull String groupTitle) {
|
||||
this(null, selectionCount, groupTitle);
|
||||
}
|
||||
|
||||
private AddMemberDialogMessageState(@Nullable Recipient recipient, int selectionCount, @NonNull String groupTitle) {
|
||||
this.recipient = recipient;
|
||||
this.groupTitle = groupTitle;
|
||||
this.selectionCount = selectionCount;
|
||||
}
|
||||
|
||||
public Recipient getRecipient() {
|
||||
return recipient;
|
||||
}
|
||||
|
||||
public int getSelectionCount() {
|
||||
return selectionCount;
|
||||
}
|
||||
|
||||
public @NonNull String getGroupTitle() {
|
||||
return groupTitle;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Factory implements ViewModelProvider.Factory {
|
||||
|
||||
private final GroupId groupId;
|
||||
|
||||
public Factory(@NonNull GroupId groupId) {
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
return Objects.requireNonNull(modelClass.cast(new AddMembersViewModel(groupId)));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -44,7 +44,7 @@ public class CreateGroupActivity extends ContactSelectionActivity {
|
|||
: ContactsCursorLoader.DisplayMode.FLAG_PUSH;
|
||||
|
||||
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, displayMode);
|
||||
intent.putExtra(ContactSelectionListFragment.SELECTION_LIMIT, FeatureFlags.gv2GroupCapacity() - 1);
|
||||
intent.putExtra(ContactSelectionListFragment.TOTAL_CAPACITY, FeatureFlags.gv2GroupCapacity() - 1);
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
|
|
@ -19,8 +19,11 @@ import androidx.appcompat.widget.Toolbar;
|
|||
import androidx.core.view.ViewCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.lifecycle.LifecycleObserver;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import org.thoughtcrime.securesms.AvatarPreviewActivity;
|
||||
import org.thoughtcrime.securesms.MediaPreviewActivity;
|
||||
import org.thoughtcrime.securesms.MuteDialog;
|
||||
|
@ -44,6 +47,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
|||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.LifecycleCursorWrapper;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
@ -322,6 +326,8 @@ public class ManageGroupFragment extends Fragment {
|
|||
} else {
|
||||
customNotificationsRow.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
viewModel.getSnackbarEvents().observe(getViewLifecycleOwner(), this::handleSnackbarEvent);
|
||||
}
|
||||
|
||||
public boolean onMenuItemSelected(@NonNull MenuItem item) {
|
||||
|
@ -349,6 +355,8 @@ public class ManageGroupFragment extends Fragment {
|
|||
if (context == null) return;
|
||||
if (this.cursorFactory != null) {
|
||||
Cursor cursor = this.cursorFactory.create();
|
||||
getViewLifecycleOwner().getLifecycle().addObserver(new LifecycleCursorWrapper(cursor));
|
||||
|
||||
threadPhotoRailView.setCursor(GlideApp.with(context), cursor);
|
||||
groupMediaCard.setVisibility(cursor.getCount() > 0 ? View.VISIBLE : View.GONE);
|
||||
} else {
|
||||
|
@ -357,6 +365,16 @@ public class ManageGroupFragment extends Fragment {
|
|||
}
|
||||
}
|
||||
|
||||
private void handleSnackbarEvent(@NonNull ManageGroupViewModel.SnackbarEvent snackbarEvent) {
|
||||
Snackbar.make(requireView(), buildSnackbarString(snackbarEvent), Snackbar.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
private @NonNull String buildSnackbarString(@NonNull ManageGroupViewModel.SnackbarEvent snackbarEvent) {
|
||||
return getResources().getQuantityString(R.plurals.ManageGroupActivity_added,
|
||||
snackbarEvent.getNumberOfMembersAdded(),
|
||||
snackbarEvent.getNumberOfMembersAdded());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
|
|
@ -6,7 +6,11 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.WorkerThread;
|
||||
import androidx.core.util.Consumer;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.zkgroup.util.UUIDUtil;
|
||||
import org.thoughtcrime.securesms.ContactSelectionListFragment;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
|
@ -18,7 +22,9 @@ import org.thoughtcrime.securesms.groups.GroupId;
|
|||
import org.thoughtcrime.securesms.groups.GroupInsufficientRightsException;
|
||||
import org.thoughtcrime.securesms.groups.GroupManager;
|
||||
import org.thoughtcrime.securesms.groups.GroupNotAMemberException;
|
||||
import org.thoughtcrime.securesms.groups.GroupProtoUtil;
|
||||
import org.thoughtcrime.securesms.groups.MembershipNotSuitableForV2Exception;
|
||||
import org.thoughtcrime.securesms.groups.ui.AddMembersResultCallback;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeErrorCallback;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
@ -27,9 +33,12 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
|
|||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
final class ManageGroupRepository {
|
||||
|
||||
|
@ -55,10 +64,17 @@ final class ManageGroupRepository {
|
|||
SimpleTask.run(SignalExecutors.BOUNDED, () -> {
|
||||
GroupDatabase.GroupRecord groupRecord = DatabaseFactory.getGroupDatabase(context).getGroup(groupId).get();
|
||||
if (groupRecord.isV2Group()) {
|
||||
DecryptedGroup decryptedGroup = groupRecord.requireV2GroupProperties().getDecryptedGroup();
|
||||
return new GroupCapacityResult(decryptedGroup.getMembersCount(), decryptedGroup.getPendingMembersCount(), FeatureFlags.gv2GroupCapacity());
|
||||
DecryptedGroup decryptedGroup = groupRecord.requireV2GroupProperties().getDecryptedGroup();
|
||||
List<RecipientId> pendingMembers = Stream.of(decryptedGroup.getPendingMembersList())
|
||||
.map(member -> GroupProtoUtil.uuidByteStringToRecipientId(member.getUuid()))
|
||||
.toList();
|
||||
List<RecipientId> members = new LinkedList<>(groupRecord.getMembers());
|
||||
|
||||
members.addAll(pendingMembers);
|
||||
|
||||
return new GroupCapacityResult(members, FeatureFlags.gv2GroupCapacity());
|
||||
} else {
|
||||
return new GroupCapacityResult(groupRecord.getMembers().size(), 0, ContactSelectionListFragment.NO_LIMIT);
|
||||
return new GroupCapacityResult(groupRecord.getMembers(), ContactSelectionListFragment.NO_LIMIT);
|
||||
}
|
||||
}, onGroupCapacityLoaded::accept);
|
||||
}
|
||||
|
@ -130,10 +146,11 @@ final class ManageGroupRepository {
|
|||
});
|
||||
}
|
||||
|
||||
void addMembers(@NonNull List<RecipientId> selected, @NonNull GroupChangeErrorCallback error) {
|
||||
void addMembers(@NonNull List<RecipientId> selected, @NonNull AddMembersResultCallback addMembersResultCallback, @NonNull GroupChangeErrorCallback error) {
|
||||
SignalExecutors.UNBOUNDED.execute(() -> {
|
||||
try {
|
||||
GroupManager.addMembers(context, groupId, selected);
|
||||
addMembersResultCallback.onMembersAdded(selected.size());
|
||||
} catch (GroupInsufficientRightsException | GroupNotAMemberException e) {
|
||||
Log.w(TAG, e);
|
||||
error.onError(GroupChangeFailureReason.NO_RIGHTS);
|
||||
|
@ -169,18 +186,20 @@ final class ManageGroupRepository {
|
|||
}
|
||||
|
||||
static final class GroupCapacityResult {
|
||||
private final int fullMembers;
|
||||
private final int pendingMembers;
|
||||
private final int totalCapacity;
|
||||
private final List<RecipientId> members;
|
||||
private final int totalCapacity;
|
||||
|
||||
GroupCapacityResult(int fullMembers, int pendingMembers, int totalCapacity) {
|
||||
this.fullMembers = fullMembers;
|
||||
this.pendingMembers = pendingMembers;
|
||||
GroupCapacityResult(@NonNull List<RecipientId> members, int totalCapacity) {
|
||||
this.members = members;
|
||||
this.totalCapacity = totalCapacity;
|
||||
}
|
||||
|
||||
public int getRemainingCapacity() {
|
||||
return totalCapacity - fullMembers - pendingMembers;
|
||||
public @NonNull List<RecipientId> getMembers() {
|
||||
return members;
|
||||
}
|
||||
|
||||
public int getTotalCapacity() {
|
||||
return totalCapacity;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.groups.ui.managegroup;
|
|||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.text.TextUtils;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
@ -18,7 +19,6 @@ import androidx.lifecycle.ViewModelProvider;
|
|||
import org.thoughtcrime.securesms.BlockUnblockDialog;
|
||||
import org.thoughtcrime.securesms.ContactSelectionListFragment;
|
||||
import org.thoughtcrime.securesms.ExpirationDialog;
|
||||
import org.thoughtcrime.securesms.PushContactSelectionActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase;
|
||||
|
@ -30,14 +30,17 @@ import org.thoughtcrime.securesms.groups.LiveGroup;
|
|||
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupErrors;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
|
||||
import org.thoughtcrime.securesms.groups.ui.addmembers.AddMembersActivity;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.util.DefaultValueLiveData;
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ManageGroupViewModel extends ViewModel {
|
||||
|
@ -46,6 +49,7 @@ public class ManageGroupViewModel extends ViewModel {
|
|||
|
||||
private final Context context;
|
||||
private final ManageGroupRepository manageGroupRepository;
|
||||
private final SingleLiveEvent<SnackbarEvent> snackbarEvents = new SingleLiveEvent<>();
|
||||
private final LiveData<String> title;
|
||||
private final LiveData<Boolean> isAdmin;
|
||||
private final LiveData<Boolean> canEditGroupAttributes;
|
||||
|
@ -72,7 +76,9 @@ public class ManageGroupViewModel extends ViewModel {
|
|||
|
||||
LiveGroup liveGroup = new LiveGroup(manageGroupRepository.getGroupId());
|
||||
|
||||
this.title = liveGroup.getTitle();
|
||||
this.title = Transformations.map(liveGroup.getTitle(),
|
||||
title -> TextUtils.isEmpty(title) ? context.getString(R.string.Recipient_unknown)
|
||||
: title);
|
||||
this.isAdmin = liveGroup.isSelfAdmin();
|
||||
this.canCollapseMemberList = LiveDataUtil.combineLatest(memberListCollapseState,
|
||||
Transformations.map(liveGroup.getFullMembers(), m -> m.size() > MAX_COLLAPSED_MEMBERS),
|
||||
|
@ -162,6 +168,10 @@ public class ManageGroupViewModel extends ViewModel {
|
|||
return hasCustomNotifications;
|
||||
}
|
||||
|
||||
SingleLiveEvent<SnackbarEvent> getSnackbarEvents() {
|
||||
return snackbarEvents;
|
||||
}
|
||||
|
||||
public LiveData<Boolean> getCanCollapseMemberList() {
|
||||
return canCollapseMemberList;
|
||||
}
|
||||
|
@ -187,7 +197,7 @@ public class ManageGroupViewModel extends ViewModel {
|
|||
}
|
||||
|
||||
void onAddMembers(List<RecipientId> selected) {
|
||||
manageGroupRepository.addMembers(selected, this::showErrorToast);
|
||||
manageGroupRepository.addMembers(selected, this::showSuccessSnackbar, this::showErrorToast);
|
||||
}
|
||||
|
||||
void setMuteUntil(long muteUntil) {
|
||||
|
@ -212,6 +222,11 @@ public class ManageGroupViewModel extends ViewModel {
|
|||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void showSuccessSnackbar(int numberOfMembersAdded) {
|
||||
snackbarEvents.postValue(new SnackbarEvent(numberOfMembersAdded));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void showErrorToast(@NonNull GroupChangeFailureReason e) {
|
||||
Util.runOnMain(() -> Toast.makeText(context, GroupErrors.getUserDisplayMessage(e), Toast.LENGTH_LONG).show());
|
||||
|
@ -219,13 +234,15 @@ public class ManageGroupViewModel extends ViewModel {
|
|||
|
||||
public void onAddMembersClick(@NonNull Fragment fragment, int resultCode) {
|
||||
manageGroupRepository.getGroupCapacity(capacity -> {
|
||||
int remainingCapacity = capacity.getRemainingCapacity();
|
||||
int remainingCapacity = capacity.getTotalCapacity();
|
||||
if (remainingCapacity <= 0) {
|
||||
Toast.makeText(fragment.requireContext(), R.string.ContactSelectionListFragment_the_group_is_full, Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
Intent intent = new Intent(fragment.requireActivity(), PushContactSelectionActivity.class);
|
||||
Intent intent = new Intent(fragment.requireActivity(), AddMembersActivity.class);
|
||||
intent.putExtra(AddMembersActivity.GROUP_ID, manageGroupRepository.getGroupId().toString());
|
||||
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, ContactsCursorLoader.DisplayMode.FLAG_PUSH);
|
||||
intent.putExtra(ContactSelectionListFragment.SELECTION_LIMIT, remainingCapacity);
|
||||
intent.putExtra(ContactSelectionListFragment.TOTAL_CAPACITY, remainingCapacity);
|
||||
intent.putParcelableArrayListExtra(ContactSelectionListFragment.CURRENT_SELECTION, new ArrayList<>(capacity.getMembers()));
|
||||
fragment.startActivityForResult(intent, resultCode);
|
||||
}
|
||||
});
|
||||
|
@ -276,6 +293,18 @@ public class ManageGroupViewModel extends ViewModel {
|
|||
}
|
||||
}
|
||||
|
||||
static final class SnackbarEvent {
|
||||
private final int numberOfMembersAdded;
|
||||
|
||||
private SnackbarEvent(int numberOfMembersAdded) {
|
||||
this.numberOfMembersAdded = numberOfMembersAdded;
|
||||
}
|
||||
|
||||
public int getNumberOfMembersAdded() {
|
||||
return numberOfMembersAdded;
|
||||
}
|
||||
}
|
||||
|
||||
private enum CollapseState {
|
||||
OPEN,
|
||||
COLLAPSED
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.database.CursorWrapper;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
import androidx.lifecycle.LifecycleObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.OnLifecycleEvent;
|
||||
|
||||
/**
|
||||
* Wraps a {@link Cursor} that will be closed automatically when the {@link Lifecycle.Event}.ON_DESTROY
|
||||
* is fired from the lifecycle this object is observing.
|
||||
*/
|
||||
public class LifecycleCursorWrapper extends CursorWrapper implements DefaultLifecycleObserver {
|
||||
|
||||
public LifecycleCursorWrapper(Cursor cursor) {
|
||||
super(cursor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy(@NonNull LifecycleOwner owner) {
|
||||
close();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_checked="true" android:state_enabled="false">
|
||||
<layer-list>
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<solid android:color="@color/core_grey_75" />
|
||||
<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="true">
|
||||
<layer-list>
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<solid android:color="@color/core_ultramarine_light" />
|
||||
<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>
|
|
@ -1,5 +1,16 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_checked="true" android:state_enabled="false">
|
||||
<layer-list>
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<solid android:color="@color/core_grey_25" />
|
||||
<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="true">
|
||||
<layer-list>
|
||||
<item>
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_pressed="true"
|
||||
android:color="@color/core_ultramarine_light"/>
|
||||
<item android:state_focused="true"
|
||||
android:color="@color/core_ultramarine_light"/>
|
||||
<item android:state_enabled="false"
|
||||
android:color="@color/core_ultramarine_light"/>
|
||||
<item android:state_enabled="true"
|
||||
android:color="@color/core_ultramarine_light"/>
|
||||
</selector>
|
|
@ -44,7 +44,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@color/core_ultramarine"
|
||||
android:background="?colorAccent"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingEnd="16dp"
|
||||
|
@ -84,9 +84,9 @@
|
|||
android:layout_marginBottom="16dp"
|
||||
android:textColor="@color/white"
|
||||
app:cpb_colorIndicator="@color/white"
|
||||
app:cpb_colorProgress="@color/core_ultramarine"
|
||||
app:cpb_colorProgress="?colorAccent"
|
||||
app:cpb_cornerRadius="28dp"
|
||||
app:cpb_selectorIdle="@drawable/progress_button_state"
|
||||
app:cpb_selectorIdle="?attr/circular_progress_button_state"
|
||||
app:cpb_textIdle="@string/AddGroupDetailsFragment__create"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
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:navigationIcon="@drawable/ic_arrow_left_24" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/contact_selection_list_fragment"
|
||||
android:name="org.thoughtcrime.securesms.ContactSelectionListFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.dd.CircularProgressButton
|
||||
android:id="@+id/done"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:textColor="@color/white"
|
||||
app:cpb_colorIndicator="@color/white"
|
||||
app:cpb_colorProgress="?colorAccent"
|
||||
app:cpb_cornerRadius="28dp"
|
||||
app:cpb_selectorIdle="?attr/circular_progress_button_state"
|
||||
app:cpb_textIdle="@string/AddMembersActivity__done"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
</FrameLayout>
|
|
@ -27,7 +27,7 @@
|
|||
|
||||
<androidx.appcompat.widget.AppCompatCheckBox
|
||||
android:id="@+id/check_box"
|
||||
android:background="@drawable/contact_selection_checkbox"
|
||||
android:background="?attr/contact_selection_checkbox_background"
|
||||
android:button="@null"
|
||||
android:layout_width="22dp"
|
||||
android:layout_height="22dp"
|
||||
|
|
|
@ -233,7 +233,7 @@
|
|||
app:cpb_colorIndicator="@color/white"
|
||||
app:cpb_colorProgress="@color/core_ultramarine"
|
||||
app:cpb_cornerRadius="4dp"
|
||||
app:cpb_selectorIdle="@drawable/progress_button_state"
|
||||
app:cpb_selectorIdle="@drawable/progress_button_state_light"
|
||||
app:cpb_textIdle="@string/CreateProfileActivity_next" />
|
||||
|
||||
</LinearLayout>
|
||||
|
|
|
@ -102,7 +102,7 @@
|
|||
app:cpb_colorIndicator="@color/white"
|
||||
app:cpb_colorProgress="@color/StickerPreviewActivity_install_button_color"
|
||||
app:cpb_cornerRadius="4dp"
|
||||
app:cpb_selectorIdle="@drawable/progress_button_state"
|
||||
app:cpb_selectorIdle="@drawable/progress_button_state_light"
|
||||
app:cpb_textIdle="@string/StickerPackPreviewActivity_install"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="@id/sticker_install_list"
|
||||
|
|
|
@ -95,7 +95,7 @@
|
|||
app:cpb_colorIndicator="@color/white"
|
||||
app:cpb_colorProgress="@color/core_ultramarine"
|
||||
app:cpb_cornerRadius="4dp"
|
||||
app:cpb_selectorIdle="@drawable/progress_button_state"
|
||||
app:cpb_selectorIdle="@drawable/progress_button_state_light"
|
||||
app:cpb_textIdle="@string/SubmitDebugLogActivity_submit"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
|
|
@ -55,7 +55,7 @@
|
|||
app:cpb_colorIndicator="@color/white"
|
||||
app:cpb_colorProgress="@color/core_ultramarine"
|
||||
app:cpb_cornerRadius="4dp"
|
||||
app:cpb_selectorIdle="@drawable/progress_button_state"
|
||||
app:cpb_selectorIdle="@drawable/progress_button_state_light"
|
||||
app:cpb_textIdle="@string/UsernameEditFragment_submit"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
|
|
@ -158,6 +158,9 @@
|
|||
<attr name="contact_selection_header_text" format="reference|color" />
|
||||
<attr name="contact_selection_invite_icon" format="reference" />
|
||||
<attr name="contact_selection_new_group_icon" format="reference" />
|
||||
<attr name="contact_selection_checkbox_background" format="reference" />
|
||||
|
||||
<attr name="circular_progress_button_state" format="reference" />
|
||||
|
||||
<attr name="contact_filter_toolbar_icon_tint" format="color" />
|
||||
<attr name="contact_filter_toolbar_keyboard_icon" format="reference" />
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<item name="cpb_cornerRadius">4dp</item>
|
||||
<item name="cpb_colorIndicator">@color/white</item>
|
||||
<item name="cpb_colorProgress">@color/core_ultramarine</item>
|
||||
<item name="cpb_selectorIdle">@drawable/progress_button_state</item>
|
||||
<item name="cpb_selectorIdle">@drawable/progress_button_state_light</item>
|
||||
</style>
|
||||
|
||||
<style name="Signal.Text.Headline.Registration">
|
||||
|
|
|
@ -490,6 +490,13 @@
|
|||
<item quantity="other">Error canceling invites</item>
|
||||
</plurals>
|
||||
|
||||
<!-- AddMembersActivity -->
|
||||
<string name="AddMembersActivity__done">Done</string>
|
||||
<plurals name="AddMembersActivity__add_d_members_to_s">
|
||||
<item quantity="one">Add \"%1$s\" to \"%2$s\"?</item>
|
||||
<item quantity="other">Add %3$d members to \"%2$s\"?</item>
|
||||
</plurals>
|
||||
|
||||
<!-- AddGroupDetailsFragment -->
|
||||
<string name="AddGroupDetailsFragment__name_this_group">Name this group</string>
|
||||
<string name="AddGroupDetailsFragment__create">Create</string>
|
||||
|
@ -525,6 +532,10 @@
|
|||
<item quantity="one">%d invited</item>
|
||||
<item quantity="other">%d invited</item>
|
||||
</plurals>
|
||||
<plurals name="ManageGroupActivity_added">
|
||||
<item quantity="one">%d member added.</item>
|
||||
<item quantity="other">%d members added.</item>
|
||||
</plurals>
|
||||
|
||||
<string name="ManageGroupActivity_you_dont_have_the_rights_to_do_this">You don\'t have the rights to do this</string>
|
||||
<string name="ManageGroupActivity_not_capable">Someone you added does not support new groups and needs to update Signal</string>
|
||||
|
|
|
@ -431,10 +431,13 @@
|
|||
|
||||
<item name="contact_selection_new_group_icon">@drawable/ic_new_group_circle_light</item>
|
||||
<item name="contact_selection_invite_icon">@drawable/ic_invite_circle_light</item>
|
||||
<item name="contact_selection_checkbox_background">@drawable/contact_selection_checkbox_light</item>
|
||||
|
||||
<item name="chipStyle">@style/Signal.Chip.Action.Light</item>
|
||||
|
||||
<item name="colorControlNormal">@color/core_grey_90</item>
|
||||
|
||||
<item name="circular_progress_button_state">@drawable/progress_button_state_light</item>
|
||||
</style>
|
||||
|
||||
<style name="TextSecure.DarkTheme" parent="@style/TextSecure.BaseDarkTheme">
|
||||
|
@ -722,10 +725,13 @@
|
|||
|
||||
<item name="contact_selection_new_group_icon">@drawable/ic_new_group_circle_dark</item>
|
||||
<item name="contact_selection_invite_icon">@drawable/ic_invite_circle_dark</item>
|
||||
<item name="contact_selection_checkbox_background">@drawable/contact_selection_checkbox_dark</item>
|
||||
|
||||
<item name="chipStyle">@style/Signal.Chip.Action</item>
|
||||
|
||||
<item name="colorControlNormal">@color/core_white</item>
|
||||
|
||||
<item name="circular_progress_button_state">@drawable/progress_button_state_dark</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Signal.AlertDialog.Light.Cornered" parent="Theme.AppCompat.Light.Dialog.Alert">
|
||||
|
|
Loading…
Reference in New Issue