diff --git a/app/src/main/java/com/anytypeio/anytype/ui/dashboard/BaseMnemonicFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/dashboard/BaseMnemonicFragment.kt new file mode 100644 index 0000000000..13401c9475 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/dashboard/BaseMnemonicFragment.kt @@ -0,0 +1,125 @@ +package com.anytypeio.anytype.ui.dashboard + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import android.view.animation.DecelerateInterpolator +import android.widget.TextView +import androidx.core.text.buildSpannedString +import androidx.core.text.inSpans +import androidx.fragment.app.viewModels +import androidx.transition.ChangeBounds +import androidx.transition.TransitionManager +import androidx.transition.TransitionSet +import androidx.viewbinding.ViewBinding +import com.anytypeio.anytype.R +import com.anytypeio.anytype.core_utils.ext.dimen +import com.anytypeio.anytype.core_utils.ext.toast +import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetFragment +import com.anytypeio.anytype.presentation.keychain.KeychainPhraseViewModel +import com.anytypeio.anytype.presentation.keychain.KeychainPhraseViewModelFactory +import com.anytypeio.anytype.presentation.keychain.KeychainViewState +import com.anytypeio.anytype.ui.profile.RoundedBackgroundColorSpan +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import javax.inject.Inject + +abstract class BaseMnemonicFragment : BaseBottomSheetFragment() { + + protected val vm: KeychainPhraseViewModel by viewModels { factory } + + private val fakePhrase by lazy { + val bg = RoundedBackgroundColorSpan( + requireContext().getColor(R.color.palette_light_ice), + dimen(R.dimen.shimmer_radius).toFloat(), + dimen(R.dimen.shimmer_radius).toFloat(), + dimen(R.dimen.shimmer_line_height).toFloat() + ) + buildSpannedString { inSpans(bg) { append(SHIMMER_PHRASE) } } + } + + protected abstract val keychain: TextView + + @Inject + lateinit var factory: KeychainPhraseViewModelFactory + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + keychain.setOnClickListener { vm.onKeychainClicked() } + binding.root.setOnClickListener { vm.onRootClicked() } + + vm.state.observe(viewLifecycleOwner) { render(it) } + + setupFullHeight() + } + + private fun render(state: KeychainViewState) { + when (state) { + is KeychainViewState.Displayed -> { + setKeychainPhrase(state.mnemonic) + copyMnemonicToClipboard(state.mnemonic) + } + is KeychainViewState.Blurred -> { + setBlurredPhrase() + } + } + } + + private fun setupFullHeight() { + binding.apply { root.layoutParams.height = resources.displayMetrics.heightPixels } + + val bottomSheetDialog = dialog as BottomSheetDialog + val behavior = bottomSheetDialog.behavior + behavior.state = BottomSheetBehavior.STATE_EXPANDED + } + + private fun animateBlurChange() { + val transitionSet = TransitionSet().apply { + addTransition(ChangeBounds()) + duration = ANIMATION_LENGTH + interpolator = DecelerateInterpolator(ANIMATION_FACTOR) + ordering = TransitionSet.ORDERING_TOGETHER + } + TransitionManager.beginDelayedTransition(binding.root as ViewGroup, transitionSet) + } + + private fun setBlurredPhrase() { + keychain + animateBlurChange() + keychain.setShadowLayer(0f, 0f, 0f, 0) + keychain.setTextColor(requireContext().getColor(R.color.palette_light_ice)) + keychain.setShadowLayer(dimen(R.dimen.shimmer_radius).toFloat(), 0f, 0f, 0) + keychain.text = fakePhrase + } + + private fun setKeychainPhrase(mnemonic: String) { + animateBlurChange() + keychain.setTextColor(requireContext().getColor(R.color.keychain_text_color)) + keychain.setShadowLayer(0f, 0f, 0f, 0) + keychain.text = mnemonic + } + + private fun copyMnemonicToClipboard(keychainPhrase: String) { + try { + val clipboard = + requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = ClipData.newPlainText( + DashboardMnemonicReminderDialog.MNEMONIC_LABEL, + keychainPhrase + ) + clipboard.setPrimaryClip(clip) + toast("Recovery phrase copied to clipboard.") + } catch (e: Exception) { + toast("Could not copy your recovery phrase. Please try again later, or copy it manually.") + } + } +} + +private const val SHIMMER_LENGTH = 50 +private val SHIMMER_PHRASE = "- ".repeat(SHIMMER_LENGTH) +private const val ANIMATION_LENGTH = 700L +private const val ANIMATION_FACTOR = 2.5f \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/dashboard/DashboardMnemonicReminderDialog.kt b/app/src/main/java/com/anytypeio/anytype/ui/dashboard/DashboardMnemonicReminderDialog.kt index 9cce8d4996..6c98815014 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/dashboard/DashboardMnemonicReminderDialog.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/dashboard/DashboardMnemonicReminderDialog.kt @@ -1,129 +1,41 @@ package com.anytypeio.anytype.ui.dashboard -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.graphics.BlurMaskFilter import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.fragment.app.viewModels -import androidx.lifecycle.Observer +import android.widget.TextView import com.anytypeio.anytype.analytics.base.EventsDictionary -import com.anytypeio.anytype.core_utils.ext.toast -import com.anytypeio.anytype.core_utils.ui.ViewState import com.anytypeio.anytype.databinding.DialogDashboardKeychainPhraseBinding import com.anytypeio.anytype.di.common.componentManager -import com.anytypeio.anytype.presentation.keychain.KeychainPhraseViewModel -import com.anytypeio.anytype.presentation.keychain.KeychainPhraseViewModelFactory -import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import javax.inject.Inject -class DashboardMnemonicReminderDialog : BottomSheetDialogFragment(), Observer> { - - private var _binding: DialogDashboardKeychainPhraseBinding? = null - private val binding: DialogDashboardKeychainPhraseBinding get() = _binding!! - - private val vm : KeychainPhraseViewModel by viewModels { factory } - - @Inject - lateinit var factory: KeychainPhraseViewModelFactory - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - injectDependencies() - } - - override fun onDestroy() { - super.onDestroy() - releaseDependencies() - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - _binding = DialogDashboardKeychainPhraseBinding.inflate(inflater, container, false) - return _binding?.root - } +class DashboardMnemonicReminderDialog : + BaseMnemonicFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) vm.sendShowEvent(EventsDictionary.Type.firstSession) - setBlur() - binding.keychain.setOnClickListener { - if (binding.keychain.layerType == View.LAYER_TYPE_SOFTWARE) { - removeBlur() - } - } - binding.btnCopy.setOnClickListener { - vm.onCopyClickedFromScreenSettings() - copyMnemonicToClipboard() - } - binding.root.setOnClickListener { - setBlur() - } - - vm.state.observe(viewLifecycleOwner, this) } - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } - - private fun copyMnemonicToClipboard() { - try { - val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - val clip = ClipData.newPlainText(MNEMONIC_LABEL, binding.keychain.text.toString()) - clipboard.setPrimaryClip(clip) - toast("Recovery phrase copied to clipboard.") - } catch (e: Exception) { - toast("Could not copy your recovery phrase. Please try again later, or copy it manually.") - } - } - - override fun onChanged(state: ViewState) { - when (state) { - is ViewState.Success -> { - binding.keychain.text = state.data - } - is ViewState.Error -> { - // TODO - } - is ViewState.Loading -> { - // TODO - } - is ViewState.Init -> { - - } - } - } - - private fun setBlur() = with(binding.keychain) { - setLayerType(View.LAYER_TYPE_SOFTWARE, null) - val radius = textSize / 3 - val filter = BlurMaskFilter(radius, BlurMaskFilter.Blur.NORMAL) - paint.maskFilter = filter - } - - private fun removeBlur() = with(binding.keychain) { - setLayerType(View.LAYER_TYPE_NONE, null) - paint.maskFilter = null - isFocusable = true - setTextIsSelectable(true) - } - - private fun injectDependencies() { + override fun injectDependencies() { componentManager().keychainPhraseComponent.get().inject(this) } - private fun releaseDependencies() { + override fun releaseDependencies() { componentManager().keychainPhraseComponent.release() } + override fun inflateBinding( + inflater: LayoutInflater, + container: ViewGroup? + ): DialogDashboardKeychainPhraseBinding { + return DialogDashboardKeychainPhraseBinding.inflate( + inflater, container, false + ) + } + + override val keychain: TextView by lazy { binding.keychain } + companion object { const val MNEMONIC_LABEL = "Your Anytype mnemonic phrase" } diff --git a/app/src/main/java/com/anytypeio/anytype/ui/profile/BackgroundColorSpan.kt b/app/src/main/java/com/anytypeio/anytype/ui/profile/BackgroundColorSpan.kt new file mode 100644 index 0000000000..5fe5c2e00a --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/profile/BackgroundColorSpan.kt @@ -0,0 +1,68 @@ +package com.anytypeio.anytype.ui.profile +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.RectF +import android.text.style.LineBackgroundSpan +import kotlin.math.abs + +class RoundedBackgroundColorSpan( + backgroundColor: Int, + private val padding: Float, + private val radius: Float, + private val lineHeight: Float +) : LineBackgroundSpan { + + companion object { + private const val NO_INIT = -1f + } + + private val rect = RectF() + private val paint = Paint().apply { + color = backgroundColor + isAntiAlias = true + } + + private var prevWidth = NO_INIT + private var prevRight = NO_INIT + + override fun drawBackground( + c: Canvas, + p: Paint, + left: Int, + right: Int, + top: Int, + baseline: Int, + bottom: Int, + text: CharSequence, + start: Int, + end: Int, + lineNumber: Int + ) { + + val actualWidth = p.measureText(text, start, end) + 2f * padding + val widthDiff = abs(prevWidth - actualWidth) + val diffIsShort = widthDiff < 2f * radius + + val width = if (lineNumber == 0) { + actualWidth + } else if ((actualWidth < prevWidth) && diffIsShort) { + prevWidth + } else if ((actualWidth > prevWidth) && diffIsShort) { + actualWidth + (2f * radius - widthDiff) + } else { + actualWidth + } + + val shiftLeft = 0f - padding + val shiftRight = width + shiftLeft + + val shiftGap = abs((bottom.toFloat() - top.toFloat() - lineHeight) /2f) + + rect.set(shiftLeft, top.toFloat() + shiftGap, shiftRight, bottom.toFloat() - shiftGap) + + c.drawRoundRect(rect, radius, radius, paint) + + prevWidth = width + prevRight = rect.right + } +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/profile/KeychainPhraseDialog.kt b/app/src/main/java/com/anytypeio/anytype/ui/profile/KeychainPhraseDialog.kt index 1425c3057b..8b107594e1 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/profile/KeychainPhraseDialog.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/profile/KeychainPhraseDialog.kt @@ -1,97 +1,22 @@ package com.anytypeio.anytype.ui.profile -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.graphics.BlurMaskFilter import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.fragment.app.viewModels -import androidx.lifecycle.Observer +import android.widget.TextView import com.anytypeio.anytype.core_utils.ext.arg -import com.anytypeio.anytype.core_utils.ext.toast -import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetFragment -import com.anytypeio.anytype.core_utils.ui.ViewState import com.anytypeio.anytype.databinding.DialogKeychainPhraseBinding import com.anytypeio.anytype.di.common.componentManager -import com.anytypeio.anytype.presentation.keychain.KeychainPhraseViewModel -import com.anytypeio.anytype.presentation.keychain.KeychainPhraseViewModelFactory -import javax.inject.Inject +import com.anytypeio.anytype.ui.dashboard.BaseMnemonicFragment -class KeychainPhraseDialog : BaseBottomSheetFragment(), Observer> { - - private val vm : KeychainPhraseViewModel by viewModels { factory } - - @Inject - lateinit var factory: KeychainPhraseViewModelFactory +class KeychainPhraseDialog : BaseMnemonicFragment() { private val screenType get() = arg(ARG_SCREEN_TYPE) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) vm.sendShowEvent(screenType) - setBlur() - binding.keychain.setOnClickListener { - if (binding.keychain.layerType == View.LAYER_TYPE_SOFTWARE) { - removeBlur() - } - } - binding.btnCopy.setOnClickListener { - vm.onCopyClickedFromLogout() - copyMnemonicToClipboard() - } - binding.root.setOnClickListener { - setBlur() - } - } - - private fun copyMnemonicToClipboard() { - try { - val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - val clip = ClipData.newPlainText(MNEMONIC_LABEL, binding.keychain.text.toString()) - clipboard.setPrimaryClip(clip) - toast("Recovery phrase copied to clipboard.") - } catch (e: Exception) { - toast("Could not copy your mnemonic. Please try again later, or copy it manually.") - } - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - vm.state.observe(viewLifecycleOwner, this) - } - - override fun onChanged(state: ViewState) { - when (state) { - is ViewState.Success -> { - binding.keychain.text = state.data - } - is ViewState.Error -> { - // TODO - } - is ViewState.Loading -> { - // TODO - } - ViewState.Init -> { - // Do nothing - } - } - } - - private fun setBlur() = with(binding.keychain) { - setLayerType(View.LAYER_TYPE_SOFTWARE, null) - val radius = textSize / 3 - val filter = BlurMaskFilter(radius, BlurMaskFilter.Blur.NORMAL) - paint.maskFilter = filter - } - - private fun removeBlur() = with(binding.keychain) { - setLayerType(View.LAYER_TYPE_NONE, null) - paint.maskFilter = null - isFocusable = true - setTextIsSelectable(true) } override fun injectDependencies() { @@ -109,8 +34,9 @@ class KeychainPhraseDialog : BaseBottomSheetFragment + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/rectangle_keychain_background.xml b/app/src/main/res/drawable/rectangle_keychain_background.xml deleted file mode 100644 index b27d4e8d32..0000000000 --- a/app/src/main/res/drawable/rectangle_keychain_background.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_dashboard_keychain_phrase.xml b/app/src/main/res/layout/dialog_dashboard_keychain_phrase.xml index cd99158b9d..458b61205e 100644 --- a/app/src/main/res/layout/dialog_dashboard_keychain_phrase.xml +++ b/app/src/main/res/layout/dialog_dashboard_keychain_phrase.xml @@ -4,20 +4,31 @@ xmlns:tools="http://schemas.android.com/tools" android:id="@+id/root" android:layout_width="match_parent" - android:layout_height="wrap_content" + android:layout_height="match_parent" tools:context="com.anytypeio.anytype.ui.dashboard.DashboardMnemonicReminderDialog"> + + + app:layout_constraintTop_toBottomOf="@+id/drag" /> - - \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_keychain_phrase.xml b/app/src/main/res/layout/dialog_keychain_phrase.xml index ea57376bd9..5ef416611b 100644 --- a/app/src/main/res/layout/dialog_keychain_phrase.xml +++ b/app/src/main/res/layout/dialog_keychain_phrase.xml @@ -4,7 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:id="@+id/root" android:layout_width="match_parent" - android:layout_height="wrap_content"> + android:layout_height="match_parent"> @@ -49,6 +49,7 @@ android:layout_marginStart="20dp" android:layout_marginTop="34dp" android:layout_marginEnd="20dp" + android:clickable="true" android:focusable="false" android:focusableInTouchMode="false" android:paddingStart="20dp" @@ -60,20 +61,4 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/subtitle" tools:text="@string/keychain_mock" /> - - - \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index f7acfa1252..23c650684d 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -31,6 +31,8 @@ 16dp 8dp + 5dp + 8dp 8dp 80dp 80dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e3b8235a86..83b5d25d8e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -86,7 +86,7 @@ Do the computation of an expensive paragraph of text on a background thread: Your public page Updates Log out - Your recovery phrase protects your account. You need it to sign in if you don\'t have access to your devices. Keep it in a safe place. + You’ll need it to sign in. Keep it in a safe place. If you lose it, you can no longer access your account. Back up your recovery \nphrase i\'ve written it down witch collapse practice feed shame open despair creek road again ice least lake tree young address brain envelope diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 34180c5ec2..0edd995805 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -153,10 +153,10 @@ diff --git a/core-ui/src/main/res/values/colors.xml b/core-ui/src/main/res/values/colors.xml index de003d9319..bc17581079 100644 --- a/core-ui/src/main/res/values/colors.xml +++ b/core-ui/src/main/res/values/colors.xml @@ -110,6 +110,7 @@ #929082 #929082 #929082 + #1BA0EB #FFB522 #CBC9BD diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/keychain/KeychainPhraseViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/keychain/KeychainPhraseViewModel.kt index 3f34f7ecdb..646e1fe867 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/keychain/KeychainPhraseViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/keychain/KeychainPhraseViewModel.kt @@ -10,7 +10,6 @@ import com.anytypeio.anytype.analytics.base.EventsDictionary.keychainPhraseScree import com.anytypeio.anytype.analytics.base.EventsPropertiesKey import com.anytypeio.anytype.analytics.base.sendEvent import com.anytypeio.anytype.analytics.props.Props -import com.anytypeio.anytype.core_utils.ui.ViewState import com.anytypeio.anytype.core_utils.ui.ViewStateViewModel import com.anytypeio.anytype.domain.auth.interactor.GetMnemonic import timber.log.Timber @@ -18,10 +17,13 @@ import timber.log.Timber class KeychainPhraseViewModel( private val getMnemonic: GetMnemonic, private val analytics: Analytics -) : ViewStateViewModel>() { +) : ViewStateViewModel() { + + private var mnemonic: String = "" init { proceedWithGettingMnemonic() + stateData.postValue(KeychainViewState.Blurred) } fun sendShowEvent(type: String) { @@ -34,7 +36,7 @@ class KeychainPhraseViewModel( ) } - fun onCopyClickedFromScreenSettings() { + private fun copyClickedFromScreenSettings() { viewModelScope.sendEvent( analytics = analytics, eventName = keychainCopy, @@ -44,21 +46,23 @@ class KeychainPhraseViewModel( ) } - fun onCopyClickedFromLogout() { - viewModelScope.sendEvent( - analytics = analytics, - eventName = keychainCopy, - props = Props( - mapOf(EventsPropertiesKey.type to EventsDictionary.Type.beforeLogout) - ) - ) + fun onKeychainClicked() { + stateData.postValue(KeychainViewState.Displayed(mnemonic)) + copyClickedFromScreenSettings() + } + + fun onRootClicked() { + stateData.postValue(KeychainViewState.Blurred) } private fun proceedWithGettingMnemonic() { getMnemonic.invoke(viewModelScope, Unit) { result -> result.either( fnL = { e -> Timber.e(e, "Error while getting mnemonic") }, - fnR = { stateData.postValue(ViewState.Success(it)) } + fnR = { mnemonic -> + this.mnemonic = mnemonic + Any() + } ) } } diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/keychain/KeychainViewState.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/keychain/KeychainViewState.kt new file mode 100644 index 0000000000..5be329b493 --- /dev/null +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/keychain/KeychainViewState.kt @@ -0,0 +1,6 @@ +package com.anytypeio.anytype.presentation.keychain + +sealed class KeychainViewState { + data class Displayed(val mnemonic: String) : KeychainViewState() + object Blurred : KeychainViewState() +} \ No newline at end of file