New group avatar and name selection screen.

fork-5.53.8
Alex Hart 2020-04-28 13:27:09 -03:00 zatwierdzone przez Greyson Parrelli
rodzic 12b7d6c0e3
commit 5eb663aa1b
22 zmienionych plików z 493 dodań i 189 usunięć

Wyświetl plik

@ -7,12 +7,26 @@ import androidx.appcompat.app.AlertDialog;
public class ClearProfileAvatarActivity extends Activity {
private static final String ARG_TITLE = "arg_title";
public static Intent createForUserProfilePhoto() {
return new Intent("org.thoughtcrime.securesms.action.CLEAR_PROFILE_PHOTO");
}
public static Intent createForGroupProfilePhoto() {
Intent intent = new Intent("org.thoughtcrime.securesms.action.CLEAR_PROFILE_PHOTO");
intent.putExtra(ARG_TITLE, R.string.ClearProfileActivity_remove_group_photo);
return intent;
}
@Override
public void onResume() {
super.onResume();
int titleId = getIntent().getIntExtra(ARG_TITLE, R.string.ClearProfileActivity_remove_profile_photo);
new AlertDialog.Builder(this)
.setTitle(R.string.ClearProfileActivity_remove_profile_photo)
.setTitle(titleId)
.setNegativeButton(android.R.string.cancel, (dialog, which) -> finish())
.setPositiveButton(R.string.ClearProfileActivity_remove, (dialog, which) -> {
Intent result = new Intent();
@ -20,6 +34,7 @@ public class ClearProfileAvatarActivity extends Activity {
setResult(Activity.RESULT_OK, result);
finish();
})
.setOnCancelListener(dialog -> finish())
.show();
}

Wyświetl plik

@ -65,11 +65,13 @@ import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.SelectedRecipientsAdapter;
import org.thoughtcrime.securesms.util.SelectedRecipientsAdapter.OnRecipientDeletedListener;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@ -203,7 +205,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
recipientsPanel.setPanelChangeListener(this);
findViewById(R.id.contacts_button).setOnClickListener(new AddRecipientButtonListener());
avatar.setImageDrawable(getDefaultGroupAvatar());
avatar.setOnClickListener(view -> AvatarSelectionBottomSheetDialogFragment.create(avatarBmp != null, false, REQUEST_CODE_SELECT_AVATAR).show(getSupportFragmentManager(), null));
avatar.setOnClickListener(view -> AvatarSelectionBottomSheetDialogFragment.create(avatarBmp != null, false, REQUEST_CODE_SELECT_AVATAR, true).show(getSupportFragmentManager(), null));
}
private Drawable getDefaultGroupAvatar() {
@ -215,6 +217,10 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
if (groupId != null) {
new FillExistingGroupInfoAsyncTask(this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, groupId);
if (FeatureFlags.newGroupUI() && groupId.isPush()) {
avatar.setOnClickListener(v -> startActivity(EditProfileActivity.getIntentForGroupProfile(this, groupId.requirePush())));
}
}
}

Wyświetl plik

@ -193,6 +193,7 @@ import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.profiles.GroupShareProfileView;
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;

Wyświetl plik

@ -9,8 +9,11 @@ import androidx.annotation.WorkerThread;
import org.signal.zkgroup.VerificationFailedException;
import org.signal.zkgroup.groups.UuidCiphertext;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
@ -18,6 +21,7 @@ import org.whispersystems.signalservice.api.util.InvalidNumberException;
import java.io.IOException;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public final class GroupManager {
@ -33,6 +37,20 @@ public final class GroupManager {
return V1GroupManager.createGroup(context, addresses, avatar, name, mms);
}
@WorkerThread
public static GroupActionResult updateGroup(@NonNull Context context,
@NonNull GroupId groupId,
@Nullable byte[] avatar,
@Nullable String name)
throws InvalidNumberException
{
List<Recipient> members = DatabaseFactory.getGroupDatabase(context)
.getGroupMembers(groupId, GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
return V1GroupManager.updateGroup(context, groupId, getMemberIds(members), avatar, name);
}
public static GroupActionResult updateGroup(@NonNull Context context,
@NonNull GroupId groupId,
@NonNull Set<Recipient> members,
@ -42,7 +60,7 @@ public final class GroupManager {
{
Set<RecipientId> addresses = getMemberIds(members);
return V1GroupManager.updateGroup(context, groupId, addresses, avatar, name);
return V1GroupManager.updateGroup(context, groupId, addresses, BitmapUtil.toByteArray(avatar), name);
}
private static Set<RecipientId> getMemberIds(Collection<Recipient> recipients) {

Wyświetl plik

@ -87,13 +87,12 @@ final class V1GroupManager {
static GroupActionResult updateGroup(@NonNull Context context,
@NonNull GroupId groupId,
@NonNull Set<RecipientId> memberAddresses,
@Nullable Bitmap avatar,
@Nullable byte[] avatarBytes,
@Nullable String name)
throws InvalidNumberException
{
final GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
final RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId);
final byte[] avatarBytes = BitmapUtil.toByteArray(avatar);
memberAddresses.add(Recipient.self().getId());
groupDatabase.updateMembers(groupId, new LinkedList<>(memberAddresses));

Wyświetl plik

@ -5,6 +5,9 @@ import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
@ -29,6 +32,7 @@ import org.thoughtcrime.securesms.groups.ui.pendingmemberinvites.PendingMemberIn
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
import java.util.Objects;
@ -68,6 +72,12 @@ public class ManageGroupFragment extends Fragment {
return fragment;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@ -99,7 +109,7 @@ public class ManageGroupFragment extends Fragment {
super.onActivityCreated(savedInstanceState);
Context context = requireContext();
GroupId.Push groupId = GroupId.parseOrThrow(Objects.requireNonNull(requireArguments().getString(GROUP_ID))).requirePush();
GroupId.Push groupId = getPushGroupId();
ManageGroupViewModel.Factory factory = new ManageGroupViewModel.Factory(context, groupId);
viewModel = ViewModelProviders.of(requireActivity(), factory).get(ManageGroupViewModel.class);
@ -177,6 +187,27 @@ public class ManageGroupFragment extends Fragment {
groupMemberList.setRecipientClickListener(recipient -> RecipientBottomSheetDialogFragment.create(recipient.getId(), groupId).show(requireFragmentManager(), "BOTTOM"));
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
inflater.inflate(R.menu.manage_group_fragment, menu);
viewModel.getCanEditGroupAttributes().observe(getViewLifecycleOwner(), canEdit -> menu.findItem(R.id.action_edit).setVisible(canEdit));
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.action_edit) {
startActivity(EditProfileActivity.getIntentForGroupProfile(requireActivity(), getPushGroupId()));
return true;
}
return false;
}
private GroupId.Push getPushGroupId() {
return GroupId.parseOrThrow(Objects.requireNonNull(requireArguments().getString(GROUP_ID))).requirePush();
}
private void setMediaCursorFactory(@Nullable ManageGroupViewModel.CursorFactory cursorFactory) {
if (this.cursorFactory != cursorFactory) {
this.cursorFactory = cursorFactory;

Wyświetl plik

@ -19,7 +19,9 @@ import androidx.recyclerview.widget.RecyclerView;
import com.annimon.stream.Stream;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import org.thoughtcrime.securesms.ClearProfileAvatarActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.util.ThemeUtil;
import java.util.ArrayList;
@ -29,8 +31,9 @@ public class AvatarSelectionBottomSheetDialogFragment extends BottomSheetDialogF
private static final String ARG_OPTIONS = "options";
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) {
public static DialogFragment create(boolean includeClear, boolean includeCamera, short resultCode, boolean isGroup) {
DialogFragment fragment = new AvatarSelectionBottomSheetDialogFragment();
List<SelectionOption> selectionOptions = new ArrayList<>(3);
Bundle args = new Bundle();
@ -51,6 +54,7 @@ public class AvatarSelectionBottomSheetDialogFragment extends BottomSheetDialogF
args.putStringArray(ARG_OPTIONS, options);
args.putShort(ARG_REQUEST_CODE, resultCode);
args.putBoolean(ARG_IS_GROUP, isGroup);
fragment.setArguments(args);
return fragment;
@ -93,7 +97,7 @@ public class AvatarSelectionBottomSheetDialogFragment extends BottomSheetDialogF
}
private void launchOptionAndDismiss(@NonNull SelectionOption option) {
Intent intent = createIntent(requireContext(), option);
Intent intent = createIntent(requireContext(), option, requireArguments().getBoolean(ARG_IS_GROUP));
int requestCode = requireArguments().getShort(ARG_REQUEST_CODE);
if (getParentFragment() != null) {
@ -105,14 +109,15 @@ public class AvatarSelectionBottomSheetDialogFragment extends BottomSheetDialogF
dismiss();
}
private static Intent createIntent(@NonNull Context context, @NonNull SelectionOption selectionOption) {
private static Intent createIntent(@NonNull Context context, @NonNull SelectionOption selectionOption, boolean isGroup) {
switch (selectionOption) {
case CAPTURE:
return AvatarSelectionActivity.getIntentForCameraCapture(context);
case GALLERY:
return AvatarSelectionActivity.getIntentForGallery(context);
case DELETE:
return new Intent("org.thoughtcrime.securesms.action.CLEAR_PROFILE_PHOTO");
return isGroup ? ClearProfileAvatarActivity.createForGroupProfilePhoto()
: ClearProfileAvatarActivity.createForUserProfilePhoto();
default:
throw new IllegalStateException("Unknown option: " + selectionOption);
}

Wyświetl plik

@ -28,7 +28,6 @@ import com.dd.CircularProgressButton;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.MainActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.lock.v2.KbsConstants;
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType;
import org.thoughtcrime.securesms.logging.Log;
@ -39,7 +38,6 @@ import org.thoughtcrime.securesms.registration.RegistrationUtil;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.whispersystems.signalservice.internal.storage.protos.SignalStorage;
import java.util.Locale;
@ -233,7 +231,7 @@ public class PinRestoreEntryFragment extends Fragment {
if (Recipient.self().getProfileName().isEmpty() || !AvatarHelper.hasAvatar(activity, Recipient.self().getId())) {
final Intent main = new Intent(activity, MainActivity.class);
final Intent profile = EditProfileActivity.getIntent(activity, false);
final Intent profile = EditProfileActivity.getIntentForUserProfile(activity);
profile.putExtra("next_intent", main);
startActivity(profile);

Wyświetl plik

@ -12,23 +12,34 @@ import androidx.navigation.Navigation;
import org.thoughtcrime.securesms.BaseActionBarActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.DynamicRegistrationTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
@SuppressLint("StaticFieldLeak")
public class EditProfileActivity extends BaseActionBarActivity implements EditProfileFragment.Controller {
public static final String NEXT_INTENT = "next_intent";
public static final String EXCLUDE_SYSTEM = "exclude_system";
public static final String DISPLAY_USERNAME = "display_username";
public static final String NEXT_BUTTON_TEXT = "next_button_text";
public static final String SHOW_TOOLBAR = "show_back_arrow";
public static final String NEXT_INTENT = "next_intent";
public static final String EXCLUDE_SYSTEM = "exclude_system";
public static final String DISPLAY_USERNAME = "display_username";
public static final String NEXT_BUTTON_TEXT = "next_button_text";
public static final String SHOW_TOOLBAR = "show_back_arrow";
public static final String GROUP_ID = "group_id";
private final DynamicTheme dynamicTheme = new DynamicRegistrationTheme();
public static @NonNull Intent getIntent(@NonNull Context context, boolean showToolbar) {
public static @NonNull Intent getIntentForUserProfile(@NonNull Context context) {
Intent intent = new Intent(context, EditProfileActivity.class);
intent.putExtra(EditProfileActivity.SHOW_TOOLBAR, showToolbar);
intent.putExtra(EditProfileActivity.SHOW_TOOLBAR, false);
return intent;
}
public static @NonNull Intent getIntentForGroupProfile(@NonNull Context context, @NonNull GroupId.Push groupId) {
Intent intent = new Intent(context, EditProfileActivity.class);
intent.putExtra(EditProfileActivity.SHOW_TOOLBAR, true);
intent.putExtra(EditProfileActivity.GROUP_ID, groupId.toString());
intent.putExtra(EditProfileActivity.NEXT_BUTTON_TEXT, R.string.save);
return intent;
}

Wyświetl plik

@ -34,8 +34,7 @@ import com.google.android.gms.common.util.IOUtils;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.lock.v2.RegistrationLockUtil;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mediasend.AvatarSelectionActivity;
import org.thoughtcrime.securesms.mediasend.AvatarSelectionBottomSheetDialogFragment;
@ -46,7 +45,6 @@ import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.registration.RegistrationUtil;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
@ -58,6 +56,7 @@ import java.io.InputStream;
import static android.app.Activity.RESULT_OK;
import static org.thoughtcrime.securesms.profiles.edit.EditProfileActivity.DISPLAY_USERNAME;
import static org.thoughtcrime.securesms.profiles.edit.EditProfileActivity.EXCLUDE_SYSTEM;
import static org.thoughtcrime.securesms.profiles.edit.EditProfileActivity.GROUP_ID;
import static org.thoughtcrime.securesms.profiles.edit.EditProfileActivity.NEXT_BUTTON_TEXT;
import static org.thoughtcrime.securesms.profiles.edit.EditProfileActivity.NEXT_INTENT;
import static org.thoughtcrime.securesms.profiles.edit.EditProfileActivity.SHOW_TOOLBAR;
@ -122,10 +121,13 @@ public class EditProfileFragment extends Fragment {
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
initializeResources(view);
initializeViewModel(requireArguments().getBoolean(EXCLUDE_SYSTEM, false), savedInstanceState != null);
initializeProfileName();
final GroupId groupId = GroupId.parseNullableOrThrow(requireArguments().getString(GROUP_ID, null));
final GroupId.Push pushGroupId = groupId != null ? groupId.requirePush() : null;
initializeResources(view, pushGroupId != null);
initializeViewModel(requireArguments().getBoolean(EXCLUDE_SYSTEM, false), pushGroupId, savedInstanceState != null);
initializeProfileAvatar();
initializeProfileName();
initializeUsername();
requireActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING);
@ -189,14 +191,21 @@ public class EditProfileFragment extends Fragment {
}
}
private void initializeViewModel(boolean excludeSystem, boolean hasSavedInstanceState) {
EditProfileRepository repository = new EditProfileRepository(requireContext(), excludeSystem);
EditProfileViewModel.Factory factory = new EditProfileViewModel.Factory(repository, hasSavedInstanceState);
private void initializeViewModel(boolean excludeSystem, @Nullable GroupId.Push groupId, boolean hasSavedInstanceState) {
EditProfileRepository repository;
if (groupId != null) {
repository = new EditPushGroupProfileRepository(requireContext(), groupId);
} else {
repository = new EditSelfProfileRepository(requireContext(), excludeSystem);
}
EditProfileViewModel.Factory factory = new EditProfileViewModel.Factory(repository, hasSavedInstanceState, groupId);
viewModel = ViewModelProviders.of(this, factory).get(EditProfileViewModel.class);
}
private void initializeResources(@NonNull View view) {
private void initializeResources(@NonNull View view, boolean isEditingGroup) {
Bundle arguments = requireArguments();
this.toolbar = view.findViewById(R.id.toolbar);
@ -228,10 +237,21 @@ public class EditProfileFragment extends Fragment {
trimInPlace(s);
viewModel.setGivenName(s.toString());
}));
this.familyName.addTextChangedListener(new AfterTextChanged(s -> {
trimInPlace(s);
viewModel.setFamilyName(s.toString());
}));
if (isEditingGroup) {
givenName.setHint(R.string.EditProfileFragment__group_name);
toolbar.setTitle(R.string.EditProfileFragment__edit_group_name_and_photo);
preview.setVisibility(View.GONE);
familyName.setVisibility(View.GONE);
familyName.setEnabled(false);
view.findViewById(R.id.description_text).setVisibility(View.GONE);
view.<ImageView>findViewById(R.id.avatar_placeholder).setImageResource(R.drawable.ic_group_outline_40);
} else {
this.familyName.addTextChangedListener(new AfterTextChanged(s -> {
trimInPlace(s);
viewModel.setFamilyName(s.toString());
}));
}
this.finishButton.setOnClickListener(v -> {
this.finishButton.setIndeterminateProgressMode(true);
@ -254,22 +274,20 @@ public class EditProfileFragment extends Fragment {
}
private void initializeProfileName() {
viewModel.givenName().observe(this, givenName -> updateFieldIfNeeded(this.givenName, givenName));
viewModel.familyName().observe(this, familyName -> updateFieldIfNeeded(this.familyName, familyName));
viewModel.profileName().observe(this, profileName -> {
preview.setText(profileName.toString());
boolean validEntry = !profileName.isGivenNameEmpty();
finishButton.setEnabled(validEntry);
finishButton.setAlpha(validEntry ? 1f : 0.5f);
viewModel.isFormValid().observe(getViewLifecycleOwner(), isValid -> {
finishButton.setEnabled(isValid);
finishButton.setAlpha(isValid ? 1f : 0.5f);
});
viewModel.givenName().observe(getViewLifecycleOwner(), givenName -> updateFieldIfNeeded(this.givenName, givenName));
viewModel.familyName().observe(getViewLifecycleOwner(), familyName -> updateFieldIfNeeded(this.familyName, familyName));
viewModel.profileName().observe(getViewLifecycleOwner(), profileName -> preview.setText(profileName.toString()));
}
private void initializeProfileAvatar() {
viewModel.avatar().observe(this, bytes -> {
viewModel.avatar().observe(getViewLifecycleOwner(), bytes -> {
if (bytes == null) return;
GlideApp.with(this)
@ -280,7 +298,7 @@ public class EditProfileFragment extends Fragment {
}
private void initializeUsername() {
viewModel.username().observe(this, this::onUsernameChanged);
viewModel.username().observe(getViewLifecycleOwner(), this::onUsernameChanged);
}
private static void updateFieldIfNeeded(@NonNull EditText field, @NonNull String value) {
@ -303,7 +321,11 @@ public class EditProfileFragment extends Fragment {
}
private void startAvatarSelection() {
AvatarSelectionBottomSheetDialogFragment.create(viewModel.hasAvatar(), true, REQUEST_CODE_SELECT_AVATAR).show(getChildFragmentManager(), null);
AvatarSelectionBottomSheetDialogFragment.create(viewModel.canRemoveProfilePhoto(),
true,
REQUEST_CODE_SELECT_AVATAR,
viewModel.isGroup())
.show(getChildFragmentManager(), null);
}
private void handleUpload() {

Wyświetl plik

@ -1,146 +1,27 @@
package org.thoughtcrime.securesms.profiles.edit;
import android.content.Context;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.core.util.Consumer;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileContentUpdateJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileKeyUpdateJob;
import org.thoughtcrime.securesms.jobs.ProfileUploadJob;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.profiles.SystemProfileUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.ProfileUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.concurrent.ExecutionException;
interface EditProfileRepository {
class EditProfileRepository {
void getCurrentProfileName(@NonNull Consumer<ProfileName> profileNameConsumer);
private static final String TAG = Log.tag(EditProfileRepository.class);
void getCurrentAvatar(@NonNull Consumer<byte[]> avatarConsumer);
private final Context context;
private final boolean excludeSystem;
void getCurrentDisplayName(@NonNull Consumer<String> displayNameConsumer);
EditProfileRepository(@NonNull Context context, boolean excludeSystem) {
this.context = context.getApplicationContext();
this.excludeSystem = excludeSystem;
}
void uploadProfile(@NonNull ProfileName profileName, @NonNull String displayName, @Nullable byte[] avatar, @NonNull Consumer<UploadResult> uploadResultConsumer);
void getCurrentProfileName(@NonNull Consumer<ProfileName> profileNameConsumer) {
ProfileName storedProfileName = Recipient.self().getProfileName();
if (!storedProfileName.isEmpty()) {
profileNameConsumer.accept(storedProfileName);
} else if (!excludeSystem) {
SystemProfileUtil.getSystemProfileName(context).addListener(new ListenableFuture.Listener<String>() {
@Override
public void onSuccess(String result) {
if (!TextUtils.isEmpty(result)) {
profileNameConsumer.accept(ProfileName.fromSerialized(result));
} else {
profileNameConsumer.accept(storedProfileName);
}
}
void getCurrentUsername(@NonNull Consumer<Optional<String>> callback);
@Override
public void onFailure(ExecutionException e) {
Log.w(TAG, e);
profileNameConsumer.accept(storedProfileName);
}
});
} else {
profileNameConsumer.accept(storedProfileName);
}
}
void getCurrentAvatar(@NonNull Consumer<byte[]> avatarConsumer) {
RecipientId selfId = Recipient.self().getId();
if (AvatarHelper.hasAvatar(context, selfId)) {
SimpleTask.run(() -> {
try {
return Util.readFully(AvatarHelper.getAvatar(context, selfId));
} catch (IOException e) {
Log.w(TAG, e);
return null;
}
}, avatarConsumer::accept);
} else if (!excludeSystem) {
SystemProfileUtil.getSystemProfileAvatar(context, new ProfileMediaConstraints()).addListener(new ListenableFuture.Listener<byte[]>() {
@Override
public void onSuccess(byte[] result) {
avatarConsumer.accept(result);
}
@Override
public void onFailure(ExecutionException e) {
Log.w(TAG, e);
avatarConsumer.accept(null);
}
});
}
}
void uploadProfile(@NonNull ProfileName profileName, @Nullable byte[] avatar, @NonNull Consumer<UploadResult> uploadResultConsumer) {
SimpleTask.run(() -> {
DatabaseFactory.getRecipientDatabase(context).setProfileName(Recipient.self().getId(), profileName);
try {
AvatarHelper.setAvatar(context, Recipient.self().getId(), avatar != null ? new ByteArrayInputStream(avatar) : null);
} catch (IOException e) {
return UploadResult.ERROR_FILE_IO;
}
ApplicationDependencies.getJobManager()
.startChain(new ProfileUploadJob())
.then(Arrays.asList(new MultiDeviceProfileKeyUpdateJob(), new MultiDeviceProfileContentUpdateJob()))
.enqueue();
return UploadResult.SUCCESS;
}, uploadResultConsumer::accept);
}
void getCurrentUsername(@NonNull Consumer<Optional<String>> callback) {
callback.accept(Optional.fromNullable(TextSecurePreferences.getLocalUsername(context)));
SignalExecutors.UNBOUNDED.execute(() -> callback.accept(getUsernameInternal()));
}
@WorkerThread
private @NonNull Optional<String> getUsernameInternal() {
try {
SignalServiceProfile profile = ProfileUtil.retrieveProfile(context, Recipient.self(), SignalServiceProfile.RequestType.PROFILE).getProfile();
TextSecurePreferences.setLocalUsername(context, profile.getUsername());
DatabaseFactory.getRecipientDatabase(context).setUsername(Recipient.self().getId(), profile.getUsername());
} catch (IOException e) {
Log.w(TAG, "Failed to retrieve username remotely! Using locally-cached version.");
}
return Optional.fromNullable(TextSecurePreferences.getLocalUsername(context));
}
public enum UploadResult {
enum UploadResult {
SUCCESS,
ERROR_FILE_IO
ERROR_IO,
ERROR_BAD_RECIPIENT
}
}

Wyświetl plik

@ -1,7 +1,10 @@
package org.thoughtcrime.securesms.profiles.edit;
import android.view.animation.Transformation;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.Consumer;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
@ -9,6 +12,7 @@ import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.util.livedata.LiveDataPair;
import org.whispersystems.libsignal.util.guava.Optional;
@ -21,18 +25,26 @@ class EditProfileViewModel extends ViewModel {
pair -> ProfileName.fromParts(pair.first(), pair.second()));
private final MutableLiveData<byte[]> internalAvatar = new MutableLiveData<>();
private final MutableLiveData<Optional<String>> internalUsername = new MutableLiveData<>();
private final LiveData<Boolean> isFormValid = Transformations.map(givenName, name -> !name.isEmpty());
private final EditProfileRepository repository;
private final GroupId groupId;
private EditProfileViewModel(@NonNull EditProfileRepository repository, boolean hasInstanceState) {
this.repository = repository;
private EditProfileViewModel(@NonNull EditProfileRepository repository, boolean hasInstanceState, @Nullable GroupId groupId) {
this.repository = repository;
this.groupId = groupId;
repository.getCurrentUsername(internalUsername::postValue);
if (!hasInstanceState) {
repository.getCurrentProfileName(name -> {
givenName.setValue(name.getGivenName());
familyName.setValue(name.getFamilyName());
});
if (groupId != null) {
repository.getCurrentDisplayName(givenName::setValue);
} else {
repository.getCurrentProfileName(name -> {
givenName.setValue(name.getGivenName());
familyName.setValue(name.getFamilyName());
});
}
repository.getCurrentAvatar(internalAvatar::setValue);
}
}
@ -49,6 +61,10 @@ class EditProfileViewModel extends ViewModel {
return Transformations.distinctUntilChanged(internalProfileName);
}
public LiveData<Boolean> isFormValid() {
return Transformations.distinctUntilChanged(isFormValid);
}
public LiveData<byte[]> avatar() {
return Transformations.distinctUntilChanged(internalAvatar);
}
@ -61,6 +77,14 @@ class EditProfileViewModel extends ViewModel {
return internalAvatar.getValue() != null;
}
public boolean isGroup() {
return groupId != null;
}
public boolean canRemoveProfilePhoto() {
return (!isGroup() || groupId.isV1()) && hasAvatar();
}
@MainThread
public byte[] getAvatarSnapshot() {
return internalAvatar.getValue();
@ -79,29 +103,33 @@ class EditProfileViewModel extends ViewModel {
}
public void submitProfile(Consumer<EditProfileRepository.UploadResult> uploadResultConsumer) {
ProfileName profileName = internalProfileName.getValue();
if (profileName == null) {
ProfileName profileName = isGroup() ? ProfileName.EMPTY : internalProfileName.getValue();
String displayName = isGroup() ? givenName.getValue() : "";
if (profileName == null || displayName == null) {
return;
}
repository.uploadProfile(profileName, internalAvatar.getValue(), uploadResultConsumer);
repository.uploadProfile(profileName, displayName, internalAvatar.getValue(), uploadResultConsumer);
}
static class Factory implements ViewModelProvider.Factory {
private final EditProfileRepository repository;
private final boolean hasInstanceState;
private final boolean hasInstanceState;
private final GroupId groupId;
Factory(@NonNull EditProfileRepository repository, boolean hasInstanceState) {
Factory(@NonNull EditProfileRepository repository, boolean hasInstanceState, @Nullable GroupId groupId) {
this.repository = repository;
this.hasInstanceState = hasInstanceState;
this.groupId = groupId;
}
@NonNull
@Override
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection unchecked
return (T) new EditProfileViewModel(repository, hasInstanceState);
return (T) new EditProfileViewModel(repository, hasInstanceState, groupId);
}
}
}

Wyświetl plik

@ -0,0 +1,95 @@
package org.thoughtcrime.securesms.profiles.edit;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.core.util.Consumer;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.GroupManager;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import java.io.IOException;
class EditPushGroupProfileRepository implements EditProfileRepository {
private static final String TAG = Log.tag(EditPushGroupProfileRepository.class);
private final Context context;
private final GroupId.Push groupId;
EditPushGroupProfileRepository(@NonNull Context context, @NonNull GroupId.Push groupId) {
this.context = context.getApplicationContext();
this.groupId = groupId;
}
@Override
public void getCurrentProfileName(@NonNull Consumer<ProfileName> profileNameConsumer) {
profileNameConsumer.accept(ProfileName.EMPTY);
}
@Override
public void getCurrentAvatar(@NonNull Consumer<byte[]> avatarConsumer) {
SimpleTask.run(() -> {
final RecipientId recipientId = getRecipientId();
if (AvatarHelper.hasAvatar(context, recipientId)) {
try {
return Util.readFully(AvatarHelper.getAvatar(context, recipientId));
} catch (IOException e) {
Log.w(TAG, e);
return null;
}
} else {
return null;
}
}, avatarConsumer::accept);
}
@Override
public void getCurrentDisplayName(@NonNull Consumer<String> displayNameConsumer) {
SimpleTask.run(() -> Recipient.resolved(getRecipientId()).getDisplayName(context), displayNameConsumer::accept);
}
@Override
public void uploadProfile(@NonNull ProfileName profileName,
@NonNull String displayName,
@Nullable byte[] avatar,
@NonNull Consumer<UploadResult> uploadResultConsumer)
{
SimpleTask.run(() -> {
try {
GroupManager.updateGroup(context, groupId, avatar, displayName);
return UploadResult.SUCCESS;
} catch (InvalidNumberException e) {
return UploadResult.ERROR_IO;
}
}, uploadResultConsumer::accept);
}
@Override
public void getCurrentUsername(@NonNull Consumer<Optional<String>> callback) {
callback.accept(Optional.absent());
}
@WorkerThread
private RecipientId getRecipientId() {
return DatabaseFactory.getRecipientDatabase(context).getByGroupId(groupId.toString())
.or(() -> {
throw new AssertionError("Recipient ID for Group ID does not exist.");
});
}
}

Wyświetl plik

@ -0,0 +1,147 @@
package org.thoughtcrime.securesms.profiles.edit;
import android.content.Context;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.core.util.Consumer;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileContentUpdateJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileKeyUpdateJob;
import org.thoughtcrime.securesms.jobs.ProfileUploadJob;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.profiles.SystemProfileUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.ProfileUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.ExecutionException;
class EditSelfProfileRepository implements EditProfileRepository {
private static final String TAG = Log.tag(EditSelfProfileRepository.class);
private final Context context;
private final boolean excludeSystem;
EditSelfProfileRepository(@NonNull Context context, boolean excludeSystem) {
this.context = context.getApplicationContext();
this.excludeSystem = excludeSystem;
}
@Override
public void getCurrentProfileName(@NonNull Consumer<ProfileName> profileNameConsumer) {
ProfileName storedProfileName = Recipient.self().getProfileName();
if (!storedProfileName.isEmpty()) {
profileNameConsumer.accept(storedProfileName);
} else if (!excludeSystem) {
SystemProfileUtil.getSystemProfileName(context).addListener(new ListenableFuture.Listener<String>() {
@Override
public void onSuccess(String result) {
if (!TextUtils.isEmpty(result)) {
profileNameConsumer.accept(ProfileName.fromSerialized(result));
} else {
profileNameConsumer.accept(storedProfileName);
}
}
@Override
public void onFailure(ExecutionException e) {
Log.w(TAG, e);
profileNameConsumer.accept(storedProfileName);
}
});
} else {
profileNameConsumer.accept(storedProfileName);
}
}
@Override
public void getCurrentAvatar(@NonNull Consumer<byte[]> avatarConsumer) {
RecipientId selfId = Recipient.self().getId();
if (AvatarHelper.hasAvatar(context, selfId)) {
SimpleTask.run(() -> {
try {
return Util.readFully(AvatarHelper.getAvatar(context, selfId));
} catch (IOException e) {
Log.w(TAG, e);
return null;
}
}, avatarConsumer::accept);
} else if (!excludeSystem) {
SystemProfileUtil.getSystemProfileAvatar(context, new ProfileMediaConstraints()).addListener(new ListenableFuture.Listener<byte[]>() {
@Override
public void onSuccess(byte[] result) {
avatarConsumer.accept(result);
}
@Override
public void onFailure(ExecutionException e) {
Log.w(TAG, e);
avatarConsumer.accept(null);
}
});
}
}
@Override
public void getCurrentDisplayName(@NonNull Consumer<String> displayNameConsumer) {
displayNameConsumer.accept("");
}
@Override
public void uploadProfile(@NonNull ProfileName profileName, @NonNull String displayName, @Nullable byte[] avatar, @NonNull Consumer<UploadResult> uploadResultConsumer) {
SimpleTask.run(() -> {
DatabaseFactory.getRecipientDatabase(context).setProfileName(Recipient.self().getId(), profileName);
try {
AvatarHelper.setAvatar(context, Recipient.self().getId(), avatar != null ? new ByteArrayInputStream(avatar) : null);
} catch (IOException e) {
return UploadResult.ERROR_IO;
}
ApplicationDependencies.getJobManager()
.startChain(new ProfileUploadJob())
.then(Arrays.asList(new MultiDeviceProfileKeyUpdateJob(), new MultiDeviceProfileContentUpdateJob()))
.enqueue();
return UploadResult.SUCCESS;
}, uploadResultConsumer::accept);
}
@Override
public void getCurrentUsername(@NonNull Consumer<Optional<String>> callback) {
callback.accept(Optional.fromNullable(TextSecurePreferences.getLocalUsername(context)));
SignalExecutors.UNBOUNDED.execute(() -> callback.accept(getUsernameInternal()));
}
@WorkerThread
private @NonNull Optional<String> getUsernameInternal() {
try {
SignalServiceProfile profile = ProfileUtil.retrieveProfile(context, Recipient.self(), SignalServiceProfile.RequestType.PROFILE).getProfile();
TextSecurePreferences.setLocalUsername(context, profile.getUsername());
DatabaseFactory.getRecipientDatabase(context).setUsername(Recipient.self().getId(), profile.getUsername());
} catch (IOException e) {
Log.w(TAG, "Failed to retrieve username remotely! Using locally-cached version.");
}
return Optional.fromNullable(TextSecurePreferences.getLocalUsername(context));
}
}

Wyświetl plik

@ -36,7 +36,7 @@ public final class RegistrationCompleteFragment extends BaseRegistrationFragment
activity.startActivity(new Intent(activity, PinRestoreActivity.class));
} else if (!isReregister()) {
final Intent main = new Intent(activity, MainActivity.class);
final Intent profile = EditProfileActivity.getIntent(activity, false);
final Intent profile = EditProfileActivity.getIntentForUserProfile(activity);
Intent kbs = CreateKbsPinActivity.getIntentForPinCreate(requireContext());
activity.startActivity(chainIntents(chainIntents(profile, kbs), main));

Wyświetl plik

@ -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="?icon_tint"
android:pathData="M21.561,4.561 L19.439,2.439a1.5,1.5 0,0 0,-2.121 0L3.823,15.934a1.5,1.5 0,0 0,-0.394 0.7L2.317,21.076a0.5,0.5 0,0 0,0.607 0.607l4.445,-1.112a1.5,1.5 0,0 0,0.7 -0.394l13.5,-13.495A1.5,1.5 0,0 0,21.561 4.561ZM7.005,19.116l-2.828,0.707L4.884,17l9.772,-9.773 2.122,2.122ZM17.838,8.283 L15.717,6.162 18.379,3.5 20.5,5.621Z"/>
</vector>

Wyświetl plik

@ -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="?icon_tint"
android:pathData="M21.561,6.682 L19.086,9.157 14.843,4.914l2.475,-2.475a1.5,1.5 0,0 1,2.121 0l2.122,2.122A1.5,1.5 0,0 1,21.561 6.682ZM3.429,16.631 L2.317,21.076a0.5,0.5 0,0 0,0.607 0.607l4.445,-1.112a1.5,1.5 0,0 0,0.7 -0.394l9.959,-9.959L13.782,5.975 3.823,15.934A1.5,1.5 0,0 0,3.429 16.631Z"/>
</vector>

Wyświetl plik

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="40"
android:viewportHeight="40">
<path
android:fillColor="?conversation_subtitle_color"
android:pathData="M29,16.75a6.508,6.508 0,0 1,6.5 6.5L35.5,24L37,24v-0.75a8,8 0,0 0,-6.7 -7.885,6.5 6.5,0 1,0 -8.6,0 7.941,7.941 0,0 0,-2.711 0.971A6.5,6.5 0,1 0,9.7 25.365,8 8,0 0,0 3,33.25L3,34L4.5,34v-0.75a6.508,6.508 0,0 1,6.5 -6.5h6a6.508,6.508 0,0 1,6.5 6.5L23.5,34L25,34v-0.75a8,8 0,0 0,-6.7 -7.885,6.468 6.468,0 0,0 1.508,-7.771A6.453,6.453 0,0 1,23 16.75ZM14,25.5a5,5 0,1 1,5 -5A5,5 0,0 1,14 25.5ZM21,10.5a5,5 0,1 1,5 5A5,5 0,0 1,21 10.5Z"/>
</vector>

Wyświetl plik

@ -0,0 +1,10 @@
<?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_edit"
android:icon="?attr/menu_edit_icon"
android:title="@string/ManageGroupActivity_edit_name_and_picture"
android:visible="false"
app:showAsAction="always" />
</menu>

Wyświetl plik

@ -194,6 +194,7 @@
<attr name="menu_reply_icon" format="reference" />
<attr name="menu_multi_select_icon" format="reference" />
<attr name="menu_archive_icon" format="reference" />
<attr name="menu_edit_icon" format="reference" />
<attr name="message_icon" format="reference" />
<attr name="notifications_icon" format="reference" />

Wyświetl plik

@ -143,6 +143,7 @@
<!-- ClearProfileActivity -->
<string name="ClearProfileActivity_remove">Remove</string>
<string name="ClearProfileActivity_remove_profile_photo">Remove profile photo?</string>
<string name="ClearProfileActivity_remove_group_photo">Remove group photo?</string>
<!-- CommunicationActions -->
<string name="CommunicationActions_no_browser_found">No web browser found.</string>
@ -497,6 +498,8 @@
<string name="ManageGroupActivity_you_dont_have_the_rights_to_do_this">You don\'t have the rights to do this</string>
<string name="ManageGroupActivity_failed_to_update_the_group">Failed to update the group</string>
<string name="ManageGroupActivity_edit_name_and_picture">Edit name and picture</string>
<string name="GroupManagement_choose_who_can_add_or_invite_new_members">Choose who can add or invite new members</string>
<string name="GroupManagement_choose_who_can_change_the_group_name_and_photo">Choose who can change the group name and photo</string>
@ -1515,6 +1518,10 @@
<string name="CreateProfileActivity__username">Username</string>
<string name="CreateProfileActivity__create_a_username">Create a username</string>
<!-- EditProfileFragment -->
<string name="EditProfileFragment__edit_group_name_and_photo">Edit group name and photo</string>
<string name="EditProfileFragment__group_name">Group name</string>
<!-- recipient_preferences_activity -->
<string name="recipient_preference_activity__shared_media">Shared media</string>

Wyświetl plik

@ -358,6 +358,7 @@
<item name="menu_reply_icon">@drawable/ic_reply_outline_24</item>
<item name="menu_multi_select_icon">@drawable/ic_select_24</item>
<item name="menu_archive_icon">@drawable/ic_archive_white_24dp</item>
<item name="menu_edit_icon">@drawable/ic_compose_outline_tinted_24</item>
<item name="message_icon">@drawable/ic_message_outline_tinted_bitmap_24</item>
<item name="notifications_icon">@drawable/ic_bell_outline_24</item>
@ -641,6 +642,7 @@
<item name="menu_reply_icon">@drawable/ic_reply_solid_24</item>
<item name="menu_multi_select_icon">@drawable/ic_select_24</item>
<item name="menu_archive_icon">@drawable/ic_archive_white_24dp</item>
<item name="menu_edit_icon">@drawable/ic_compose_solid_tinted_24</item>
<item name="message_icon">@drawable/ic_message_solid_tinted_bitmap_24</item>
<item name="notifications_icon">@drawable/ic_bell_solid_24</item>