From 2483a929759527bb7f620c9d9a8b6076145ac502 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Wed, 2 Mar 2022 14:20:49 -0400 Subject: [PATCH] Implement story error slates. Co-authored-by: Rashad Sookram --- .../mediapreview/MediaPreviewFragment.java | 3 +- .../securesms/stories/StorySlateView.kt | 169 ++++++++++++++++++ .../viewer/page/StoryViewerPageFragment.kt | 117 +++++++++--- .../viewer/page/StoryViewerPageRepository.kt | 14 ++ .../viewer/page/StoryViewerPageViewModel.kt | 38 +++- .../viewer/page/StoryViewerPlaybackState.kt | 6 +- .../drawable/stories_slate_indicator_ring.xml | 7 + ...es_slate_view_error_message_background.xml | 6 + .../main/res/layout/stories_slate_view.xml | 72 ++++++++ .../layout/stories_viewer_fragment_page.xml | 12 +- app/src/main/res/values/strings.xml | 6 + 11 files changed, 415 insertions(+), 35 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/stories/StorySlateView.kt create mode 100644 app/src/main/res/drawable/stories_slate_indicator_ring.xml create mode 100644 app/src/main/res/drawable/stories_slate_view_error_message_background.xml create mode 100644 app/src/main/res/layout/stories_slate_view.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewFragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewFragment.java index 5ca20c079..6eb350447 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewFragment.java @@ -20,7 +20,8 @@ import java.util.Objects; public abstract class MediaPreviewFragment extends Fragment { - static final String DATA_URI = "DATA_URI"; + public static final String DATA_URI = "DATA_URI"; + static final String DATA_SIZE = "DATA_SIZE"; static final String DATA_CONTENT_TYPE = "DATA_CONTENT_TYPE"; static final String AUTO_PLAY = "AUTO_PLAY"; diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/StorySlateView.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/StorySlateView.kt new file mode 100644 index 000000000..d76721547 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/StorySlateView.kt @@ -0,0 +1,169 @@ +package org.thoughtcrime.securesms.stories + +import android.content.Context +import android.os.Bundle +import android.os.Parcelable +import android.util.AttributeSet +import android.view.View +import android.widget.FrameLayout +import android.widget.TextView +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies +import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint +import org.thoughtcrime.securesms.util.visible + +/** + * Displays loading / error slate in Story viewer. + */ +class StorySlateView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : FrameLayout(context, attrs) { + + companion object { + private val TAG = Log.tag(StorySlateView::class.java) + } + + var callback: Callback? = null + + var state: State = State.HIDDEN + private set + + private var postId: Long = 0L + + init { + inflate(context, R.layout.stories_slate_view, this) + } + + private val background: View = findViewById(R.id.background) + private val loadingSpinner: View = findViewById(R.id.loading_spinner) + private val errorCircle: View = findViewById(R.id.error_circle) + private val unavailableText: View = findViewById(R.id.unavailable) + private val errorText: TextView = findViewById(R.id.error_text) + + fun moveToState(state: State, postId: Long) { + if (this.state == state && this.postId == postId) { + return + } + + if (this.postId != postId) { + this.postId = postId + moveToHiddenState() + callback?.onStateChanged(State.HIDDEN, postId) + } + + if (this.state.isValidTransitionTo(state)) { + when (state) { + State.LOADING -> moveToProgressState(State.LOADING) + State.ERROR -> moveToErrorState() + State.RETRY -> moveToProgressState(State.RETRY) + State.NOT_FOUND -> moveToNotFoundState() + State.HIDDEN -> moveToHiddenState() + } + + callback?.onStateChanged(state, postId) + } else { + Log.d(TAG, "Invalid state transfer: ${this.state} -> $state") + } + } + + private fun moveToProgressState(state: State) { + this.state = state + visible = true + background.visible = true + loadingSpinner.visible = true + errorCircle.visible = false + unavailableText.visible = false + errorText.visible = false + } + + private fun moveToErrorState() { + state = State.ERROR + visible = true + background.visible = true + loadingSpinner.visible = false + errorCircle.visible = true + unavailableText.visible = false + errorText.visible = true + + if (NetworkConstraint.isMet(ApplicationDependencies.getApplication())) { + errorText.setText(R.string.StorySlateView__couldnt_load_content) + } else { + errorText.setText(R.string.StorySlateView__no_internet_connection) + } + } + + private fun moveToNotFoundState() { + state = State.NOT_FOUND + visible = true + background.visible = true + loadingSpinner.visible = false + errorCircle.visible = false + unavailableText.visible = true + errorText.visible = false + } + + private fun moveToHiddenState() { + state = State.HIDDEN + visible = false + } + + override fun onSaveInstanceState(): Parcelable { + val rootState = super.onSaveInstanceState() + return Bundle().apply { + putParcelable("ROOT", rootState) + putInt("STATE", state.code) + putLong("ID", postId) + } + } + + override fun onRestoreInstanceState(state: Parcelable?) { + if (state is Bundle) { + val rootState: Parcelable? = state.getParcelable("ROOT") + this.state = State.fromCode(state.getInt("STATE", State.HIDDEN.code)) + this.postId = state.getLong("ID") + super.onRestoreInstanceState(rootState) + } else { + super.onRestoreInstanceState(state) + } + } + + init { + errorCircle.setOnClickListener { moveToState(State.RETRY, postId) } + } + + interface Callback { + fun onStateChanged(state: State, postId: Long) + } + + enum class State(val code: Int) { + LOADING(0), + ERROR(1), + RETRY(2), + NOT_FOUND(3), + HIDDEN(4); + + fun isValidTransitionTo(newState: State): Boolean { + if (newState in listOf(HIDDEN, NOT_FOUND)) { + return true + } + + return when (this) { + LOADING -> newState == ERROR + ERROR -> newState == RETRY + RETRY -> newState == ERROR + HIDDEN -> newState == LOADING + else -> false + } + } + + companion object { + fun fromCode(code: Int): State { + return values().firstOrNull { + it.code == code + } ?: HIDDEN + } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt index c4744bdb2..cddcd68ee 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt @@ -5,6 +5,7 @@ import android.animation.ObjectAnimator import android.annotation.SuppressLint import android.content.Context import android.graphics.drawable.Drawable +import android.net.Uri import android.os.Bundle import android.view.GestureDetector import android.view.MotionEvent @@ -18,6 +19,8 @@ import androidx.core.view.doOnNextLayout import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.core.Observable import org.signal.core.util.DimensionUnit import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.AvatarImageView @@ -31,11 +34,13 @@ import org.thoughtcrime.securesms.conversation.colors.AvatarColor import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardBottomSheet import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragment import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs +import org.thoughtcrime.securesms.database.AttachmentDatabase import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord import org.thoughtcrime.securesms.mediapreview.MediaPreviewFragment import org.thoughtcrime.securesms.mediapreview.VideoControlsDelegate import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.stories.StorySlateView import org.thoughtcrime.securesms.stories.dialogs.StoryContextMenu import org.thoughtcrime.securesms.stories.viewer.StoryViewerViewModel import org.thoughtcrime.securesms.stories.viewer.reply.direct.StoryDirectReplyDialogFragment @@ -53,9 +58,11 @@ import java.util.Locale import java.util.concurrent.TimeUnit import kotlin.math.abs -class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page), MediaPreviewFragment.Events, MultiselectForwardBottomSheet.Callback { +class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page), MediaPreviewFragment.Events, MultiselectForwardBottomSheet.Callback, StorySlateView.Callback { private lateinit var progressBar: SegmentedProgressBar + private lateinit var storySlate: StorySlateView + private lateinit var viewsAndReplies: TextView private lateinit var callback: Callback @@ -75,6 +82,7 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page), private val videoControlsDelegate = VideoControlsDelegate() private val lifecycleDisposable = LifecycleDisposable() + private val timeoutDisposable = LifecycleDisposable() private val storyRecipientId: RecipientId get() = requireArguments().getParcelable(ARG_STORY_RECIPIENT_ID)!! @@ -90,14 +98,17 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page), val date: TextView = view.findViewById(R.id.date) val moreButton: View = view.findViewById(R.id.more) val distributionList: TextView = view.findViewById(R.id.distribution_list) - val viewsAndReplies: TextView = view.findViewById(R.id.views_and_replies_bar) val cardWrapper: TouchInterceptingFrameLayout = view.findViewById(R.id.story_content_card_touch_interceptor) val card: CardView = view.findViewById(R.id.story_content_card) val caption: TextView = view.findViewById(R.id.story_caption) val largeCaption: TextView = view.findViewById(R.id.story_large_caption) val largeCaptionOverlay: View = view.findViewById(R.id.story_large_caption_overlay) + storySlate = view.findViewById(R.id.story_slate) progressBar = view.findViewById(R.id.progress) + viewsAndReplies = view.findViewById(R.id.views_and_replies_bar) + + storySlate.callback = this chrome = listOf( closeView, @@ -122,12 +133,13 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page), requireContext(), StoryGestureListener( cardWrapper, - progressBar, + viewModel::goToNextPost, + viewModel::goToPreviousPost, this::startReply ) ) - cardWrapper.setOnInterceptTouchEventListener { true } + cardWrapper.setOnInterceptTouchEventListener { storySlate.state == StorySlateView.State.HIDDEN } cardWrapper.setOnTouchListener { _, event -> val result = gestureDetector.onTouchEvent(event) if (event.actionMasked == MotionEvent.ACTION_DOWN) { @@ -151,10 +163,6 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page), override fun onPage(oldPageIndex: Int, newPageIndex: Int) { if (oldPageIndex != newPageIndex && context != null) { viewModel.setSelectedPostIndex(newPageIndex) - - childFragmentManager.beginTransaction() - .replace(R.id.story_content_container, createFragmentForPost(viewModel.getPostAt(newPageIndex))) - .commit() } if (oldPageIndex == newPageIndex) { @@ -163,7 +171,7 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page), } override fun onFinished() { - callback.onFinishedPosts(storyRecipientId) + viewModel.goToNextPost() } } @@ -190,7 +198,7 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page), if (state.posts.isNotEmpty() && state.selectedPostIndex < state.posts.size) { val post = state.posts[state.selectedPostIndex] - presentViewsAndReplies(viewsAndReplies, post) + presentViewsAndReplies(post) presentSenderAvatar(senderAvatar, post) presentGroupAvatar(groupAvatar, post) presentFrom(from, post) @@ -209,6 +217,9 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page), progressBar.segmentDurations = durations } + presentStory(post, state.selectedPostIndex) + presentSlate(post) + viewModel.setAreSegmentsInitialized(true) } else if (state.selectedPostIndex >= state.posts.size) { callback.onFinishedPosts(storyRecipientId) @@ -223,6 +234,7 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page), } } + timeoutDisposable.bindTo(viewLifecycleOwner) lifecycleDisposable.bindTo(viewLifecycleOwner) lifecycleDisposable += viewModel.groupDirectReplyObservable.subscribe { opt -> if (opt.isPresent) { @@ -304,9 +316,12 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page), } private fun resumeProgress() { - if (progressBar.segmentCount != 0) { - progressBar.start() - videoControlsDelegate.resume(viewModel.getPost().attachment.uri!!) + if (progressBar.segmentCount != 0 && viewModel.hasPost()) { + val postUri = viewModel.getPost().attachment.uri + if (postUri != null) { + progressBar.start() + videoControlsDelegate.resume(postUri) + } } } @@ -349,6 +364,64 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page), } } + private fun presentStory(post: StoryPost, index: Int) { + val fragment = childFragmentManager.findFragmentById(R.id.story_content_container) + if (fragment != null && fragment.requireArguments().getParcelable(MediaPreviewFragment.DATA_URI) == post.attachment.uri) { + progressBar.setPosition(index) + return + } + + if (post.attachment.uri == null) { + progressBar.setPosition(index) + progressBar.invalidate() + } else { + progressBar.setPosition(index) + storySlate.moveToState(StorySlateView.State.HIDDEN, post.id) + viewModel.setIsDisplayingSlate(false) + childFragmentManager.beginTransaction() + .replace(R.id.story_content_container, createFragmentForPost(post)) + .commitNow() + } + } + + private fun presentSlate(post: StoryPost) { + when (post.attachment.transferState) { + AttachmentDatabase.TRANSFER_PROGRESS_DONE -> { + storySlate.moveToState(StorySlateView.State.HIDDEN, post.id) + viewModel.setIsDisplayingSlate(false) + } + AttachmentDatabase.TRANSFER_PROGRESS_PENDING -> { + storySlate.moveToState(StorySlateView.State.LOADING, post.id) + viewModel.setIsDisplayingSlate(true) + } + AttachmentDatabase.TRANSFER_PROGRESS_STARTED -> { + storySlate.moveToState(StorySlateView.State.LOADING, post.id) + viewModel.setIsDisplayingSlate(true) + } + AttachmentDatabase.TRANSFER_PROGRESS_FAILED -> { + storySlate.moveToState(StorySlateView.State.NOT_FOUND, post.id) + viewModel.setIsDisplayingSlate(true) + } + } + } + + override fun onStateChanged(state: StorySlateView.State, postId: Long) { + if (state == StorySlateView.State.LOADING || state == StorySlateView.State.RETRY) { + timeoutDisposable.disposables.clear() + timeoutDisposable += Observable.interval(10, TimeUnit.SECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + storySlate.moveToState(StorySlateView.State.ERROR, postId) + } + + viewModel.forceDownloadSelectedPost() + } else { + timeoutDisposable.disposables.clear() + } + + viewsAndReplies.visible = state == StorySlateView.State.HIDDEN + } + private fun presentDistributionList(distributionList: TextView, storyPost: StoryPost) { distributionList.text = storyPost.distributionList?.getDisplayName(requireContext()) distributionList.visible = storyPost.distributionList != null && !storyPost.distributionList.isMyStory @@ -445,7 +518,7 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page), } } - private fun presentViewsAndReplies(viewsAndReplies: TextView, post: StoryPost) { + private fun presentViewsAndReplies(post: StoryPost) { val views = resources.getQuantityString(R.plurals.StoryViewerFragment__d_views, post.viewCount, post.viewCount) val replies = resources.getQuantityString(R.plurals.StoryViewerFragment__d_replies, post.replyCount, post.replyCount) @@ -527,7 +600,8 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page), private class StoryGestureListener( private val container: View, - private val progress: SegmentedProgressBar, + private val onGoToNext: () -> Unit, + private val onGoToPrevious: () -> Unit, private val onReplyToPost: () -> Unit ) : GestureDetector.SimpleOnGestureListener() { @@ -561,18 +635,18 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page), } private fun performLeftAction() { - if (progress.layoutDirection == View.LAYOUT_DIRECTION_RTL) { - progress.next() + if (container.layoutDirection == View.LAYOUT_DIRECTION_RTL) { + onGoToNext() } else { - progress.previous() + onGoToPrevious() } } private fun performRightAction() { - if (progress.layoutDirection == View.LAYOUT_DIRECTION_RTL) { - progress.previous() + if (container.layoutDirection == View.LAYOUT_DIRECTION_RTL) { + onGoToPrevious() } else { - progress.next() + onGoToNext() } } } @@ -610,7 +684,6 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page), } override fun mediaNotAvailable() { - // TODO [stories] -- Display appropriate error slate } interface Callback { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageRepository.kt index 84ccc8640..44e9a8d6e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageRepository.kt @@ -5,6 +5,7 @@ import io.reactivex.rxjava3.core.Completable import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.schedulers.Schedulers import org.signal.core.util.concurrent.SignalExecutors +import org.thoughtcrime.securesms.attachments.DatabaseAttachment import org.thoughtcrime.securesms.conversation.ConversationMessage import org.thoughtcrime.securesms.database.DatabaseObserver import org.thoughtcrime.securesms.database.NoSuchMessageException @@ -13,6 +14,7 @@ import org.thoughtcrime.securesms.database.model.MessageId import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.dependencies.ApplicationDependencies +import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob import org.thoughtcrime.securesms.jobs.MultiDeviceViewedUpdateJob import org.thoughtcrime.securesms.jobs.SendViewedReceiptJob import org.thoughtcrime.securesms.recipients.Recipient @@ -94,6 +96,11 @@ class StoryViewerPageRepository(context: Context) { } } + val conversationObserver = DatabaseObserver.Observer { + refresh(SignalDatabase.mms.getMessageRecord(record.id)) + } + + ApplicationDependencies.getDatabaseObserver().registerConversationObserver(record.threadId, conversationObserver) ApplicationDependencies.getDatabaseObserver().registerMessageUpdateObserver(messageUpdateObserver) val messageInsertObserver = DatabaseObserver.MessageObserver { @@ -105,6 +112,7 @@ class StoryViewerPageRepository(context: Context) { } emitter.setCancellable { + ApplicationDependencies.getDatabaseObserver().unregisterObserver(conversationObserver) ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageUpdateObserver) if (recipient.isGroup) { @@ -116,6 +124,12 @@ class StoryViewerPageRepository(context: Context) { } } + fun forceDownload(post: StoryPost) { + ApplicationDependencies.getJobManager().add( + AttachmentDownloadJob(post.id, (post.attachment as DatabaseAttachment).attachmentId, true) + ) + } + fun getStoryPostsFor(recipientId: RecipientId): Observable> { return getStoryRecords(recipientId) .switchMap { records -> diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageViewModel.kt index 64b871cea..9a0f57bc3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageViewModel.kt @@ -13,6 +13,7 @@ import io.reactivex.rxjava3.subjects.Subject import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.util.livedata.Store import java.util.Optional +import kotlin.math.max import kotlin.math.min /** @@ -59,10 +60,6 @@ class StoryViewerPageViewModel( } } - fun kickPlaybackState() { - storyViewerPlaybackStore.update { it } - } - override fun onCleared() { disposables.clear() } @@ -72,7 +69,12 @@ class StoryViewerPageViewModel( } fun setSelectedPostIndex(index: Int) { - repository.markViewed(getPostAt(index)) + val selectedPost = getPostAt(index) + + if (selectedPost != null) { + repository.markViewed(selectedPost) + } + store.update { it.copy( selectedPostIndex = index, @@ -81,6 +83,16 @@ class StoryViewerPageViewModel( } } + fun goToNextPost() { + val postIndex = store.state.selectedPostIndex + setSelectedPostIndex(postIndex + 1) + } + + fun goToPreviousPost() { + val postIndex = store.state.selectedPostIndex + setSelectedPostIndex(max(0, postIndex - 1)) + } + fun getRestartIndex(): Int { return min(store.state.selectedPostIndex, store.state.posts.lastIndex) } @@ -89,10 +101,18 @@ class StoryViewerPageViewModel( return store.state.replyState } + fun hasPost(): Boolean { + return store.state.selectedPostIndex in store.state.posts.indices + } + fun getPost(): StoryPost { return store.state.posts[store.state.selectedPostIndex] } + fun forceDownloadSelectedPost() { + repository.forceDownload(getPost()) + } + fun startDirectReply(storyId: Long, recipientId: RecipientId) { storyViewerDialogSubject.onNext(Optional.of(StoryViewerDialog.GroupDirectReply(recipientId, storyId))) } @@ -101,6 +121,10 @@ class StoryViewerPageViewModel( storyViewerPlaybackStore.update { it.copy(isUserScrollingParent = isUserScrollingParent) } } + fun setIsDisplayingSlate(isDisplayingSlate: Boolean) { + storyViewerPlaybackStore.update { it.copy(isDisplayingSlate = isDisplayingSlate) } + } + fun setIsSelectedPage(isSelectedPage: Boolean) { storyViewerPlaybackStore.update { it.copy(isSelectedPage = isSelectedPage) } } @@ -153,8 +177,8 @@ class StoryViewerPageViewModel( } } - fun getPostAt(index: Int): StoryPost { - return store.state.posts[index] + fun getPostAt(index: Int): StoryPost? { + return store.state.posts.getOrNull(index) } class Factory(private val recipientId: RecipientId, private val repository: StoryViewerPageRepository) : ViewModelProvider.Factory { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPlaybackState.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPlaybackState.kt index c50d1c172..6a2edee7e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPlaybackState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPlaybackState.kt @@ -10,7 +10,8 @@ data class StoryViewerPlaybackState( val isDisplayingDirectReplyDialog: Boolean = false, val isDisplayingCaptionOverlay: Boolean = false, val isUserScrollingParent: Boolean = false, - val isSelectedPage: Boolean = false + val isSelectedPage: Boolean = false, + val isDisplayingSlate: Boolean = false ) { val isPaused: Boolean = !areSegmentsInitialized || isUserTouching || @@ -22,5 +23,6 @@ data class StoryViewerPlaybackState( isDisplayingDirectReplyDialog || isDisplayingCaptionOverlay || isUserScrollingParent || - !isSelectedPage + !isSelectedPage || + isDisplayingSlate } diff --git a/app/src/main/res/drawable/stories_slate_indicator_ring.xml b/app/src/main/res/drawable/stories_slate_indicator_ring.xml new file mode 100644 index 000000000..d7fd9e1f4 --- /dev/null +++ b/app/src/main/res/drawable/stories_slate_indicator_ring.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/stories_slate_view_error_message_background.xml b/app/src/main/res/drawable/stories_slate_view_error_message_background.xml new file mode 100644 index 000000000..6a10fdf9c --- /dev/null +++ b/app/src/main/res/drawable/stories_slate_view_error_message_background.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/stories_slate_view.xml b/app/src/main/res/layout/stories_slate_view.xml new file mode 100644 index 000000000..436d9a8f7 --- /dev/null +++ b/app/src/main/res/layout/stories_slate_view.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/stories_viewer_fragment_page.xml b/app/src/main/res/layout/stories_viewer_fragment_page.xml index af1163e8c..63a936723 100644 --- a/app/src/main/res/layout/stories_viewer_fragment_page.xml +++ b/app/src/main/res/layout/stories_viewer_fragment_page.xml @@ -38,6 +38,12 @@ android:layout_height="160dp" android:layout_gravity="bottom" android:background="@drawable/story_gradient_bottom" /> + + + @@ -95,12 +101,11 @@ android:id="@+id/story_large_caption_overlay" android:layout_width="match_parent" android:layout_height="match_parent" - android:visibility="gone" - android:background="@color/transparent_black_40" /> + android:background="@color/transparent_black_40" + android:visibility="gone" /> … See More Reply sent + + This story is no longer available. + + No Internet Connection + + Couldn\'t Load Content