diff --git a/app/src/main/java/org/thoughtcrime/securesms/avatar/picker/AvatarPickerFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/avatar/picker/AvatarPickerFragment.kt index ee0ba36a77..2fd3babe78 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/avatar/picker/AvatarPickerFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/avatar/picker/AvatarPickerFragment.kt @@ -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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/avatar/picker/AvatarPickerItem.kt b/app/src/main/java/org/thoughtcrime/securesms/avatar/picker/AvatarPickerItem.kt index f7ab75ff8d..2ae0745482 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/avatar/picker/AvatarPickerItem.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/avatar/picker/AvatarPickerItem.kt @@ -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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/avatar/text/TextAvatarCreationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/avatar/text/TextAvatarCreationFragment.kt index 15aa885833..c991536f6c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/avatar/text/TextAvatarCreationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/avatar/text/TextAvatarCreationFragment.kt @@ -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) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiSpan.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiSpan.java index 1733cb5cc2..a0e34e476b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiSpan.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiSpan.java @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java index 8dfa090a1a..a658081429 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java @@ -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); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java index bae602f36c..a9cc647138 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java @@ -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); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java index a4529178cf..f2c87cae2b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java +++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java @@ -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 { 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 ded430643d..3558b4c011 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 @@ -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) 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 index 0e02f9640f..182179a3e6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditSelfProfileRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditSelfProfileRepository.java @@ -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); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileRepository.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileRepository.java index 7eef53aa79..10863ddb8a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileRepository.java @@ -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); diff --git a/app/src/main/res/layout/avatar_picker_fragment.xml b/app/src/main/res/layout/avatar_picker_fragment.xml index bfb2f7d7bb..2dc4005a8e 100644 --- a/app/src/main/res/layout/avatar_picker_fragment.xml +++ b/app/src/main/res/layout/avatar_picker_fragment.xml @@ -5,6 +5,140 @@ android:layout_width="match_parent" android:layout_height="match_parent"> + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/text_avatar_creation_fragment.xml b/app/src/main/res/layout/text_avatar_creation_fragment.xml index 08cbec917d..d5fc9a9c25 100644 --- a/app/src/main/res/layout/text_avatar_creation_fragment.xml +++ b/app/src/main/res/layout/text_avatar_creation_fragment.xml @@ -1,7 +1,6 @@ @@ -17,6 +16,7 @@ app:title="@string/TextAvatarCreationFragment__preview" app:titleTextAppearance="@style/Signal.Text.Title" /> + - - - - - - - - + app:layout_constraintTop_toBottomOf="@id/text_avatar_creation_tabs"> + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/text_avatar_creation_fragment_hidden_recycler.xml b/app/src/main/res/layout/text_avatar_creation_fragment_content_hidden_recycler.xml similarity index 51% rename from app/src/main/res/layout/text_avatar_creation_fragment_hidden_recycler.xml rename to app/src/main/res/layout/text_avatar_creation_fragment_content_hidden_recycler.xml index b91bce69ea..c592549fa8 100644 --- a/app/src/main/res/layout/text_avatar_creation_fragment_hidden_recycler.xml +++ b/app/src/main/res/layout/text_avatar_creation_fragment_content_hidden_recycler.xml @@ -2,63 +2,15 @@ - - - - - - - - - - - - + android:layout_height="wrap_content"> - - \ No newline at end of file diff --git a/app/src/main/res/layout/vector_avatar_creation_fragment.xml b/app/src/main/res/layout/vector_avatar_creation_fragment.xml index a5ad0c5e66..55816e7fa0 100644 --- a/app/src/main/res/layout/vector_avatar_creation_fragment.xml +++ b/app/src/main/res/layout/vector_avatar_creation_fragment.xml @@ -8,7 +8,7 @@ - - - - - - - - - - - + app:layout_constraintTop_toBottomOf="@id/vector_avatar_creation_toolbar"> + + + + + + + + + + + + + + + + + %1$s · %2$s + + Avatar preview Camera Take a picture @@ -3634,17 +3636,22 @@ Text Save Select an avatar + Clear avatar + Edit + Failed to save avatar + + Preview Done Text Color + + Select a color + SMS · %1$s - Clear avatar - Edit - Failed to save avatar