Add RxStore and StoryViewerPage forward navigation.

This commit is contained in:
Alex Hart
2022-04-01 09:27:09 -03:00
committed by Cody Henthorne
parent 11c3ea769e
commit 3e42c044b8
8 changed files with 300 additions and 37 deletions

View File

@@ -20,6 +20,7 @@ import android.content.Context;
import android.text.SpannableString;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import org.thoughtcrime.securesms.database.MmsSmsColumns;
import org.thoughtcrime.securesms.database.SmsDatabase;
@@ -80,6 +81,11 @@ public abstract class DisplayRecord {
!MmsSmsColumns.Types.isIdentityDefault(type);
}
@VisibleForTesting
public long getType() {
return type;
}
public boolean isSent() {
return MmsSmsColumns.Types.isSentType(type);
}

View File

@@ -10,7 +10,7 @@ import org.thoughtcrime.securesms.util.MediaUtil
/**
* Each story is made up of a collection of posts
*/
class StoryPost(
data class StoryPost(
val id: Long,
val sender: Recipient,
val group: Recipient?,
@@ -20,7 +20,8 @@ class StoryPost(
val dateInMilliseconds: Long,
val content: Content,
val conversationMessage: ConversationMessage,
val allowsReplies: Boolean
val allowsReplies: Boolean,
val hasSelfViewed: Boolean
) {
sealed class Content(val uri: Uri?) {
class AttachmentContent(val attachment: Attachment) : Content(attachment.uri) {

View File

@@ -22,6 +22,7 @@ import androidx.core.view.doOnNextLayout
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.LiveDataReactiveStreams
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observable
import org.signal.core.util.DimensionUnit
@@ -252,42 +253,44 @@ class StoryViewerPageFragment :
}
}
viewModel.state.observe(viewLifecycleOwner) { state ->
if (state.posts.isNotEmpty() && state.selectedPostIndex < state.posts.size) {
val post = state.posts[state.selectedPostIndex]
LiveDataReactiveStreams
.fromPublisher(viewModel.state.observeOn(AndroidSchedulers.mainThread()))
.observe(viewLifecycleOwner) { state ->
if (state.posts.isNotEmpty() && state.selectedPostIndex < state.posts.size) {
val post = state.posts[state.selectedPostIndex]
presentViewsAndReplies(post)
presentSenderAvatar(senderAvatar, post)
presentGroupAvatar(groupAvatar, post)
presentFrom(from, post)
presentDate(date, post)
presentDistributionList(distributionList, post)
presentCaption(caption, largeCaption, largeCaptionOverlay, post)
presentBlur(blurContainer, post)
presentViewsAndReplies(post)
presentSenderAvatar(senderAvatar, post)
presentGroupAvatar(groupAvatar, post)
presentFrom(from, post)
presentDate(date, post)
presentDistributionList(distributionList, post)
presentCaption(caption, largeCaption, largeCaptionOverlay, post)
presentBlur(blurContainer, post)
val durations: Map<Int, Long> = state.posts
.mapIndexed { index, storyPost ->
index to when {
storyPost.content.isVideo() -> -1L
storyPost.content is StoryPost.Content.TextContent -> calculateDurationForText(storyPost.content)
else -> DEFAULT_DURATION
val durations: Map<Int, Long> = state.posts
.mapIndexed { index, storyPost ->
index to when {
storyPost.content.isVideo() -> -1L
storyPost.content is StoryPost.Content.TextContent -> calculateDurationForText(storyPost.content)
else -> DEFAULT_DURATION
}
}
.toMap()
if (progressBar.segmentCount != state.posts.size || progressBar.segmentDurations != durations) {
progressBar.segmentCount = state.posts.size
progressBar.segmentDurations = durations
}
.toMap()
if (progressBar.segmentCount != state.posts.size || progressBar.segmentDurations != durations) {
progressBar.segmentCount = state.posts.size
progressBar.segmentDurations = durations
presentStory(post, state.selectedPostIndex)
presentSlate(post)
viewModel.setAreSegmentsInitialized(true)
} else if (state.selectedPostIndex >= state.posts.size) {
callback.onFinishedPosts(storyRecipientId)
}
presentStory(post, state.selectedPostIndex)
presentSlate(post)
viewModel.setAreSegmentsInitialized(true)
} else if (state.selectedPostIndex >= state.posts.size) {
callback.onFinishedPosts(storyRecipientId)
}
}
viewModel.storyViewerPlaybackState.observe(viewLifecycleOwner) { state ->
if (state.isPaused) {

View File

@@ -24,7 +24,10 @@ import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.Base64
class StoryViewerPageRepository(context: Context) {
/**
* Open for testing.
*/
open class StoryViewerPageRepository(context: Context) {
private val context = context.applicationContext
@@ -77,7 +80,8 @@ class StoryViewerPageRepository(context: Context) {
dateInMilliseconds = record.dateSent,
content = getContent(record as MmsMessageRecord),
conversationMessage = ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(context, record),
allowsReplies = record.storyType.isStoryWithReplies
allowsReplies = record.storyType.isStoryWithReplies,
hasSelfViewed = if (record.isOutgoing) true else record.viewedReceiptCount > 0
)
emitter.onNext(story)

View File

@@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
@@ -11,6 +12,7 @@ import io.reactivex.rxjava3.subjects.PublishSubject
import io.reactivex.rxjava3.subjects.Subject
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.livedata.Store
import org.thoughtcrime.securesms.util.rx.RxStore
import java.util.Optional
import kotlin.math.max
import kotlin.math.min
@@ -24,7 +26,7 @@ class StoryViewerPageViewModel(
private val repository: StoryViewerPageRepository
) : ViewModel() {
private val store = Store(StoryViewerPageState())
private val store = RxStore(StoryViewerPageState())
private val disposables = CompositeDisposable()
private val storyViewerDialogSubject: Subject<Optional<StoryViewerDialog>> = PublishSubject.create()
@@ -34,7 +36,7 @@ class StoryViewerPageViewModel(
val groupDirectReplyObservable: Observable<Optional<StoryViewerDialog>> = storyViewerDialogSubject
val state: LiveData<StoryViewerPageState> = store.stateLiveData
val state: Flowable<StoryViewerPageState> = store.stateFlowable
fun getStateSnapshot(): StoryViewerPageState = store.state
@@ -50,7 +52,8 @@ class StoryViewerPageViewModel(
val initialIndex = posts.indexOfFirst { it.id == initialStoryId }
initialIndex.takeIf { it > -1 } ?: state.selectedPostIndex
} else if (state.posts.isEmpty()) {
val initialIndex = posts.indexOfFirst { !it.conversationMessage.messageRecord.isOutgoing && it.conversationMessage.messageRecord.viewedReceiptCount == 0 }
val initialPost = getNextUnreadPost(posts)
val initialIndex = initialPost?.let { posts.indexOf(it) } ?: -1
initialIndex.takeIf { it > -1 } ?: state.selectedPostIndex
} else {
state.selectedPostIndex
@@ -89,11 +92,24 @@ class StoryViewerPageViewModel(
}
fun goToNextPost() {
if (store.state.posts.isEmpty()) {
return
}
val postIndex = store.state.selectedPostIndex
setSelectedPostIndex(postIndex + 1)
val nextUnreadPost: StoryPost? = getNextUnreadPost(store.state.posts.drop(postIndex + 1))
if (nextUnreadPost == null) {
setSelectedPostIndex(postIndex + 1)
} else {
setSelectedPostIndex(store.state.posts.indexOf(nextUnreadPost))
}
}
fun goToPreviousPost() {
if (store.state.posts.isEmpty()) {
return
}
val postIndex = store.state.selectedPostIndex
setSelectedPostIndex(max(0, postIndex - 1))
}
@@ -194,6 +210,10 @@ class StoryViewerPageViewModel(
}
}
private fun getNextUnreadPost(list: List<StoryPost>): StoryPost? {
return list.firstOrNull { !it.hasSelfViewed }
}
fun getPostAt(index: Int): StoryPost? {
return store.state.posts.getOrNull(index)
}

View File

@@ -0,0 +1,30 @@
package org.thoughtcrime.securesms.util.rx
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.processors.BehaviorProcessor
import io.reactivex.rxjava3.schedulers.Schedulers
import io.reactivex.rxjava3.subjects.PublishSubject
/**
* Rx replacement for Store.
* Actions are run on the computation thread.
*/
class RxStore<T : Any>(defaultValue: T) {
private val behaviorProcessor = BehaviorProcessor.createDefault(defaultValue)
private val actionSubject = PublishSubject.create<(T) -> T>().toSerialized()
val state: T get() = behaviorProcessor.value!!
val stateFlowable: Flowable<T> = behaviorProcessor
init {
actionSubject
.observeOn(Schedulers.computation())
.scan(defaultValue) { v, f -> f(v) }
.subscribe { behaviorProcessor.onNext(it) }
}
fun update(transformer: (T) -> T) {
actionSubject.onNext(transformer)
}
}