mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-22 18:55:12 +00:00
Username UX refresh.
This commit is contained in:
committed by
Cody Henthorne
parent
3252871ed5
commit
28310a88f5
@@ -12,14 +12,12 @@ import android.view.View;
|
||||
import android.view.ViewAnimationUtils;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.navigation.Navigation;
|
||||
|
||||
@@ -28,11 +26,13 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
|
||||
import org.signal.core.util.EditTextUtil;
|
||||
import org.signal.core.util.StreamUtil;
|
||||
import org.signal.core.util.concurrent.SimpleTask;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.avatar.Avatars;
|
||||
import org.thoughtcrime.securesms.avatar.picker.AvatarPickerFragment;
|
||||
import org.thoughtcrime.securesms.databinding.ProfileCreateFragmentBinding;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.ParcelableGroupId;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
@@ -41,11 +41,8 @@ import org.thoughtcrime.securesms.profiles.manage.EditProfileNameFragment;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.signal.core.util.concurrent.SimpleTask;
|
||||
import org.thoughtcrime.securesms.util.navigation.SafeNavigation;
|
||||
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
|
||||
import org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton;
|
||||
import org.thoughtcrime.securesms.util.views.LearnMoreTextView;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@@ -62,20 +59,10 @@ public class EditProfileFragment extends LoggingFragment {
|
||||
private static final int MAX_DESCRIPTION_GLYPHS = 480;
|
||||
private static final int MAX_DESCRIPTION_BYTES = 8192;
|
||||
|
||||
private Toolbar toolbar;
|
||||
private View title;
|
||||
private ImageView avatar;
|
||||
private CircularProgressMaterialButton finishButton;
|
||||
private EditText givenName;
|
||||
private EditText familyName;
|
||||
private View reveal;
|
||||
private TextView preview;
|
||||
private ImageView avatarPreviewBackground;
|
||||
private ImageView avatarPreview;
|
||||
|
||||
private Intent nextIntent;
|
||||
|
||||
private EditProfileViewModel viewModel;
|
||||
private EditProfileViewModel viewModel;
|
||||
private ProfileCreateFragmentBinding binding;
|
||||
|
||||
private Controller controller;
|
||||
|
||||
@@ -92,7 +79,8 @@ public class EditProfileFragment extends LoggingFragment {
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.profile_create_fragment, container, false);
|
||||
binding = ProfileCreateFragmentBinding.inflate(inflater, container, false);
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -108,7 +96,7 @@ public class EditProfileFragment extends LoggingFragment {
|
||||
if (bundle.getBoolean(AvatarPickerFragment.SELECT_AVATAR_CLEAR)) {
|
||||
viewModel.setAvatarMedia(null);
|
||||
viewModel.setAvatar(null);
|
||||
avatar.setImageDrawable(null);
|
||||
binding.avatar.setImageDrawable(null);
|
||||
} else {
|
||||
Media media = bundle.getParcelable(AvatarPickerFragment.SELECT_AVATAR_MEDIA);
|
||||
handleMediaFromResult(media);
|
||||
@@ -116,6 +104,12 @@ public class EditProfileFragment extends LoggingFragment {
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
|
||||
private void handleMediaFromResult(@NonNull Media media) {
|
||||
SimpleTask.run(() -> {
|
||||
try {
|
||||
@@ -136,7 +130,7 @@ public class EditProfileFragment extends LoggingFragment {
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.circleCrop()
|
||||
.into(avatar);
|
||||
.into(binding.avatar);
|
||||
} else {
|
||||
Toast.makeText(requireActivity(), R.string.CreateProfileActivity_error_setting_profile_photo, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
@@ -162,111 +156,108 @@ public class EditProfileFragment extends LoggingFragment {
|
||||
Bundle arguments = requireArguments();
|
||||
boolean isEditingGroup = groupId != null;
|
||||
|
||||
this.toolbar = view.findViewById(R.id.toolbar);
|
||||
this.title = view.findViewById(R.id.title);
|
||||
this.avatar = view.findViewById(R.id.avatar);
|
||||
this.givenName = view.findViewById(R.id.given_name);
|
||||
this.familyName = view.findViewById(R.id.family_name);
|
||||
this.finishButton = view.findViewById(R.id.finish_button);
|
||||
this.reveal = view.findViewById(R.id.reveal);
|
||||
this.preview = view.findViewById(R.id.name_preview);
|
||||
this.avatarPreviewBackground = view.findViewById(R.id.avatar_background);
|
||||
this.avatarPreview = view.findViewById(R.id.avatar_placeholder);
|
||||
this.nextIntent = arguments.getParcelable(NEXT_INTENT);
|
||||
this.nextIntent = arguments.getParcelable(NEXT_INTENT);
|
||||
|
||||
this.avatar.setOnClickListener(v -> startAvatarSelection());
|
||||
|
||||
view.findViewById(R.id.mms_group_hint)
|
||||
.setVisibility(isEditingGroup && groupId.isMms() ? View.VISIBLE : View.GONE);
|
||||
binding.avatar.setOnClickListener(v -> startAvatarSelection());
|
||||
binding.mmsGroupHint.setVisibility(isEditingGroup && groupId.isMms() ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (isEditingGroup) {
|
||||
EditTextUtil.addGraphemeClusterLimitFilter(givenName, FeatureFlags.getMaxGroupNameGraphemeLength());
|
||||
givenName.addTextChangedListener(new AfterTextChanged(s -> viewModel.setGivenName(s.toString())));
|
||||
givenName.setHint(R.string.EditProfileFragment__group_name);
|
||||
givenName.requestFocus();
|
||||
toolbar.setTitle(R.string.EditProfileFragment__edit_group);
|
||||
preview.setVisibility(View.GONE);
|
||||
EditTextUtil.addGraphemeClusterLimitFilter(binding.givenName, FeatureFlags.getMaxGroupNameGraphemeLength());
|
||||
binding.profileDescriptionText.setVisibility(View.GONE);
|
||||
binding.whoCanFindMeContainer.setVisibility(View.GONE);
|
||||
binding.givenName.addTextChangedListener(new AfterTextChanged(s -> viewModel.setGivenName(s.toString())));
|
||||
binding.givenNameWrapper.setHint(R.string.EditProfileFragment__group_name);
|
||||
binding.givenName.requestFocus();
|
||||
binding.toolbar.setTitle(R.string.EditProfileFragment__edit_group);
|
||||
binding.namePreview.setVisibility(View.GONE);
|
||||
|
||||
if (groupId.isV2()) {
|
||||
EditTextUtil.addGraphemeClusterLimitFilter(familyName, MAX_DESCRIPTION_GLYPHS);
|
||||
familyName.addTextChangedListener(new AfterTextChanged(s -> {
|
||||
EditTextUtil.addGraphemeClusterLimitFilter(binding.familyName, MAX_DESCRIPTION_GLYPHS);
|
||||
binding.familyName.addTextChangedListener(new AfterTextChanged(s -> {
|
||||
EditProfileNameFragment.trimFieldToMaxByteLength(s, MAX_DESCRIPTION_BYTES);
|
||||
viewModel.setFamilyName(s.toString());
|
||||
}));
|
||||
familyName.setHint(R.string.EditProfileFragment__group_description);
|
||||
familyName.setSingleLine(false);
|
||||
familyName.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES);
|
||||
binding.familyNameWrapper.setHint(R.string.EditProfileFragment__group_description);
|
||||
binding.familyName.setSingleLine(false);
|
||||
binding.familyName.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES);
|
||||
|
||||
LearnMoreTextView descriptionText = view.findViewById(R.id.description_text);
|
||||
descriptionText.setLearnMoreVisible(false);
|
||||
descriptionText.setText(R.string.CreateProfileActivity_group_descriptions_will_be_visible_to_members_of_this_group_and_people_who_have_been_invited);
|
||||
binding.groupDescriptionText.setLearnMoreVisible(false);
|
||||
binding.groupDescriptionText.setText(R.string.CreateProfileActivity_group_descriptions_will_be_visible_to_members_of_this_group_and_people_who_have_been_invited);
|
||||
} else {
|
||||
familyName.setVisibility(View.GONE);
|
||||
familyName.setEnabled(false);
|
||||
view.findViewById(R.id.description_text).setVisibility(View.GONE);
|
||||
binding.familyNameWrapper.setVisibility(View.GONE);
|
||||
binding.familyName.setEnabled(false);
|
||||
binding.groupDescriptionText.setVisibility(View.GONE);
|
||||
}
|
||||
view.<ImageView>findViewById(R.id.avatar_placeholder).setImageResource(R.drawable.ic_group_outline_40);
|
||||
binding.avatarPlaceholder.setImageResource(R.drawable.ic_group_outline_40);
|
||||
} else {
|
||||
EditTextUtil.addGraphemeClusterLimitFilter(givenName, EditProfileNameFragment.NAME_MAX_GLYPHS);
|
||||
EditTextUtil.addGraphemeClusterLimitFilter(familyName, EditProfileNameFragment.NAME_MAX_GLYPHS);
|
||||
this.givenName.addTextChangedListener(new AfterTextChanged(s -> {
|
||||
EditTextUtil.addGraphemeClusterLimitFilter(binding.givenName, EditProfileNameFragment.NAME_MAX_GLYPHS);
|
||||
EditTextUtil.addGraphemeClusterLimitFilter(binding.familyName, EditProfileNameFragment.NAME_MAX_GLYPHS);
|
||||
binding.givenName.addTextChangedListener(new AfterTextChanged(s -> {
|
||||
EditProfileNameFragment.trimFieldToMaxByteLength(s);
|
||||
viewModel.setGivenName(s.toString());
|
||||
}));
|
||||
this.familyName.addTextChangedListener(new AfterTextChanged(s -> {
|
||||
binding.familyName.addTextChangedListener(new AfterTextChanged(s -> {
|
||||
EditProfileNameFragment.trimFieldToMaxByteLength(s);
|
||||
viewModel.setFamilyName(s.toString());
|
||||
}));
|
||||
LearnMoreTextView descriptionText = view.findViewById(R.id.description_text);
|
||||
descriptionText.setLearnMoreVisible(true);
|
||||
descriptionText.setOnLinkClickListener(v -> CommunicationActions.openBrowserLink(requireContext(), getString(R.string.EditProfileFragment__support_link)));
|
||||
binding.groupDescriptionText.setVisibility(View.GONE);
|
||||
binding.profileDescriptionText.setLearnMoreVisible(true);
|
||||
binding.profileDescriptionText.setLinkColor(ContextCompat.getColor(requireContext(), R.color.signal_colorPrimary));
|
||||
binding.profileDescriptionText.setOnLinkClickListener(v -> CommunicationActions.openBrowserLink(requireContext(), getString(R.string.EditProfileFragment__support_link)));
|
||||
|
||||
if (FeatureFlags.phoneNumberPrivacy()) {
|
||||
binding.whoCanFindMeContainer.setVisibility(View.VISIBLE);
|
||||
binding.whoCanFindMeContainer.setOnClickListener(v -> SafeNavigation.safeNavigate(Navigation.findNavController(v), EditProfileFragmentDirections.actionCreateProfileFragmentToPhoneNumberPrivacy()));
|
||||
// TODO [alex] -- Where does this value come from?
|
||||
binding.whoCanFindMeDescription.setText(R.string.PhoneNumberPrivacy_everyone);
|
||||
}
|
||||
}
|
||||
|
||||
this.finishButton.setOnClickListener(v -> {
|
||||
this.finishButton.setSpinning();
|
||||
binding.finishButton.setOnClickListener(v -> {
|
||||
binding.finishButton.setSpinning();
|
||||
handleUpload();
|
||||
});
|
||||
|
||||
this.finishButton.setText(arguments.getInt(NEXT_BUTTON_TEXT, R.string.CreateProfileActivity_next));
|
||||
binding.finishButton.setText(arguments.getInt(NEXT_BUTTON_TEXT, R.string.CreateProfileActivity_next));
|
||||
|
||||
if (arguments.getBoolean(SHOW_TOOLBAR, true)) {
|
||||
this.toolbar.setVisibility(View.VISIBLE);
|
||||
this.toolbar.setNavigationOnClickListener(v -> requireActivity().finish());
|
||||
this.title.setVisibility(View.GONE);
|
||||
binding.toolbar.setVisibility(View.VISIBLE);
|
||||
binding.toolbar.setNavigationOnClickListener(v -> requireActivity().finish());
|
||||
binding.title.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeProfileName() {
|
||||
viewModel.isFormValid().observe(getViewLifecycleOwner(), isValid -> {
|
||||
finishButton.setEnabled(isValid);
|
||||
finishButton.setAlpha(isValid ? 1f : 0.5f);
|
||||
binding.finishButton.setEnabled(isValid);
|
||||
binding.finishButton.setAlpha(isValid ? 1f : 0.5f);
|
||||
});
|
||||
|
||||
viewModel.givenName().observe(getViewLifecycleOwner(), givenName -> updateFieldIfNeeded(this.givenName, givenName));
|
||||
viewModel.givenName().observe(getViewLifecycleOwner(), givenName -> updateFieldIfNeeded(binding.givenName, givenName));
|
||||
|
||||
viewModel.familyName().observe(getViewLifecycleOwner(), familyName -> updateFieldIfNeeded(this.familyName, familyName));
|
||||
viewModel.familyName().observe(getViewLifecycleOwner(), familyName -> updateFieldIfNeeded(binding.familyName, familyName));
|
||||
|
||||
viewModel.profileName().observe(getViewLifecycleOwner(), profileName -> preview.setText(profileName.toString()));
|
||||
viewModel.profileName().observe(getViewLifecycleOwner(), profileName -> binding.namePreview.setText(profileName.toString()));
|
||||
}
|
||||
|
||||
private void initializeProfileAvatar() {
|
||||
viewModel.avatar().observe(getViewLifecycleOwner(), bytes -> {
|
||||
if (bytes == null) {
|
||||
GlideApp.with(this).clear(avatar);
|
||||
GlideApp.with(this).clear(binding.avatar);
|
||||
return;
|
||||
}
|
||||
|
||||
GlideApp.with(this)
|
||||
.load(bytes)
|
||||
.circleCrop()
|
||||
.into(avatar);
|
||||
.into(binding.avatar);
|
||||
});
|
||||
|
||||
viewModel.avatarColor().observe(getViewLifecycleOwner(), avatarColor -> {
|
||||
Avatars.ForegroundColor foregroundColor = Avatars.getForegroundColor(avatarColor);
|
||||
|
||||
avatarPreview.getDrawable().setColorFilter(new SimpleColorFilter(foregroundColor.getColorInt()));
|
||||
avatarPreviewBackground.getDrawable().setColorFilter(new SimpleColorFilter(avatarColor.colorInt()));
|
||||
binding.avatarPlaceholder.getDrawable().setColorFilter(new SimpleColorFilter(foregroundColor.getColorInt()));
|
||||
binding.avatarBackground.getDrawable().setColorFilter(new SimpleColorFilter(avatarColor.colorInt()));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -312,7 +303,7 @@ public class EditProfileFragment extends LoggingFragment {
|
||||
}
|
||||
|
||||
private void handleFinishedLegacy() {
|
||||
finishButton.cancelSpinning();
|
||||
binding.finishButton.cancelSpinning();
|
||||
if (nextIntent != null) startActivity(nextIntent);
|
||||
|
||||
controller.onProfileNameUploadCompleted();
|
||||
@@ -323,16 +314,16 @@ public class EditProfileFragment extends LoggingFragment {
|
||||
int[] finishButtonLocation = new int[2];
|
||||
int[] revealLocation = new int[2];
|
||||
|
||||
finishButton.getLocationInWindow(finishButtonLocation);
|
||||
reveal.getLocationInWindow(revealLocation);
|
||||
binding.finishButton.getLocationInWindow(finishButtonLocation);
|
||||
binding.reveal.getLocationInWindow(revealLocation);
|
||||
|
||||
int finishX = finishButtonLocation[0] - revealLocation[0];
|
||||
int finishY = finishButtonLocation[1] - revealLocation[1];
|
||||
|
||||
finishX += finishButton.getWidth() / 2;
|
||||
finishY += finishButton.getHeight() / 2;
|
||||
finishX += binding.finishButton.getWidth() / 2;
|
||||
finishY += binding.finishButton.getHeight() / 2;
|
||||
|
||||
Animator animation = ViewAnimationUtils.createCircularReveal(reveal, finishX, finishY, 0f, (float) Math.max(reveal.getWidth(), reveal.getHeight()));
|
||||
Animator animation = ViewAnimationUtils.createCircularReveal(binding.reveal, finishX, finishY, 0f, (float) Math.max(binding.reveal.getWidth(), binding.reveal.getHeight()));
|
||||
animation.setDuration(500);
|
||||
animation.addListener(new Animator.AnimatorListener() {
|
||||
@Override
|
||||
@@ -340,7 +331,7 @@ public class EditProfileFragment extends LoggingFragment {
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
finishButton.cancelSpinning();
|
||||
binding.finishButton.cancelSpinning();
|
||||
if (nextIntent != null && getActivity() != null) {
|
||||
startActivity(nextIntent);
|
||||
}
|
||||
@@ -355,7 +346,7 @@ public class EditProfileFragment extends LoggingFragment {
|
||||
public void onAnimationRepeat(Animator animation) {}
|
||||
});
|
||||
|
||||
reveal.setVisibility(View.VISIBLE);
|
||||
binding.reveal.setVisibility(View.VISIBLE);
|
||||
animation.start();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.thoughtcrime.securesms.profiles.edit.pnp
|
||||
|
||||
import androidx.fragment.app.viewModels
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
|
||||
/**
|
||||
* Allows the user to select who can see their phone number during registration.
|
||||
*/
|
||||
class WhoCanSeeMyPhoneNumberFragment : DSLSettingsFragment(titleId = R.string.WhoCanSeeMyPhoneNumberFragment__who_can_find_me_by_number) {
|
||||
|
||||
private val viewModel: WhoCanSeeMyPhoneNumberViewModel by viewModels()
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
require(FeatureFlags.phoneNumberPrivacy())
|
||||
|
||||
lifecycleDisposable += viewModel.state.subscribe {
|
||||
adapter.submitList(getConfiguration(it).toMappingModelList())
|
||||
}
|
||||
}
|
||||
|
||||
private fun getConfiguration(state: WhoCanSeeMyPhoneNumberState): DSLConfiguration {
|
||||
return configure {
|
||||
radioPref(
|
||||
title = DSLSettingsText.from(R.string.PhoneNumberPrivacy_everyone),
|
||||
summary = DSLSettingsText.from(R.string.WhoCanSeeMyPhoneNumberFragment__anyone_who_has),
|
||||
isChecked = state == WhoCanSeeMyPhoneNumberState.EVERYONE,
|
||||
onClick = { viewModel.onEveryoneCanSeeMyPhoneNumberSelected() }
|
||||
)
|
||||
|
||||
radioPref(
|
||||
title = DSLSettingsText.from(R.string.PhoneNumberPrivacy_nobody),
|
||||
summary = DSLSettingsText.from(R.string.WhoCanSeeMyPhoneNumberFragment__nobody_on_signal),
|
||||
isChecked = state == WhoCanSeeMyPhoneNumberState.NOBODY,
|
||||
onClick = { viewModel.onNobodyCanSeeMyPhoneNumberSelected() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package org.thoughtcrime.securesms.profiles.edit.pnp
|
||||
|
||||
enum class WhoCanSeeMyPhoneNumberState {
|
||||
EVERYONE,
|
||||
NOBODY
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.thoughtcrime.securesms.profiles.edit.pnp
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
|
||||
class WhoCanSeeMyPhoneNumberViewModel : ViewModel() {
|
||||
|
||||
private val store = RxStore(WhoCanSeeMyPhoneNumberState.EVERYONE)
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
val state: Flowable<WhoCanSeeMyPhoneNumberState> = store.stateFlowable.subscribeOn(AndroidSchedulers.mainThread())
|
||||
|
||||
fun onEveryoneCanSeeMyPhoneNumberSelected() {
|
||||
store.update { WhoCanSeeMyPhoneNumberState.EVERYONE }
|
||||
}
|
||||
|
||||
fun onNobodyCanSeeMyPhoneNumberSelected() {
|
||||
store.update { WhoCanSeeMyPhoneNumberState.NOBODY }
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.clear()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.thoughtcrime.securesms.profiles.manage
|
||||
|
||||
import org.thoughtcrime.securesms.databinding.CopyButtonBinding
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.BindingFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.BindingViewHolder
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
|
||||
/**
|
||||
* Outlined button that allows the user to copy a piece of data.
|
||||
*/
|
||||
object CopyButton {
|
||||
fun register(mappingAdapter: MappingAdapter) {
|
||||
mappingAdapter.registerFactory(Model::class.java, BindingFactory(::ViewHolder, CopyButtonBinding::inflate))
|
||||
}
|
||||
|
||||
class Model(
|
||||
val text: CharSequence,
|
||||
val onClick: (Model) -> Unit
|
||||
) : MappingModel<Model> {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean = true
|
||||
|
||||
override fun areContentsTheSame(newItem: Model): Boolean = text == newItem.text
|
||||
}
|
||||
|
||||
private class ViewHolder(binding: CopyButtonBinding) : BindingViewHolder<Model, CopyButtonBinding>(binding) {
|
||||
override fun bind(model: Model) {
|
||||
binding.root.text = model.text
|
||||
binding.root.setOnClickListener { model.onClick(model) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,33 +7,31 @@ import android.util.TypedValue;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.content.res.ResourcesCompat;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.Navigation;
|
||||
|
||||
import com.airbnb.lottie.SimpleColorFilter;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.AvatarPreviewActivity;
|
||||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.avatar.Avatars;
|
||||
import org.thoughtcrime.securesms.avatar.picker.AvatarPickerFragment;
|
||||
import org.thoughtcrime.securesms.badges.BadgeImageView;
|
||||
import org.thoughtcrime.securesms.badges.models.Badge;
|
||||
import org.thoughtcrime.securesms.badges.self.none.BecomeASustainerFragment;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiUtil;
|
||||
import org.thoughtcrime.securesms.databinding.ManageProfileFragmentBinding;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
import org.thoughtcrime.securesms.profiles.manage.ManageProfileViewModel.AvatarState;
|
||||
@@ -49,64 +47,42 @@ import java.util.Optional;
|
||||
|
||||
public class ManageProfileFragment extends LoggingFragment {
|
||||
|
||||
private static final String TAG = Log.tag(ManageProfileFragment.class);
|
||||
|
||||
private Toolbar toolbar;
|
||||
private ImageView avatarView;
|
||||
private ImageView avatarPlaceholderView;
|
||||
private TextView profileNameView;
|
||||
private View profileNameContainer;
|
||||
private TextView usernameView;
|
||||
private View usernameContainer;
|
||||
private TextView aboutView;
|
||||
private View aboutContainer;
|
||||
private ImageView aboutEmojiView;
|
||||
private AlertDialog avatarProgress;
|
||||
private TextView avatarInitials;
|
||||
private ImageView avatarBackground;
|
||||
private View badgesContainer;
|
||||
private BadgeImageView badgeView;
|
||||
|
||||
private ManageProfileViewModel viewModel;
|
||||
private AlertDialog avatarProgress;
|
||||
private ManageProfileViewModel viewModel;
|
||||
private ManageProfileFragmentBinding binding;
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.manage_profile_fragment, container, false);
|
||||
binding = ManageProfileFragmentBinding.inflate(inflater, container, false);
|
||||
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
this.toolbar = view.findViewById(R.id.toolbar);
|
||||
this.avatarView = view.findViewById(R.id.manage_profile_avatar);
|
||||
this.avatarPlaceholderView = view.findViewById(R.id.manage_profile_avatar_placeholder);
|
||||
this.profileNameView = view.findViewById(R.id.manage_profile_name);
|
||||
this.profileNameContainer = view.findViewById(R.id.manage_profile_name_container);
|
||||
this.usernameView = view.findViewById(R.id.manage_profile_username);
|
||||
this.usernameContainer = view.findViewById(R.id.manage_profile_username_container);
|
||||
this.aboutView = view.findViewById(R.id.manage_profile_about);
|
||||
this.aboutContainer = view.findViewById(R.id.manage_profile_about_container);
|
||||
this.aboutEmojiView = view.findViewById(R.id.manage_profile_about_icon);
|
||||
this.avatarInitials = view.findViewById(R.id.manage_profile_avatar_initials);
|
||||
this.avatarBackground = view.findViewById(R.id.manage_profile_avatar_background);
|
||||
this.badgesContainer = view.findViewById(R.id.manage_profile_badges_container);
|
||||
this.badgeView = view.findViewById(R.id.manage_profile_badge);
|
||||
new UsernameEditFragment.ResultContract().registerForResult(getParentFragmentManager(), getViewLifecycleOwner(), isUsernameCreated -> {
|
||||
Snackbar.make(view, R.string.ManageProfileFragment__username_created, Snackbar.LENGTH_SHORT).show();
|
||||
});
|
||||
|
||||
UsernameShareBottomSheet.ResultContract.INSTANCE.registerForResult(getParentFragmentManager(), getViewLifecycleOwner(), isCopiedToClipboard -> {
|
||||
Snackbar.make(view, R.string.ManageProfileFragment__username_copied, Snackbar.LENGTH_SHORT).show();
|
||||
});
|
||||
|
||||
initializeViewModel();
|
||||
|
||||
this.toolbar.setNavigationOnClickListener(v -> requireActivity().finish());
|
||||
binding.toolbar.setNavigationOnClickListener(v -> requireActivity().finish());
|
||||
|
||||
View editAvatar = view.findViewById(R.id.manage_profile_edit_photo);
|
||||
editAvatar.setOnClickListener(v -> onEditAvatarClicked());
|
||||
binding.manageProfileEditPhoto.setOnClickListener(v -> onEditAvatarClicked());
|
||||
|
||||
this.profileNameContainer.setOnClickListener(v -> {
|
||||
binding.manageProfileNameContainer.setOnClickListener(v -> {
|
||||
SafeNavigation.safeNavigate(Navigation.findNavController(v), ManageProfileFragmentDirections.actionManageProfileName());
|
||||
});
|
||||
|
||||
this.usernameContainer.setOnClickListener(v -> {
|
||||
binding.manageProfileUsernameContainer.setOnClickListener(v -> {
|
||||
SafeNavigation.safeNavigate(Navigation.findNavController(v), ManageProfileFragmentDirections.actionManageUsername());
|
||||
});
|
||||
|
||||
this.aboutContainer.setOnClickListener(v -> {
|
||||
binding.manageProfileAboutContainer.setOnClickListener(v -> {
|
||||
SafeNavigation.safeNavigate(Navigation.findNavController(v), ManageProfileFragmentDirections.actionManageAbout());
|
||||
});
|
||||
|
||||
@@ -119,6 +95,7 @@ public class ManageProfileFragment extends LoggingFragment {
|
||||
}
|
||||
});
|
||||
|
||||
EmojiTextView avatarInitials = binding.manageProfileAvatarInitials;
|
||||
avatarInitials.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
|
||||
if (avatarInitials.length() > 0) {
|
||||
updateInitials(avatarInitials.getText().toString());
|
||||
@@ -126,7 +103,7 @@ public class ManageProfileFragment extends LoggingFragment {
|
||||
});
|
||||
|
||||
if (FeatureFlags.donorBadges()) {
|
||||
badgesContainer.setOnClickListener(v -> {
|
||||
binding.manageProfileBadgesContainer.setOnClickListener(v -> {
|
||||
if (Recipient.self().getBadges().isEmpty()) {
|
||||
BecomeASustainerFragment.show(getParentFragmentManager());
|
||||
} else {
|
||||
@@ -134,17 +111,27 @@ public class ManageProfileFragment extends LoggingFragment {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
badgesContainer.setVisibility(View.GONE);
|
||||
binding.manageProfileBadgesContainer.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
avatarView.setOnClickListener(v -> {
|
||||
binding.manageProfileAvatar.setOnClickListener(v -> {
|
||||
startActivity(AvatarPreviewActivity.intentFromRecipientId(requireContext(), Recipient.self().getId()),
|
||||
AvatarPreviewActivity.createTransitionBundle(requireActivity(), avatarView));
|
||||
AvatarPreviewActivity.createTransitionBundle(requireActivity(), binding.manageProfileAvatar));
|
||||
});
|
||||
|
||||
binding.manageProfileUsernameShare.setOnClickListener(v -> {
|
||||
SafeNavigation.safeNavigate(Navigation.findNavController(v), ManageProfileFragmentDirections.actionManageProfileFragmentToShareUsernameDialog());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
|
||||
private void initializeViewModel() {
|
||||
viewModel = ViewModelProviders.of(this, new ManageProfileViewModel.Factory()).get(ManageProfileViewModel.class);
|
||||
viewModel = new ViewModelProvider(this, new ManageProfileViewModel.Factory()).get(ManageProfileViewModel.class);
|
||||
|
||||
LiveData<Optional<byte[]>> avatarImage = Transformations.map(LiveDataUtil.distinctUntilChanged(viewModel.getAvatar(), (b1, b2) -> Arrays.equals(b1.getAvatar(), b2.getAvatar())),
|
||||
b -> Optional.ofNullable(b.getAvatar()));
|
||||
@@ -160,7 +147,7 @@ public class ManageProfileFragment extends LoggingFragment {
|
||||
if (viewModel.shouldShowUsername()) {
|
||||
viewModel.getUsername().observe(getViewLifecycleOwner(), this::presentUsername);
|
||||
} else {
|
||||
usernameContainer.setVisibility(View.GONE);
|
||||
binding.manageProfileUsernameContainer.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,9 +156,9 @@ public class ManageProfileFragment extends LoggingFragment {
|
||||
Glide.with(this)
|
||||
.load(avatarData.get())
|
||||
.circleCrop()
|
||||
.into(avatarView);
|
||||
.into(binding.manageProfileAvatar);
|
||||
} else {
|
||||
Glide.with(this).load((Drawable) null).into(avatarView);
|
||||
Glide.with(this).load((Drawable) null).into(binding.manageProfileAvatar);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,21 +167,21 @@ public class ManageProfileFragment extends LoggingFragment {
|
||||
CharSequence initials = NameUtil.getAbbreviation(avatarState.getSelf().getDisplayName(requireContext()));
|
||||
Avatars.ForegroundColor foregroundColor = Avatars.getForegroundColor(avatarState.getSelf().getAvatarColor());
|
||||
|
||||
avatarBackground.setColorFilter(new SimpleColorFilter(avatarState.getSelf().getAvatarColor().colorInt()));
|
||||
avatarPlaceholderView.setColorFilter(new SimpleColorFilter(foregroundColor.getColorInt()));
|
||||
avatarInitials.setTextColor(foregroundColor.getColorInt());
|
||||
binding.manageProfileAvatarBackground.setColorFilter(new SimpleColorFilter(avatarState.getSelf().getAvatarColor().colorInt()));
|
||||
binding.manageProfileAvatarPlaceholder.setColorFilter(new SimpleColorFilter(foregroundColor.getColorInt()));
|
||||
binding.manageProfileAvatarInitials.setTextColor(foregroundColor.getColorInt());
|
||||
|
||||
if (TextUtils.isEmpty(initials)) {
|
||||
avatarPlaceholderView.setVisibility(View.VISIBLE);
|
||||
avatarInitials.setVisibility(View.GONE);
|
||||
binding.manageProfileAvatarPlaceholder.setVisibility(View.VISIBLE);
|
||||
binding.manageProfileAvatarInitials.setVisibility(View.GONE);
|
||||
} else {
|
||||
updateInitials(initials.toString());
|
||||
avatarPlaceholderView.setVisibility(View.GONE);
|
||||
avatarInitials.setVisibility(View.VISIBLE);
|
||||
binding.manageProfileAvatarPlaceholder.setVisibility(View.GONE);
|
||||
binding.manageProfileAvatarInitials.setVisibility(View.VISIBLE);
|
||||
}
|
||||
} else {
|
||||
avatarPlaceholderView.setVisibility(View.GONE);
|
||||
avatarInitials.setVisibility(View.GONE);
|
||||
binding.manageProfileAvatarPlaceholder.setVisibility(View.GONE);
|
||||
binding.manageProfileAvatarInitials.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (avatarProgress == null && avatarState.getLoadingState() == ManageProfileViewModel.LoadingState.LOADING) {
|
||||
@@ -205,53 +192,59 @@ public class ManageProfileFragment extends LoggingFragment {
|
||||
}
|
||||
|
||||
private void updateInitials(String initials) {
|
||||
avatarInitials.setTextSize(TypedValue.COMPLEX_UNIT_PX, Avatars.getTextSizeForLength(requireContext(), initials, avatarInitials.getMeasuredWidth() * 0.8f, avatarInitials.getMeasuredWidth() * 0.45f));
|
||||
avatarInitials.setText(initials);
|
||||
binding.manageProfileAvatarInitials.setTextSize(TypedValue.COMPLEX_UNIT_PX,
|
||||
Avatars.getTextSizeForLength(requireContext(),
|
||||
initials,
|
||||
binding.manageProfileAvatarInitials.getMeasuredWidth() * 0.8f,
|
||||
binding.manageProfileAvatarInitials.getMeasuredWidth() * 0.45f));
|
||||
binding.manageProfileAvatarInitials.setText(initials);
|
||||
}
|
||||
|
||||
private void presentProfileName(@Nullable ProfileName profileName) {
|
||||
if (profileName == null || profileName.isEmpty()) {
|
||||
profileNameView.setText(R.string.ManageProfileFragment_profile_name);
|
||||
binding.manageProfileName.setText(R.string.ManageProfileFragment_profile_name);
|
||||
} else {
|
||||
profileNameView.setText(profileName.toString());
|
||||
binding.manageProfileName.setText(profileName.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private void presentUsername(@Nullable String username) {
|
||||
if (username == null || username.isEmpty()) {
|
||||
usernameView.setText(R.string.ManageProfileFragment_username);
|
||||
binding.manageProfileUsername.setText(R.string.ManageProfileFragment_username);
|
||||
binding.manageProfileUsernameShare.setVisibility(View.GONE);
|
||||
} else {
|
||||
usernameView.setText(username);
|
||||
binding.manageProfileUsername.setText(username);
|
||||
binding.manageProfileUsernameShare.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void presentAbout(@Nullable String about) {
|
||||
if (about == null || about.isEmpty()) {
|
||||
aboutView.setText(R.string.ManageProfileFragment_about);
|
||||
binding.manageProfileAbout.setText(R.string.ManageProfileFragment_about);
|
||||
} else {
|
||||
aboutView.setText(about);
|
||||
binding.manageProfileAbout.setText(about);
|
||||
}
|
||||
}
|
||||
|
||||
private void presentAboutEmoji(@NonNull String aboutEmoji) {
|
||||
if (aboutEmoji == null || aboutEmoji.isEmpty()) {
|
||||
aboutEmojiView.setImageDrawable(ResourcesCompat.getDrawable(getResources(), R.drawable.ic_compose_24, null));
|
||||
binding.manageProfileAboutIcon.setImageDrawable(ResourcesCompat.getDrawable(getResources(), R.drawable.ic_compose_24, null));
|
||||
} else {
|
||||
Drawable emoji = EmojiUtil.convertToDrawable(requireContext(), aboutEmoji);
|
||||
|
||||
if (emoji != null) {
|
||||
aboutEmojiView.setImageDrawable(emoji);
|
||||
binding.manageProfileAboutIcon.setImageDrawable(emoji);
|
||||
} else {
|
||||
aboutEmojiView.setImageDrawable(ResourcesCompat.getDrawable(getResources(), R.drawable.ic_compose_24, null));
|
||||
binding.manageProfileAboutIcon.setImageDrawable(ResourcesCompat.getDrawable(getResources(), R.drawable.ic_compose_24, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void presentBadge(@NonNull Optional<Badge> badge) {
|
||||
if (badge.isPresent() && badge.get().getVisible() && !badge.get().isExpired()) {
|
||||
badgeView.setBadge(badge.orElse(null));
|
||||
binding.manageProfileBadge.setBadge(badge.orElse(null));
|
||||
} else {
|
||||
badgeView.setBadge(null);
|
||||
binding.manageProfileBadge.setBadge(null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.thoughtcrime.securesms.profiles.manage
|
||||
|
||||
import org.thoughtcrime.securesms.databinding.ShareButtonBinding
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.BindingFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.BindingViewHolder
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
|
||||
object ShareButton {
|
||||
fun register(adapter: MappingAdapter) {
|
||||
adapter.registerFactory(Model::class.java, BindingFactory(::ViewHolder, ShareButtonBinding::inflate))
|
||||
}
|
||||
|
||||
class Model(
|
||||
val text: CharSequence,
|
||||
val onClick: (Model) -> Unit
|
||||
) : MappingModel<Model> {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean = true
|
||||
|
||||
override fun areContentsTheSame(newItem: Model): Boolean = text == newItem.text
|
||||
}
|
||||
|
||||
private class ViewHolder(binding: ShareButtonBinding) : BindingViewHolder<Model, ShareButtonBinding>(binding) {
|
||||
override fun bind(model: Model) {
|
||||
binding.shareButton.setOnClickListener { model.onClick(model) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,38 +1,56 @@
|
||||
package org.thoughtcrime.securesms.profiles.manage;
|
||||
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.Navigation;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
import com.google.android.material.button.MaterialButton;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
|
||||
import org.signal.core.util.DimensionUnit;
|
||||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher;
|
||||
import org.thoughtcrime.securesms.databinding.UsernameEditFragmentBinding;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.FragmentResultContract;
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable;
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton;
|
||||
import org.thoughtcrime.securesms.util.views.LearnMoreTextView;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class UsernameEditFragment extends LoggingFragment {
|
||||
|
||||
private static final float DISABLED_ALPHA = 0.5f;
|
||||
|
||||
private UsernameEditViewModel viewModel;
|
||||
|
||||
private EditText usernameInput;
|
||||
private TextView usernameSubtext;
|
||||
private CircularProgressMaterialButton submitButton;
|
||||
private CircularProgressMaterialButton deleteButton;
|
||||
private UsernameEditViewModel viewModel;
|
||||
private UsernameEditFragmentBinding binding;
|
||||
private ImageView suffixProgress;
|
||||
private LifecycleDisposable lifecycleDisposable;
|
||||
|
||||
public static UsernameEditFragment newInstance() {
|
||||
return new UsernameEditFragment();
|
||||
@@ -40,46 +58,97 @@ public class UsernameEditFragment extends LoggingFragment {
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.username_edit_fragment, container, false);
|
||||
binding = UsernameEditFragmentBinding.inflate(inflater, container, false);
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
usernameInput = view.findViewById(R.id.username_text);
|
||||
usernameSubtext = view.findViewById(R.id.username_subtext);
|
||||
submitButton = view.findViewById(R.id.username_submit_button);
|
||||
deleteButton = view.findViewById(R.id.username_delete_button);
|
||||
binding.toolbar.setNavigationOnClickListener(v -> Navigation.findNavController(view).popBackStack());
|
||||
|
||||
view.<Toolbar>findViewById(R.id.toolbar)
|
||||
.setNavigationOnClickListener(v -> Navigation.findNavController(view)
|
||||
.popBackStack());
|
||||
binding.usernameTextWrapper.setErrorIconDrawable(null);
|
||||
|
||||
viewModel = ViewModelProviders.of(this, new UsernameEditViewModel.Factory()).get(UsernameEditViewModel.class);
|
||||
lifecycleDisposable = new LifecycleDisposable();
|
||||
lifecycleDisposable.bindTo(getViewLifecycleOwner());
|
||||
|
||||
viewModel.getUiState().observe(getViewLifecycleOwner(), this::onUiStateChanged);
|
||||
viewModel = new ViewModelProvider(this, new UsernameEditViewModel.Factory()).get(UsernameEditViewModel.class);
|
||||
|
||||
lifecycleDisposable.add(viewModel.getUiState().subscribe(this::onUiStateChanged));
|
||||
viewModel.getEvents().observe(getViewLifecycleOwner(), this::onEvent);
|
||||
|
||||
submitButton.setOnClickListener(v -> viewModel.onUsernameSubmitted(usernameInput.getText().toString()));
|
||||
deleteButton.setOnClickListener(v -> viewModel.onUsernameDeleted());
|
||||
binding.usernameSubmitButton.setOnClickListener(v -> viewModel.onUsernameSubmitted(binding.usernameText.getText().toString()));
|
||||
binding.usernameDeleteButton.setOnClickListener(v -> viewModel.onUsernameDeleted());
|
||||
|
||||
usernameInput.setText(Recipient.self().getUsername().orElse(null));
|
||||
usernameInput.addTextChangedListener(new SimpleTextWatcher() {
|
||||
binding.usernameText.setText(Recipient.self().getUsername().orElse(null));
|
||||
binding.usernameText.addTextChangedListener(new SimpleTextWatcher() {
|
||||
@Override
|
||||
public void onTextChanged(String text) {
|
||||
viewModel.onUsernameUpdated(text);
|
||||
}
|
||||
});
|
||||
usernameInput.setOnEditorActionListener((v, actionId, event) -> {
|
||||
binding.usernameText.setOnEditorActionListener((v, actionId, event) -> {
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
viewModel.onUsernameSubmitted(usernameInput.getText().toString());
|
||||
viewModel.onUsernameSubmitted(binding.usernameText.getText().toString());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
binding.usernameDescription.setLinkColor(ContextCompat.getColor(requireContext(), R.color.signal_colorPrimary));
|
||||
binding.usernameDescription.setLearnMoreVisible(true);
|
||||
binding.usernameDescription.setOnLinkClickListener(this::onLearnMore);
|
||||
|
||||
initializeSuffix();
|
||||
ViewUtil.focusAndShowKeyboard(binding.usernameText);
|
||||
}
|
||||
|
||||
private void initializeSuffix() {
|
||||
TextView suffixTextView = binding.usernameTextWrapper.getSuffixTextView();
|
||||
Drawable pipe = Objects.requireNonNull(ContextCompat.getDrawable(requireContext(), R.drawable.pipe_divider));
|
||||
|
||||
pipe.setBounds(0, 0, (int) DimensionUnit.DP.toPixels(1f), (int) DimensionUnit.DP.toPixels(20f));
|
||||
suffixTextView.setCompoundDrawablesRelative(pipe, null, null, null);
|
||||
|
||||
LinearLayout suffixParent = (LinearLayout) suffixTextView.getParent();
|
||||
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT);
|
||||
|
||||
ViewUtil.setLeftMargin(suffixTextView, (int) DimensionUnit.DP.toPixels(16f));
|
||||
|
||||
binding.usernameTextWrapper.getSuffixTextView().setCompoundDrawablePadding((int) DimensionUnit.DP.toPixels(16f));
|
||||
|
||||
layoutParams.topMargin = suffixTextView.getPaddingTop();
|
||||
layoutParams.bottomMargin = suffixTextView.getPaddingBottom();
|
||||
|
||||
suffixProgress = new ImageView(requireContext());
|
||||
suffixProgress.setImageDrawable(UsernameSuffix.getInProgressDrawable(requireContext()));
|
||||
suffixParent.addView(suffixProgress, 0, layoutParams);
|
||||
|
||||
suffixTextView.setOnClickListener(this::onLearnMore);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
suffixProgress = null;
|
||||
}
|
||||
|
||||
private void onLearnMore(@Nullable View unused) {
|
||||
new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(new StringBuilder("#\n").append(getString(R.string.UsernameEditFragment__what_is_this_number)))
|
||||
.setMessage(R.string.UsernameEditFragment__these_digits_help_keep)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {})
|
||||
.show();
|
||||
}
|
||||
|
||||
private void onUiStateChanged(@NonNull UsernameEditViewModel.State state) {
|
||||
EditText usernameInput = binding.usernameText;
|
||||
CircularProgressMaterialButton submitButton = binding.usernameSubmitButton;
|
||||
CircularProgressMaterialButton deleteButton = binding.usernameDeleteButton;
|
||||
TextInputLayout usernameInputWrapper = binding.usernameTextWrapper;
|
||||
|
||||
usernameInput.setEnabled(true);
|
||||
presentSuffix(state.getUsernameSuffix());
|
||||
|
||||
switch (state.getButtonState()) {
|
||||
case SUBMIT:
|
||||
@@ -128,39 +197,57 @@ public class UsernameEditFragment extends LoggingFragment {
|
||||
|
||||
switch (state.getUsernameStatus()) {
|
||||
case NONE:
|
||||
usernameSubtext.setText("");
|
||||
usernameInputWrapper.setError(null);
|
||||
break;
|
||||
case TOO_SHORT:
|
||||
case TOO_LONG:
|
||||
usernameSubtext.setText(getResources().getString(R.string.UsernameEditFragment_usernames_must_be_between_a_and_b_characters, UsernameUtil.MIN_LENGTH, UsernameUtil.MAX_LENGTH));
|
||||
usernameSubtext.setTextColor(getResources().getColor(R.color.core_red));
|
||||
usernameInputWrapper.setError(getResources().getString(R.string.UsernameEditFragment_usernames_must_be_between_a_and_b_characters, UsernameUtil.MIN_LENGTH, UsernameUtil.MAX_LENGTH));
|
||||
usernameInputWrapper.setErrorTextColor(ColorStateList.valueOf(getResources().getColor(R.color.signal_colorError)));
|
||||
|
||||
break;
|
||||
case INVALID_CHARACTERS:
|
||||
usernameSubtext.setText(R.string.UsernameEditFragment_usernames_can_only_include);
|
||||
usernameSubtext.setTextColor(getResources().getColor(R.color.core_red));
|
||||
usernameInputWrapper.setError(getResources().getString(R.string.UsernameEditFragment_usernames_can_only_include));
|
||||
usernameInputWrapper.setErrorTextColor(ColorStateList.valueOf(getResources().getColor(R.color.signal_colorError)));
|
||||
|
||||
break;
|
||||
case CANNOT_START_WITH_NUMBER:
|
||||
usernameSubtext.setText(R.string.UsernameEditFragment_usernames_cannot_begin_with_a_number);
|
||||
usernameSubtext.setTextColor(getResources().getColor(R.color.core_red));
|
||||
usernameInputWrapper.setError(getResources().getString(R.string.UsernameEditFragment_usernames_cannot_begin_with_a_number));
|
||||
usernameInputWrapper.setErrorTextColor(ColorStateList.valueOf(getResources().getColor(R.color.signal_colorError)));
|
||||
|
||||
break;
|
||||
case INVALID_GENERIC:
|
||||
usernameSubtext.setText(R.string.UsernameEditFragment_username_is_invalid);
|
||||
usernameSubtext.setTextColor(getResources().getColor(R.color.core_red));
|
||||
usernameInputWrapper.setError(getResources().getString(R.string.UsernameEditFragment_username_is_invalid));
|
||||
usernameInputWrapper.setErrorTextColor(ColorStateList.valueOf(getResources().getColor(R.color.signal_colorError)));
|
||||
|
||||
break;
|
||||
case TAKEN:
|
||||
usernameSubtext.setText(R.string.UsernameEditFragment_this_username_is_taken);
|
||||
usernameSubtext.setTextColor(getResources().getColor(R.color.core_red));
|
||||
usernameInputWrapper.setError(getResources().getString(R.string.UsernameEditFragment_this_username_is_taken));
|
||||
usernameInputWrapper.setErrorTextColor(ColorStateList.valueOf(getResources().getColor(R.color.signal_colorError)));
|
||||
|
||||
break;
|
||||
case AVAILABLE:
|
||||
usernameSubtext.setText(R.string.UsernameEditFragment_this_username_is_available);
|
||||
usernameSubtext.setTextColor(getResources().getColor(R.color.core_green));
|
||||
usernameInputWrapper.setError(getResources().getString(R.string.UsernameEditFragment_this_username_is_available));
|
||||
usernameInputWrapper.setErrorTextColor(ColorStateList.valueOf(getResources().getColor(R.color.signal_accent_green)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void presentSuffix(@NonNull UsernameSuffix usernameSuffix) {
|
||||
binding.usernameTextWrapper.setSuffixText(usernameSuffix.getCharSequence());
|
||||
|
||||
boolean isInProgress = usernameSuffix.isInProgress();
|
||||
|
||||
if (isInProgress) {
|
||||
suffixProgress.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
suffixProgress.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void onEvent(@NonNull UsernameEditViewModel.Event event) {
|
||||
switch (event) {
|
||||
case SUBMIT_SUCCESS:
|
||||
ResultContract.setUsernameCreated(getParentFragmentManager());
|
||||
Toast.makeText(requireContext(), R.string.UsernameEditFragment_successfully_set_username, Toast.LENGTH_SHORT).show();
|
||||
NavHostFragment.findNavController(this).popBackStack();
|
||||
break;
|
||||
@@ -179,4 +266,23 @@ public class UsernameEditFragment extends LoggingFragment {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static class ResultContract extends FragmentResultContract<Boolean> {
|
||||
private static final String REQUEST_KEY = "username_created";
|
||||
|
||||
protected ResultContract() {
|
||||
super(REQUEST_KEY);
|
||||
}
|
||||
|
||||
static void setUsernameCreated(@NonNull FragmentManager fragmentManager) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putBoolean(REQUEST_KEY, true);
|
||||
fragmentManager.setFragmentResult(REQUEST_KEY, bundle);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean getResult(@NonNull Bundle bundle) {
|
||||
return bundle.getBoolean(REQUEST_KEY, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
@@ -16,81 +15,80 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil;
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil.InvalidReason;
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
|
||||
class UsernameEditViewModel extends ViewModel {
|
||||
|
||||
private static final String TAG = Log.tag(UsernameEditViewModel.class);
|
||||
|
||||
private final Application application;
|
||||
private final MutableLiveData<State> uiState;
|
||||
private final SingleLiveEvent<Event> events;
|
||||
private final UsernameEditRepository repo;
|
||||
private final RxStore<State> uiState;
|
||||
|
||||
private UsernameEditViewModel() {
|
||||
this.application = ApplicationDependencies.getApplication();
|
||||
this.repo = new UsernameEditRepository();
|
||||
this.uiState = new MutableLiveData<>();
|
||||
this.uiState = new RxStore<>(new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, UsernameSuffix.NONE), Schedulers.computation());
|
||||
this.events = new SingleLiveEvent<>();
|
||||
|
||||
uiState.setValue(new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE));
|
||||
}
|
||||
|
||||
void onUsernameUpdated(@NonNull String username) {
|
||||
if (TextUtils.isEmpty(username) && Recipient.self().getUsername().isPresent()) {
|
||||
uiState.setValue(new State(ButtonState.DELETE, UsernameStatus.NONE));
|
||||
return;
|
||||
}
|
||||
uiState.update(state -> {
|
||||
if (TextUtils.isEmpty(username) && Recipient.self().getUsername().isPresent()) {
|
||||
return new State(ButtonState.DELETE, UsernameStatus.NONE, state.usernameSuffix);
|
||||
}
|
||||
|
||||
if (username.equals(Recipient.self().getUsername().orElse(null))) {
|
||||
uiState.setValue(new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE));
|
||||
return;
|
||||
}
|
||||
if (username.equals(Recipient.self().getUsername().orElse(null))) {
|
||||
return new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, state.usernameSuffix);
|
||||
}
|
||||
|
||||
Optional<InvalidReason> invalidReason = UsernameUtil.checkUsername(username);
|
||||
Optional<InvalidReason> invalidReason = UsernameUtil.checkUsername(username);
|
||||
|
||||
if (invalidReason.isPresent()) {
|
||||
uiState.setValue(new State(ButtonState.SUBMIT_DISABLED, mapUsernameError(invalidReason.get())));
|
||||
return;
|
||||
}
|
||||
|
||||
uiState.setValue(new State(ButtonState.SUBMIT, UsernameStatus.NONE));
|
||||
return invalidReason.map(reason -> new State(ButtonState.SUBMIT_DISABLED, mapUsernameError(reason), state.usernameSuffix))
|
||||
.orElseGet(() -> new State(ButtonState.SUBMIT, UsernameStatus.NONE, state.usernameSuffix));
|
||||
});
|
||||
}
|
||||
|
||||
void onUsernameSubmitted(@NonNull String username) {
|
||||
if (username.equals(Recipient.self().getUsername().orElse(null))) {
|
||||
uiState.setValue(new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE));
|
||||
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, state.usernameSuffix));
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<InvalidReason> invalidReason = UsernameUtil.checkUsername(username);
|
||||
|
||||
if (invalidReason.isPresent()) {
|
||||
uiState.setValue(new State(ButtonState.SUBMIT_DISABLED, mapUsernameError(invalidReason.get())));
|
||||
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, mapUsernameError(invalidReason.get()), state.usernameSuffix));
|
||||
return;
|
||||
}
|
||||
|
||||
uiState.setValue(new State(ButtonState.SUBMIT_LOADING, UsernameStatus.NONE));
|
||||
uiState.update(state -> new State(ButtonState.SUBMIT_LOADING, UsernameStatus.NONE, state.usernameSuffix));
|
||||
|
||||
repo.setUsername(username, (result) -> {
|
||||
ThreadUtil.runOnMain(() -> {
|
||||
switch (result) {
|
||||
case SUCCESS:
|
||||
uiState.setValue(new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE));
|
||||
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, state.usernameSuffix));
|
||||
events.postValue(Event.SUBMIT_SUCCESS);
|
||||
break;
|
||||
case USERNAME_INVALID:
|
||||
uiState.setValue(new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.INVALID_GENERIC));
|
||||
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.INVALID_GENERIC, state.usernameSuffix));
|
||||
events.postValue(Event.SUBMIT_FAIL_INVALID);
|
||||
break;
|
||||
case USERNAME_UNAVAILABLE:
|
||||
uiState.setValue(new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.TAKEN));
|
||||
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.TAKEN, state.usernameSuffix));
|
||||
events.postValue(Event.SUBMIT_FAIL_TAKEN);
|
||||
break;
|
||||
case NETWORK_ERROR:
|
||||
uiState.setValue(new State(ButtonState.SUBMIT, UsernameStatus.NONE));
|
||||
uiState.update(state -> new State(ButtonState.SUBMIT, UsernameStatus.NONE, state.usernameSuffix));
|
||||
events.postValue(Event.NETWORK_FAILURE);
|
||||
break;
|
||||
}
|
||||
@@ -99,17 +97,17 @@ class UsernameEditViewModel extends ViewModel {
|
||||
}
|
||||
|
||||
void onUsernameDeleted() {
|
||||
uiState.setValue(new State(ButtonState.DELETE_LOADING, UsernameStatus.NONE));
|
||||
uiState.update(state -> new State(ButtonState.DELETE_LOADING, UsernameStatus.NONE, state.usernameSuffix));
|
||||
|
||||
repo.deleteUsername((result) -> {
|
||||
ThreadUtil.runOnMain(() -> {
|
||||
switch (result) {
|
||||
case SUCCESS:
|
||||
uiState.postValue(new State(ButtonState.DELETE_DISABLED, UsernameStatus.NONE));
|
||||
uiState.update(state -> new State(ButtonState.DELETE_DISABLED, UsernameStatus.NONE, state.usernameSuffix));
|
||||
events.postValue(Event.DELETE_SUCCESS);
|
||||
break;
|
||||
case NETWORK_ERROR:
|
||||
uiState.postValue(new State(ButtonState.DELETE, UsernameStatus.NONE));
|
||||
uiState.update(state -> new State(ButtonState.DELETE, UsernameStatus.NONE, state.usernameSuffix));
|
||||
events.postValue(Event.NETWORK_FAILURE);
|
||||
break;
|
||||
}
|
||||
@@ -117,8 +115,8 @@ class UsernameEditViewModel extends ViewModel {
|
||||
});
|
||||
}
|
||||
|
||||
@NonNull LiveData<State> getUiState() {
|
||||
return uiState;
|
||||
@NonNull Flowable<State> getUiState() {
|
||||
return uiState.getStateFlowable().observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
@NonNull LiveData<Event> getEvents() {
|
||||
@@ -138,12 +136,15 @@ class UsernameEditViewModel extends ViewModel {
|
||||
static class State {
|
||||
private final ButtonState buttonState;
|
||||
private final UsernameStatus usernameStatus;
|
||||
private final UsernameSuffix usernameSuffix;
|
||||
|
||||
private State(@NonNull ButtonState buttonState,
|
||||
@NonNull UsernameStatus usernameStatus)
|
||||
@NonNull UsernameStatus usernameStatus,
|
||||
@NonNull UsernameSuffix usernameSuffix)
|
||||
{
|
||||
this.buttonState = buttonState;
|
||||
this.usernameStatus = usernameStatus;
|
||||
this.usernameSuffix = usernameSuffix;
|
||||
}
|
||||
|
||||
@NonNull ButtonState getButtonState() {
|
||||
@@ -153,6 +154,10 @@ class UsernameEditViewModel extends ViewModel {
|
||||
@NonNull UsernameStatus getUsernameStatus() {
|
||||
return usernameStatus;
|
||||
}
|
||||
|
||||
@NonNull UsernameSuffix getUsernameSuffix() {
|
||||
return usernameSuffix;
|
||||
}
|
||||
}
|
||||
|
||||
enum UsernameStatus {
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
package org.thoughtcrime.securesms.profiles.manage
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.ShareCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsBottomSheetFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.FragmentResultContract
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
|
||||
/**
|
||||
* Allows the user to either share their username directly or to copy it to their clipboard.
|
||||
*/
|
||||
class UsernameShareBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
|
||||
companion object {
|
||||
private const val REQUEST_KEY = "copy_username"
|
||||
}
|
||||
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
CopyButton.register(adapter)
|
||||
ShareButton.register(adapter)
|
||||
|
||||
lifecycleDisposable += Recipient.observable(Recipient.self().id).subscribe {
|
||||
adapter.submitList(getConfiguration(it).toMappingModelList())
|
||||
}
|
||||
}
|
||||
|
||||
private fun getConfiguration(recipient: Recipient): DSLConfiguration {
|
||||
return configure {
|
||||
noPadTextPref(
|
||||
title = DSLSettingsText.from(
|
||||
R.string.UsernameShareBottomSheet__copy_or_share_a_username_link,
|
||||
DSLSettingsText.TextAppearanceModifier(R.style.Signal_Text_BodyMedium),
|
||||
DSLSettingsText.CenterModifier,
|
||||
DSLSettingsText.ColorModifier(
|
||||
ContextCompat.getColor(requireContext(), R.color.signal_colorOnSurfaceVariant),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
space(DimensionUnit.DP.toPixels(32f).toInt())
|
||||
|
||||
val username = recipient.username.get()
|
||||
customPref(
|
||||
CopyButton.Model(
|
||||
text = username,
|
||||
onClick = {
|
||||
copyToClipboard(it)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
space(DimensionUnit.DP.toPixels(20f).toInt())
|
||||
|
||||
customPref(
|
||||
CopyButton.Model(
|
||||
text = getString(R.string.signal_me_url, username),
|
||||
onClick = {
|
||||
copyToClipboard(it)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
space(DimensionUnit.DP.toPixels(24f).toInt())
|
||||
|
||||
customPref(
|
||||
ShareButton.Model(
|
||||
text = getString(R.string.signal_me_url, username),
|
||||
onClick = {
|
||||
openShareSheet(it.text)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
space(DimensionUnit.DP.toPixels(18f).toInt())
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyToClipboard(model: CopyButton.Model) {
|
||||
Util.copyToClipboard(requireContext(), model.text)
|
||||
setFragmentResult(REQUEST_KEY, Bundle().apply { putBoolean(REQUEST_KEY, true) })
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
|
||||
private fun openShareSheet(charSequence: CharSequence) {
|
||||
val mimeType = Intent.normalizeMimeType("text/plain")
|
||||
val shareIntent = ShareCompat.IntentBuilder(requireContext())
|
||||
.setText(charSequence)
|
||||
.setType(mimeType)
|
||||
.createChooserIntent()
|
||||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
|
||||
try {
|
||||
startActivity(shareIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Toast.makeText(requireContext(), R.string.MediaPreviewActivity_cant_find_an_app_able_to_share_this_media, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
object ResultContract : FragmentResultContract<Boolean>(REQUEST_KEY) {
|
||||
override fun getResult(bundle: Bundle): Boolean {
|
||||
return bundle.getBoolean(REQUEST_KEY, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package org.thoughtcrime.securesms.profiles.manage
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.google.android.material.progressindicator.CircularProgressIndicatorSpec
|
||||
import com.google.android.material.progressindicator.IndeterminateDrawable
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
/**
|
||||
* Describes the state of the username suffix, which is a spanned CharSequence.
|
||||
*/
|
||||
data class UsernameSuffix(
|
||||
val charSequence: CharSequence?
|
||||
) {
|
||||
|
||||
val isInProgress = charSequence == null
|
||||
|
||||
companion object {
|
||||
@JvmField
|
||||
val LOADING = UsernameSuffix(null)
|
||||
|
||||
@JvmField
|
||||
val NONE = UsernameSuffix("")
|
||||
|
||||
@JvmStatic
|
||||
fun fromCode(code: Int) = UsernameSuffix("#$code")
|
||||
|
||||
@JvmStatic
|
||||
fun getInProgressDrawable(context: Context): IndeterminateDrawable<CircularProgressIndicatorSpec> {
|
||||
val progressIndicatorSpec = CircularProgressIndicatorSpec(context, null).apply {
|
||||
indicatorInset = 0
|
||||
indicatorSize = DimensionUnit.DP.toPixels(16f).toInt()
|
||||
trackColor = ContextCompat.getColor(context, R.color.signal_colorOnSurfaceVariant)
|
||||
trackThickness = DimensionUnit.DP.toPixels(1f).toInt()
|
||||
}
|
||||
|
||||
return IndeterminateDrawable.createCircularDrawable(context, progressIndicatorSpec).apply {
|
||||
setBounds(0, 0, DimensionUnit.DP.toPixels(16f).toInt(), DimensionUnit.DP.toPixels(16f).toInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.thoughtcrime.securesms.util
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import java.util.function.Consumer
|
||||
|
||||
/**
|
||||
* Generic Fragment result contract.
|
||||
*/
|
||||
abstract class FragmentResultContract<T> protected constructor(private val resultKey: String) {
|
||||
|
||||
protected abstract fun getResult(bundle: Bundle): T
|
||||
|
||||
fun registerForResult(fragmentManager: FragmentManager, lifecycleOwner: LifecycleOwner, consumer: Consumer<T>) {
|
||||
fragmentManager.setFragmentResultListener(resultKey, lifecycleOwner) { key, bundle ->
|
||||
if (key == resultKey) {
|
||||
val result = getResult(bundle)
|
||||
consumer.accept(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.thoughtcrime.securesms.util.adapter.mapping
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.viewbinding.ViewBinding
|
||||
|
||||
/**
|
||||
* Allows ViewHolders to be generated with a ViewBinding. Intended usage is as follows:
|
||||
*
|
||||
* BindingFactory(::MyBindingViewHolder, MyBinding::inflate)
|
||||
*/
|
||||
class BindingFactory<T : MappingModel<T>, B : ViewBinding>(
|
||||
private val creator: (B) -> BindingViewHolder<T, B>,
|
||||
private val inflater: (LayoutInflater, ViewGroup, Boolean) -> B
|
||||
) : Factory<T> {
|
||||
override fun createViewHolder(parent: ViewGroup): MappingViewHolder<T> {
|
||||
val binding = inflater(LayoutInflater.from(parent.context), parent, false)
|
||||
return creator(binding)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.thoughtcrime.securesms.util.adapter.mapping
|
||||
|
||||
import androidx.viewbinding.ViewBinding
|
||||
|
||||
/**
|
||||
* A ViewHolder which is populated with a ViewBinding, used in conjunction with BindingFactory
|
||||
*/
|
||||
abstract class BindingViewHolder<T, B : ViewBinding>(protected val binding: B) : MappingViewHolder<T>(binding.root)
|
||||
@@ -13,7 +13,7 @@ import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
*/
|
||||
class RxStore<T : Any>(
|
||||
defaultValue: T,
|
||||
private val scheduler: Scheduler = Schedulers.computation()
|
||||
scheduler: Scheduler = Schedulers.computation()
|
||||
) {
|
||||
|
||||
private val behaviorProcessor = BehaviorProcessor.createDefault(defaultValue)
|
||||
|
||||
Reference in New Issue
Block a user