kopia lustrzana https://github.com/ryukoposting/Signal-Android
Refactor viewer to prepare for enhanced video duration support.
rodzic
9a5fcdbe4d
commit
bd58c91d2c
|
@ -101,5 +101,8 @@ public abstract class MediaPreviewFragment extends Fragment {
|
||||||
public interface Events {
|
public interface Events {
|
||||||
boolean singleTapOnMedia();
|
boolean singleTapOnMedia();
|
||||||
void mediaNotAvailable();
|
void mediaNotAvailable();
|
||||||
|
default @Nullable VideoControlsDelegate getVideoControlsDelegate() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
package org.thoughtcrime.securesms.mediapreview
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.rxjava3.core.Observable
|
||||||
|
import io.reactivex.rxjava3.subjects.BehaviorSubject
|
||||||
|
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||||
|
import org.thoughtcrime.securesms.video.VideoPlayer
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to manage video playback in preview screen.
|
||||||
|
*/
|
||||||
|
class VideoControlsDelegate {
|
||||||
|
|
||||||
|
private val playWhenReady: MutableMap<Uri, Boolean> = mutableMapOf()
|
||||||
|
private val playerSubject = BehaviorSubject.create<Player>()
|
||||||
|
private val playerReadySignal = PublishSubject.create<Unit>()
|
||||||
|
val playerUpdates: Observable<PlayerUpdate> = playerReadySignal
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.flatMap { playerSubject }
|
||||||
|
.filter { it.videoPlayer != null }
|
||||||
|
.map { PlayerUpdate(it.uri, it.videoPlayer?.duration!!) }
|
||||||
|
|
||||||
|
fun pause() = playerSubject.value?.videoPlayer?.pause()
|
||||||
|
|
||||||
|
fun resume(uri: Uri) {
|
||||||
|
val player = playerSubject.value
|
||||||
|
if (player?.uri == uri) {
|
||||||
|
player.videoPlayer?.play()
|
||||||
|
} else {
|
||||||
|
playWhenReady[uri] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
playerSubject.value?.videoPlayer?.play()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun restart() {
|
||||||
|
playerSubject.value?.videoPlayer?.playbackPosition = 0L
|
||||||
|
}
|
||||||
|
|
||||||
|
fun attachPlayer(uri: Uri, videoPlayer: VideoPlayer?) {
|
||||||
|
playerSubject.onNext(Player(uri, videoPlayer))
|
||||||
|
|
||||||
|
if ((videoPlayer?.duration ?: -1L) > 0L) {
|
||||||
|
playerReadySignal.onNext(Unit)
|
||||||
|
} else {
|
||||||
|
videoPlayer?.setPlayerStateCallbacks {
|
||||||
|
playerReadySignal.onNext(Unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playWhenReady[uri] == true) {
|
||||||
|
playWhenReady[uri] = false
|
||||||
|
videoPlayer?.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun detachPlayer() {
|
||||||
|
playerSubject.onNext(Player())
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class Player(
|
||||||
|
val uri: Uri = Uri.EMPTY,
|
||||||
|
val videoPlayer: VideoPlayer? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class PlayerUpdate(
|
||||||
|
val mediaUri: Uri,
|
||||||
|
val duration: Long
|
||||||
|
)
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
|
@ -28,7 +29,8 @@ public final class VideoMediaPreviewFragment extends MediaPreviewFragment {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||||
Bundle savedInstanceState) {
|
Bundle savedInstanceState)
|
||||||
|
{
|
||||||
View itemView = inflater.inflate(R.layout.media_preview_video_fragment, container, false);
|
View itemView = inflater.inflate(R.layout.media_preview_video_fragment, container, false);
|
||||||
Bundle arguments = requireArguments();
|
Bundle arguments = requireArguments();
|
||||||
Uri uri = arguments.getParcelable(DATA_URI);
|
Uri uri = arguments.getParcelable(DATA_URI);
|
||||||
|
@ -70,6 +72,10 @@ public final class VideoMediaPreviewFragment extends MediaPreviewFragment {
|
||||||
if (videoView != null && isVideoGif) {
|
if (videoView != null && isVideoGif) {
|
||||||
videoView.play();
|
videoView.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (events.getVideoControlsDelegate() != null) {
|
||||||
|
events.getVideoControlsDelegate().attachPlayer(getUri(), videoView);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -77,10 +83,18 @@ public final class VideoMediaPreviewFragment extends MediaPreviewFragment {
|
||||||
if (videoView != null) {
|
if (videoView != null) {
|
||||||
videoView.pause();
|
videoView.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (events.getVideoControlsDelegate() != null) {
|
||||||
|
events.getVideoControlsDelegate().detachPlayer();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View getPlaybackControls() {
|
public View getPlaybackControls() {
|
||||||
return videoView != null && !isVideoGif ? videoView.getControlView() : null;
|
return videoView != null && !isVideoGif ? videoView.getControlView() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private @NonNull Uri getUri() {
|
||||||
|
return requireArguments().getParcelable(DATA_URI);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,6 @@ class StoryPost(
|
||||||
val viewCount: Int,
|
val viewCount: Int,
|
||||||
val replyCount: Int,
|
val replyCount: Int,
|
||||||
val dateInMilliseconds: Long,
|
val dateInMilliseconds: Long,
|
||||||
val durationMillis: Long,
|
|
||||||
val attachment: Attachment,
|
val attachment: Attachment,
|
||||||
val conversationMessage: ConversationMessage
|
val conversationMessage: ConversationMessage
|
||||||
)
|
)
|
||||||
|
|
|
@ -33,6 +33,7 @@ import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectFor
|
||||||
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs
|
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs
|
||||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
||||||
import org.thoughtcrime.securesms.mediapreview.MediaPreviewFragment
|
import org.thoughtcrime.securesms.mediapreview.MediaPreviewFragment
|
||||||
|
import org.thoughtcrime.securesms.mediapreview.VideoControlsDelegate
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import org.thoughtcrime.securesms.stories.dialogs.StoryContextMenu
|
import org.thoughtcrime.securesms.stories.dialogs.StoryContextMenu
|
||||||
|
@ -48,6 +49,7 @@ import org.thoughtcrime.securesms.util.fragments.requireListener
|
||||||
import org.thoughtcrime.securesms.util.views.TouchInterceptingFrameLayout
|
import org.thoughtcrime.securesms.util.views.TouchInterceptingFrameLayout
|
||||||
import org.thoughtcrime.securesms.util.visible
|
import org.thoughtcrime.securesms.util.visible
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlin.math.abs
|
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 {
|
||||||
|
@ -65,6 +67,8 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private val videoControlsDelegate = VideoControlsDelegate()
|
||||||
|
|
||||||
private val lifecycleDisposable = LifecycleDisposable()
|
private val lifecycleDisposable = LifecycleDisposable()
|
||||||
|
|
||||||
private val storyRecipientId: RecipientId
|
private val storyRecipientId: RecipientId
|
||||||
|
@ -122,10 +126,10 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
|
||||||
cardWrapper.setOnTouchListener { _, event ->
|
cardWrapper.setOnTouchListener { _, event ->
|
||||||
val result = gestureDetector.onTouchEvent(event)
|
val result = gestureDetector.onTouchEvent(event)
|
||||||
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
|
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
|
||||||
progressBar.pause()
|
viewModel.setIsUserTouching(true)
|
||||||
hideChrome()
|
hideChrome()
|
||||||
} else if (event.actionMasked == MotionEvent.ACTION_UP || event.actionMasked == MotionEvent.ACTION_CANCEL) {
|
} else if (event.actionMasked == MotionEvent.ACTION_UP || event.actionMasked == MotionEvent.ACTION_CANCEL) {
|
||||||
resumeProgressIfNotDisplayingDialog()
|
viewModel.setIsUserTouching(false)
|
||||||
showChrome()
|
showChrome()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,6 +151,10 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
|
||||||
.replace(R.id.story_content_container, createFragmentForPost(viewModel.getPostAt(newPageIndex)))
|
.replace(R.id.story_content_container, createFragmentForPost(viewModel.getPostAt(newPageIndex)))
|
||||||
.commit()
|
.commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (oldPageIndex == newPageIndex) {
|
||||||
|
videoControlsDelegate.restart()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFinished() {
|
override fun onFinished() {
|
||||||
|
@ -166,27 +174,45 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
|
||||||
presentDistributionList(distributionList, post)
|
presentDistributionList(distributionList, post)
|
||||||
presentCaption(caption, largeCaption, largeCaptionOverlay, post)
|
presentCaption(caption, largeCaption, largeCaptionOverlay, post)
|
||||||
|
|
||||||
if (progressBar.segmentCount != state.posts.size) {
|
val durations: Map<Int, Long> = state.posts
|
||||||
progressBar.segmentCount = state.posts.size
|
.mapIndexed { index, storyPost ->
|
||||||
progressBar.segmentDurations = state.posts.mapIndexed { index, storyPost -> index to storyPost.durationMillis }.toMap()
|
index to (storyPost.attachment.uri?.let { state.durations[it] } ?: TimeUnit.SECONDS.toMillis(5))
|
||||||
progressBar.start()
|
|
||||||
}
|
}
|
||||||
|
.toMap()
|
||||||
|
|
||||||
|
if (progressBar.segmentCount != state.posts.size || progressBar.segmentDurations != durations) {
|
||||||
|
progressBar.segmentCount = state.posts.size
|
||||||
|
progressBar.segmentDurations = durations
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.setAreSegmentsInitialized(true)
|
||||||
} else if (state.selectedPostIndex >= state.posts.size) {
|
} else if (state.selectedPostIndex >= state.posts.size) {
|
||||||
callback.onFinishedPosts(storyRecipientId)
|
callback.onFinishedPosts(storyRecipientId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viewModel.storyViewerPlaybackState.observe(viewLifecycleOwner) { state ->
|
||||||
|
if (state.isPaused) {
|
||||||
|
pauseProgress()
|
||||||
|
} else {
|
||||||
|
resumeProgress()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
lifecycleDisposable.bindTo(viewLifecycleOwner)
|
lifecycleDisposable.bindTo(viewLifecycleOwner)
|
||||||
lifecycleDisposable += viewModel.groupDirectReplyObservable.subscribe { opt ->
|
lifecycleDisposable += viewModel.groupDirectReplyObservable.subscribe { opt ->
|
||||||
if (opt.isPresent) {
|
if (opt.isPresent) {
|
||||||
progressBar.pause()
|
|
||||||
when (val sheet = opt.get()) {
|
when (val sheet = opt.get()) {
|
||||||
is StoryViewerDialog.GroupDirectReply -> {
|
is StoryViewerDialog.GroupDirectReply -> {
|
||||||
onStartDirectReply(sheet.storyId, sheet.recipientId)
|
onStartDirectReply(sheet.storyId, sheet.recipientId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
resumeProgress()
|
}
|
||||||
|
|
||||||
|
lifecycleDisposable += videoControlsDelegate.playerUpdates.subscribe { update ->
|
||||||
|
if (update.duration > 0L) {
|
||||||
|
viewModel.setDuration(update.mediaUri, update.duration)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,7 +221,6 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
progressBar.pause()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
|
@ -205,14 +230,12 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
|
||||||
progressBar.reset()
|
progressBar.reset()
|
||||||
progressBar.setPosition(viewModel.getRestartIndex())
|
progressBar.setPosition(viewModel.getRestartIndex())
|
||||||
}
|
}
|
||||||
|
|
||||||
resumeProgressIfNotDisplayingDialog()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFinishForwardAction() = Unit
|
override fun onFinishForwardAction() = Unit
|
||||||
|
|
||||||
override fun onDismissForwardSheet() {
|
override fun onDismissForwardSheet() {
|
||||||
viewModel.onForwardDismissed()
|
viewModel.setIsDisplayingForwardDialog(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hideChrome() {
|
private fun hideChrome() {
|
||||||
|
@ -264,18 +287,18 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
|
||||||
constraintSet.applyTo(requireView() as ConstraintLayout)
|
constraintSet.applyTo(requireView() as ConstraintLayout)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resumeProgressIfNotDisplayingDialog() {
|
|
||||||
if (childFragmentManager.findFragmentByTag(BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG) == null) {
|
|
||||||
resumeProgress()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun resumeProgress() {
|
private fun resumeProgress() {
|
||||||
if (progressBar.segmentCount != 0) {
|
if (progressBar.segmentCount != 0) {
|
||||||
progressBar.start()
|
progressBar.start()
|
||||||
|
videoControlsDelegate.resume(viewModel.getPost().attachment.uri!!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun pauseProgress() {
|
||||||
|
progressBar.pause()
|
||||||
|
videoControlsDelegate.pause()
|
||||||
|
}
|
||||||
|
|
||||||
private fun startReply() {
|
private fun startReply() {
|
||||||
val replyFragment: DialogFragment = when (viewModel.getSwipeToReplyState()) {
|
val replyFragment: DialogFragment = when (viewModel.getSwipeToReplyState()) {
|
||||||
StoryViewerPageState.ReplyState.NONE -> return
|
StoryViewerPageState.ReplyState.NONE -> return
|
||||||
|
@ -285,12 +308,17 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
|
||||||
StoryViewerPageState.ReplyState.GROUP_SELF -> StoryViewsAndRepliesDialogFragment.create(viewModel.getPost().id, viewModel.getPost().group!!.id, getViewsAndRepliesDialogStartPage())
|
StoryViewerPageState.ReplyState.GROUP_SELF -> StoryViewsAndRepliesDialogFragment.create(viewModel.getPost().id, viewModel.getPost().group!!.id, getViewsAndRepliesDialogStartPage())
|
||||||
}
|
}
|
||||||
|
|
||||||
progressBar.pause()
|
if (viewModel.getSwipeToReplyState() == StoryViewerPageState.ReplyState.PRIVATE) {
|
||||||
|
viewModel.setIsDisplayingDirectReplyDialog(true)
|
||||||
|
} else {
|
||||||
|
viewModel.setIsDisplayingViewsAndRepliesDialog(true)
|
||||||
|
}
|
||||||
|
|
||||||
replyFragment.showNow(childFragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
|
replyFragment.showNow(childFragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onStartDirectReply(storyId: Long, recipientId: RecipientId) {
|
private fun onStartDirectReply(storyId: Long, recipientId: RecipientId) {
|
||||||
progressBar.pause()
|
viewModel.setIsDisplayingDirectReplyDialog(true)
|
||||||
StoryDirectReplyDialogFragment.create(
|
StoryDirectReplyDialogFragment.create(
|
||||||
storyId = storyId,
|
storyId = storyId,
|
||||||
recipientId = recipientId
|
recipientId = recipientId
|
||||||
|
@ -359,7 +387,7 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
|
||||||
largeCaptionOverlay.setOnClickListener {
|
largeCaptionOverlay.setOnClickListener {
|
||||||
onHideCaptionOverlay(caption, largeCaption, largeCaptionOverlay)
|
onHideCaptionOverlay(caption, largeCaption, largeCaptionOverlay)
|
||||||
}
|
}
|
||||||
progressBar.pause()
|
viewModel.setIsDisplayingCaptionOverlay(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onHideCaptionOverlay(caption: TextView, largeCaption: TextView, largeCaptionOverlay: View) {
|
private fun onHideCaptionOverlay(caption: TextView, largeCaption: TextView, largeCaptionOverlay: View) {
|
||||||
|
@ -367,7 +395,7 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
|
||||||
largeCaption.visible = false
|
largeCaption.visible = false
|
||||||
largeCaptionOverlay.visible = false
|
largeCaptionOverlay.visible = false
|
||||||
largeCaptionOverlay.setOnClickListener(null)
|
largeCaptionOverlay.setOnClickListener(null)
|
||||||
resumeProgress()
|
viewModel.setIsDisplayingCaptionOverlay(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun presentFrom(from: TextView, storyPost: StoryPost) {
|
private fun presentFrom(from: TextView, storyPost: StoryPost) {
|
||||||
|
@ -420,20 +448,24 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createFragmentForPost(storyPost: StoryPost): Fragment {
|
private fun createFragmentForPost(storyPost: StoryPost): Fragment {
|
||||||
return MediaPreviewFragment.newInstance(storyPost.attachment, true)
|
return MediaPreviewFragment.newInstance(storyPost.attachment, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getVideoControlsDelegate(): VideoControlsDelegate {
|
||||||
|
return videoControlsDelegate
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun displayMoreContextMenu(anchor: View) {
|
private fun displayMoreContextMenu(anchor: View) {
|
||||||
progressBar.pause()
|
viewModel.setIsDisplayingContextMenu(true)
|
||||||
StoryContextMenu.show(
|
StoryContextMenu.show(
|
||||||
context = requireContext(),
|
context = requireContext(),
|
||||||
anchorView = anchor,
|
anchorView = anchor,
|
||||||
storyViewerPageState = viewModel.getStateSnapshot(),
|
storyViewerPageState = viewModel.getStateSnapshot(),
|
||||||
onDismiss = {
|
onDismiss = {
|
||||||
viewModel.onDismissContextMenu()
|
viewModel.setIsDisplayingContextMenu(false)
|
||||||
},
|
},
|
||||||
onForward = { storyPost ->
|
onForward = { storyPost ->
|
||||||
viewModel.startForward()
|
viewModel.setIsDisplayingForwardDialog(true)
|
||||||
MultiselectForwardFragmentArgs.create(
|
MultiselectForwardFragmentArgs.create(
|
||||||
requireContext(),
|
requireContext(),
|
||||||
storyPost.conversationMessage.multiselectCollection.toSet(),
|
storyPost.conversationMessage.multiselectCollection.toSet(),
|
||||||
|
@ -456,9 +488,9 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
|
||||||
StoryContextMenu.save(requireContext(), it.conversationMessage.messageRecord)
|
StoryContextMenu.save(requireContext(), it.conversationMessage.messageRecord)
|
||||||
},
|
},
|
||||||
onDelete = {
|
onDelete = {
|
||||||
viewModel.startDelete()
|
viewModel.setIsDisplayingDeleteDialog(true)
|
||||||
lifecycleDisposable += StoryContextMenu.delete(requireContext(), setOf(it.conversationMessage.messageRecord)).subscribe { _ ->
|
lifecycleDisposable += StoryContextMenu.delete(requireContext(), setOf(it.conversationMessage.messageRecord)).subscribe { _ ->
|
||||||
viewModel.onDeleteDismissed()
|
viewModel.setIsDisplayingDeleteDialog(false)
|
||||||
viewModel.refresh()
|
viewModel.refresh()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,8 +66,7 @@ class StoryViewerPageRepository(context: Context) {
|
||||||
viewCount = record.viewedReceiptCount,
|
viewCount = record.viewedReceiptCount,
|
||||||
replyCount = SignalDatabase.mms.getNumberOfStoryReplies(record.id),
|
replyCount = SignalDatabase.mms.getNumberOfStoryReplies(record.id),
|
||||||
dateInMilliseconds = record.dateSent,
|
dateInMilliseconds = record.dateSent,
|
||||||
durationMillis = getDurationMillis(record as MmsMessageRecord),
|
attachment = (record as MmsMessageRecord).slideDeck.firstSlide!!.asAttachment(),
|
||||||
attachment = record.slideDeck.firstSlide!!.asAttachment(),
|
|
||||||
conversationMessage = ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(context, record)
|
conversationMessage = ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(context, record)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -125,16 +124,6 @@ class StoryViewerPageRepository(context: Context) {
|
||||||
}.observeOn(Schedulers.io())
|
}.observeOn(Schedulers.io())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDurationMillis(record: MmsMessageRecord): Long {
|
|
||||||
val slide = record.slideDeck.firstSlide!!
|
|
||||||
return if (slide.hasVideo()) {
|
|
||||||
// TODO [stories] Remove duration from this stuff... Videos will need to actually start playback before we know how long they are...
|
|
||||||
5000
|
|
||||||
} else {
|
|
||||||
5000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hideStory(recipientId: RecipientId): Completable {
|
fun hideStory(recipientId: RecipientId): Completable {
|
||||||
return Completable.fromAction {
|
return Completable.fromAction {
|
||||||
SignalDatabase.recipients.setHideStory(recipientId, true)
|
SignalDatabase.recipients.setHideStory(recipientId, true)
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
package org.thoughtcrime.securesms.stories.viewer.page
|
package org.thoughtcrime.securesms.stories.viewer.page
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
|
||||||
data class StoryViewerPageState(
|
data class StoryViewerPageState(
|
||||||
val posts: List<StoryPost> = emptyList(),
|
val posts: List<StoryPost> = emptyList(),
|
||||||
|
val durations: Map<Uri, Long> = emptyMap(),
|
||||||
val selectedPostIndex: Int = 0,
|
val selectedPostIndex: Int = 0,
|
||||||
val replyState: ReplyState = ReplyState.NONE
|
val replyState: ReplyState = ReplyState.NONE
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package org.thoughtcrime.securesms.stories.viewer.page
|
package org.thoughtcrime.securesms.stories.viewer.page
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
@ -28,6 +29,10 @@ class StoryViewerPageViewModel(
|
||||||
private val storyViewerDialogSubject: Subject<Optional<StoryViewerDialog>> = BehaviorSubject.createDefault(Optional.empty())
|
private val storyViewerDialogSubject: Subject<Optional<StoryViewerDialog>> = BehaviorSubject.createDefault(Optional.empty())
|
||||||
private val dismissSubject = PublishSubject.create<StoryViewerDialog.Type>()
|
private val dismissSubject = PublishSubject.create<StoryViewerDialog.Type>()
|
||||||
|
|
||||||
|
private val storyViewerPlaybackStore = Store(StoryViewerPlaybackState())
|
||||||
|
|
||||||
|
val storyViewerPlaybackState: LiveData<StoryViewerPlaybackState> = storyViewerPlaybackStore.stateLiveData
|
||||||
|
|
||||||
val groupDirectReplyObservable: Observable<Optional<StoryViewerDialog>> = Observable.combineLatest(storyViewerDialogSubject, dismissSubject) { sheet, dismissed ->
|
val groupDirectReplyObservable: Observable<Optional<StoryViewerDialog>> = Observable.combineLatest(storyViewerDialogSubject, dismissSubject) { sheet, dismissed ->
|
||||||
if (sheet.isPresent && sheet.get().type != dismissed) {
|
if (sheet.isPresent && sheet.get().type != dismissed) {
|
||||||
sheet
|
sheet
|
||||||
|
@ -50,6 +55,12 @@ class StoryViewerPageViewModel(
|
||||||
refresh()
|
refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setDuration(uri: Uri, duration: Long) {
|
||||||
|
store.update {
|
||||||
|
it.copy(durations = it.durations + (uri to duration))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun refresh() {
|
fun refresh() {
|
||||||
disposables.clear()
|
disposables.clear()
|
||||||
disposables += repository.getStoryPostsFor(recipientId).subscribe { posts ->
|
disposables += repository.getStoryPostsFor(recipientId).subscribe { posts ->
|
||||||
|
@ -95,32 +106,36 @@ class StoryViewerPageViewModel(
|
||||||
storyViewerDialogSubject.onNext(Optional.of(StoryViewerDialog.GroupDirectReply(recipientId, storyId)))
|
storyViewerDialogSubject.onNext(Optional.of(StoryViewerDialog.GroupDirectReply(recipientId, storyId)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startForward() {
|
fun setIsDisplayingContextMenu(isDisplayingContextMenu: Boolean) {
|
||||||
storyViewerDialogSubject.onNext(Optional.of(StoryViewerDialog.Forward))
|
storyViewerPlaybackStore.update { it.copy(isDisplayingContextMenu = isDisplayingContextMenu) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDelete() {
|
fun setIsDisplayingForwardDialog(isDisplayingForwardDialog: Boolean) {
|
||||||
storyViewerDialogSubject.onNext(Optional.of(StoryViewerDialog.Delete))
|
storyViewerPlaybackStore.update { it.copy(isDisplayingForwardDialog = isDisplayingForwardDialog) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onForwardDismissed() {
|
fun setIsDisplayingDeleteDialog(isDisplayingDeleteDialog: Boolean) {
|
||||||
dismissSubject.onNext(StoryViewerDialog.Type.FORWARD)
|
storyViewerPlaybackStore.update { it.copy(isDisplayingDeleteDialog = isDisplayingDeleteDialog) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onDeleteDismissed() {
|
fun setIsDisplayingViewsAndRepliesDialog(isDisplayingViewsAndRepliesDialog: Boolean) {
|
||||||
dismissSubject.onNext(StoryViewerDialog.Type.DELETE)
|
storyViewerPlaybackStore.update { it.copy(isDisplayingViewsAndRepliesDialog = isDisplayingViewsAndRepliesDialog) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onDismissContextMenu() {
|
fun setIsDisplayingDirectReplyDialog(isDisplayingDirectReplyDialog: Boolean) {
|
||||||
dismissSubject.onNext(StoryViewerDialog.Type.CONTEXT_MENU)
|
storyViewerPlaybackStore.update { it.copy(isDisplayingDirectReplyDialog = isDisplayingDirectReplyDialog) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onViewsAndRepliesSheetDismissed() {
|
fun setIsDisplayingCaptionOverlay(isDisplayingCaptionOverlay: Boolean) {
|
||||||
dismissSubject.onNext(StoryViewerDialog.Type.VIEWS_AND_REPLIES)
|
storyViewerPlaybackStore.update { it.copy(isDisplayingCaptionOverlay = isDisplayingCaptionOverlay) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onDirectReplyDismissed() {
|
fun setIsUserTouching(isUserTouching: Boolean) {
|
||||||
dismissSubject.onNext(StoryViewerDialog.Type.DIRECT_REPLY)
|
storyViewerPlaybackStore.update { it.copy(isUserTouching = isUserTouching) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAreSegmentsInitialized(areSegmentsInitialized: Boolean) {
|
||||||
|
storyViewerPlaybackStore.update { it.copy(areSegmentsInitialized = areSegmentsInitialized) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resolveSwipeToReplyState(state: StoryViewerPageState, index: Int = state.selectedPostIndex): StoryViewerPageState.ReplyState {
|
private fun resolveSwipeToReplyState(state: StoryViewerPageState, index: Int = state.selectedPostIndex): StoryViewerPageState.ReplyState {
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
package org.thoughtcrime.securesms.stories.viewer.page
|
||||||
|
|
||||||
|
data class StoryViewerPlaybackState(
|
||||||
|
val areSegmentsInitialized: Boolean = false,
|
||||||
|
val isUserTouching: Boolean = false,
|
||||||
|
val isDisplayingForwardDialog: Boolean = false,
|
||||||
|
val isDisplayingDeleteDialog: Boolean = false,
|
||||||
|
val isDisplayingContextMenu: Boolean = false,
|
||||||
|
val isDisplayingViewsAndRepliesDialog: Boolean = false,
|
||||||
|
val isDisplayingDirectReplyDialog: Boolean = false,
|
||||||
|
val isDisplayingCaptionOverlay: Boolean = false
|
||||||
|
) {
|
||||||
|
val isPaused: Boolean = !areSegmentsInitialized ||
|
||||||
|
isUserTouching ||
|
||||||
|
isDisplayingCaptionOverlay ||
|
||||||
|
isDisplayingForwardDialog ||
|
||||||
|
isDisplayingDeleteDialog ||
|
||||||
|
isDisplayingContextMenu ||
|
||||||
|
isDisplayingViewsAndRepliesDialog ||
|
||||||
|
isDisplayingDirectReplyDialog ||
|
||||||
|
isDisplayingCaptionOverlay
|
||||||
|
}
|
|
@ -122,7 +122,7 @@ class StoryDirectReplyDialogFragment :
|
||||||
|
|
||||||
override fun onDismiss(dialog: DialogInterface) {
|
override fun onDismiss(dialog: DialogInterface) {
|
||||||
super.onDismiss(dialog)
|
super.onDismiss(dialog)
|
||||||
storyViewerPageViewModel.onDirectReplyDismissed()
|
storyViewerPageViewModel.setIsDisplayingDirectReplyDialog(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -66,7 +66,7 @@ class StoryGroupReplyBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDi
|
||||||
|
|
||||||
override fun onDismiss(dialog: DialogInterface) {
|
override fun onDismiss(dialog: DialogInterface) {
|
||||||
super.onDismiss(dialog)
|
super.onDismiss(dialog)
|
||||||
storyViewerPageViewModel.onViewsAndRepliesSheetDismissed()
|
storyViewerPageViewModel.setIsDisplayingViewsAndRepliesDialog(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartDirectReply(recipientId: RecipientId) {
|
override fun onStartDirectReply(recipientId: RecipientId) {
|
||||||
|
|
|
@ -102,7 +102,7 @@ class StoryViewsAndRepliesDialogFragment : FixedRoundedCornerBottomSheetDialogFr
|
||||||
|
|
||||||
override fun onDismiss(dialog: DialogInterface) {
|
override fun onDismiss(dialog: DialogInterface) {
|
||||||
super.onDismiss(dialog)
|
super.onDismiss(dialog)
|
||||||
storyViewerPageViewModel.onViewsAndRepliesSheetDismissed()
|
storyViewerPageViewModel.setIsDisplayingViewsAndRepliesDialog(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartDirectReply(recipientId: RecipientId) {
|
override fun onStartDirectReply(recipientId: RecipientId) {
|
||||||
|
|
|
@ -40,7 +40,7 @@ class StoryViewsBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogF
|
||||||
|
|
||||||
override fun onDismiss(dialog: DialogInterface) {
|
override fun onDismiss(dialog: DialogInterface) {
|
||||||
super.onDismiss(dialog)
|
super.onDismiss(dialog)
|
||||||
storyViewerPageViewModel.onViewsAndRepliesSheetDismissed()
|
storyViewerPageViewModel.setIsDisplayingViewsAndRepliesDialog(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
Ładowanie…
Reference in New Issue