kopia lustrzana https://github.com/ryukoposting/Signal-Android
Implement story error slates.
Co-authored-by: Rashad Sookram <rashad@signal.org>fork-5.53.8
rodzic
34bbb98c96
commit
2483a92975
|
@ -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";
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Uri>(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 {
|
||||
|
|
|
@ -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<List<StoryPost>> {
|
||||
return getStoryRecords(recipientId)
|
||||
.switchMap { records ->
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<stroke
|
||||
android:width="3dp"
|
||||
android:color="@color/core_grey_60" />
|
||||
</shape>
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/transparent_black_60" />
|
||||
<corners android:radius="24dp" />
|
||||
</shape>
|
|
@ -0,0 +1,72 @@
|
|||
<?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"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:parentTag="android.widget.FrameLayout">
|
||||
|
||||
<View
|
||||
android:id="@+id/background"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/core_grey_80"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
android:id="@+id/loading_spinner"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:layout_gravity="center"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone"
|
||||
app:indicatorColor="@color/core_grey_05"
|
||||
app:indicatorInset="0dp"
|
||||
app:indicatorSize="64dp"
|
||||
app:trackColor="@color/transparent"
|
||||
app:trackThickness="3dp"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<View
|
||||
android:id="@+id/error_circle"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:layout_gravity="center"
|
||||
android:background="@drawable/stories_slate_indicator_ring"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/unavailable"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:alpha="0.6"
|
||||
android:gravity="center_horizontal"
|
||||
android:paddingHorizontal="46dp"
|
||||
android:text="@string/StorySlateView__this_story_is_no_longer_available"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body2"
|
||||
android:textColor="@color/core_grey_05"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/error_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="68dp"
|
||||
android:background="@drawable/stories_slate_view_error_message_background"
|
||||
android:drawablePadding="13dp"
|
||||
android:paddingHorizontal="13dp"
|
||||
android:paddingTop="13dp"
|
||||
android:paddingBottom="13dp"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textColor="@color/white"
|
||||
android:visibility="gone"
|
||||
app:drawableStartCompat="@drawable/ic_error_outline_24"
|
||||
app:drawableTint="@color/core_white"
|
||||
tools:text="@string/StorySlateView__couldnt_load_content"
|
||||
tools:visibility="visible" />
|
||||
</merge>
|
|
@ -38,6 +38,12 @@
|
|||
android:layout_height="160dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:background="@drawable/story_gradient_bottom" />
|
||||
|
||||
<org.thoughtcrime.securesms.stories.StorySlateView
|
||||
android:id="@+id/story_slate"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
</org.thoughtcrime.securesms.util.views.TouchInterceptingFrameLayout>
|
||||
|
||||
|
@ -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" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/story_large_caption"
|
||||
android:visibility="gone"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="16dp"
|
||||
|
@ -110,6 +115,7 @@
|
|||
android:gravity="bottom"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textStyle="bold"
|
||||
android:visibility="gone"
|
||||
app:layout_constrainedHeight="true"
|
||||
app:layout_constraintBottom_toTopOf="@id/story_from_barrier"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
|
|
@ -4590,6 +4590,12 @@
|
|||
<string name="StoryViewerPageFragment__see_more">… See More</string>
|
||||
<!-- Displayed in toast after sending a direct reply -->
|
||||
<string name="StoryDirectReplyDialogFragment__reply_sent">Reply sent</string>
|
||||
<!-- Displayed in the viewer when a story is no longer available -->
|
||||
<string name="StorySlateView__this_story_is_no_longer_available">This story is no longer available.</string>
|
||||
<!-- Displayed in the viewer when the network is not available -->
|
||||
<string name="StorySlateView__no_internet_connection">No Internet Connection</string>
|
||||
<!-- Displayed in the viewer when network is available but content could not be downloaded -->
|
||||
<string name="StorySlateView__couldnt_load_content">Couldn\'t Load Content</string>
|
||||
|
||||
<!-- endregion -->
|
||||
<!-- Content description for expand contacts chevron -->
|
||||
|
|
Ładowanie…
Reference in New Issue