diff --git a/app/src/main/java/org/thoughtcrime/securesms/ClearProfileAvatarActivity.java b/app/src/main/java/org/thoughtcrime/securesms/ClearProfileAvatarActivity.java index b62ffa2a21..ee783c08fb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ClearProfileAvatarActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ClearProfileAvatarActivity.java @@ -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(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/GroupCreateActivity.java b/app/src/main/java/org/thoughtcrime/securesms/GroupCreateActivity.java index a0f9ee8664..57a3f08c1c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/GroupCreateActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/GroupCreateActivity.java @@ -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()))); + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java index 9c37dd5449..4e1f8849cf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -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; diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java index 81a63f9cf7..5c90230e1a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java @@ -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 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 members, @@ -42,7 +60,7 @@ public final class GroupManager { { Set addresses = getMemberIds(members); - return V1GroupManager.updateGroup(context, groupId, addresses, avatar, name); + return V1GroupManager.updateGroup(context, groupId, addresses, BitmapUtil.toByteArray(avatar), name); } private static Set getMemberIds(Collection recipients) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/V1GroupManager.java b/app/src/main/java/org/thoughtcrime/securesms/groups/V1GroupManager.java index 50dd0c42f7..2c3551400c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/V1GroupManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/V1GroupManager.java @@ -87,13 +87,12 @@ final class V1GroupManager { static GroupActionResult updateGroup(@NonNull Context context, @NonNull GroupId groupId, @NonNull Set 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)); diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupFragment.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupFragment.java index 8f17562bd9..5722a8ca20 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupFragment.java @@ -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; diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionBottomSheetDialogFragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionBottomSheetDialogFragment.java index 7e3fedb6e5..18bcb9238b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionBottomSheetDialogFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionBottomSheetDialogFragment.java @@ -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 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); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreEntryFragment.java b/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreEntryFragment.java index c0f2c33b8c..25c9c66950 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreEntryFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreEntryFragment.java @@ -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); diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileActivity.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileActivity.java index 0388aba00b..d6326308d5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileActivity.java @@ -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; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileFragment.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileFragment.java index bfb51e0630..c8963aaed8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileFragment.java @@ -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.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() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileRepository.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileRepository.java index 26ee62ca08..ead215b7bd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileRepository.java @@ -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 profileNameConsumer); - private static final String TAG = Log.tag(EditProfileRepository.class); + void getCurrentAvatar(@NonNull Consumer avatarConsumer); - private final Context context; - private final boolean excludeSystem; + void getCurrentDisplayName(@NonNull Consumer 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 uploadResultConsumer); - void getCurrentProfileName(@NonNull Consumer profileNameConsumer) { - ProfileName storedProfileName = Recipient.self().getProfileName(); - if (!storedProfileName.isEmpty()) { - profileNameConsumer.accept(storedProfileName); - } else if (!excludeSystem) { - SystemProfileUtil.getSystemProfileName(context).addListener(new ListenableFuture.Listener() { - @Override - public void onSuccess(String result) { - if (!TextUtils.isEmpty(result)) { - profileNameConsumer.accept(ProfileName.fromSerialized(result)); - } else { - profileNameConsumer.accept(storedProfileName); - } - } + void getCurrentUsername(@NonNull Consumer> callback); - @Override - public void onFailure(ExecutionException e) { - Log.w(TAG, e); - profileNameConsumer.accept(storedProfileName); - } - }); - } else { - profileNameConsumer.accept(storedProfileName); - } - } - - void getCurrentAvatar(@NonNull Consumer 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() { - @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 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> callback) { - callback.accept(Optional.fromNullable(TextSecurePreferences.getLocalUsername(context))); - SignalExecutors.UNBOUNDED.execute(() -> callback.accept(getUsernameInternal())); - } - - @WorkerThread - private @NonNull Optional 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 } - } diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileViewModel.java index 1878853f73..e46d279161 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileViewModel.java @@ -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 internalAvatar = new MutableLiveData<>(); private final MutableLiveData> internalUsername = new MutableLiveData<>(); + private final LiveData 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 isFormValid() { + return Transformations.distinctUntilChanged(isFormValid); + } + public LiveData 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 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 create(@NonNull Class modelClass) { //noinspection unchecked - return (T) new EditProfileViewModel(repository, hasInstanceState); + return (T) new EditProfileViewModel(repository, hasInstanceState, groupId); } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditPushGroupProfileRepository.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditPushGroupProfileRepository.java new file mode 100644 index 0000000000..88bc36c37b --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditPushGroupProfileRepository.java @@ -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 profileNameConsumer) { + profileNameConsumer.accept(ProfileName.EMPTY); + } + + @Override + public void getCurrentAvatar(@NonNull Consumer 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 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 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> 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."); + }); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditSelfProfileRepository.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditSelfProfileRepository.java new file mode 100644 index 0000000000..9e504e1764 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditSelfProfileRepository.java @@ -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 profileNameConsumer) { + ProfileName storedProfileName = Recipient.self().getProfileName(); + if (!storedProfileName.isEmpty()) { + profileNameConsumer.accept(storedProfileName); + } else if (!excludeSystem) { + SystemProfileUtil.getSystemProfileName(context).addListener(new ListenableFuture.Listener() { + @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 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() { + @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 displayNameConsumer) { + displayNameConsumer.accept(""); + } + + @Override + public void uploadProfile(@NonNull ProfileName profileName, @NonNull String displayName, @Nullable byte[] avatar, @NonNull Consumer 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> callback) { + callback.accept(Optional.fromNullable(TextSecurePreferences.getLocalUsername(context))); + SignalExecutors.UNBOUNDED.execute(() -> callback.accept(getUsernameInternal())); + } + + @WorkerThread + private @NonNull Optional 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)); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationCompleteFragment.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationCompleteFragment.java index 1d42a2f2ea..d87ced1787 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationCompleteFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationCompleteFragment.java @@ -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)); diff --git a/app/src/main/res/drawable/ic_compose_outline_tinted_24.xml b/app/src/main/res/drawable/ic_compose_outline_tinted_24.xml new file mode 100644 index 0000000000..cc22585cb4 --- /dev/null +++ b/app/src/main/res/drawable/ic_compose_outline_tinted_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_compose_solid_tinted_24.xml b/app/src/main/res/drawable/ic_compose_solid_tinted_24.xml new file mode 100644 index 0000000000..49013ba351 --- /dev/null +++ b/app/src/main/res/drawable/ic_compose_solid_tinted_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_group_outline_40.xml b/app/src/main/res/drawable/ic_group_outline_40.xml new file mode 100644 index 0000000000..ea3278892c --- /dev/null +++ b/app/src/main/res/drawable/ic_group_outline_40.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/menu/manage_group_fragment.xml b/app/src/main/res/menu/manage_group_fragment.xml new file mode 100644 index 0000000000..0dcd8df502 --- /dev/null +++ b/app/src/main/res/menu/manage_group_fragment.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index ef731ba15a..7c309b4365 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -194,6 +194,7 @@ + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dc23d12435..15ed751bca 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -143,6 +143,7 @@ Remove Remove profile photo? + Remove group photo? No web browser found. @@ -497,6 +498,8 @@ You don\'t have the rights to do this Failed to update the group + Edit name and picture + Choose who can add or invite new members Choose who can change the group name and photo @@ -1515,6 +1518,10 @@ Username Create a username + + Edit group name and photo + Group name + Shared media diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 9001e07e34..063786c47a 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -358,6 +358,7 @@ @drawable/ic_reply_outline_24 @drawable/ic_select_24 @drawable/ic_archive_white_24dp + @drawable/ic_compose_outline_tinted_24 @drawable/ic_message_outline_tinted_bitmap_24 @drawable/ic_bell_outline_24 @@ -641,6 +642,7 @@ @drawable/ic_reply_solid_24 @drawable/ic_select_24 @drawable/ic_archive_white_24dp + @drawable/ic_compose_solid_tinted_24 @drawable/ic_message_solid_tinted_bitmap_24 @drawable/ic_bell_solid_24