Fix several issues with new avatar picker.

* Fix silliness with text behaviour
* Fix long click behaviour
* Make views play nicer with landscape mode
* Do not show megaphone if user has an avatar (or had one and removed it)
* Fix bad heading on vector color picker
This commit is contained in:
Alex Hart
2021-07-22 13:28:03 -03:00
committed by Greyson Parrelli
parent ab56856f41
commit c1b54b3532
16 changed files with 389 additions and 361 deletions

View File

@@ -160,8 +160,8 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
val menuRes = when (avatar) {
is Avatar.Photo -> R.menu.avatar_picker_context
is Avatar.Text -> R.menu.avatar_picker_context
is Avatar.Vector -> return false
is Avatar.Resource -> return false
is Avatar.Vector -> return true
is Avatar.Resource -> return true
}
val popup = PopupMenu(context, anchorView, Gravity.TOP)

View File

@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.avatar.picker
import android.util.TypedValue
import android.view.View
import android.widget.EditText
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.content.res.AppCompatResources
@@ -57,14 +58,17 @@ object AvatarPickerItem {
init {
textView.typeface = AvatarRenderer.getTypeface(context)
textView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
updateAndApplyText(textView.text.toString())
updateFontSize(textView.text.toString())
}
}
private fun updateAndApplyText(text: String) {
private fun updateFontSize(text: String) {
val textSize = Avatars.getTextSizeForLength(context, text, textView.measuredWidth * 0.8f, textView.measuredHeight * 0.45f)
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
textView.text = text
if (textView !is EditText) {
textView.text = text
}
}
override fun bind(model: Model) {
@@ -76,12 +80,8 @@ object AvatarPickerItem {
selectedOverlay?.animate()?.cancel()
selectedFader?.animate()?.cancel()
if (model.isSelected) {
itemView.setOnLongClickListener {
onAvatarLongClickListener?.invoke(itemView, model.avatar) ?: false
}
} else {
itemView.setOnLongClickListener(null)
itemView.setOnLongClickListener {
onAvatarLongClickListener?.invoke(itemView, model.avatar) ?: false
}
itemView.setOnClickListener { onAvatarClickListener?.invoke(model.avatar, model.isSelected) }
@@ -108,7 +108,10 @@ object AvatarPickerItem {
is Avatar.Text -> {
textView.visible = true
updateAndApplyText(model.avatar.text)
updateFontSize(model.avatar.text)
if (textView.text.toString() != model.avatar.text) {
textView.text = model.avatar.text
}
imageView.setImageDrawable(null)
imageView.background.colorFilter = SimpleColorFilter(model.avatar.color.backgroundColor)

View File

@@ -31,12 +31,13 @@ import org.thoughtcrime.securesms.util.ViewUtil
/**
* Fragment to create an avatar based off of a Vector or Text (via a pager)
*/
class TextAvatarCreationFragment : Fragment(R.layout.text_avatar_creation_fragment_hidden_recycler) {
class TextAvatarCreationFragment : Fragment(R.layout.text_avatar_creation_fragment) {
private val viewModel: TextAvatarCreationViewModel by viewModels(factoryProducer = this::createFactory)
private lateinit var textInput: EditText
private lateinit var recycler: RecyclerView
private lateinit var content: ConstraintLayout
private val withRecyclerSet = ConstraintSet()
private val withoutRecyclerSet = ConstraintSet()
@@ -60,9 +61,10 @@ class TextAvatarCreationFragment : Fragment(R.layout.text_avatar_creation_fragme
val tabLayout: ControllableTabLayout = view.findViewById(R.id.text_avatar_creation_tabs)
val doneButton: View = view.findViewById(R.id.text_avatar_creation_done)
withRecyclerSet.load(requireContext(), R.layout.text_avatar_creation_fragment)
withoutRecyclerSet.load(requireContext(), R.layout.text_avatar_creation_fragment_hidden_recycler)
withRecyclerSet.load(requireContext(), R.layout.text_avatar_creation_fragment_content)
withoutRecyclerSet.load(requireContext(), R.layout.text_avatar_creation_fragment_content_hidden_recycler)
content = view.findViewById(R.id.content)
recycler = view.findViewById(R.id.text_avatar_creation_recycler)
textInput = view.findViewById(R.id.avatar_picker_item_text)
@@ -83,19 +85,7 @@ class TextAvatarCreationFragment : Fragment(R.layout.text_avatar_creation_fragme
val viewHolder = AvatarPickerItem.ViewHolder(view)
viewModel.state.observe(viewLifecycleOwner) { state ->
EditTextUtil.setCursorColor(textInput, state.currentAvatar.color.foregroundColor)
val hadText = textInput.length() > 0
val selectionStart = textInput.selectionStart
val selectionEnd = textInput.selectionEnd
viewHolder.bind(AvatarPickerItem.Model(state.currentAvatar, false))
textInput.post {
if (!hadText) {
textInput.setSelection(textInput.length())
} else {
textInput.setSelection(selectionStart, selectionEnd)
}
}
adapter.submitList(state.colors().map { AvatarColorItem.Model(it) })
hasBoundFromViewModel = true
@@ -114,8 +104,8 @@ class TextAvatarCreationFragment : Fragment(R.layout.text_avatar_creation_fragme
}
textInput.setOnEditorActionListener { v, actionId, event ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
doneButton.performClick()
if (actionId == EditorInfo.IME_ACTION_NEXT) {
tabLayout.getTabAt(1)?.select()
true
} else {
false
@@ -130,20 +120,18 @@ class TextAvatarCreationFragment : Fragment(R.layout.text_avatar_creation_fragme
textInput.isEnabled = true
ViewUtil.focusAndShowKeyboard(textInput)
val constraintLayout = requireView() as ConstraintLayout
TransitionManager.endTransitions(constraintLayout)
withoutRecyclerSet.applyTo(constraintLayout)
TransitionManager.beginDelayedTransition(constraintLayout)
TransitionManager.endTransitions(content)
withoutRecyclerSet.applyTo(content)
TransitionManager.beginDelayedTransition(content)
textInput.setSelection(textInput.length())
}
1 -> {
textInput.isEnabled = false
ViewUtil.hideKeyboard(requireContext(), textInput)
val constraintLayout = requireView() as ConstraintLayout
TransitionManager.endTransitions(constraintLayout)
withRecyclerSet.applyTo(constraintLayout)
TransitionManager.beginDelayedTransition(constraintLayout)
TransitionManager.endTransitions(content)
withRecyclerSet.applyTo(content)
TransitionManager.beginDelayedTransition(content)
}
}
}

View File

@@ -14,29 +14,33 @@ public class EmojiSpan extends AnimatingImageSpan {
private final float SHIFT_FACTOR = 1.5f;
private final int size;
private final FontMetricsInt fm;
private int size;
private FontMetricsInt fontMetrics;
public EmojiSpan(@NonNull Drawable drawable, @NonNull TextView tv) {
super(drawable, tv);
fm = tv.getPaint().getFontMetricsInt();
size = fm != null ? Math.abs(fm.descent) + Math.abs(fm.ascent)
: tv.getResources().getDimensionPixelSize(R.dimen.conversation_item_body_text_size);
fontMetrics = tv.getPaint().getFontMetricsInt();
size = fontMetrics != null ? Math.abs(fontMetrics.descent) + Math.abs(fontMetrics.ascent)
: tv.getResources().getDimensionPixelSize(R.dimen.conversation_item_body_text_size);
getDrawable().setBounds(0, 0, size, size);
}
@Override
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, FontMetricsInt fm) {
if (fm != null && this.fm != null) {
fm.ascent = this.fm.ascent;
fm.descent = this.fm.descent;
fm.top = this.fm.top;
fm.bottom = this.fm.bottom;
fm.leading = this.fm.leading;
return size;
if (fm != null && this.fontMetrics != null) {
fm.ascent = this.fontMetrics.ascent;
fm.descent = this.fontMetrics.descent;
fm.top = this.fontMetrics.top;
fm.bottom = this.fontMetrics.bottom;
fm.leading = this.fontMetrics.leading;
} else {
return super.getSize(paint, text, start, end, fm);
this.fontMetrics = paint.getFontMetricsInt();
this.size = Math.abs(this.fontMetrics.descent) + Math.abs(this.fontMetrics.ascent);
getDrawable().setBounds(0, 0, size, size);
}
return size;
}
@Override

View File

@@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
@@ -100,6 +101,10 @@ public class RetrieveProfileAvatarJob extends BaseJob {
try {
AvatarHelper.setAvatar(context, recipient.getId(), avatarStream);
if (recipient.isSelf()) {
SignalStore.misc().markHasEverHadAnAvatar();
}
} catch (AssertionError e) {
throw new IOException("Failed to copy stream. Likely a Conscrypt issue.", e);
}

View File

@@ -14,6 +14,7 @@ public final class MiscellaneousValues extends SignalStoreValues {
private static final String USERNAME_SHOW_REMINDER = "username.show.reminder";
private static final String CLIENT_DEPRECATED = "misc.client_deprecated";
private static final String OLD_DEVICE_TRANSFER_LOCKED = "misc.old_device.transfer.locked";
private static final String HAS_EVER_HAD_AN_AVATAR = "misc.has.ever.had.an.avatar";
MiscellaneousValues(@NonNull KeyValueStore store) {
super(store);
@@ -88,4 +89,12 @@ public final class MiscellaneousValues extends SignalStoreValues {
public void clearOldDeviceTransferLocked() {
putBoolean(OLD_DEVICE_TRANSFER_LOCKED, false);
}
public boolean hasEverHadAnAvatar() {
return getBoolean(HAS_EVER_HAD_AN_AVATAR, false);
}
public void markHasEverHadAnAvatar() {
putBoolean(HAS_EVER_HAD_AN_AVATAR, true);
}
}

View File

@@ -107,7 +107,7 @@ public final class Megaphones {
put(Event.ONBOARDING, shouldShowOnboardingMegaphone(context) ? ALWAYS : NEVER);
put(Event.NOTIFICATIONS, shouldShowNotificationsMegaphone(context) ? RecurringSchedule.every(TimeUnit.DAYS.toMillis(30)) : NEVER);
put(Event.CHAT_COLORS, ALWAYS);
put(Event.ADD_A_PROFILE_PHOTO, shouldShowAddAProfileMegaphone(context) ? ALWAYS : NEVER);
put(Event.ADD_A_PROFILE_PHOTO, shouldShowAddAProfilePhotoMegaphone(context) ? ALWAYS : NEVER);
}};
}
@@ -383,8 +383,18 @@ public final class Megaphones {
return shouldShow;
}
private static boolean shouldShowAddAProfileMegaphone(@NonNull Context context) {
return !AvatarHelper.hasAvatar(context, Recipient.self().getId());
private static boolean shouldShowAddAProfilePhotoMegaphone(@NonNull Context context) {
if (SignalStore.misc().hasEverHadAnAvatar()) {
return false;
}
boolean hasAnAvatar = AvatarHelper.hasAvatar(context, Recipient.self().getId());
if (hasAnAvatar) {
SignalStore.misc().markHasEverHadAnAvatar();
return false;
}
return true;
}
public enum Event {

View File

@@ -253,7 +253,10 @@ public class EditProfileFragment extends LoggingFragment {
private void initializeProfileAvatar() {
viewModel.avatar().observe(getViewLifecycleOwner(), bytes -> {
if (bytes == null) return;
if (bytes == null) {
GlideApp.with(this).clear(avatar);
return;
}
GlideApp.with(this)
.load(bytes)

View File

@@ -15,6 +15,7 @@ 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.keyvalue.SignalStore;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints;
import org.thoughtcrime.securesms.profiles.ProfileName;
@@ -146,6 +147,10 @@ public class EditSelfProfileRepository implements EditProfileRepository {
RegistrationUtil.maybeMarkRegistrationComplete(context);
if (avatar != null) {
SignalStore.misc().markHasEverHadAnAvatar();
}
return UploadResult.SUCCESS;
}, uploadResultConsumer::accept);
}

View File

@@ -9,6 +9,7 @@ import androidx.core.util.Consumer;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -53,6 +54,7 @@ final class ManageProfileRepository {
try {
ProfileUtil.uploadProfileWithAvatar(context, new StreamDetails(new ByteArrayInputStream(data), contentType, data.length));
AvatarHelper.setAvatar(context, Recipient.self().getId(), new ByteArrayInputStream(data));
SignalStore.misc().markHasEverHadAnAvatar();
callback.accept(Result.SUCCESS);
} catch (IOException e) {
Log.w(TAG, "Failed to upload profile during avatar change.", e);