Implement first-time-nav screen for stories.

fork-5.53.8
Alex Hart 2022-06-24 15:34:17 -03:00 zatwierdzone przez Cody Henthorne
rodzic 858c7a7f2e
commit 521bd2cce4
16 zmienionych plików z 2840 dodań i 103 usunięć

Wyświetl plik

@ -34,11 +34,22 @@ internal class StoryValues(store: KeyValueStore) : SignalStoreValues(store) {
* Cannot send to story tooltip marker
*/
private const val CANNOT_SEND_SEEN_MARKER = "stories.cannot.send.video.tooltip.seen"
/**
* Whether or not the user has see the "Navigation education" view
*/
private const val USER_HAS_SEEN_FIRST_NAV_VIEW = "stories.user.has.seen.first.navigation.view"
}
override fun onFirstEverAppLaunch() = Unit
override fun getKeysToIncludeInBackup(): MutableList<String> = mutableListOf(MANUAL_FEATURE_DISABLE, USER_HAS_ADDED_TO_A_STORY, VIDEO_TOOLTIP_SEEN_MARKER, CANNOT_SEND_SEEN_MARKER)
override fun getKeysToIncludeInBackup(): MutableList<String> = mutableListOf(
MANUAL_FEATURE_DISABLE,
USER_HAS_ADDED_TO_A_STORY,
VIDEO_TOOLTIP_SEEN_MARKER,
CANNOT_SEND_SEEN_MARKER,
USER_HAS_SEEN_FIRST_NAV_VIEW
)
var isFeatureDisabled: Boolean by booleanValue(MANUAL_FEATURE_DISABLE, false)
@ -50,6 +61,8 @@ internal class StoryValues(store: KeyValueStore) : SignalStoreValues(store) {
var cannotSendTooltipSeen by booleanValue(CANNOT_SEND_SEEN_MARKER, false)
var userHasSeenFirstNavView: Boolean by booleanValue(USER_HAS_SEEN_FIRST_NAV_VIEW, false)
fun setLatestStorySend(storySend: StorySend) {
synchronized(this) {
val storySends: List<StorySend> = getList(LATEST_STORY_SENDS, StorySendSerializer)

Wyświetl plik

@ -0,0 +1,120 @@
package org.thoughtcrime.securesms.stories
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.os.Build
import android.util.AttributeSet
import android.view.View
import android.widget.ImageView
import androidx.constraintlayout.widget.ConstraintLayout
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
import org.signal.core.util.DimensionUnit
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.blurhash.BlurHash
import org.thoughtcrime.securesms.components.CornerMask
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.util.visible
class StoryFirstTimeNavigationView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : ConstraintLayout(context, attrs) {
companion object {
private const val BLUR_ALPHA = 0x3D
private const val NO_BLUR_ALPHA = 0xB3
}
init {
inflate(context, R.layout.story_first_time_navigation_view, this)
}
private val blurHashView: ImageView = findViewById(R.id.edu_blur_hash)
private val overlayView: ImageView = findViewById(R.id.edu_overlay)
private val gotIt: View = findViewById(R.id.edu_got_it)
private val cornerMask = CornerMask(this).apply {
setRadius(DimensionUnit.DP.toPixels(18f).toInt())
}
var callback: Callback? = null
init {
if (isRenderEffectSupported()) {
blurHashView.visible = false
overlayView.visible = true
overlayView.setImageDrawable(ColorDrawable(Color.argb(BLUR_ALPHA, 0, 0, 0)))
}
gotIt.setOnClickListener {
callback?.onGotItClicked()
GlideApp.with(this).clear(blurHashView)
blurHashView.setImageDrawable(null)
hide()
}
}
override fun dispatchDraw(canvas: Canvas) {
super.dispatchDraw(canvas)
cornerMask.mask(canvas)
}
fun setBlurHash(blurHash: BlurHash?) {
if (isRenderEffectSupported() || callback?.userHasSeenFirstNavigationView() == true) {
return
}
if (blurHash == null) {
blurHashView.visible = false
overlayView.visible = true
overlayView.setImageDrawable(ColorDrawable(Color.argb(NO_BLUR_ALPHA, 0, 0, 0)))
GlideApp.with(this).clear(blurHashView)
return
} else {
blurHashView.visible = true
overlayView.visible = true
overlayView.setImageDrawable(ColorDrawable(Color.argb(BLUR_ALPHA, 0, 0, 0)))
}
GlideApp.with(this)
.load(blurHash)
.addListener(object : RequestListener<Drawable> {
override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>?, isFirstResource: Boolean): Boolean {
setBlurHash(null)
return false
}
override fun onResourceReady(resource: Drawable?, model: Any?, target: Target<Drawable>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
return false
}
})
.into(blurHashView)
}
fun show() {
if (callback?.userHasSeenFirstNavigationView() == true) {
return
}
visible = true
}
fun hide() {
visible = false
}
private fun isRenderEffectSupported(): Boolean {
return Build.VERSION.SDK_INT >= 31
}
interface Callback {
fun userHasSeenFirstNavigationView(): Boolean
fun onGotItClicked()
}
}

Wyświetl plik

@ -0,0 +1,44 @@
package org.thoughtcrime.securesms.stories.viewer.page
import android.view.ViewStub
import org.thoughtcrime.securesms.blurhash.BlurHash
import org.thoughtcrime.securesms.stories.StoryFirstTimeNavigationView
import org.thoughtcrime.securesms.util.views.Stub
/**
* Specialized stub that allows for early arrival of the blurhash and callback.
*/
class StoryFirstNavigationStub(viewStub: ViewStub) : Stub<StoryFirstTimeNavigationView>(viewStub) {
private var callback: StoryFirstTimeNavigationView.Callback? = null
private var blurHash: BlurHash? = null
fun setCallback(callback: StoryFirstTimeNavigationView.Callback) {
if (resolved()) {
get().callback = callback
} else {
this.callback = callback
}
}
fun setBlurHash(blurHash: BlurHash?) {
if (resolved()) {
get().setBlurHash(blurHash)
} else {
this.blurHash = blurHash
}
}
override fun get(): StoryFirstTimeNavigationView {
val needsResolve = !resolved()
val view = super.get()
if (needsResolve) {
view.setBlurHash(blurHash)
view.callback = callback
blurHash = null
callback = null
}
return view
}
}

Wyświetl plik

@ -4,8 +4,11 @@ import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.RenderEffect
import android.graphics.Shader
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.view.GestureDetector
import android.view.MotionEvent
@ -20,6 +23,7 @@ import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.view.GestureDetectorCompat
import androidx.core.view.animation.PathInterpolatorCompat
import androidx.core.view.doOnNextLayout
import androidx.core.view.isVisible
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
@ -42,11 +46,13 @@ import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectFor
import org.thoughtcrime.securesms.database.AttachmentDatabase
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mediapreview.MediaPreviewFragment
import org.thoughtcrime.securesms.mediapreview.VideoControlsDelegate
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.stories.StoryFirstTimeNavigationView
import org.thoughtcrime.securesms.stories.StorySlateView
import org.thoughtcrime.securesms.stories.dialogs.StoryContextMenu
import org.thoughtcrime.securesms.stories.viewer.StoryViewerState
@ -78,7 +84,8 @@ class StoryViewerPageFragment :
MultiselectForwardBottomSheet.Callback,
StorySlateView.Callback,
StoryTextPostPreviewFragment.Callback,
StoriesSharedElementCrossFaderView.Callback {
StoriesSharedElementCrossFaderView.Callback,
StoryFirstTimeNavigationView.Callback {
private lateinit var progressBar: SegmentedProgressBar
private lateinit var storySlate: StorySlateView
@ -87,6 +94,7 @@ class StoryViewerPageFragment :
private lateinit var blurContainer: ImageView
private lateinit var storyCaptionContainer: FrameLayout
private lateinit var storyContentContainer: FrameLayout
private lateinit var storyFirstTimeNavigationViewStub: StoryFirstNavigationStub
private lateinit var callback: Callback
@ -150,9 +158,11 @@ class StoryViewerPageFragment :
progressBar = view.findViewById(R.id.progress)
viewsAndReplies = view.findViewById(R.id.views_and_replies_bar)
storyCrossfader = view.findViewById(R.id.story_content_crossfader)
storyFirstTimeNavigationViewStub = StoryFirstNavigationStub(view.findViewById(R.id.story_first_time_nav_stub))
storySlate.callback = this
storyCrossfader.callback = this
storyFirstTimeNavigationViewStub.setCallback(this)
chrome = listOf(
closeView,
@ -165,7 +175,7 @@ class StoryViewerPageFragment :
viewsAndReplies,
progressBar,
storyGradientTop,
storyGradientBottom
storyGradientBottom,
)
senderAvatar.setFallbackPhotoProvider(FallbackPhotoProvider())
@ -338,20 +348,37 @@ class StoryViewerPageFragment :
resumeProgress()
}
val wasDisplayingNavigationView = isFirstTimeNavVisible()
when {
state.hideChromeImmediate -> {
hideChromeImmediate()
storyCaptionContainer.visible = false
storyFirstTimeNavigationViewStub.takeIf { it.resolved() }?.get()?.hide()
}
state.hideChrome -> {
hideChrome()
storyCaptionContainer.visible = true
storyFirstTimeNavigationViewStub.get().show()
}
else -> {
showChrome()
storyCaptionContainer.visible = true
storyFirstTimeNavigationViewStub.get().show()
}
}
val isDisplayingNavigationView = isFirstTimeNavVisible()
if (isDisplayingNavigationView && Build.VERSION.SDK_INT >= 31) {
hideChromeImmediate()
storyContentContainer.setRenderEffect(RenderEffect.createBlurEffect(100f, 100f, Shader.TileMode.CLAMP))
} else if (Build.VERSION.SDK_INT >= 31) {
storyContentContainer.setRenderEffect(null)
}
if (wasDisplayingNavigationView xor isDisplayingNavigationView) {
viewModel.setIsDisplayingFirstTimeNavigation(isFirstTimeNavVisible())
}
}
timeoutDisposable.bindTo(viewLifecycleOwner)
@ -402,6 +429,10 @@ class StoryViewerPageFragment :
viewModel.setIsDisplayingForwardDialog(false)
}
private fun isFirstTimeNavVisible(): Boolean {
return storyFirstTimeNavigationViewStub.takeIf { it.resolved() }?.get()?.isVisible ?: false
}
private fun calculateDurationForText(textContent: StoryPost.Content.TextContent): Long {
val divisionsOf15 = textContent.length / CHARACTERS_PER_SECOND
return TimeUnit.SECONDS.toMillis(divisionsOf15) + MIN_TEXT_STORY_PLAYBACK
@ -623,6 +654,8 @@ class StoryViewerPageFragment :
val record = storyPost.conversationMessage.messageRecord as? MediaMmsMessageRecord
val blurHash = record?.slideDeck?.thumbnailSlide?.placeholderBlur
storyFirstTimeNavigationViewStub.setBlurHash(blurHash)
if (blurHash == null) {
GlideApp.with(blur).clear(blur)
} else {
@ -1035,4 +1068,13 @@ class StoryViewerPageFragment :
fun onFinishedPosts(recipientId: RecipientId)
fun onStoryHidden(recipientId: RecipientId)
}
override fun userHasSeenFirstNavigationView(): Boolean {
return SignalStore.storyValues().userHasSeenFirstNavView
}
override fun onGotItClicked() {
SignalStore.storyValues().userHasSeenFirstNavView = true
viewModel.setIsDisplayingFirstTimeNavigation(false)
}
}

Wyświetl plik

@ -214,6 +214,10 @@ class StoryViewerPageViewModel(
storyViewerPlaybackStore.update { it.copy(isRunningSharedElementAnimation = isRunningSharedElementAnimation) }
}
fun setIsDisplayingFirstTimeNavigation(isDisplayingFirstTimeNavigation: Boolean) {
storyViewerPlaybackStore.update { it.copy(isDisplayingFirstTimeNavigation = isDisplayingFirstTimeNavigation) }
}
private fun resolveSwipeToReplyState(state: StoryViewerPageState, index: Int): StoryViewerPageState.ReplyState {
if (index !in state.posts.indices) {
return StoryViewerPageState.ReplyState.NONE

Wyświetl plik

@ -15,7 +15,8 @@ data class StoryViewerPlaybackState(
val isFragmentResumed: Boolean = false,
val isDisplayingLinkPreviewTooltip: Boolean = false,
val isDisplayingReactionAnimation: Boolean = false,
val isRunningSharedElementAnimation: Boolean = false
val isRunningSharedElementAnimation: Boolean = false,
val isDisplayingFirstTimeNavigation: Boolean = false
) {
val hideChromeImmediate: Boolean = isRunningSharedElementAnimation
@ -36,5 +37,6 @@ data class StoryViewerPlaybackState(
!isFragmentResumed ||
isDisplayingLinkPreviewTooltip ||
isDisplayingReactionAnimation ||
isRunningSharedElementAnimation
isRunningSharedElementAnimation ||
isDisplayingFirstTimeNavigation
}

Wyświetl plik

@ -0,0 +1,25 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="72dp"
android:height="72dp"
android:viewportWidth="72"
android:viewportHeight="72">
<path
android:pathData="M38.773,12.241H54.773C57.964,12.241 60.551,14.851 60.551,18.069V53.931C60.551,57.15 57.964,59.759 54.773,59.759H38.773C36.699,59.759 34.879,58.656 33.86,57H31.372C32.569,59.934 35.432,62 38.773,62H54.773C59.191,62 62.773,58.387 62.773,53.931V18.069C62.773,13.613 59.191,10 54.773,10H38.773C34.714,10 31.361,13.049 30.843,17H33.092C33.59,14.292 35.944,12.241 38.773,12.241Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
<group>
<clip-path
android:pathData="M55.131,28l-0,16l-16,0l-0,-16z"/>
<path
android:pathData="M47.646,41.514L46.585,40.453L49.456,37.583L50.581,36.78L38.131,36.78L38.131,35.28L50.581,35.28L49.54,34.536L46.582,31.542L47.649,30.488L53.128,36.033L47.646,41.514Z"
android:fillColor="#000000"/>
<path
android:pathData="M53.481,36.032L47.646,41.866L46.232,40.451L49.294,37.39L49.801,37.028L37.881,37.028L37.881,35.028L49.801,35.028L49.377,34.725L46.229,31.538L47.651,30.133L53.481,36.032ZM47.647,30.84L46.936,31.542L49.703,34.343L51.361,35.528L38.381,35.528L38.381,36.528L51.362,36.528L49.618,37.773L46.939,40.451L47.646,41.159L52.776,36.03L47.647,30.84Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
</group>
<path
android:pathData="M31.373,24.166C31.639,24.466 31.845,24.815 31.976,25.194C32.108,25.573 32.164,25.975 32.141,26.375C32.118,26.776 32.015,27.168 31.841,27.529C31.666,27.89 31.422,28.213 31.123,28.479C31.123,28.48 31.122,28.48 31.122,28.481L28.154,31.142C28.481,31.313 28.775,31.542 29.021,31.818C29.288,32.118 29.493,32.467 29.625,32.846C29.757,33.225 29.812,33.627 29.789,34.027C29.766,34.428 29.664,34.82 29.489,35.181C29.429,35.305 29.361,35.424 29.285,35.538C29.635,35.709 29.953,35.947 30.218,36.243C30.759,36.847 31.038,37.64 30.994,38.45C30.962,39.03 30.766,39.584 30.435,40.051C30.79,40.223 31.112,40.465 31.379,40.766C31.918,41.371 32.194,42.166 32.147,42.975C32.1,43.784 31.734,44.541 31.128,45.08C31.121,45.087 31.114,45.093 31.106,45.099L30.599,45.525L30.172,45.903C29.898,46.146 29.511,46.489 29.061,46.888C28.161,47.688 27.011,48.711 26.007,49.612C25.963,49.651 25.917,49.693 25.87,49.736C24.854,50.651 22.953,52.364 20.465,53.036C19.135,53.395 17.636,53.459 16.024,52.97C14.417,52.482 12.757,51.463 11.068,49.746C9.342,47.991 8.376,46.117 7.988,44.145C7.603,42.187 7.8,40.197 8.295,38.219C8.788,36.246 9.591,34.237 10.457,32.235C10.734,31.594 11.016,30.957 11.298,30.32C11.907,28.945 12.516,27.571 13.071,26.167C13.631,24.603 15.219,24.061 16.475,24.459C17.12,24.664 17.707,25.12 18.034,25.817C18.361,26.513 18.382,27.343 18.108,28.225C17.597,29.919 17.119,31.695 16.749,33.133L16.857,33.043L27.056,23.917L27.058,23.916C27.358,23.649 27.707,23.444 28.087,23.312C28.465,23.18 28.867,23.124 29.267,23.147C29.668,23.171 30.06,23.273 30.421,23.447C30.783,23.622 31.106,23.867 31.373,24.166ZM15.041,35.872C14.068,35.644 14.068,35.644 14.068,35.644L14.079,35.595L14.112,35.454C14.141,35.332 14.184,35.153 14.239,34.927C14.349,34.474 14.507,33.832 14.7,33.071C15.087,31.551 15.619,29.551 16.195,27.644L16.197,27.635C16.359,27.115 16.293,26.815 16.224,26.667C16.154,26.519 16.033,26.417 15.871,26.366C15.519,26.254 15.106,26.404 14.951,26.849C14.946,26.861 14.942,26.873 14.937,26.885C14.365,28.334 13.726,29.777 13.107,31.176C12.829,31.804 12.555,32.422 12.293,33.029C11.434,35.014 10.687,36.898 10.235,38.705C9.784,40.506 9.641,42.183 9.95,43.759C10.257,45.319 11.022,46.847 12.494,48.343C14.002,49.876 15.383,50.685 16.605,51.056C17.819,51.425 18.934,51.377 19.944,51.105C21.958,50.561 23.547,49.134 24.584,48.202C24.614,48.175 24.643,48.149 24.671,48.124C25.678,47.22 26.832,46.193 27.733,45.393C28.183,44.993 28.571,44.649 28.846,44.405L29.283,44.018C29.29,44.013 29.297,44.007 29.303,44.001L29.808,43.578C30.011,43.392 30.135,43.134 30.151,42.859C30.167,42.579 30.071,42.305 29.885,42.096C29.699,41.886 29.437,41.76 29.158,41.743C28.878,41.727 28.604,41.823 28.394,42.009C28.337,42.06 28.275,42.103 28.211,42.139L25.892,44.206C25.48,44.574 24.848,44.537 24.48,44.125C24.113,43.713 24.149,43.081 24.561,42.713L28.647,39.07C28.647,39.069 28.647,39.07 28.647,39.07C28.855,38.883 28.982,38.62 28.997,38.341C29.012,38.061 28.916,37.787 28.729,37.578C28.542,37.37 28.279,37.244 28,37.229C27.72,37.214 27.446,37.31 27.237,37.497C27.19,37.539 27.141,37.576 27.089,37.607L23.621,40.649C23.206,41.013 22.574,40.972 22.21,40.556C21.846,40.141 21.887,39.509 22.302,39.145L27.443,34.637C27.545,34.545 27.629,34.434 27.689,34.31C27.749,34.185 27.784,34.05 27.792,33.911C27.801,33.773 27.781,33.634 27.736,33.503C27.69,33.372 27.619,33.251 27.527,33.148C27.435,33.044 27.323,32.96 27.198,32.9C27.074,32.839 26.938,32.804 26.8,32.796C26.661,32.788 26.523,32.807 26.392,32.853C26.261,32.898 26.14,32.969 26.036,33.061C25.993,33.1 25.947,33.134 25.899,33.164L20.923,37.626C20.512,37.995 19.879,37.96 19.511,37.549C19.142,37.138 19.177,36.506 19.588,36.137L29.789,26.989L29.792,26.987C29.896,26.895 29.98,26.783 30.041,26.658C30.101,26.533 30.136,26.398 30.144,26.259C30.152,26.121 30.133,25.982 30.087,25.851C30.042,25.72 29.971,25.6 29.879,25.496C29.787,25.392 29.675,25.308 29.55,25.248C29.425,25.187 29.29,25.152 29.151,25.144C29.013,25.136 28.874,25.155 28.743,25.201C28.613,25.246 28.492,25.317 28.389,25.409C28.389,25.409 28.388,25.409 28.388,25.409L18.179,34.544C18.174,34.548 18.17,34.551 18.166,34.555C18.162,34.558 18.159,34.561 18.155,34.565L15.684,36.638C15.354,36.914 14.885,36.949 14.518,36.724C14.151,36.498 13.97,36.063 14.068,35.644L15.041,35.872Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
</vector>

Wyświetl plik

@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="72dp"
android:height="72dp"
android:viewportWidth="72"
android:viewportHeight="72">
<group>
<clip-path
android:pathData="M0,0h72v72h-72z"/>
<path
android:pathData="M21,9.241H37C40.191,9.241 42.778,11.851 42.778,15.069V34H45V15.069C45,10.613 41.418,7 37,7H21C16.582,7 13,10.613 13,15.069V50.931C13,55.387 16.582,59 21,59H26.642V56.759H21C17.809,56.759 15.222,54.15 15.222,50.931V15.069C15.222,11.851 17.809,9.241 21,9.241Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
<group>
<clip-path
android:pathData="M21.358,23h16v16h-16z"/>
<path
android:pathData="M34.871,30.486L33.81,31.547L30.94,28.676L30.137,27.551V40.001H28.637V27.551L27.893,28.592L24.899,31.55L23.845,30.483L29.39,25.004L34.871,30.486Z"
android:strokeWidth="0.5"
android:fillColor="#000000"
android:strokeColor="#000000"/>
</group>
<group>
<clip-path
android:pathData="M19.358,56.125l21.16,-29.125l29.125,21.16l-21.16,29.125z"/>
<path
android:pathData="M47.663,51.466L44.216,45.556C44.078,45.324 43.896,45.121 43.68,44.959C43.464,44.798 43.218,44.68 42.956,44.613C42.695,44.546 42.423,44.532 42.155,44.57C41.888,44.608 41.631,44.699 41.399,44.837C41.167,44.974 40.964,45.157 40.802,45.373C40.64,45.589 40.522,45.835 40.456,46.096C40.389,46.358 40.374,46.63 40.413,46.897C40.451,47.164 40.542,47.422 40.679,47.654M51.592,49.922L48.794,45.214C48.514,44.746 48.06,44.408 47.531,44.275C47.002,44.142 46.442,44.225 45.974,44.505C45.506,44.784 45.168,45.239 45.035,45.768C44.902,46.296 44.985,46.856 45.265,47.324M44.186,53.547L37.153,41.788C37.016,41.555 36.833,41.353 36.617,41.191C36.401,41.029 36.155,40.911 35.894,40.845C35.632,40.778 35.36,40.763 35.093,40.801C34.826,40.84 34.569,40.93 34.336,41.068C34.104,41.206 33.901,41.388 33.739,41.604C33.578,41.82 33.46,42.066 33.393,42.328C33.326,42.589 33.312,42.861 33.35,43.129C33.388,43.396 33.479,43.653 33.617,43.885L40.637,55.652L42.2,58.473C42.2,58.473 38.299,56.725 34.81,54.886C32.219,53.492 30.969,56.621 32.821,57.703C40.358,62.513 46.908,69.224 54.509,64.187C62.11,59.15 58.31,53.162 56.923,50.844C55.535,48.526 53.776,45.539 53.776,45.539L53.449,44.949C53.171,44.48 52.718,44.14 52.189,44.006C51.661,43.871 51.101,43.951 50.632,44.229C50.163,44.507 49.824,44.96 49.689,45.489C49.554,46.017 49.634,46.577 49.912,47.046"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
</group>
</group>
</vector>

File diff suppressed because one or more lines are too long

Wyświetl plik

@ -122,8 +122,8 @@
android:layout_marginEnd="16dp"
android:layout_marginBottom="24dp"
android:gravity="bottom"
android:textColor="@color/signal_colorNeutralInverse"
android:textAppearance="@style/Signal.Text.BodyLarge"
android:textColor="@color/signal_colorNeutralInverse"
tools:text="Ugh." />
</FrameLayout>
@ -217,8 +217,8 @@
android:layout_marginStart="8dp"
android:layout_marginEnd="12dp"
android:alpha="0.64"
android:textColor="@color/signal_colorNeutralVariantInverse"
android:textAppearance="@style/Signal.Text.MaterialCaption"
android:textColor="@color/signal_colorNeutralVariantInverse"
app:layout_constraintBottom_toBottomOf="@id/from"
app:layout_constraintEnd_toStartOf="@id/more"
app:layout_constraintStart_toEndOf="@id/from"
@ -278,4 +278,13 @@
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ViewStub
android:id="@+id/story_first_time_nav_stub"
android:layout_width="match_parent"
android:layout_height="0dp"
android:inflatedId="@+id/story_first_time_navigation_view"
android:layout="@layout/story_viewer_first_time_navigation_stub"
app:layout_constraintBottom_toBottomOf="@id/story_content_card_touch_interceptor"
app:layout_constraintTop_toTopOf="@id/story_content_card_touch_interceptor" />
</androidx.constraintlayout.widget.ConstraintLayout>

Wyświetl plik

@ -0,0 +1,111 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
<ImageView
android:id="@+id/edu_blur_hash"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:importantForAccessibility="no" />
<ImageView
android:id="@+id/edu_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@color/transparent_black_70"
android:importantForAccessibility="no" />
<ImageView
android:id="@+id/edu_tap_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:importantForAccessibility="no"
app:layout_constraintBottom_toTopOf="@id/edu_swipe_up_icon"
app:layout_constraintEnd_toStartOf="@id/edu_tap_label"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_tap_72"
app:tint="@color/core_white" />
<TextView
android:id="@+id/edu_tap_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/StoryFirstTimeNavigationView__tap_to_advance"
android:textAppearance="@style/Signal.Text.BodyLarge"
android:textColor="@color/core_white"
app:layout_constraintBottom_toBottomOf="@id/edu_tap_icon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/edu_tap_icon"
app:layout_constraintTop_toTopOf="@id/edu_tap_icon" />
<ImageView
android:id="@+id/edu_swipe_up_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:importantForAccessibility="no"
app:layout_constraintBottom_toTopOf="@id/edu_swipe_right_icon"
app:layout_constraintEnd_toStartOf="@id/edu_swipe_up_label"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/edu_tap_icon"
app:srcCompat="@drawable/ic_swipe_up_72"
app:tint="@color/core_white" />
<TextView
android:id="@+id/edu_swipe_up_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/StoryFirstTimeNavigationView__swipe_up_to_skip"
android:textAppearance="@style/Signal.Text.BodyLarge"
android:textColor="@color/core_white"
app:layout_constraintBottom_toBottomOf="@id/edu_swipe_up_icon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/edu_swipe_up_icon"
app:layout_constraintTop_toTopOf="@id/edu_swipe_up_icon" />
<ImageView
android:id="@+id/edu_swipe_right_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:importantForAccessibility="no"
app:layout_constraintBottom_toTopOf="@id/edu_got_it"
app:layout_constraintEnd_toStartOf="@id/edu_swipe_right_label"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/edu_swipe_up_icon"
app:srcCompat="@drawable/ic_swipe_right_72"
app:tint="@color/core_white" />
<TextView
android:id="@+id/edu_swipe_right_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/StoryFirstTimeNavigationView__swipe_right_to_exit"
android:textAppearance="@style/Signal.Text.BodyLarge"
android:textColor="@color/core_white"
app:layout_constraintBottom_toBottomOf="@id/edu_swipe_right_icon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/edu_swipe_right_icon"
app:layout_constraintTop_toTopOf="@id/edu_swipe_right_icon" />
<com.google.android.material.button.MaterialButton
android:id="@+id/edu_got_it"
style="@style/Signal.Widget.Button.Medium.Tonal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/StoryFirstTimeNagivationView__got_it"
android:textColor="@color/signal_colorNeutral"
app:backgroundTint="@color/signal_colorNeutralInverse"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/edu_swipe_right_icon" />
</merge>

Wyświetl plik

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.stories.StoryFirstTimeNavigationView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="@id/story_content_card_touch_interceptor"
app:layout_constraintTop_toTopOf="@id/story_content_card_touch_interceptor" />

Wyświetl plik

@ -4903,6 +4903,15 @@
<!-- Gift expiry expired -->
<string name="Gifts__expired">Expired</string>
<!-- Label indicating that a user can tap to advance to the next post in a story -->
<string name="StoryFirstTimeNavigationView__tap_to_advance">Tap to advance</string>
<!-- Label indicating swipe direction to skip current story -->
<string name="StoryFirstTimeNavigationView__swipe_up_to_skip">Swipe up to skip</string>
<!-- Label indicating swipe direction to exit story viewer -->
<string name="StoryFirstTimeNavigationView__swipe_right_to_exit">Swipe right to exit</string>
<!-- Button label to confirm understanding of story navigation -->
<string name="StoryFirstTimeNagivationView__got_it">Got it</string>
<!-- EOF -->
</resources>

Wyświetl plik

@ -0,0 +1,159 @@
package org.thoughtcrime.securesms.stories
import android.app.Application
import android.graphics.drawable.Drawable
import android.os.Looper.getMainLooper
import android.view.ContextThemeWrapper
import android.view.View
import android.widget.ImageView
import androidx.test.core.app.ApplicationProvider
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockedStatic
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
import org.mockito.kotlin.any
import org.mockito.kotlin.eq
import org.mockito.kotlin.never
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
import org.thoughtcrime.securesms.blurhash.BlurHash
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.mms.GlideRequest
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.util.visible
@RunWith(RobolectricTestRunner::class)
@Config(application = Application::class)
class StoryFirstTimeNavigationViewTest {
@Rule
@JvmField
val mockitoRule: MockitoRule = MockitoJUnit.rule()
private lateinit var testSubject: StoryFirstTimeNavigationView
@Mock
private lateinit var glideApp: MockedStatic<GlideApp>
@Mock
private lateinit var glideRequests: GlideRequests
@Mock
private lateinit var glideRequest: GlideRequest<Drawable>
@Before
fun setUp() {
testSubject = StoryFirstTimeNavigationView(ContextThemeWrapper(ApplicationProvider.getApplicationContext(), org.thoughtcrime.securesms.R.style.Signal_DayNight))
whenever(GlideApp.with(any<View>())).thenReturn(glideRequests)
whenever(glideRequests.load(any<BlurHash>())).thenReturn(glideRequest)
whenever(glideRequest.addListener(any())).thenReturn(glideRequest)
}
@Test
@Config(sdk = [31])
fun `Given sdk 31, when I create testSubject, then I expect overlay visible and blur hash not visible`() {
shadowOf(getMainLooper()).idle()
assertTrue(testSubject.findViewById<View>(org.thoughtcrime.securesms.R.id.edu_overlay).visible)
assertFalse(testSubject.findViewById<View>(org.thoughtcrime.securesms.R.id.edu_blur_hash).visible)
}
@Test
@Config(sdk = [30])
fun `Given sdk 30, when I create testSubject, then I expect overlay visible and blur hash visible`() {
shadowOf(getMainLooper()).idle()
assertTrue(testSubject.findViewById<View>(org.thoughtcrime.securesms.R.id.edu_overlay).visible)
assertTrue(testSubject.findViewById<View>(org.thoughtcrime.securesms.R.id.edu_blur_hash).visible)
}
@Test
@Config(sdk = [31])
fun `Given sdk 31 when I set blur hash, then blur has is visible`() {
shadowOf(getMainLooper()).idle()
testSubject.setBlurHash(BlurHash.parseOrNull("0000")!!)
assertFalse(testSubject.findViewById<View>(org.thoughtcrime.securesms.R.id.edu_blur_hash).visible)
}
@Test
@Config(sdk = [30])
fun `Given sdk 30, when I set blur hash, then blur hash is loaded`() {
shadowOf(getMainLooper()).idle()
testSubject.setBlurHash(BlurHash.parseOrNull("0000")!!)
val blurHashView = testSubject.findViewById<ImageView>(org.thoughtcrime.securesms.R.id.edu_blur_hash)
verify(glideRequest).into(eq(blurHashView))
}
@Test
@Config(sdk = [30])
fun `Given sdk 30, when I set blur hash to null, then blur hash is hidden and cleared`() {
shadowOf(getMainLooper()).idle()
testSubject.setBlurHash(null)
val blurHashView = testSubject.findViewById<ImageView>(org.thoughtcrime.securesms.R.id.edu_blur_hash)
assertFalse(blurHashView.visible)
verify(glideRequests).clear(blurHashView)
}
@Test
@Config(sdk = [30])
fun `Given sdk 30 and user has seen overlay, when I set blur hash, then nothing happens`() {
shadowOf(getMainLooper()).idle()
testSubject.callback = object : StoryFirstTimeNavigationView.Callback {
override fun userHasSeenFirstNavigationView(): Boolean = true
override fun onGotItClicked() = error("Unused")
}
testSubject.setBlurHash(BlurHash.parseOrNull("0000")!!)
val blurHashView = testSubject.findViewById<ImageView>(org.thoughtcrime.securesms.R.id.edu_blur_hash)
verify(glideRequest, never()).into(eq(blurHashView))
}
@Test
@Config
fun `When I hide then I expect not to be visible`() {
testSubject.hide()
assertFalse(testSubject.visible)
}
@Test
@Config
fun `Given I hide, when I show, then I expect to be visible`() {
testSubject.hide()
testSubject.show()
assertTrue(testSubject.visible)
}
@Test
@Config
fun `Given I hide and user has seen overlay, when I show, then I expect to not be visible`() {
testSubject.hide()
testSubject.callback = object : StoryFirstTimeNavigationView.Callback {
override fun userHasSeenFirstNavigationView(): Boolean = true
override fun onGotItClicked() = error("Unused")
}
testSubject.show()
assertFalse(testSubject.visible)
}
}

Wyświetl plik

@ -123,14 +123,15 @@ dependencyResolutionManagement {
}
testLibs {
version('robolectric', '4.4')
version('robolectric', '4.8.1')
version('androidx-test', '1.4.0')
alias('junit-junit').to('junit:junit:4.13.2')
alias('androidx-test-core').to('androidx.test:core:1.2.0')
alias('androidx-test-core-ktx').to('androidx.test:core-ktx:1.2.0')
alias('androidx-test-core').to('androidx.test', 'core').versionRef('androidx-test')
alias('androidx-test-core-ktx').to('androidx.test', 'core-ktx').versionRef('androidx-test')
alias('androidx-test-ext-junit').to('androidx.test.ext:junit:1.1.1')
alias('androidx-test-ext-junit-ktx').to('androidx.test.ext:junit-ktx:1.1.1')
alias('espresso-core').to('androidx.test.espresso:espresso-core:3.2.0')
alias('espresso-core').to('androidx.test.espresso:espresso-core:3.4.0')
alias('mockito-core').to('org.mockito:mockito-inline:4.4.0')
alias('mockito-kotlin').to('org.mockito.kotlin:mockito-kotlin:4.0.0')
alias('robolectric-robolectric').to('org.robolectric', 'robolectric').versionRef('robolectric')