2022-02-24 17:40:28 +00:00
|
|
|
package org.thoughtcrime.securesms.stories.viewer.page
|
|
|
|
|
2022-06-27 12:19:33 +00:00
|
|
|
import android.animation.Animator
|
2022-02-24 17:40:28 +00:00
|
|
|
import android.animation.AnimatorSet
|
|
|
|
import android.animation.ObjectAnimator
|
|
|
|
import android.annotation.SuppressLint
|
|
|
|
import android.content.Context
|
2022-09-21 19:57:20 +00:00
|
|
|
import android.content.res.ColorStateList
|
2022-02-24 17:40:28 +00:00
|
|
|
import android.graphics.drawable.Drawable
|
2022-06-27 12:19:33 +00:00
|
|
|
import android.media.AudioManager
|
2022-02-24 17:40:28 +00:00
|
|
|
import android.os.Bundle
|
2022-09-19 17:24:44 +00:00
|
|
|
import android.text.method.ScrollingMovementMethod
|
2022-02-24 17:40:28 +00:00
|
|
|
import android.view.GestureDetector
|
|
|
|
import android.view.MotionEvent
|
2022-09-16 15:35:06 +00:00
|
|
|
import android.view.ScaleGestureDetector
|
2022-02-24 17:40:28 +00:00
|
|
|
import android.view.View
|
2022-03-23 12:01:22 +00:00
|
|
|
import android.view.animation.Interpolator
|
2022-04-07 19:08:05 +00:00
|
|
|
import android.widget.FrameLayout
|
2022-02-24 17:40:28 +00:00
|
|
|
import android.widget.TextView
|
|
|
|
import androidx.cardview.widget.CardView
|
|
|
|
import androidx.constraintlayout.widget.ConstraintLayout
|
|
|
|
import androidx.constraintlayout.widget.ConstraintSet
|
2022-09-21 19:57:20 +00:00
|
|
|
import androidx.core.content.ContextCompat
|
2022-07-14 20:00:15 +00:00
|
|
|
import androidx.core.os.bundleOf
|
2022-02-24 17:40:28 +00:00
|
|
|
import androidx.core.view.GestureDetectorCompat
|
2022-03-23 12:01:22 +00:00
|
|
|
import androidx.core.view.animation.PathInterpolatorCompat
|
2022-02-24 17:40:28 +00:00
|
|
|
import androidx.core.view.doOnNextLayout
|
|
|
|
import androidx.fragment.app.DialogFragment
|
|
|
|
import androidx.fragment.app.Fragment
|
|
|
|
import androidx.fragment.app.viewModels
|
2022-05-12 18:08:24 +00:00
|
|
|
import com.google.android.material.button.MaterialButton
|
2022-09-21 19:57:20 +00:00
|
|
|
import com.google.android.material.progressindicator.CircularProgressIndicatorSpec
|
|
|
|
import com.google.android.material.progressindicator.IndeterminateDrawable
|
2022-03-02 18:20:49 +00:00
|
|
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
|
|
|
import io.reactivex.rxjava3.core.Observable
|
2022-02-24 17:40:28 +00:00
|
|
|
import org.signal.core.util.DimensionUnit
|
2022-09-21 19:57:20 +00:00
|
|
|
import org.signal.core.util.dp
|
2022-07-06 12:45:13 +00:00
|
|
|
import org.signal.core.util.logging.Log
|
2022-02-24 17:40:28 +00:00
|
|
|
import org.thoughtcrime.securesms.R
|
2022-09-16 15:35:06 +00:00
|
|
|
import org.thoughtcrime.securesms.animation.AnimationCompleteListener
|
2022-02-24 17:40:28 +00:00
|
|
|
import org.thoughtcrime.securesms.components.AvatarImageView
|
|
|
|
import org.thoughtcrime.securesms.components.segmentedprogressbar.SegmentedProgressBar
|
|
|
|
import org.thoughtcrime.securesms.components.segmentedprogressbar.SegmentedProgressBarListener
|
|
|
|
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto
|
|
|
|
import org.thoughtcrime.securesms.contacts.avatars.FallbackPhoto20dp
|
|
|
|
import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto
|
2022-09-21 19:57:20 +00:00
|
|
|
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
|
2022-02-24 17:40:28 +00:00
|
|
|
import org.thoughtcrime.securesms.conversation.ConversationIntents
|
|
|
|
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
|
2022-03-02 18:20:49 +00:00
|
|
|
import org.thoughtcrime.securesms.database.AttachmentDatabase
|
2022-02-24 17:40:28 +00:00
|
|
|
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
|
|
|
import org.thoughtcrime.securesms.mediapreview.MediaPreviewFragment
|
2022-02-24 21:39:56 +00:00
|
|
|
import org.thoughtcrime.securesms.mediapreview.VideoControlsDelegate
|
2022-03-30 16:15:25 +00:00
|
|
|
import org.thoughtcrime.securesms.mms.GlideApp
|
2022-02-24 17:40:28 +00:00
|
|
|
import org.thoughtcrime.securesms.recipients.Recipient
|
|
|
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
2022-11-08 22:34:02 +00:00
|
|
|
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment
|
2022-09-21 19:57:20 +00:00
|
|
|
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet
|
2022-03-02 18:20:49 +00:00
|
|
|
import org.thoughtcrime.securesms.stories.StorySlateView
|
2022-06-27 12:19:33 +00:00
|
|
|
import org.thoughtcrime.securesms.stories.StoryVolumeOverlayView
|
2022-02-24 17:40:28 +00:00
|
|
|
import org.thoughtcrime.securesms.stories.dialogs.StoryContextMenu
|
2022-09-16 16:29:39 +00:00
|
|
|
import org.thoughtcrime.securesms.stories.dialogs.StoryDialogs
|
2022-02-25 20:33:33 +00:00
|
|
|
import org.thoughtcrime.securesms.stories.viewer.StoryViewerViewModel
|
2022-06-27 12:19:33 +00:00
|
|
|
import org.thoughtcrime.securesms.stories.viewer.StoryVolumeViewModel
|
2022-07-14 20:00:15 +00:00
|
|
|
import org.thoughtcrime.securesms.stories.viewer.info.StoryInfoBottomSheetDialogFragment
|
2022-10-12 13:02:27 +00:00
|
|
|
import org.thoughtcrime.securesms.stories.viewer.post.StoryPostFragment
|
2022-02-24 17:40:28 +00:00
|
|
|
import org.thoughtcrime.securesms.stories.viewer.reply.direct.StoryDirectReplyDialogFragment
|
|
|
|
import org.thoughtcrime.securesms.stories.viewer.reply.group.StoryGroupReplyBottomSheetDialogFragment
|
2022-03-21 13:39:06 +00:00
|
|
|
import org.thoughtcrime.securesms.stories.viewer.reply.reaction.OnReactionSentView
|
2022-02-24 17:40:28 +00:00
|
|
|
import org.thoughtcrime.securesms.stories.viewer.reply.tabs.StoryViewsAndRepliesDialogFragment
|
|
|
|
import org.thoughtcrime.securesms.stories.viewer.views.StoryViewsBottomSheetDialogFragment
|
|
|
|
import org.thoughtcrime.securesms.util.AvatarUtil
|
|
|
|
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
|
|
|
import org.thoughtcrime.securesms.util.DateUtils
|
2022-06-27 12:19:33 +00:00
|
|
|
import org.thoughtcrime.securesms.util.Debouncer
|
2022-02-24 17:40:28 +00:00
|
|
|
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
2022-06-27 12:19:33 +00:00
|
|
|
import org.thoughtcrime.securesms.util.ServiceUtil
|
2022-03-18 19:10:41 +00:00
|
|
|
import org.thoughtcrime.securesms.util.ViewUtil
|
2022-02-24 17:40:28 +00:00
|
|
|
import org.thoughtcrime.securesms.util.fragments.requireListener
|
|
|
|
import org.thoughtcrime.securesms.util.views.TouchInterceptingFrameLayout
|
|
|
|
import org.thoughtcrime.securesms.util.visible
|
|
|
|
import java.util.Locale
|
2022-02-24 21:39:56 +00:00
|
|
|
import java.util.concurrent.TimeUnit
|
2022-02-24 17:40:28 +00:00
|
|
|
import kotlin.math.abs
|
2022-03-23 12:01:22 +00:00
|
|
|
import kotlin.math.max
|
2022-03-30 19:30:06 +00:00
|
|
|
import kotlin.math.min
|
2022-02-24 17:40:28 +00:00
|
|
|
|
2022-03-09 17:11:56 +00:00
|
|
|
class StoryViewerPageFragment :
|
|
|
|
Fragment(R.layout.stories_viewer_fragment_page),
|
2022-10-12 13:02:27 +00:00
|
|
|
StoryPostFragment.Callback,
|
2022-03-09 17:11:56 +00:00
|
|
|
MultiselectForwardBottomSheet.Callback,
|
|
|
|
StorySlateView.Callback,
|
2022-09-21 19:57:20 +00:00
|
|
|
StoryInfoBottomSheetDialogFragment.OnInfoSheetDismissedListener,
|
2022-11-08 22:34:02 +00:00
|
|
|
SafetyNumberBottomSheet.Callbacks,
|
|
|
|
RecipientBottomSheetDialogFragment.Callback {
|
2022-02-24 17:40:28 +00:00
|
|
|
|
2022-07-05 15:38:35 +00:00
|
|
|
private val storyVolumeViewModel: StoryVolumeViewModel by viewModels(ownerProducer = { requireActivity() })
|
2022-06-27 12:19:33 +00:00
|
|
|
|
2022-02-24 17:40:28 +00:00
|
|
|
private lateinit var progressBar: SegmentedProgressBar
|
2022-03-02 18:20:49 +00:00
|
|
|
private lateinit var storySlate: StorySlateView
|
2022-05-12 18:08:24 +00:00
|
|
|
private lateinit var viewsAndReplies: MaterialButton
|
2022-04-07 19:08:05 +00:00
|
|
|
private lateinit var storyCaptionContainer: FrameLayout
|
|
|
|
private lateinit var storyContentContainer: FrameLayout
|
2022-10-27 16:43:52 +00:00
|
|
|
private lateinit var storyPageContainer: ConstraintLayout
|
2022-09-29 13:20:08 +00:00
|
|
|
private lateinit var sendingBarTextView: TextView
|
|
|
|
private lateinit var sendingBar: View
|
2022-11-16 18:38:52 +00:00
|
|
|
private lateinit var storyNormalBottomGradient: View
|
|
|
|
private lateinit var storyCaptionBottomGradient: View
|
2022-02-24 17:40:28 +00:00
|
|
|
|
|
|
|
private lateinit var callback: Callback
|
|
|
|
|
|
|
|
private lateinit var chrome: List<View>
|
|
|
|
private var animatorSet: AnimatorSet? = null
|
|
|
|
|
2022-06-27 12:19:33 +00:00
|
|
|
private var volumeInAnimator: Animator? = null
|
|
|
|
private var volumeOutAnimator: Animator? = null
|
|
|
|
private var volumeDebouncer: Debouncer = Debouncer(3, TimeUnit.SECONDS)
|
|
|
|
|
2022-10-28 19:51:13 +00:00
|
|
|
private val storyViewStateViewModel: StoryViewStateViewModel by viewModels()
|
|
|
|
|
2022-02-24 17:40:28 +00:00
|
|
|
private val viewModel: StoryViewerPageViewModel by viewModels(
|
|
|
|
factoryProducer = {
|
2022-07-05 15:38:35 +00:00
|
|
|
StoryViewerPageViewModel.Factory(
|
2022-09-26 17:20:50 +00:00
|
|
|
storyViewerPageArgs,
|
2022-07-05 15:38:35 +00:00
|
|
|
StoryViewerPageRepository(
|
2022-10-28 19:51:13 +00:00
|
|
|
requireContext(),
|
|
|
|
storyViewStateViewModel.storyViewStateCache
|
2022-07-05 15:38:35 +00:00
|
|
|
),
|
|
|
|
StoryCache(
|
|
|
|
GlideApp.with(requireActivity()),
|
|
|
|
StoryDisplay.getStorySize(resources)
|
|
|
|
)
|
|
|
|
)
|
2022-02-24 17:40:28 +00:00
|
|
|
}
|
|
|
|
)
|
|
|
|
|
2022-02-25 20:33:33 +00:00
|
|
|
private val sharedViewModel: StoryViewerViewModel by viewModels(
|
|
|
|
ownerProducer = { requireParentFragment() }
|
|
|
|
)
|
|
|
|
|
2022-02-24 21:39:56 +00:00
|
|
|
private val videoControlsDelegate = VideoControlsDelegate()
|
|
|
|
|
2022-02-24 17:40:28 +00:00
|
|
|
private val lifecycleDisposable = LifecycleDisposable()
|
2022-03-02 18:20:49 +00:00
|
|
|
private val timeoutDisposable = LifecycleDisposable()
|
2022-02-24 17:40:28 +00:00
|
|
|
|
2022-09-21 19:57:20 +00:00
|
|
|
private var sendingProgressDrawable: IndeterminateDrawable<CircularProgressIndicatorSpec>? = null
|
|
|
|
|
2022-09-26 17:20:50 +00:00
|
|
|
private val storyViewerPageArgs: StoryViewerPageArgs by lazy(LazyThreadSafetyMode.NONE) { requireArguments().getParcelable(ARGS)!! }
|
2022-07-14 20:00:15 +00:00
|
|
|
|
2022-02-24 17:40:28 +00:00
|
|
|
@SuppressLint("ClickableViewAccessibility")
|
|
|
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
|
|
callback = requireListener()
|
|
|
|
|
2022-07-05 15:38:35 +00:00
|
|
|
if (storyVolumeViewModel.snapshot.isMuted) {
|
2022-06-27 12:19:33 +00:00
|
|
|
videoControlsDelegate.mute()
|
|
|
|
} else {
|
|
|
|
videoControlsDelegate.unmute()
|
|
|
|
}
|
|
|
|
|
2022-02-24 17:40:28 +00:00
|
|
|
val closeView: View = view.findViewById(R.id.close)
|
|
|
|
val senderAvatar: AvatarImageView = view.findViewById(R.id.sender_avatar)
|
|
|
|
val groupAvatar: AvatarImageView = view.findViewById(R.id.group_avatar)
|
|
|
|
val from: TextView = view.findViewById(R.id.from)
|
|
|
|
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 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)
|
2022-03-21 13:39:06 +00:00
|
|
|
val reactionAnimationView: OnReactionSentView = view.findViewById(R.id.on_reaction_sent_view)
|
2022-04-07 19:08:05 +00:00
|
|
|
val storyGradientTop: View = view.findViewById(R.id.story_gradient_top)
|
2022-11-16 18:38:52 +00:00
|
|
|
val storyGradientBottom: View = view.findViewById(R.id.story_bottom_gradient_container)
|
2022-06-27 12:19:33 +00:00
|
|
|
val storyVolumeOverlayView: StoryVolumeOverlayView = view.findViewById(R.id.story_volume_overlay)
|
2022-02-24 17:40:28 +00:00
|
|
|
|
2022-11-16 18:38:52 +00:00
|
|
|
storyNormalBottomGradient = view.findViewById(R.id.story_gradient_bottom)
|
|
|
|
storyCaptionBottomGradient = view.findViewById(R.id.story_caption_gradient)
|
2022-10-27 16:43:52 +00:00
|
|
|
storyPageContainer = view.findViewById(R.id.story_page_container)
|
2022-04-07 19:08:05 +00:00
|
|
|
storyContentContainer = view.findViewById(R.id.story_content_container)
|
|
|
|
storyCaptionContainer = view.findViewById(R.id.story_caption_container)
|
2022-03-02 18:20:49 +00:00
|
|
|
storySlate = view.findViewById(R.id.story_slate)
|
2022-02-24 17:40:28 +00:00
|
|
|
progressBar = view.findViewById(R.id.progress)
|
2022-03-02 18:20:49 +00:00
|
|
|
viewsAndReplies = view.findViewById(R.id.views_and_replies_bar)
|
2022-09-29 13:20:08 +00:00
|
|
|
sendingBarTextView = view.findViewById(R.id.sending_text_view)
|
|
|
|
sendingBar = view.findViewById(R.id.sending_bar)
|
2022-03-02 18:20:49 +00:00
|
|
|
|
|
|
|
storySlate.callback = this
|
2022-02-24 17:40:28 +00:00
|
|
|
|
|
|
|
chrome = listOf(
|
|
|
|
closeView,
|
|
|
|
senderAvatar,
|
|
|
|
groupAvatar,
|
|
|
|
from,
|
|
|
|
date,
|
|
|
|
moreButton,
|
|
|
|
distributionList,
|
|
|
|
viewsAndReplies,
|
2022-04-07 19:08:05 +00:00
|
|
|
progressBar,
|
|
|
|
storyGradientTop,
|
2022-06-24 18:34:17 +00:00
|
|
|
storyGradientBottom,
|
2022-11-16 18:38:52 +00:00
|
|
|
storyCaptionContainer
|
2022-02-24 17:40:28 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
senderAvatar.setFallbackPhotoProvider(FallbackPhotoProvider())
|
|
|
|
groupAvatar.setFallbackPhotoProvider(FallbackPhotoProvider())
|
|
|
|
|
|
|
|
closeView.setOnClickListener {
|
|
|
|
requireActivity().onBackPressed()
|
|
|
|
}
|
|
|
|
|
2022-10-12 16:33:03 +00:00
|
|
|
val singleTapHandler = SingleTapHandler(
|
|
|
|
cardWrapper,
|
|
|
|
viewModel::goToNextPost,
|
|
|
|
viewModel::goToPreviousPost,
|
|
|
|
)
|
|
|
|
|
2022-02-24 17:40:28 +00:00
|
|
|
val gestureDetector = GestureDetectorCompat(
|
|
|
|
requireContext(),
|
|
|
|
StoryGestureListener(
|
|
|
|
cardWrapper,
|
2022-10-12 16:33:03 +00:00
|
|
|
singleTapHandler,
|
2022-03-23 12:01:22 +00:00
|
|
|
this::startReply,
|
2022-09-27 17:57:39 +00:00
|
|
|
requireListener<Callback>()::onContentTranslation,
|
2022-03-23 12:01:22 +00:00
|
|
|
sharedViewModel = sharedViewModel
|
2022-02-24 17:40:28 +00:00
|
|
|
)
|
|
|
|
)
|
|
|
|
|
2022-10-14 13:31:55 +00:00
|
|
|
gestureDetector.setOnDoubleTapListener(null)
|
|
|
|
|
2022-09-16 15:35:06 +00:00
|
|
|
val scaleListener = StoryScaleListener(
|
|
|
|
viewModel, sharedViewModel, card
|
|
|
|
)
|
|
|
|
|
|
|
|
val scaleDetector = ScaleGestureDetector(
|
|
|
|
requireContext(),
|
|
|
|
scaleListener
|
|
|
|
)
|
|
|
|
|
2022-10-12 13:02:27 +00:00
|
|
|
cardWrapper.setOnInterceptTouchEventListener { !storySlate.state.hasClickableContent && viewModel.getPost()?.content?.isText() != true }
|
2022-02-24 17:40:28 +00:00
|
|
|
cardWrapper.setOnTouchListener { _, event ->
|
2022-09-16 15:35:06 +00:00
|
|
|
scaleDetector.onTouchEvent(event)
|
|
|
|
val result = if (scaleDetector.isInProgress || scaleListener.isPerformingEndAnimation) {
|
|
|
|
true
|
|
|
|
} else {
|
|
|
|
gestureDetector.onTouchEvent(event)
|
|
|
|
}
|
|
|
|
|
2022-02-24 17:40:28 +00:00
|
|
|
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
|
2022-02-24 21:39:56 +00:00
|
|
|
viewModel.setIsUserTouching(true)
|
2022-02-24 17:40:28 +00:00
|
|
|
} else if (event.actionMasked == MotionEvent.ACTION_UP || event.actionMasked == MotionEvent.ACTION_CANCEL) {
|
2022-02-24 21:39:56 +00:00
|
|
|
viewModel.setIsUserTouching(false)
|
2022-03-23 12:01:22 +00:00
|
|
|
|
|
|
|
val canCloseFromHorizontalSlide = requireView().translationX > DimensionUnit.DP.toPixels(56f)
|
2022-06-23 18:44:22 +00:00
|
|
|
val canCloseFromVerticalSlide = requireView().translationY > DimensionUnit.DP.toPixels(56f) || requireView().translationY < -DimensionUnit.DP.toPixels(56f)
|
2022-03-23 12:01:22 +00:00
|
|
|
if ((canCloseFromHorizontalSlide || canCloseFromVerticalSlide) && event.actionMasked == MotionEvent.ACTION_UP) {
|
2022-03-23 18:20:05 +00:00
|
|
|
requireActivity().onBackPressed()
|
2022-03-23 12:01:22 +00:00
|
|
|
} else {
|
2022-07-22 17:39:35 +00:00
|
|
|
sharedViewModel.setIsChildScrolling(false)
|
2022-03-23 12:01:22 +00:00
|
|
|
requireView().animate()
|
|
|
|
.setInterpolator(StoryGestureListener.INTERPOLATOR)
|
|
|
|
.setDuration(100)
|
|
|
|
.translationX(0f)
|
|
|
|
.translationY(0f)
|
2022-09-27 17:57:39 +00:00
|
|
|
|
|
|
|
requireListener<Callback>().onContentTranslation(0f, 0f)
|
2022-03-23 12:01:22 +00:00
|
|
|
}
|
2022-02-24 17:40:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
result
|
|
|
|
}
|
|
|
|
|
|
|
|
viewsAndReplies.setOnClickListener {
|
|
|
|
startReply()
|
|
|
|
}
|
|
|
|
|
|
|
|
moreButton.setOnClickListener(this::displayMoreContextMenu)
|
|
|
|
|
|
|
|
progressBar.listener = object : SegmentedProgressBarListener {
|
|
|
|
override fun onPage(oldPageIndex: Int, newPageIndex: Int) {
|
|
|
|
if (oldPageIndex != newPageIndex && context != null) {
|
|
|
|
viewModel.setSelectedPostIndex(newPageIndex)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onFinished() {
|
2022-03-02 18:20:49 +00:00
|
|
|
viewModel.goToNextPost()
|
2022-02-24 17:40:28 +00:00
|
|
|
}
|
2022-03-03 17:12:28 +00:00
|
|
|
|
|
|
|
override fun onRequestSegmentProgressPercentage(): Float? {
|
2022-07-05 12:43:36 +00:00
|
|
|
val storyPost = viewModel.getPost() ?: return null
|
|
|
|
val attachmentUri = if (storyPost.content.isVideo()) {
|
|
|
|
storyPost.content.uri
|
2022-03-03 17:12:28 +00:00
|
|
|
} else {
|
|
|
|
null
|
|
|
|
}
|
|
|
|
|
|
|
|
return if (attachmentUri != null) {
|
|
|
|
val playerState = videoControlsDelegate.getPlayerState(attachmentUri)
|
|
|
|
if (playerState != null) {
|
2022-03-30 19:30:06 +00:00
|
|
|
getVideoPlaybackPosition(playerState) / getVideoPlaybackDuration(playerState)
|
2022-03-03 17:12:28 +00:00
|
|
|
} else {
|
|
|
|
null
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
null
|
|
|
|
}
|
|
|
|
}
|
2022-02-24 17:40:28 +00:00
|
|
|
}
|
|
|
|
|
2022-03-21 13:39:06 +00:00
|
|
|
reactionAnimationView.callback = object : OnReactionSentView.Callback {
|
|
|
|
override fun onFinished() {
|
|
|
|
viewModel.setIsDisplayingReactionAnimation(false)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-25 20:33:33 +00:00
|
|
|
sharedViewModel.isScrolling.observe(viewLifecycleOwner) { isScrolling ->
|
|
|
|
viewModel.setIsUserScrollingParent(isScrolling)
|
|
|
|
}
|
|
|
|
|
2022-07-22 17:39:35 +00:00
|
|
|
lifecycleDisposable += sharedViewModel.isChildScrolling.subscribe {
|
|
|
|
viewModel.setIsUserScrollingChild(it)
|
|
|
|
}
|
|
|
|
|
2022-10-27 16:43:52 +00:00
|
|
|
lifecycleDisposable += sharedViewModel.isFirstTimeNavigationShowing.subscribe {
|
|
|
|
viewModel.setIsDisplayingFirstTimeNavigation(it)
|
|
|
|
}
|
|
|
|
|
2022-07-05 15:38:35 +00:00
|
|
|
lifecycleDisposable += storyVolumeViewModel.state.distinctUntilChanged().observeOn(AndroidSchedulers.mainThread()).subscribe { volumeState ->
|
2022-06-27 12:19:33 +00:00
|
|
|
if (volumeState.isMuted) {
|
|
|
|
videoControlsDelegate.mute()
|
|
|
|
return@subscribe
|
|
|
|
}
|
|
|
|
|
2022-07-05 12:43:36 +00:00
|
|
|
if (!viewModel.hasPost() || viewModel.getPost()?.content?.isVideo() != true || volumeState.level < 0) {
|
2022-06-27 12:19:33 +00:00
|
|
|
return@subscribe
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!volumeState.isMuted) {
|
|
|
|
videoControlsDelegate.unmute()
|
|
|
|
}
|
|
|
|
|
|
|
|
val audioManager = ServiceUtil.getAudioManager(requireContext())
|
|
|
|
if (audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) != volumeState.level) {
|
|
|
|
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volumeState.level, 0)
|
|
|
|
storyVolumeOverlayView.setVolumeLevel(volumeState.level)
|
|
|
|
storyVolumeOverlayView.setVideoHaNoAudio(!videoControlsDelegate.hasAudioStream())
|
|
|
|
displayStoryVolumeOverlayForTimeout(storyVolumeOverlayView)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-12 18:37:28 +00:00
|
|
|
lifecycleDisposable += sharedViewModel.state.distinctUntilChanged().observeOn(AndroidSchedulers.mainThread()).subscribe { parentState ->
|
2022-07-05 15:38:35 +00:00
|
|
|
viewModel.setIsRunningSharedElementAnimation(!parentState.loadState.isCrossfaderReady)
|
|
|
|
storyContentContainer.visible = parentState.loadState.isCrossfaderReady
|
|
|
|
|
2022-02-25 20:33:33 +00:00
|
|
|
if (parentState.pages.size <= parentState.page) {
|
|
|
|
viewModel.setIsSelectedPage(false)
|
2022-09-26 17:20:50 +00:00
|
|
|
} else if (storyViewerPageArgs.recipientId == parentState.pages[parentState.page]) {
|
2022-02-25 20:33:33 +00:00
|
|
|
if (progressBar.segmentCount != 0) {
|
|
|
|
progressBar.reset()
|
|
|
|
progressBar.setPosition(viewModel.getRestartIndex())
|
|
|
|
videoControlsDelegate.restart()
|
|
|
|
}
|
2022-05-12 16:31:13 +00:00
|
|
|
viewModel.setIsFirstPage(parentState.page == 0)
|
2022-02-25 20:33:33 +00:00
|
|
|
viewModel.setIsSelectedPage(true)
|
|
|
|
} else {
|
|
|
|
viewModel.setIsSelectedPage(false)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-12 18:37:28 +00:00
|
|
|
lifecycleDisposable += viewModel.state.observeOn(AndroidSchedulers.mainThread()).subscribe { state ->
|
2022-05-27 17:40:34 +00:00
|
|
|
if (!state.isReady) {
|
|
|
|
return@subscribe
|
|
|
|
}
|
|
|
|
|
2022-07-06 12:45:13 +00:00
|
|
|
if (context == null) {
|
|
|
|
Log.d(TAG, "Subscriber called while fragment is detached. Ignoring state update.")
|
2022-08-01 13:08:04 +00:00
|
|
|
return@subscribe
|
2022-07-06 12:45:13 +00:00
|
|
|
}
|
|
|
|
|
2022-05-12 18:37:28 +00:00
|
|
|
if (state.posts.isNotEmpty() && state.selectedPostIndex in state.posts.indices) {
|
|
|
|
val post = state.posts[state.selectedPostIndex]
|
|
|
|
|
2022-09-21 19:57:20 +00:00
|
|
|
presentBottomBar(post, state.replyState, state.isReceiptsEnabled)
|
2022-05-12 18:37:28 +00:00
|
|
|
presentSenderAvatar(senderAvatar, post)
|
|
|
|
presentGroupAvatar(groupAvatar, post)
|
|
|
|
presentFrom(from, post)
|
|
|
|
presentDate(date, post)
|
|
|
|
presentDistributionList(distributionList, post)
|
|
|
|
presentCaption(caption, largeCaption, largeCaptionOverlay, post)
|
|
|
|
|
|
|
|
val durations: Map<Int, Long> = state.posts
|
|
|
|
.mapIndexed { index, storyPost ->
|
|
|
|
index to when {
|
2022-10-27 17:18:10 +00:00
|
|
|
storyPost.sender.isReleaseNotes -> ONBOARDING_DURATION
|
2022-05-12 18:37:28 +00:00
|
|
|
storyPost.content.isVideo() -> -1L
|
|
|
|
storyPost.content is StoryPost.Content.TextContent -> calculateDurationForText(storyPost.content)
|
2022-11-14 19:47:59 +00:00
|
|
|
storyPost.content is StoryPost.Content.AttachmentContent -> calculateDurationForAttachment(storyPost.content)
|
2022-05-12 18:37:28 +00:00
|
|
|
else -> DEFAULT_DURATION
|
2022-03-30 19:30:06 +00:00
|
|
|
}
|
2022-04-01 12:27:09 +00:00
|
|
|
}
|
2022-05-12 18:37:28 +00:00
|
|
|
.toMap()
|
2022-02-24 21:39:56 +00:00
|
|
|
|
2022-05-12 18:37:28 +00:00
|
|
|
if (progressBar.segmentCount != state.posts.size || progressBar.segmentDurations != durations) {
|
|
|
|
progressBar.segmentCount = state.posts.size
|
|
|
|
progressBar.segmentDurations = durations
|
|
|
|
}
|
2022-03-02 18:20:49 +00:00
|
|
|
|
2022-05-12 18:37:28 +00:00
|
|
|
presentStory(post, state.selectedPostIndex)
|
|
|
|
presentSlate(post)
|
2022-04-07 19:08:05 +00:00
|
|
|
|
2022-05-12 18:37:28 +00:00
|
|
|
viewModel.setAreSegmentsInitialized(true)
|
|
|
|
} else if (state.selectedPostIndex >= state.posts.size) {
|
2022-09-26 17:20:50 +00:00
|
|
|
callback.onFinishedPosts(storyViewerPageArgs.recipientId)
|
2022-05-12 18:37:28 +00:00
|
|
|
} else if (state.selectedPostIndex < 0) {
|
2022-09-26 17:20:50 +00:00
|
|
|
callback.onGoToPreviousStory(storyViewerPageArgs.recipientId)
|
2022-02-24 17:40:28 +00:00
|
|
|
}
|
|
|
|
|
2022-07-05 15:38:35 +00:00
|
|
|
if (state.isDisplayingInitialState && !sharedViewModel.hasConsumedInitialState) {
|
2022-05-12 18:37:28 +00:00
|
|
|
sharedViewModel.consumeInitialState()
|
2022-09-26 17:20:50 +00:00
|
|
|
if (storyViewerPageArgs.source == StoryViewerPageArgs.Source.NOTIFICATION) {
|
|
|
|
startReply(isFromNotification = true, groupReplyStartPosition = storyViewerPageArgs.groupReplyStartPosition)
|
|
|
|
} else if (storyViewerPageArgs.source == StoryViewerPageArgs.Source.INFO_CONTEXT && state.selectedPostIndex in state.posts.indices) {
|
2022-07-14 20:00:15 +00:00
|
|
|
showInfo(state.posts[state.selectedPostIndex])
|
2022-07-05 15:38:35 +00:00
|
|
|
}
|
2022-05-12 18:37:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-24 21:39:56 +00:00
|
|
|
viewModel.storyViewerPlaybackState.observe(viewLifecycleOwner) { state ->
|
|
|
|
if (state.isPaused) {
|
|
|
|
pauseProgress()
|
|
|
|
} else {
|
|
|
|
resumeProgress()
|
|
|
|
}
|
2022-04-07 19:08:05 +00:00
|
|
|
|
|
|
|
when {
|
|
|
|
state.hideChromeImmediate -> {
|
|
|
|
hideChromeImmediate()
|
|
|
|
}
|
|
|
|
state.hideChrome -> {
|
|
|
|
hideChrome()
|
|
|
|
}
|
|
|
|
else -> {
|
|
|
|
showChrome()
|
|
|
|
}
|
|
|
|
}
|
2022-02-24 21:39:56 +00:00
|
|
|
}
|
|
|
|
|
2022-03-02 18:20:49 +00:00
|
|
|
timeoutDisposable.bindTo(viewLifecycleOwner)
|
2022-02-24 17:40:28 +00:00
|
|
|
lifecycleDisposable.bindTo(viewLifecycleOwner)
|
|
|
|
lifecycleDisposable += viewModel.groupDirectReplyObservable.subscribe { opt ->
|
|
|
|
if (opt.isPresent) {
|
|
|
|
when (val sheet = opt.get()) {
|
|
|
|
is StoryViewerDialog.GroupDirectReply -> {
|
|
|
|
onStartDirectReply(sheet.storyId, sheet.recipientId)
|
|
|
|
}
|
|
|
|
}
|
2022-02-24 21:39:56 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-24 17:40:28 +00:00
|
|
|
adjustConstraintsForScreenDimensions(viewsAndReplies, cardWrapper, card)
|
2022-03-21 13:39:06 +00:00
|
|
|
|
|
|
|
childFragmentManager.setFragmentResultListener(StoryDirectReplyDialogFragment.REQUEST_EMOJI, viewLifecycleOwner) { _, bundle ->
|
|
|
|
val emoji = bundle.getString(StoryDirectReplyDialogFragment.REQUEST_EMOJI)
|
|
|
|
if (emoji != null) {
|
|
|
|
reactionAnimationView.playForEmoji(emoji)
|
|
|
|
viewModel.setIsDisplayingReactionAnimation(true)
|
|
|
|
}
|
|
|
|
}
|
2022-02-24 17:40:28 +00:00
|
|
|
}
|
|
|
|
|
2022-03-03 17:12:28 +00:00
|
|
|
override fun onResume() {
|
|
|
|
super.onResume()
|
|
|
|
viewModel.setIsFragmentResumed(true)
|
2022-06-30 17:02:35 +00:00
|
|
|
viewModel.checkReadReceiptState()
|
2022-07-05 15:38:35 +00:00
|
|
|
markViewedIfAble()
|
2022-03-03 17:12:28 +00:00
|
|
|
}
|
|
|
|
|
2022-02-24 17:40:28 +00:00
|
|
|
override fun onPause() {
|
|
|
|
super.onPause()
|
2022-03-03 17:12:28 +00:00
|
|
|
viewModel.setIsFragmentResumed(false)
|
2022-02-24 17:40:28 +00:00
|
|
|
}
|
|
|
|
|
2022-04-06 17:37:16 +00:00
|
|
|
override fun onDestroyView() {
|
|
|
|
super.onDestroyView()
|
|
|
|
childFragmentManager.fragments.forEach {
|
|
|
|
if (it is MediaPreviewFragment) {
|
|
|
|
it.cleanUp()
|
|
|
|
}
|
|
|
|
}
|
2022-06-27 12:19:33 +00:00
|
|
|
|
|
|
|
volumeDebouncer.clear()
|
2022-04-06 17:37:16 +00:00
|
|
|
}
|
|
|
|
|
2022-02-24 17:40:28 +00:00
|
|
|
override fun onFinishForwardAction() = Unit
|
|
|
|
|
|
|
|
override fun onDismissForwardSheet() {
|
2022-02-24 21:39:56 +00:00
|
|
|
viewModel.setIsDisplayingForwardDialog(false)
|
2022-02-24 17:40:28 +00:00
|
|
|
}
|
|
|
|
|
2022-03-30 19:30:06 +00:00
|
|
|
private fun calculateDurationForText(textContent: StoryPost.Content.TextContent): Long {
|
2022-11-14 19:47:59 +00:00
|
|
|
return calculateDurationForContentLength(textContent.length)
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun calculateDurationForAttachment(attachmentContent: StoryPost.Content.AttachmentContent): Long {
|
|
|
|
val caption: String? = attachmentContent.attachment.caption
|
|
|
|
return if (caption.isNullOrEmpty()) {
|
|
|
|
DEFAULT_DURATION
|
|
|
|
} else {
|
|
|
|
calculateDurationForContentLength(caption.length)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun calculateDurationForContentLength(contentLength: Int): Long {
|
|
|
|
val divisionsOf15 = contentLength / CHARACTERS_PER_SECOND
|
2022-03-30 19:30:06 +00:00
|
|
|
return TimeUnit.SECONDS.toMillis(divisionsOf15) + MIN_TEXT_STORY_PLAYBACK
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun getVideoPlaybackPosition(playerState: VideoControlsDelegate.PlayerState): Float {
|
|
|
|
return if (playerState.isGif) {
|
|
|
|
playerState.position.toFloat() + (playerState.duration * playerState.loopCount)
|
|
|
|
} else {
|
|
|
|
playerState.position.toFloat()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun getVideoPlaybackDuration(playerState: VideoControlsDelegate.PlayerState): Long {
|
|
|
|
return if (playerState.isGif) {
|
|
|
|
val timeToPlayMinLoops = playerState.duration * MIN_GIF_LOOPS
|
|
|
|
max(MIN_GIF_PLAYBACK_DURATION, timeToPlayMinLoops)
|
|
|
|
} else {
|
|
|
|
min(playerState.duration, MAX_VIDEO_PLAYBACK_DURATION)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-27 12:19:33 +00:00
|
|
|
private fun displayStoryVolumeOverlayForTimeout(view: View) {
|
|
|
|
if (volumeInAnimator?.isRunning != true) {
|
|
|
|
volumeOutAnimator?.cancel()
|
|
|
|
volumeInAnimator = ObjectAnimator.ofFloat(view, View.ALPHA, 1f).apply {
|
|
|
|
duration = 200
|
|
|
|
start()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
volumeDebouncer.publish {
|
|
|
|
if (volumeOutAnimator?.isRunning != true) {
|
|
|
|
volumeInAnimator?.cancel()
|
|
|
|
volumeOutAnimator = ObjectAnimator.ofFloat(view, View.ALPHA, 0f).apply {
|
|
|
|
duration = 200
|
|
|
|
start()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-07 19:08:05 +00:00
|
|
|
private fun hideChromeImmediate() {
|
|
|
|
animatorSet?.cancel()
|
|
|
|
chrome.map {
|
|
|
|
it.alpha = 0f
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-24 17:40:28 +00:00
|
|
|
private fun hideChrome() {
|
|
|
|
animateChrome(0f)
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun showChrome() {
|
|
|
|
animateChrome(1f)
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun animateChrome(alphaTarget: Float) {
|
|
|
|
animatorSet?.cancel()
|
|
|
|
animatorSet = AnimatorSet().apply {
|
2022-04-13 18:30:37 +00:00
|
|
|
duration = 100
|
|
|
|
interpolator = StoryGestureListener.INTERPOLATOR
|
2022-02-24 17:40:28 +00:00
|
|
|
playTogether(
|
|
|
|
chrome.map {
|
|
|
|
ObjectAnimator.ofFloat(it, View.ALPHA, alphaTarget)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
start()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun adjustConstraintsForScreenDimensions(
|
|
|
|
viewsAndReplies: View,
|
|
|
|
cardWrapper: View,
|
|
|
|
card: CardView
|
|
|
|
) {
|
|
|
|
val constraintSet = ConstraintSet()
|
2022-10-27 16:43:52 +00:00
|
|
|
constraintSet.clone(storyPageContainer)
|
2022-02-24 17:40:28 +00:00
|
|
|
|
2022-03-10 14:28:05 +00:00
|
|
|
when (StoryDisplay.getStoryDisplay(resources.displayMetrics.widthPixels.toFloat(), resources.displayMetrics.heightPixels.toFloat())) {
|
2022-02-24 17:40:28 +00:00
|
|
|
StoryDisplay.LARGE -> {
|
2022-03-21 17:18:32 +00:00
|
|
|
constraintSet.setDimensionRatio(cardWrapper.id, "9:16")
|
2022-02-24 17:40:28 +00:00
|
|
|
constraintSet.connect(viewsAndReplies.id, ConstraintSet.TOP, cardWrapper.id, ConstraintSet.BOTTOM)
|
|
|
|
constraintSet.connect(viewsAndReplies.id, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM)
|
|
|
|
card.radius = DimensionUnit.DP.toPixels(18f)
|
|
|
|
}
|
|
|
|
StoryDisplay.MEDIUM -> {
|
2022-03-21 17:18:32 +00:00
|
|
|
constraintSet.setDimensionRatio(cardWrapper.id, "9:16")
|
2022-02-24 17:40:28 +00:00
|
|
|
constraintSet.clear(viewsAndReplies.id, ConstraintSet.TOP)
|
|
|
|
constraintSet.connect(viewsAndReplies.id, ConstraintSet.BOTTOM, cardWrapper.id, ConstraintSet.BOTTOM)
|
|
|
|
card.radius = DimensionUnit.DP.toPixels(18f)
|
|
|
|
}
|
|
|
|
StoryDisplay.SMALL -> {
|
2022-03-21 17:18:32 +00:00
|
|
|
constraintSet.setDimensionRatio(cardWrapper.id, null)
|
2022-02-24 17:40:28 +00:00
|
|
|
constraintSet.clear(viewsAndReplies.id, ConstraintSet.TOP)
|
|
|
|
constraintSet.connect(viewsAndReplies.id, ConstraintSet.BOTTOM, cardWrapper.id, ConstraintSet.BOTTOM)
|
|
|
|
card.radius = DimensionUnit.DP.toPixels(0f)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-27 16:43:52 +00:00
|
|
|
constraintSet.applyTo(storyPageContainer)
|
2022-02-24 17:40:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private fun resumeProgress() {
|
2022-07-05 12:43:36 +00:00
|
|
|
val storyPost = viewModel.getPost() ?: return
|
|
|
|
if (progressBar.segmentCount != 0) {
|
|
|
|
val postUri = storyPost.content.uri
|
2022-03-02 18:20:49 +00:00
|
|
|
if (postUri != null) {
|
|
|
|
progressBar.start()
|
|
|
|
videoControlsDelegate.resume(postUri)
|
|
|
|
}
|
2022-02-24 17:40:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-24 21:39:56 +00:00
|
|
|
private fun pauseProgress() {
|
|
|
|
progressBar.pause()
|
|
|
|
videoControlsDelegate.pause()
|
|
|
|
}
|
|
|
|
|
2022-05-12 18:37:28 +00:00
|
|
|
private fun startReply(isFromNotification: Boolean = false, groupReplyStartPosition: Int = -1) {
|
2022-07-05 12:43:36 +00:00
|
|
|
val storyPost = viewModel.getPost() ?: return
|
|
|
|
val storyPostId: Long = storyPost.id
|
2022-02-24 17:40:28 +00:00
|
|
|
val replyFragment: DialogFragment = when (viewModel.getSwipeToReplyState()) {
|
|
|
|
StoryViewerPageState.ReplyState.NONE -> return
|
2022-05-12 18:37:28 +00:00
|
|
|
StoryViewerPageState.ReplyState.SELF -> StoryViewsBottomSheetDialogFragment.create(storyPostId)
|
|
|
|
StoryViewerPageState.ReplyState.GROUP -> StoryGroupReplyBottomSheetDialogFragment.create(
|
|
|
|
storyPostId,
|
2022-07-05 12:43:36 +00:00
|
|
|
storyPost.group!!.id,
|
2022-05-12 18:37:28 +00:00
|
|
|
isFromNotification,
|
|
|
|
groupReplyStartPosition
|
|
|
|
)
|
|
|
|
StoryViewerPageState.ReplyState.PRIVATE -> StoryDirectReplyDialogFragment.create(storyPostId)
|
|
|
|
StoryViewerPageState.ReplyState.GROUP_SELF -> StoryViewsAndRepliesDialogFragment.create(
|
|
|
|
storyPostId,
|
2022-07-05 12:43:36 +00:00
|
|
|
storyPost.group!!.id,
|
2022-05-12 18:37:28 +00:00
|
|
|
if (isFromNotification) StoryViewsAndRepliesDialogFragment.StartPage.REPLIES else getViewsAndRepliesDialogStartPage(),
|
|
|
|
isFromNotification,
|
|
|
|
groupReplyStartPosition
|
|
|
|
)
|
2022-09-21 19:57:20 +00:00
|
|
|
StoryViewerPageState.ReplyState.PARTIAL_SEND -> {
|
|
|
|
handleResend(storyPost)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
StoryViewerPageState.ReplyState.SEND_FAILURE -> {
|
|
|
|
handleResend(storyPost)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
StoryViewerPageState.ReplyState.SENDING -> return
|
2022-02-24 17:40:28 +00:00
|
|
|
}
|
|
|
|
|
2022-02-24 21:39:56 +00:00
|
|
|
if (viewModel.getSwipeToReplyState() == StoryViewerPageState.ReplyState.PRIVATE) {
|
|
|
|
viewModel.setIsDisplayingDirectReplyDialog(true)
|
|
|
|
} else {
|
|
|
|
viewModel.setIsDisplayingViewsAndRepliesDialog(true)
|
|
|
|
}
|
|
|
|
|
2022-02-24 17:40:28 +00:00
|
|
|
replyFragment.showNow(childFragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
|
|
|
|
}
|
|
|
|
|
2022-09-21 19:57:20 +00:00
|
|
|
private fun handleResend(storyPost: StoryPost) {
|
|
|
|
viewModel.setIsDisplayingPartialSendDialog(true)
|
|
|
|
if (storyPost.conversationMessage.messageRecord.isIdentityMismatchFailure) {
|
|
|
|
SafetyNumberBottomSheet
|
|
|
|
.forMessageRecord(requireContext(), storyPost.conversationMessage.messageRecord)
|
|
|
|
.show(childFragmentManager)
|
|
|
|
} else {
|
|
|
|
StoryDialogs.resendStory(requireContext(), { viewModel.setIsDisplayingPartialSendDialog(false) }) {
|
|
|
|
lifecycleDisposable += viewModel.resend(storyPost).subscribe()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-14 20:00:15 +00:00
|
|
|
private fun showInfo(storyPost: StoryPost) {
|
|
|
|
viewModel.setIsDisplayingInfoDialog(true)
|
|
|
|
StoryInfoBottomSheetDialogFragment.create(storyPost.id).show(childFragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
|
|
|
|
}
|
|
|
|
|
2022-07-05 15:38:35 +00:00
|
|
|
private fun markViewedIfAble() {
|
2022-07-15 18:29:59 +00:00
|
|
|
val post = viewModel.getPost() ?: return
|
|
|
|
if (post.content.transferState == AttachmentDatabase.TRANSFER_PROGRESS_DONE) {
|
2022-07-05 15:38:35 +00:00
|
|
|
if (isResumed) {
|
|
|
|
viewModel.markViewed(post)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-24 17:40:28 +00:00
|
|
|
private fun onStartDirectReply(storyId: Long, recipientId: RecipientId) {
|
2022-02-24 21:39:56 +00:00
|
|
|
viewModel.setIsDisplayingDirectReplyDialog(true)
|
2022-02-24 17:40:28 +00:00
|
|
|
StoryDirectReplyDialogFragment.create(
|
|
|
|
storyId = storyId,
|
|
|
|
recipientId = recipientId
|
|
|
|
).show(childFragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun getViewsAndRepliesDialogStartPage(): StoryViewsAndRepliesDialogFragment.StartPage {
|
2022-07-05 12:43:36 +00:00
|
|
|
return if (viewModel.requirePost().replyCount > 0) {
|
2022-02-24 17:40:28 +00:00
|
|
|
StoryViewsAndRepliesDialogFragment.StartPage.REPLIES
|
|
|
|
} else {
|
|
|
|
StoryViewsAndRepliesDialogFragment.StartPage.VIEWS
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-02 18:20:49 +00:00
|
|
|
private fun presentStory(post: StoryPost, index: Int) {
|
2022-03-09 17:11:56 +00:00
|
|
|
if (post.content.uri == null) {
|
2022-03-02 18:20:49 +00:00
|
|
|
progressBar.setPosition(index)
|
|
|
|
progressBar.invalidate()
|
|
|
|
} else {
|
|
|
|
progressBar.setPosition(index)
|
|
|
|
storySlate.moveToState(StorySlateView.State.HIDDEN, post.id)
|
|
|
|
viewModel.setIsDisplayingSlate(false)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun presentSlate(post: StoryPost) {
|
2022-04-14 19:51:52 +00:00
|
|
|
storySlate.setBackground((post.conversationMessage.messageRecord as? MediaMmsMessageRecord)?.slideDeck?.thumbnailSlide?.placeholderBlur)
|
|
|
|
|
2022-07-26 16:24:15 +00:00
|
|
|
if (post.conversationMessage.messageRecord.isOutgoing) {
|
|
|
|
storySlate.moveToState(StorySlateView.State.HIDDEN, post.id)
|
|
|
|
viewModel.setIsDisplayingSlate(false)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-03-09 17:11:56 +00:00
|
|
|
when (post.content.transferState) {
|
2022-03-02 18:20:49 +00:00
|
|
|
AttachmentDatabase.TRANSFER_PROGRESS_DONE -> {
|
|
|
|
storySlate.moveToState(StorySlateView.State.HIDDEN, post.id)
|
|
|
|
viewModel.setIsDisplayingSlate(false)
|
2022-07-05 15:38:35 +00:00
|
|
|
markViewedIfAble()
|
2022-03-02 18:20:49 +00:00
|
|
|
}
|
|
|
|
AttachmentDatabase.TRANSFER_PROGRESS_PENDING -> {
|
|
|
|
storySlate.moveToState(StorySlateView.State.LOADING, post.id)
|
2022-07-21 12:57:13 +00:00
|
|
|
sharedViewModel.setContentIsReady()
|
2022-03-02 18:20:49 +00:00
|
|
|
viewModel.setIsDisplayingSlate(true)
|
|
|
|
}
|
|
|
|
AttachmentDatabase.TRANSFER_PROGRESS_STARTED -> {
|
|
|
|
storySlate.moveToState(StorySlateView.State.LOADING, post.id)
|
2022-07-21 12:57:13 +00:00
|
|
|
sharedViewModel.setContentIsReady()
|
2022-03-02 18:20:49 +00:00
|
|
|
viewModel.setIsDisplayingSlate(true)
|
|
|
|
}
|
|
|
|
AttachmentDatabase.TRANSFER_PROGRESS_FAILED -> {
|
2022-07-15 19:28:49 +00:00
|
|
|
storySlate.moveToState(StorySlateView.State.ERROR, post.id)
|
2022-07-15 18:29:59 +00:00
|
|
|
sharedViewModel.setContentIsReady()
|
2022-03-02 18:20:49 +00:00
|
|
|
viewModel.setIsDisplayingSlate(true)
|
|
|
|
}
|
2022-10-27 20:33:33 +00:00
|
|
|
AttachmentDatabase.TRANSFER_PROGRESS_PERMANENT_FAILURE -> {
|
|
|
|
storySlate.moveToState(StorySlateView.State.FAILED, post.id, post.sender)
|
|
|
|
sharedViewModel.setContentIsReady()
|
|
|
|
viewModel.setIsDisplayingSlate(true)
|
|
|
|
}
|
2022-03-02 18:20:49 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-02-24 17:40:28 +00:00
|
|
|
private fun presentDistributionList(distributionList: TextView, storyPost: StoryPost) {
|
|
|
|
distributionList.text = storyPost.distributionList?.getDisplayName(requireContext())
|
|
|
|
distributionList.visible = storyPost.distributionList != null && !storyPost.distributionList.isMyStory
|
|
|
|
}
|
|
|
|
|
|
|
|
@SuppressLint("SetTextI18n")
|
|
|
|
private fun presentCaption(caption: TextView, largeCaption: TextView, largeCaptionOverlay: View, storyPost: StoryPost) {
|
2022-03-22 18:12:44 +00:00
|
|
|
val displayBody: String = if (storyPost.content is StoryPost.Content.AttachmentContent) {
|
|
|
|
storyPost.content.attachment.caption ?: ""
|
2022-03-09 17:11:56 +00:00
|
|
|
} else {
|
|
|
|
""
|
|
|
|
}
|
|
|
|
|
2022-11-16 18:38:52 +00:00
|
|
|
storyNormalBottomGradient.visible = !displayBody.isNotEmpty()
|
|
|
|
storyCaptionBottomGradient.visible = displayBody.isNotEmpty()
|
|
|
|
|
2022-02-24 17:40:28 +00:00
|
|
|
caption.text = displayBody
|
|
|
|
largeCaption.text = displayBody
|
|
|
|
caption.visible = displayBody.isNotEmpty()
|
|
|
|
caption.requestLayout()
|
|
|
|
|
|
|
|
caption.doOnNextLayout {
|
|
|
|
val maxLines = 5
|
2022-04-06 17:37:16 +00:00
|
|
|
if (displayBody.isNotEmpty() && caption.lineCount > maxLines) {
|
2022-02-24 17:40:28 +00:00
|
|
|
val lastCharShown = caption.layout.getLineVisibleEnd(maxLines - 1)
|
|
|
|
caption.maxLines = maxLines
|
|
|
|
|
|
|
|
val seeMore = (getString(R.string.StoryViewerPageFragment__see_more))
|
|
|
|
|
|
|
|
val seeMoreWidth = caption.paint.measureText(seeMore)
|
|
|
|
var offset = seeMore.length
|
|
|
|
while (true) {
|
|
|
|
val start = lastCharShown - offset
|
|
|
|
if (start < 0) {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
val widthOfRemovedChunk = caption.paint.measureText(displayBody.subSequence(start, lastCharShown).toString())
|
|
|
|
if (widthOfRemovedChunk > seeMoreWidth) {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
offset += 1
|
|
|
|
}
|
|
|
|
|
|
|
|
caption.text = displayBody.substring(0, lastCharShown - offset) + seeMore
|
2022-04-06 17:37:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (caption.text.length == displayBody.length) {
|
|
|
|
caption.setOnClickListener(null)
|
|
|
|
caption.isClickable = false
|
|
|
|
} else {
|
2022-02-24 17:40:28 +00:00
|
|
|
caption.setOnClickListener {
|
|
|
|
onShowCaptionOverlay(caption, largeCaption, largeCaptionOverlay)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun onShowCaptionOverlay(caption: TextView, largeCaption: TextView, largeCaptionOverlay: View) {
|
2022-09-19 17:24:44 +00:00
|
|
|
sharedViewModel.setIsChildScrolling(true)
|
|
|
|
|
2022-02-24 17:40:28 +00:00
|
|
|
caption.visible = false
|
|
|
|
largeCaption.visible = true
|
|
|
|
largeCaptionOverlay.visible = true
|
2022-09-19 17:24:44 +00:00
|
|
|
largeCaption.movementMethod = ScrollingMovementMethod()
|
|
|
|
largeCaption.scrollY = 0
|
|
|
|
largeCaption.setOnClickListener {
|
|
|
|
onHideCaptionOverlay(caption, largeCaption, largeCaptionOverlay)
|
|
|
|
}
|
2022-02-24 17:40:28 +00:00
|
|
|
largeCaptionOverlay.setOnClickListener {
|
|
|
|
onHideCaptionOverlay(caption, largeCaption, largeCaptionOverlay)
|
|
|
|
}
|
2022-02-24 21:39:56 +00:00
|
|
|
viewModel.setIsDisplayingCaptionOverlay(true)
|
2022-02-24 17:40:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private fun onHideCaptionOverlay(caption: TextView, largeCaption: TextView, largeCaptionOverlay: View) {
|
|
|
|
caption.visible = true
|
|
|
|
largeCaption.visible = false
|
|
|
|
largeCaptionOverlay.visible = false
|
2022-09-19 17:24:44 +00:00
|
|
|
largeCaption.setOnClickListener(null)
|
2022-02-24 17:40:28 +00:00
|
|
|
largeCaptionOverlay.setOnClickListener(null)
|
2022-02-24 21:39:56 +00:00
|
|
|
viewModel.setIsDisplayingCaptionOverlay(false)
|
2022-09-19 17:24:44 +00:00
|
|
|
sharedViewModel.setIsChildScrolling(false)
|
2022-02-24 17:40:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private fun presentFrom(from: TextView, storyPost: StoryPost) {
|
|
|
|
val name = if (storyPost.sender.isSelf) {
|
|
|
|
getString(R.string.StoryViewerPageFragment__you)
|
|
|
|
} else {
|
|
|
|
storyPost.sender.getDisplayName(requireContext())
|
|
|
|
}
|
|
|
|
|
|
|
|
if (storyPost.group != null) {
|
|
|
|
from.text = getString(R.string.StoryViewerPageFragment__s_to_s, name, storyPost.group.getDisplayName(requireContext()))
|
|
|
|
} else {
|
|
|
|
from.text = name
|
|
|
|
}
|
2022-11-08 22:34:02 +00:00
|
|
|
|
|
|
|
from.setOnClickListener { onSenderClicked(storyPost.sender.id) }
|
2022-02-24 17:40:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private fun presentDate(date: TextView, storyPost: StoryPost) {
|
2022-09-29 13:20:47 +00:00
|
|
|
date.text = DateUtils.getBriefRelativeTimeSpanString(context, Locale.getDefault(), storyPost.dateInMilliseconds)
|
2022-02-24 17:40:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private fun presentSenderAvatar(senderAvatar: AvatarImageView, post: StoryPost) {
|
|
|
|
AvatarUtil.loadIconIntoImageView(post.sender, senderAvatar, DimensionUnit.DP.toPixels(32f).toInt())
|
2022-11-08 22:34:02 +00:00
|
|
|
senderAvatar.setOnClickListener { onSenderClicked(post.sender.id) }
|
2022-02-24 17:40:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private fun presentGroupAvatar(groupAvatar: AvatarImageView, post: StoryPost) {
|
|
|
|
if (post.group != null) {
|
|
|
|
groupAvatar.setRecipient(post.group)
|
|
|
|
groupAvatar.visible = true
|
2022-11-08 22:34:02 +00:00
|
|
|
groupAvatar.setOnClickListener { onSenderClicked(post.sender.id) }
|
2022-02-24 17:40:28 +00:00
|
|
|
} else {
|
|
|
|
groupAvatar.visible = false
|
2022-11-08 22:34:02 +00:00
|
|
|
groupAvatar.setOnClickListener(null)
|
2022-02-24 17:40:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-08 22:34:02 +00:00
|
|
|
private fun onSenderClicked(senderId: RecipientId) {
|
|
|
|
viewModel.setIsDisplayingRecipientBottomSheet(true)
|
|
|
|
RecipientBottomSheetDialogFragment
|
|
|
|
.create(senderId, null)
|
|
|
|
.show(childFragmentManager, "BOTTOM")
|
|
|
|
}
|
|
|
|
|
2022-09-21 19:57:20 +00:00
|
|
|
private fun presentBottomBar(post: StoryPost, replyState: StoryViewerPageState.ReplyState, isReceiptsEnabled: Boolean) {
|
2022-04-08 00:36:04 +00:00
|
|
|
if (replyState == StoryViewerPageState.ReplyState.NONE) {
|
|
|
|
viewsAndReplies.visible = false
|
|
|
|
return
|
|
|
|
} else {
|
|
|
|
viewsAndReplies.visible = true
|
|
|
|
}
|
|
|
|
|
2022-09-29 13:20:08 +00:00
|
|
|
sendingBar.visible = false
|
|
|
|
viewsAndReplies.isEnabled = true
|
2022-09-21 19:57:20 +00:00
|
|
|
viewsAndReplies.iconTint = ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.signal_colorOnSurface))
|
|
|
|
|
|
|
|
when (replyState) {
|
|
|
|
StoryViewerPageState.ReplyState.SENDING -> presentSendingBottomBar()
|
|
|
|
StoryViewerPageState.ReplyState.PARTIAL_SEND -> presentPartialSendBottomBar()
|
|
|
|
StoryViewerPageState.ReplyState.SEND_FAILURE -> presentSendFailureBottomBar()
|
|
|
|
else -> presentViewsAndRepliesBottomBar(post, isReceiptsEnabled)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun presentSendingBottomBar() {
|
|
|
|
if (sendingProgressDrawable == null) {
|
|
|
|
sendingProgressDrawable = IndeterminateDrawable.createCircularDrawable(
|
|
|
|
requireContext(),
|
|
|
|
CircularProgressIndicatorSpec(requireContext(), null).apply {
|
|
|
|
indicatorSize = 18.dp
|
|
|
|
indicatorInset = 2.dp
|
|
|
|
trackColor = ContextCompat.getColor(requireContext(), R.color.transparent_white_40)
|
|
|
|
indicatorColors = intArrayOf(ContextCompat.getColor(requireContext(), R.color.signal_dark_colorNeutralInverse))
|
|
|
|
trackThickness = 2.dp
|
|
|
|
}
|
2022-09-29 13:20:08 +00:00
|
|
|
).apply {
|
|
|
|
setBounds(0, 0, 20.dp, 20.dp)
|
|
|
|
}
|
2022-09-21 19:57:20 +00:00
|
|
|
}
|
|
|
|
|
2022-09-29 13:20:08 +00:00
|
|
|
sendingBarTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(
|
|
|
|
sendingProgressDrawable,
|
|
|
|
null,
|
|
|
|
null,
|
|
|
|
null
|
|
|
|
)
|
|
|
|
|
|
|
|
sendingBar.visible = true
|
|
|
|
viewsAndReplies.isEnabled = false
|
2022-09-21 19:57:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private fun presentPartialSendBottomBar() {
|
|
|
|
viewsAndReplies.setIconResource(R.drawable.ic_error_outline_24)
|
|
|
|
viewsAndReplies.iconTint = ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.signal_light_colorError))
|
|
|
|
viewsAndReplies.iconSize = 20.dp
|
|
|
|
viewsAndReplies.setText(R.string.StoryViewerPageFragment__partially_sent)
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun presentSendFailureBottomBar() {
|
|
|
|
viewsAndReplies.setIconResource(R.drawable.ic_error_outline_24)
|
|
|
|
viewsAndReplies.iconTint = ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.signal_light_colorError))
|
|
|
|
viewsAndReplies.iconSize = 20.dp
|
|
|
|
viewsAndReplies.setText(R.string.StoryViewerPageFragment__send_failed)
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun presentViewsAndRepliesBottomBar(post: StoryPost, isReceiptsEnabled: Boolean) {
|
2022-02-24 17:40:28 +00:00
|
|
|
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)
|
|
|
|
|
|
|
|
if (Recipient.self() == post.sender) {
|
2022-06-30 17:02:35 +00:00
|
|
|
if (isReceiptsEnabled) {
|
|
|
|
if (post.replyCount == 0) {
|
2022-07-22 18:32:27 +00:00
|
|
|
viewsAndReplies.setIconResource(R.drawable.ic_chevron_end_bold_16)
|
|
|
|
viewsAndReplies.iconSize = DimensionUnit.DP.toPixels(16f).toInt()
|
2022-06-30 17:02:35 +00:00
|
|
|
viewsAndReplies.iconGravity = MaterialButton.ICON_GRAVITY_TEXT_END
|
|
|
|
viewsAndReplies.text = views
|
|
|
|
} else {
|
2022-07-22 18:32:27 +00:00
|
|
|
viewsAndReplies.setIconResource(R.drawable.ic_chevron_end_bold_16)
|
|
|
|
viewsAndReplies.iconSize = DimensionUnit.DP.toPixels(16f).toInt()
|
2022-06-30 17:02:35 +00:00
|
|
|
viewsAndReplies.iconGravity = MaterialButton.ICON_GRAVITY_TEXT_END
|
|
|
|
viewsAndReplies.text = getString(R.string.StoryViewerFragment__s_s, views, replies)
|
|
|
|
}
|
2022-02-24 17:40:28 +00:00
|
|
|
} else {
|
2022-06-30 17:02:35 +00:00
|
|
|
if (post.replyCount == 0) {
|
|
|
|
viewsAndReplies.icon = null
|
|
|
|
viewsAndReplies.setText(R.string.StoryViewerPageFragment__views_off)
|
|
|
|
} else {
|
2022-07-22 18:32:27 +00:00
|
|
|
viewsAndReplies.setIconResource(R.drawable.ic_chevron_end_bold_16)
|
|
|
|
viewsAndReplies.iconSize = DimensionUnit.DP.toPixels(16f).toInt()
|
2022-06-30 17:02:35 +00:00
|
|
|
viewsAndReplies.iconGravity = MaterialButton.ICON_GRAVITY_TEXT_END
|
|
|
|
viewsAndReplies.text = replies
|
|
|
|
}
|
2022-02-24 17:40:28 +00:00
|
|
|
}
|
|
|
|
} else if (post.replyCount > 0) {
|
2022-07-22 18:32:27 +00:00
|
|
|
viewsAndReplies.setIconResource(R.drawable.ic_chevron_end_bold_16)
|
|
|
|
viewsAndReplies.iconSize = DimensionUnit.DP.toPixels(16f).toInt()
|
2022-05-12 18:08:24 +00:00
|
|
|
viewsAndReplies.iconGravity = MaterialButton.ICON_GRAVITY_TEXT_END
|
2022-02-24 17:40:28 +00:00
|
|
|
viewsAndReplies.text = replies
|
2022-05-12 18:08:24 +00:00
|
|
|
} else if (post.group != null) {
|
|
|
|
viewsAndReplies.setIconResource(R.drawable.ic_reply_24_outline)
|
2022-09-02 18:39:22 +00:00
|
|
|
viewsAndReplies.iconSize = DimensionUnit.DP.toPixels(20f).toInt()
|
2022-05-12 18:08:24 +00:00
|
|
|
viewsAndReplies.iconGravity = MaterialButton.ICON_GRAVITY_TEXT_START
|
|
|
|
viewsAndReplies.setText(R.string.StoryViewerPageFragment__reply_to_group)
|
2022-02-24 17:40:28 +00:00
|
|
|
} else {
|
2022-05-12 18:08:24 +00:00
|
|
|
viewsAndReplies.setIconResource(R.drawable.ic_reply_24_outline)
|
2022-09-02 18:39:22 +00:00
|
|
|
viewsAndReplies.iconSize = DimensionUnit.DP.toPixels(20f).toInt()
|
2022-05-12 18:08:24 +00:00
|
|
|
viewsAndReplies.iconGravity = MaterialButton.ICON_GRAVITY_TEXT_START
|
2022-02-24 17:40:28 +00:00
|
|
|
viewsAndReplies.setText(R.string.StoryViewerPageFragment__reply)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-09 17:11:56 +00:00
|
|
|
override fun setIsDisplayingLinkPreviewTooltip(isDisplayingLinkPreviewTooltip: Boolean) {
|
|
|
|
viewModel.setIsDisplayingLinkPreviewTooltip(isDisplayingLinkPreviewTooltip)
|
2022-02-24 21:39:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun getVideoControlsDelegate(): VideoControlsDelegate {
|
|
|
|
return videoControlsDelegate
|
2022-02-24 17:40:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private fun displayMoreContextMenu(anchor: View) {
|
2022-02-24 21:39:56 +00:00
|
|
|
viewModel.setIsDisplayingContextMenu(true)
|
2022-02-24 17:40:28 +00:00
|
|
|
StoryContextMenu.show(
|
|
|
|
context = requireContext(),
|
|
|
|
anchorView = anchor,
|
|
|
|
storyViewerPageState = viewModel.getStateSnapshot(),
|
|
|
|
onDismiss = {
|
2022-02-24 21:39:56 +00:00
|
|
|
viewModel.setIsDisplayingContextMenu(false)
|
2022-02-24 17:40:28 +00:00
|
|
|
},
|
|
|
|
onForward = { storyPost ->
|
2022-02-24 21:39:56 +00:00
|
|
|
viewModel.setIsDisplayingForwardDialog(true)
|
2022-02-24 17:40:28 +00:00
|
|
|
MultiselectForwardFragmentArgs.create(
|
|
|
|
requireContext(),
|
|
|
|
storyPost.conversationMessage.multiselectCollection.toSet(),
|
|
|
|
) {
|
|
|
|
MultiselectForwardFragment.showBottomSheet(childFragmentManager, it)
|
|
|
|
}
|
|
|
|
},
|
|
|
|
onGoToChat = {
|
2022-09-26 17:20:50 +00:00
|
|
|
startActivity(ConversationIntents.createBuilder(requireContext(), storyViewerPageArgs.recipientId, -1L).build())
|
2022-02-24 17:40:28 +00:00
|
|
|
},
|
|
|
|
onHide = {
|
2022-09-16 16:29:39 +00:00
|
|
|
viewModel.setIsDisplayingHideDialog(true)
|
2022-09-27 18:24:54 +00:00
|
|
|
StoryDialogs.hideStory(requireContext(), Recipient.resolved(storyViewerPageArgs.recipientId).getDisplayName(requireContext()), { viewModel.setIsDisplayingHideDialog(false) }) {
|
2022-09-16 16:29:39 +00:00
|
|
|
lifecycleDisposable += viewModel.hideStory().subscribe {
|
2022-09-26 17:20:50 +00:00
|
|
|
callback.onStoryHidden(storyViewerPageArgs.recipientId)
|
2022-09-16 16:29:39 +00:00
|
|
|
}
|
2022-02-24 17:40:28 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
onShare = {
|
|
|
|
StoryContextMenu.share(this, it.conversationMessage.messageRecord as MediaMmsMessageRecord)
|
|
|
|
},
|
|
|
|
onSave = {
|
|
|
|
StoryContextMenu.save(requireContext(), it.conversationMessage.messageRecord)
|
|
|
|
},
|
|
|
|
onDelete = {
|
2022-02-24 21:39:56 +00:00
|
|
|
viewModel.setIsDisplayingDeleteDialog(true)
|
2022-02-24 17:40:28 +00:00
|
|
|
lifecycleDisposable += StoryContextMenu.delete(requireContext(), setOf(it.conversationMessage.messageRecord)).subscribe { _ ->
|
2022-02-24 21:39:56 +00:00
|
|
|
viewModel.setIsDisplayingDeleteDialog(false)
|
2022-02-24 17:40:28 +00:00
|
|
|
viewModel.refresh()
|
|
|
|
}
|
2022-07-14 20:00:15 +00:00
|
|
|
},
|
|
|
|
onInfo = {
|
|
|
|
showInfo(it)
|
2022-02-24 17:40:28 +00:00
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-10-12 16:33:03 +00:00
|
|
|
class SingleTapHandler(
|
|
|
|
private val container: View,
|
|
|
|
private val onGoToNext: () -> Unit,
|
|
|
|
private val onGoToPrevious: () -> Unit
|
|
|
|
) {
|
|
|
|
|
|
|
|
companion object {
|
|
|
|
private const val BOUNDARY_NEXT = 0.80f
|
|
|
|
private const val BOUNDARY_PREV = 1f - BOUNDARY_NEXT
|
|
|
|
}
|
|
|
|
|
|
|
|
fun onActionUp(e: MotionEvent) {
|
|
|
|
if (e.x < container.measuredWidth * getLeftBoundary()) {
|
|
|
|
onGoToPrevious()
|
2022-10-14 13:40:26 +00:00
|
|
|
} else if (e.x > container.measuredWidth - (container.measuredWidth * getRightBoundary())) {
|
2022-10-12 16:33:03 +00:00
|
|
|
onGoToNext()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun getLeftBoundary(): Float {
|
|
|
|
return if (container.layoutDirection == View.LAYOUT_DIRECTION_LTR) {
|
|
|
|
BOUNDARY_PREV
|
|
|
|
} else {
|
|
|
|
BOUNDARY_NEXT
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun getRightBoundary(): Float {
|
|
|
|
return if (container.layoutDirection == View.LAYOUT_DIRECTION_LTR) {
|
|
|
|
BOUNDARY_NEXT
|
|
|
|
} else {
|
|
|
|
BOUNDARY_PREV
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-24 17:40:28 +00:00
|
|
|
companion object {
|
2022-07-06 12:45:13 +00:00
|
|
|
private val TAG = Log.tag(StoryViewerPageFragment::class.java)
|
|
|
|
|
2022-03-30 19:30:06 +00:00
|
|
|
private val MAX_VIDEO_PLAYBACK_DURATION: Long = TimeUnit.SECONDS.toMillis(30)
|
|
|
|
private val MIN_GIF_LOOPS: Long = 3L
|
|
|
|
private val MIN_GIF_PLAYBACK_DURATION = TimeUnit.SECONDS.toMillis(5)
|
|
|
|
private val MIN_TEXT_STORY_PLAYBACK = TimeUnit.SECONDS.toMillis(3)
|
|
|
|
private val CHARACTERS_PER_SECOND = 15L
|
|
|
|
private val DEFAULT_DURATION = TimeUnit.SECONDS.toMillis(5)
|
2022-10-27 17:18:10 +00:00
|
|
|
private val ONBOARDING_DURATION = TimeUnit.SECONDS.toMillis(10)
|
2022-03-30 19:30:06 +00:00
|
|
|
|
2022-09-26 17:20:50 +00:00
|
|
|
private const val ARGS = "args"
|
|
|
|
|
|
|
|
fun create(args: StoryViewerPageArgs): Fragment {
|
2022-02-24 17:40:28 +00:00
|
|
|
return StoryViewerPageFragment().apply {
|
2022-07-14 20:00:15 +00:00
|
|
|
arguments = bundleOf(
|
2022-09-26 17:20:50 +00:00
|
|
|
ARGS to args
|
2022-07-14 20:00:15 +00:00
|
|
|
)
|
2022-02-24 17:40:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-16 15:35:06 +00:00
|
|
|
private class StoryScaleListener(
|
|
|
|
val viewModel: StoryViewerPageViewModel,
|
|
|
|
val sharedViewModel: StoryViewerViewModel,
|
|
|
|
val card: View
|
|
|
|
) : ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
|
|
|
|
|
|
|
private var scaleFactor = 1f
|
|
|
|
|
|
|
|
var isPerformingEndAnimation: Boolean = false
|
|
|
|
private set
|
|
|
|
|
|
|
|
override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
|
|
|
|
viewModel.setIsUserScaling(true)
|
|
|
|
sharedViewModel.setIsChildScrolling(true)
|
|
|
|
card.animate().cancel()
|
|
|
|
card.apply {
|
|
|
|
pivotX = detector.focusX
|
|
|
|
pivotY = detector.focusY
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onScale(detector: ScaleGestureDetector): Boolean {
|
|
|
|
scaleFactor *= detector.scaleFactor
|
|
|
|
|
|
|
|
card.apply {
|
|
|
|
scaleX = max(scaleFactor, 1f)
|
|
|
|
scaleY = max(scaleFactor, 1f)
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onScaleEnd(detector: ScaleGestureDetector) {
|
|
|
|
scaleFactor = 1f
|
|
|
|
isPerformingEndAnimation = true
|
|
|
|
card.animate().scaleX(1f).scaleY(1f).setListener(object : AnimationCompleteListener() {
|
|
|
|
override fun onAnimationEnd(animation: Animator?) {
|
|
|
|
isPerformingEndAnimation = false
|
|
|
|
viewModel.setIsUserScaling(false)
|
|
|
|
sharedViewModel.setIsChildScrolling(false)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-24 17:40:28 +00:00
|
|
|
private class StoryGestureListener(
|
|
|
|
private val container: View,
|
2022-10-12 16:33:03 +00:00
|
|
|
private val singleTapHandler: SingleTapHandler,
|
2022-03-23 12:01:22 +00:00
|
|
|
private val onReplyToPost: () -> Unit,
|
2022-09-27 17:57:39 +00:00
|
|
|
private val onContentTranslation: (Float, Float) -> Unit,
|
2022-03-23 12:01:22 +00:00
|
|
|
private val viewToTranslate: View = container.parent as View,
|
|
|
|
private val sharedViewModel: StoryViewerViewModel
|
2022-02-24 17:40:28 +00:00
|
|
|
) : GestureDetector.SimpleOnGestureListener() {
|
|
|
|
|
2022-03-09 17:25:01 +00:00
|
|
|
companion object {
|
2022-03-23 12:01:22 +00:00
|
|
|
val INTERPOLATOR: Interpolator = PathInterpolatorCompat.create(0.4f, 0f, 0.2f, 1f)
|
2022-03-09 17:25:01 +00:00
|
|
|
}
|
|
|
|
|
2022-03-23 12:01:22 +00:00
|
|
|
private val maxSlide = DimensionUnit.DP.toPixels(56f * 2)
|
|
|
|
|
2022-09-16 15:35:06 +00:00
|
|
|
override fun onDown(e: MotionEvent): Boolean {
|
2022-10-14 13:31:55 +00:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onSingleTapUp(e: MotionEvent): Boolean {
|
|
|
|
singleTapHandler.onActionUp(e)
|
2022-02-24 17:40:28 +00:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2022-03-23 12:01:22 +00:00
|
|
|
override fun onScroll(e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
|
2022-04-01 18:36:54 +00:00
|
|
|
val isFirstStory = sharedViewModel.stateSnapshot.page == 0
|
2022-06-23 18:44:22 +00:00
|
|
|
val isLastStory = sharedViewModel.stateSnapshot.pages.lastIndex == sharedViewModel.stateSnapshot.page
|
2022-03-23 12:01:22 +00:00
|
|
|
val isXMagnitudeGreaterThanYMagnitude = abs(distanceX) > abs(distanceY) || viewToTranslate.translationX > 0f
|
|
|
|
val isFirstAndHasYTranslationOrNegativeY = isFirstStory && (viewToTranslate.translationY > 0f || distanceY < 0f)
|
2022-06-23 18:44:22 +00:00
|
|
|
val isLastAndHasYTranslationOrNegativeY = isLastStory && (viewToTranslate.translationY < 0f || distanceY > 0f)
|
2022-03-23 12:01:22 +00:00
|
|
|
|
2022-06-23 18:44:22 +00:00
|
|
|
sharedViewModel.setIsChildScrolling(isXMagnitudeGreaterThanYMagnitude || isFirstAndHasYTranslationOrNegativeY || isLastAndHasYTranslationOrNegativeY)
|
2022-03-23 12:01:22 +00:00
|
|
|
if (isFirstStory) {
|
|
|
|
val delta = max(0f, (e2.rawY - e1.rawY)) / 3f
|
2022-03-23 15:50:32 +00:00
|
|
|
val percent = INTERPOLATOR.getInterpolation(delta / maxSlide)
|
2022-06-23 18:44:22 +00:00
|
|
|
val distance = maxSlide * percent
|
|
|
|
|
|
|
|
viewToTranslate.animate().cancel()
|
|
|
|
viewToTranslate.translationY = distance
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isLastStory) {
|
|
|
|
val delta = max(0f, (e1.rawY - e2.rawY)) / 3f
|
|
|
|
val percent = -INTERPOLATOR.getInterpolation(delta / maxSlide)
|
2022-03-23 12:01:22 +00:00
|
|
|
val distance = maxSlide * percent
|
|
|
|
|
|
|
|
viewToTranslate.animate().cancel()
|
|
|
|
viewToTranslate.translationY = distance
|
|
|
|
}
|
|
|
|
|
|
|
|
val delta = max(0f, (e2.rawX - e1.rawX)) / 3f
|
|
|
|
val percent = INTERPOLATOR.getInterpolation(delta / maxSlide)
|
|
|
|
val distance = maxSlide * percent
|
|
|
|
|
|
|
|
viewToTranslate.animate().cancel()
|
|
|
|
viewToTranslate.translationX = distance
|
|
|
|
|
2022-09-27 17:57:39 +00:00
|
|
|
onContentTranslation(viewToTranslate.translationX, viewToTranslate.translationY)
|
|
|
|
|
2022-03-23 12:01:22 +00:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2022-02-24 17:40:28 +00:00
|
|
|
override fun onFling(e1: MotionEvent?, e2: MotionEvent?, velocityX: Float, velocityY: Float): Boolean {
|
|
|
|
val isSideSwipe = abs(velocityX) > abs(velocityY)
|
|
|
|
if (!isSideSwipe) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2022-04-07 16:53:53 +00:00
|
|
|
if (viewToTranslate.translationX != 0f || viewToTranslate.translationY != 0f) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2022-03-18 19:10:41 +00:00
|
|
|
if (ViewUtil.isLtr(container)) {
|
|
|
|
if (velocityX < 0) {
|
|
|
|
onReplyToPost()
|
|
|
|
}
|
|
|
|
} else if (velocityX > 0) {
|
2022-02-24 17:40:28 +00:00
|
|
|
onReplyToPost()
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private class FallbackPhotoProvider : Recipient.FallbackPhotoProvider() {
|
|
|
|
override fun getPhotoForGroup(): FallbackContactPhoto {
|
|
|
|
return FallbackPhoto20dp(R.drawable.ic_group_outline_20)
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun getPhotoForResolvingRecipient(): FallbackContactPhoto {
|
|
|
|
throw UnsupportedOperationException("This provider does not support resolving recipients")
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun getPhotoForLocalNumber(): FallbackContactPhoto {
|
|
|
|
throw UnsupportedOperationException("This provider does not support local number")
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun getPhotoForRecipientWithName(name: String, targetSize: Int): FallbackContactPhoto {
|
|
|
|
return FixedSizeGeneratedContactPhoto(name, R.drawable.ic_profile_outline_20)
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun getPhotoForRecipientWithoutName(): FallbackContactPhoto {
|
|
|
|
return FallbackPhoto20dp(R.drawable.ic_profile_outline_20)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private class FixedSizeGeneratedContactPhoto(name: String, fallbackResId: Int) : GeneratedContactPhoto(name, fallbackResId) {
|
|
|
|
override fun newFallbackDrawable(context: Context, color: AvatarColor, inverted: Boolean): Drawable {
|
|
|
|
return FallbackPhoto20dp(fallbackResId).asDrawable(context, color, inverted)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-12 13:02:27 +00:00
|
|
|
override fun onContentReady() {
|
2022-04-07 19:08:05 +00:00
|
|
|
sharedViewModel.setContentIsReady()
|
|
|
|
}
|
|
|
|
|
2022-10-12 13:02:27 +00:00
|
|
|
override fun onContentNotAvailable() {
|
2022-04-07 19:08:05 +00:00
|
|
|
sharedViewModel.setContentIsReady()
|
|
|
|
}
|
|
|
|
|
2022-07-14 20:00:15 +00:00
|
|
|
override fun onInfoSheetDismissed() {
|
|
|
|
viewModel.setIsDisplayingInfoDialog(false)
|
|
|
|
}
|
2022-09-21 19:57:20 +00:00
|
|
|
|
|
|
|
override fun sendAnywayAfterSafetyNumberChangedInBottomSheet(destinations: List<ContactSearchKey.RecipientSearchKey>) {
|
|
|
|
error("Not supported, we handed a message record to the bottom sheet.")
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onMessageResentAfterSafetyNumberChangeInBottomSheet() {
|
|
|
|
viewModel.setIsDisplayingPartialSendDialog(false)
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onCanceled() {
|
|
|
|
viewModel.setIsDisplayingPartialSendDialog(false)
|
|
|
|
}
|
2022-09-27 17:57:39 +00:00
|
|
|
|
2022-11-08 22:34:02 +00:00
|
|
|
override fun onRecipientBottomSheetDismissed() {
|
|
|
|
viewModel.setIsDisplayingRecipientBottomSheet(false)
|
|
|
|
}
|
|
|
|
|
2022-09-27 17:57:39 +00:00
|
|
|
interface Callback {
|
|
|
|
fun onGoToPreviousStory(recipientId: RecipientId)
|
|
|
|
fun onFinishedPosts(recipientId: RecipientId)
|
|
|
|
fun onStoryHidden(recipientId: RecipientId)
|
|
|
|
fun onContentTranslation(x: Float, y: Float)
|
|
|
|
}
|
2022-02-24 17:40:28 +00:00
|
|
|
}
|