mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-25 12:17:22 +00:00
Implement "unviewed only" mode for story viewer.
This commit is contained in:
committed by
Cody Henthorne
parent
89a6730efe
commit
858c7a7f2e
@@ -19,7 +19,8 @@ data class StoryViewerArgs(
|
||||
val storyThumbBlur: BlurHash? = null,
|
||||
val recipientIds: List<RecipientId> = emptyList(),
|
||||
val isFromNotification: Boolean = false,
|
||||
val groupReplyStartPosition: Int = -1
|
||||
val groupReplyStartPosition: Int = -1,
|
||||
val isUnviewedOnly: Boolean = false
|
||||
) : Parcelable {
|
||||
|
||||
class Builder(private val recipientId: RecipientId, private val isInHiddenStoryMode: Boolean) {
|
||||
@@ -31,6 +32,7 @@ data class StoryViewerArgs(
|
||||
private var recipientIds: List<RecipientId> = emptyList()
|
||||
private var isFromNotification: Boolean = false
|
||||
private var groupReplyStartPosition: Int = -1
|
||||
private var isUnviewedOnly: Boolean = false
|
||||
|
||||
fun withStoryId(storyId: Long): Builder {
|
||||
this.storyId = storyId
|
||||
@@ -67,6 +69,11 @@ data class StoryViewerArgs(
|
||||
return this
|
||||
}
|
||||
|
||||
fun isUnviewedOnly(isUnviewedOnly: Boolean): Builder {
|
||||
this.isUnviewedOnly = isUnviewedOnly
|
||||
return this
|
||||
}
|
||||
|
||||
fun build(): StoryViewerArgs {
|
||||
return StoryViewerArgs(
|
||||
recipientId = recipientId,
|
||||
@@ -77,7 +84,8 @@ data class StoryViewerArgs(
|
||||
storyThumbBlur = storyThumbBlur,
|
||||
recipientIds = recipientIds,
|
||||
isFromNotification = isFromNotification,
|
||||
groupReplyStartPosition = groupReplyStartPosition
|
||||
groupReplyStartPosition = groupReplyStartPosition,
|
||||
isUnviewedOnly = isUnviewedOnly
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectFor
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.StoryViewState
|
||||
import org.thoughtcrime.securesms.main.Material3OnScrollHelperBinder
|
||||
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
@@ -237,7 +238,8 @@ class StoriesLandingFragment : DSLSettingsFragment(layoutId = R.layout.stories_l
|
||||
storyThumbTextModel = text,
|
||||
storyThumbUri = image,
|
||||
storyThumbBlur = blur,
|
||||
recipientIds = viewModel.getRecipientIds(model.data.isHidden)
|
||||
recipientIds = viewModel.getRecipientIds(model.data.isHidden, model.data.storyViewState == StoryViewState.UNVIEWED),
|
||||
isUnviewedOnly = model.data.storyViewState == StoryViewState.UNVIEWED
|
||||
)
|
||||
),
|
||||
options.toBundle()
|
||||
|
||||
@@ -7,6 +7,7 @@ import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.StoryViewState
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
@@ -46,8 +47,11 @@ class StoriesLandingViewModel(private val storiesLandingRepository: StoriesLandi
|
||||
store.update { it.copy(isHiddenContentVisible = isExpanded) }
|
||||
}
|
||||
|
||||
fun getRecipientIds(hidden: Boolean): List<RecipientId> {
|
||||
return store.state.storiesLandingItems.filter { it.isHidden == hidden }.map { it.storyRecipient.id }
|
||||
fun getRecipientIds(hidden: Boolean, isUnviewed: Boolean): List<RecipientId> {
|
||||
return store.state.storiesLandingItems
|
||||
.filter { it.isHidden == hidden }
|
||||
.filter { if (isUnviewed) it.storyViewState == StoryViewState.UNVIEWED else true }
|
||||
.map { it.storyRecipient.id }
|
||||
}
|
||||
|
||||
class Factory(private val storiesLandingRepository: StoriesLandingRepository) : ViewModelProvider.Factory {
|
||||
|
||||
@@ -34,8 +34,16 @@ class StoryViewerFragment : Fragment(R.layout.stories_viewer_fragment), StoryVie
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
storyPager = view.findViewById(R.id.story_item_pager)
|
||||
|
||||
val adapter = StoryViewerPagerAdapter(this, storyViewerArgs.storyId, storyViewerArgs.isFromNotification, storyViewerArgs.groupReplyStartPosition)
|
||||
val adapter = StoryViewerPagerAdapter(
|
||||
this,
|
||||
storyViewerArgs.storyId,
|
||||
storyViewerArgs.isFromNotification,
|
||||
storyViewerArgs.groupReplyStartPosition,
|
||||
storyViewerArgs.isUnviewedOnly
|
||||
)
|
||||
|
||||
storyPager.adapter = adapter
|
||||
storyPager.overScrollMode = ViewPager2.OVER_SCROLL_NEVER
|
||||
|
||||
viewModel.isChildScrolling.observe(viewLifecycleOwner) {
|
||||
storyPager.isUserInputEnabled = !it
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.stories.viewer
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.stories.viewer.page.StoryViewerPageFragment
|
||||
@@ -10,7 +11,8 @@ class StoryViewerPagerAdapter(
|
||||
fragment: Fragment,
|
||||
private val initialStoryId: Long,
|
||||
private val isFromNotification: Boolean,
|
||||
private val groupReplyStartPosition: Int
|
||||
private val groupReplyStartPosition: Int,
|
||||
private val isUnviewedOnly: Boolean
|
||||
) : FragmentStateAdapter(fragment) {
|
||||
|
||||
private var pages: List<RecipientId> = emptyList()
|
||||
@@ -23,10 +25,15 @@ class StoryViewerPagerAdapter(
|
||||
DiffUtil.calculateDiff(callback).dispatchUpdatesTo(this)
|
||||
}
|
||||
|
||||
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
||||
super.onAttachedToRecyclerView(recyclerView)
|
||||
recyclerView.overScrollMode = RecyclerView.OVER_SCROLL_NEVER
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = pages.size
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
return StoryViewerPageFragment.create(pages[position], initialStoryId, isFromNotification, groupReplyStartPosition)
|
||||
return StoryViewerPageFragment.create(pages[position], initialStoryId, isFromNotification, groupReplyStartPosition, isUnviewedOnly)
|
||||
}
|
||||
|
||||
private class Callback(
|
||||
|
||||
@@ -4,6 +4,7 @@ import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.database.model.StoryViewState
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
@@ -11,7 +12,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
* Open for testing
|
||||
*/
|
||||
open class StoryViewerRepository {
|
||||
fun getStories(hiddenStories: Boolean): Single<List<RecipientId>> {
|
||||
fun getStories(hiddenStories: Boolean, unviewedOnly: Boolean): Single<List<RecipientId>> {
|
||||
return Single.create<List<RecipientId>> { emitter ->
|
||||
val myStoriesId = SignalDatabase.recipients.getOrInsertFromDistributionListId(DistributionListId.MY_STORY)
|
||||
val myStories = Recipient.resolved(myStoriesId)
|
||||
@@ -28,6 +29,16 @@ open class StoryViewerRepository {
|
||||
} else {
|
||||
!it.shouldHideStory()
|
||||
}
|
||||
}.filter {
|
||||
if (unviewedOnly) {
|
||||
if (it.isSelf || it.isMyStory) {
|
||||
false
|
||||
} else {
|
||||
SignalDatabase.mms.getStoryViewState(it.id) == StoryViewState.UNVIEWED
|
||||
}
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}.map { it.id }
|
||||
|
||||
emitter.onSuccess(
|
||||
|
||||
@@ -70,7 +70,10 @@ class StoryViewerViewModel(
|
||||
return if (storyViewerArgs.recipientIds.isNotEmpty()) {
|
||||
Single.just(storyViewerArgs.recipientIds)
|
||||
} else {
|
||||
repository.getStories(storyViewerArgs.isInHiddenStoryMode)
|
||||
repository.getStories(
|
||||
hiddenStories = storyViewerArgs.isInHiddenStoryMode,
|
||||
unviewedOnly = storyViewerArgs.isUnviewedOnly
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ class StoryViewerPageFragment :
|
||||
|
||||
private val viewModel: StoryViewerPageViewModel by viewModels(
|
||||
factoryProducer = {
|
||||
StoryViewerPageViewModel.Factory(storyRecipientId, initialStoryId, StoryViewerPageRepository(requireContext()))
|
||||
StoryViewerPageViewModel.Factory(storyRecipientId, initialStoryId, isUnviewedOnly, StoryViewerPageRepository(requireContext()))
|
||||
}
|
||||
)
|
||||
|
||||
@@ -120,6 +120,9 @@ class StoryViewerPageFragment :
|
||||
private val groupReplyStartPosition: Int
|
||||
get() = requireArguments().getInt(ARG_GROUP_REPLY_START_POSITION, -1)
|
||||
|
||||
private val isUnviewedOnly: Boolean
|
||||
get() = requireArguments().getBoolean(ARG_IS_UNVIEWED_ONLY, false)
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
callback = requireListener()
|
||||
@@ -832,14 +835,16 @@ class StoryViewerPageFragment :
|
||||
private const val ARG_STORY_ID = "arg.story.id"
|
||||
private const val ARG_IS_FROM_NOTIFICATION = "is_from_notification"
|
||||
private const val ARG_GROUP_REPLY_START_POSITION = "group_reply_start_position"
|
||||
private const val ARG_IS_UNVIEWED_ONLY = "is_unviewed_only"
|
||||
|
||||
fun create(recipientId: RecipientId, initialStoryId: Long, isFromNotification: Boolean, groupReplyStartPosition: Int): Fragment {
|
||||
fun create(recipientId: RecipientId, initialStoryId: Long, isFromNotification: Boolean, groupReplyStartPosition: Int, isUnviewedOnly: Boolean): Fragment {
|
||||
return StoryViewerPageFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putParcelable(ARG_STORY_RECIPIENT_ID, recipientId)
|
||||
putLong(ARG_STORY_ID, initialStoryId)
|
||||
putBoolean(ARG_IS_FROM_NOTIFICATION, isFromNotification)
|
||||
putInt(ARG_GROUP_REPLY_START_POSITION, groupReplyStartPosition)
|
||||
putBoolean(ARG_IS_UNVIEWED_ONLY, isUnviewedOnly)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,13 +35,15 @@ open class StoryViewerPageRepository(context: Context) {
|
||||
|
||||
private val context = context.applicationContext
|
||||
|
||||
private fun getStoryRecords(recipientId: RecipientId): Observable<List<MessageRecord>> {
|
||||
private fun getStoryRecords(recipientId: RecipientId, isUnviewedOnly: Boolean): Observable<List<MessageRecord>> {
|
||||
return Observable.create { emitter ->
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
|
||||
fun refresh() {
|
||||
val stories = if (recipient.isMyStory) {
|
||||
SignalDatabase.mms.getAllOutgoingStories(false)
|
||||
} else if (isUnviewedOnly) {
|
||||
SignalDatabase.mms.getUnreadStories(recipientId, 100)
|
||||
} else {
|
||||
SignalDatabase.mms.getAllStoriesFor(recipientId)
|
||||
}
|
||||
@@ -144,8 +146,8 @@ open class StoryViewerPageRepository(context: Context) {
|
||||
return Stories.enqueueAttachmentsFromStoryForDownload(post.conversationMessage.messageRecord as MmsMessageRecord, true)
|
||||
}
|
||||
|
||||
fun getStoryPostsFor(recipientId: RecipientId): Observable<List<StoryPost>> {
|
||||
return getStoryRecords(recipientId)
|
||||
fun getStoryPostsFor(recipientId: RecipientId, isUnviewedOnly: Boolean): Observable<List<StoryPost>> {
|
||||
return getStoryRecords(recipientId, isUnviewedOnly)
|
||||
.switchMap { records ->
|
||||
val posts = records.map { getStoryPostFromRecord(recipientId, it) }
|
||||
if (posts.isEmpty()) {
|
||||
|
||||
@@ -24,6 +24,7 @@ import kotlin.math.min
|
||||
class StoryViewerPageViewModel(
|
||||
private val recipientId: RecipientId,
|
||||
private val initialStoryId: Long,
|
||||
private val isUnviewedOnly: Boolean,
|
||||
private val repository: StoryViewerPageRepository
|
||||
) : ViewModel() {
|
||||
|
||||
@@ -47,7 +48,7 @@ class StoryViewerPageViewModel(
|
||||
|
||||
fun refresh() {
|
||||
disposables.clear()
|
||||
disposables += repository.getStoryPostsFor(recipientId).subscribe { posts ->
|
||||
disposables += repository.getStoryPostsFor(recipientId, isUnviewedOnly).subscribe { posts ->
|
||||
store.update { state ->
|
||||
var isDisplayingInitialState = false
|
||||
val startIndex = if (state.posts.isEmpty() && initialStoryId > 0) {
|
||||
@@ -237,9 +238,14 @@ class StoryViewerPageViewModel(
|
||||
return store.state.posts.getOrNull(index)
|
||||
}
|
||||
|
||||
class Factory(private val recipientId: RecipientId, private val initialStoryId: Long, private val repository: StoryViewerPageRepository) : ViewModelProvider.Factory {
|
||||
class Factory(
|
||||
private val recipientId: RecipientId,
|
||||
private val initialStoryId: Long,
|
||||
private val isUnviewedOnly: Boolean,
|
||||
private val repository: StoryViewerPageRepository
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.cast(StoryViewerPageViewModel(recipientId, initialStoryId, repository)) as T
|
||||
return modelClass.cast(StoryViewerPageViewModel(recipientId, initialStoryId, isUnviewedOnly, repository)) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ class RxStore<T : Any>(
|
||||
private val actionSubject = PublishSubject.create<(T) -> T>().toSerialized()
|
||||
|
||||
val state: T get() = behaviorProcessor.value!!
|
||||
val stateFlowable: Flowable<T> = behaviorProcessor
|
||||
val stateFlowable: Flowable<T> = behaviorProcessor.onBackpressureLatest()
|
||||
|
||||
init {
|
||||
actionSubject
|
||||
|
||||
Reference in New Issue
Block a user