1
0
Fork 0
mirror of https://github.com/KokaKiwi/BarInsta synced 2026-03-14 00:11:40 +00:00

Merge branch 'master' into pr/1542

This commit is contained in:
Austin Huang 2021-07-06 17:05:01 -04:00
commit f0a2feeaa8
No known key found for this signature in database
GPG key ID: 84C23AA04587A91F
64 changed files with 2355 additions and 2140 deletions

View file

@ -1,5 +1,7 @@
package awais.instagrabber.adapters;
import java.util.List;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -72,6 +74,16 @@ public final class StoriesAdapter extends ListAdapter<StoryMedia, StoriesAdapter
}
}
public void paginate(final int newIndex) {
final List<StoryMedia> list = getCurrentList();
for (int i = 0; i < list.size(); i++) {
final StoryMedia item = list.get(i);
if (!item.isCurrentSlide() && i != newIndex) continue;
item.setCurrentSlide(i == newIndex);
notifyItemChanged(i, item);
}
}
public interface OnItemClickListener {
void onItemClick(StoryMedia storyModel, int position);
}

View file

@ -0,0 +1,905 @@
package awais.instagrabber.fragments
import android.annotation.SuppressLint
import android.content.DialogInterface.OnClickListener
import android.graphics.drawable.Animatable
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.util.Log
import android.view.*
import android.view.GestureDetector.SimpleOnGestureListener
import android.widget.*
import android.widget.SeekBar.OnSeekBarChangeListener
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ContextThemeWrapper
import androidx.appcompat.widget.PopupMenu
import androidx.core.view.GestureDetectorCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.recyclerview.widget.LinearLayoutManager
import awais.instagrabber.BuildConfig
import awais.instagrabber.R
import awais.instagrabber.adapters.StoriesAdapter
import awais.instagrabber.customviews.helpers.SwipeGestureListener
import awais.instagrabber.databinding.FragmentStoryViewerBinding
import awais.instagrabber.fragments.main.ProfileFragment
import awais.instagrabber.fragments.settings.PreferenceKeys
import awais.instagrabber.interfaces.SwipeEvent
import awais.instagrabber.models.Resource
import awais.instagrabber.models.enums.FavoriteType
import awais.instagrabber.models.enums.MediaItemType
import awais.instagrabber.models.enums.StoryPaginationType
import awais.instagrabber.repositories.requests.StoryViewerOptions
import awais.instagrabber.repositories.responses.directmessages.RankedRecipient
import awais.instagrabber.repositories.responses.stories.*
import awais.instagrabber.utils.Constants
import awais.instagrabber.utils.DownloadUtils.download
import awais.instagrabber.utils.TextUtils.epochSecondToString
import awais.instagrabber.utils.ResponseBodyUtils
import awais.instagrabber.utils.Utils
import awais.instagrabber.utils.extensions.TAG
import awais.instagrabber.viewmodels.ArchivesViewModel
import awais.instagrabber.viewmodels.FeedStoriesViewModel
import awais.instagrabber.viewmodels.StoryFragmentViewModel
import awais.instagrabber.webservices.MediaRepository
import awais.instagrabber.webservices.StoriesRepository
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.drawee.controller.BaseControllerListener
import com.facebook.drawee.interfaces.DraweeController
import com.facebook.imagepipeline.image.ImageInfo
import com.facebook.imagepipeline.request.ImageRequestBuilder
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.SimpleExoPlayer
import com.google.android.exoplayer2.source.*
import com.google.android.exoplayer2.source.dash.DashMediaSource
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
import com.google.android.material.textfield.TextInputEditText
import java.io.IOException
import java.text.NumberFormat
import java.util.*
class StoryViewerFragment : Fragment() {
private val TAG = "StoryViewerFragment"
private var root: View? = null
private var currentStoryUsername: String? = null
private var storiesAdapter: StoriesAdapter? = null
private var swipeEvent: SwipeEvent? = null
private var gestureDetector: GestureDetectorCompat? = null
private val storiesRepository: StoriesRepository? = null
private val mediaRepository: MediaRepository? = null
private var live: Broadcast? = null
private var menuProfile: MenuItem? = null
private var profileVisible: Boolean = false
private var player: SimpleExoPlayer? = null
private var actionBarTitle: String? = null
private var actionBarSubtitle: String? = null
private var shouldRefresh = true
private var currentFeedStoryIndex = 0
private var sliderValue = 0.0
private var options: StoryViewerOptions? = null
private var listViewModel: ViewModel? = null
private var backStackSavedStateResultLiveData: MutableLiveData<Any?>? = null
private lateinit var fragmentActivity: AppCompatActivity
private lateinit var storiesViewModel: StoryFragmentViewModel
private lateinit var binding: FragmentStoryViewerBinding
@Suppress("UNCHECKED_CAST")
private val backStackSavedStateObserver = Observer<Any?> { result ->
if (result == null) return@Observer
if ((result is RankedRecipient)) {
if (context != null) {
Toast.makeText(context, R.string.sending, Toast.LENGTH_SHORT).show()
}
storiesViewModel.shareDm(result)
} else if ((result is Set<*>)) {
try {
if (context != null) {
Toast.makeText(context, R.string.sending, Toast.LENGTH_SHORT).show()
}
storiesViewModel.shareDm(result as Set<RankedRecipient>)
} catch (e: Exception) {
Log.e(TAG, "share: ", e)
}
}
// clear result
backStackSavedStateResultLiveData?.postValue(null)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
fragmentActivity = requireActivity() as AppCompatActivity
storiesViewModel = ViewModelProvider(this).get(StoryFragmentViewModel::class.java)
setHasOptionsMenu(true)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
if (root != null) {
shouldRefresh = false
return root
}
binding = FragmentStoryViewerBinding.inflate(inflater, container, false)
root = binding.root
return root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
if (!shouldRefresh) return
init()
shouldRefresh = false
}
override fun onCreateOptionsMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.story_menu, menu)
menuProfile = menu.findItem(R.id.action_profile)
menuProfile!!.isVisible = profileVisible
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val itemId = item.itemId
if (itemId == R.id.action_profile) {
val username = storiesViewModel.getCurrentStory().value?.user?.username
openProfile(Pair(username, FavoriteType.USER))
return true
}
return false
}
override fun onPause() {
super.onPause()
player?.pause() ?: return
}
override fun onResume() {
super.onResume()
setHasOptionsMenu(true)
try {
val backStackEntry = NavHostFragment.findNavController(this).currentBackStackEntry
if (backStackEntry != null) {
backStackSavedStateResultLiveData = backStackEntry.savedStateHandle.getLiveData("result")
backStackSavedStateResultLiveData?.observe(viewLifecycleOwner, backStackSavedStateObserver)
}
} catch (e: Exception) {
Log.e(TAG, "onResume: ", e)
}
val actionBar = fragmentActivity.supportActionBar ?: return
actionBar.title = storiesViewModel.getTitle().value
actionBar.subtitle = storiesViewModel.getDate().value
}
override fun onDestroy() {
releasePlayer()
val actionBar = fragmentActivity.supportActionBar
actionBar?.subtitle = null
super.onDestroy()
}
private fun init() {
val args = arguments ?: return
val fragmentArgs = StoryViewerFragmentArgs.fromBundle(args)
options = fragmentArgs.options
currentFeedStoryIndex = options!!.currentFeedStoryIndex
val type = options!!.type
if (currentFeedStoryIndex >= 0) {
listViewModel = when (type) {
StoryViewerOptions.Type.STORY_ARCHIVE ->
ViewModelProvider(fragmentActivity).get(ArchivesViewModel::class.java)
StoryViewerOptions.Type.FEED_STORY_POSITION ->
ViewModelProvider(fragmentActivity).get(FeedStoriesViewModel::class.java)
else -> null
}
}
setupButtons()
setupStories()
}
private fun setupStories() {
setupListeners()
val context = context ?: return
binding.storiesList.layoutManager =
LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
storiesAdapter = StoriesAdapter { _, position ->
storiesViewModel.setMedia(position)
}
binding.storiesList.adapter = storiesAdapter
storiesViewModel.getCurrentStory().observe(fragmentActivity, {
if (it?.items != null) {
val storyMedias = it.items.toMutableList()
val newItem = storyMedias.get(0)
newItem.isCurrentSlide = true
storyMedias.set(0, newItem)
storiesAdapter!!.submitList(storyMedias)
storiesViewModel.setMedia(0)
binding.listToggle.isEnabled = true
binding.storiesList.visibility = if (Utils.settingsHelper.getBoolean(PreferenceKeys.PREF_STORY_SHOW_LIST)) View.VISIBLE
else View.GONE
}
else {
binding.listToggle.isEnabled = false
binding.storiesList.visibility = View.GONE
}
})
storiesViewModel.getDate().observe(fragmentActivity, {
val actionBar = fragmentActivity.supportActionBar
if (actionBar != null && it != null) actionBar.subtitle = it
})
storiesViewModel.getTitle().observe(fragmentActivity, {
val actionBar = fragmentActivity.supportActionBar
if (actionBar != null && it != null) actionBar.title = it
})
storiesViewModel.getCurrentMedia().observe(fragmentActivity, { refreshStory(it) })
storiesViewModel.getCurrentIndex().observe(fragmentActivity, {
storiesAdapter!!.paginate(it)
})
storiesViewModel.getOptions().observe(fragmentActivity, {
binding.stickers.isEnabled = it.first.size > 0
})
}
private fun setupButtons() {
binding.btnDownload.setOnClickListener({ _ -> downloadStory() })
binding.btnForward.setOnClickListener({ _ -> storiesViewModel.skip(false) })
binding.btnBackward.setOnClickListener({ _ -> storiesViewModel.skip(true) })
binding.btnShare.setOnClickListener({ _ -> shareStoryViaDm() })
binding.btnReply.setOnClickListener({ _ -> createReplyDialog(null) })
binding.stickers.setOnClickListener({ _ -> showStickerMenu() })
binding.listToggle.setOnClickListener({ _ ->
binding.storiesList.visibility = if (binding.storiesList.visibility == View.GONE) View.VISIBLE
else View.GONE
})
}
@SuppressLint("ClickableViewAccessibility")
private fun setupListeners() {
var liveModels: LiveData<List<Story>?>? = null
if (currentFeedStoryIndex >= 0) {
val type = options!!.type
when (type) {
StoryViewerOptions.Type.HIGHLIGHT -> {
storiesViewModel.fetchHighlights(options!!.id)
liveModels = storiesViewModel.getHighlights()
}
StoryViewerOptions.Type.FEED_STORY_POSITION -> {
val feedStoriesViewModel = listViewModel as FeedStoriesViewModel?
liveModels = feedStoriesViewModel!!.list
}
StoryViewerOptions.Type.STORY_ARCHIVE -> {
val archivesViewModel = listViewModel as ArchivesViewModel?
liveModels = archivesViewModel!!.list
}
StoryViewerOptions.Type.USER -> {
resetView()
}
}
}
if (liveModels != null) liveModels.observe(viewLifecycleOwner, { models ->
storiesViewModel.getPagination().observe(fragmentActivity, {
if (models != null) {
when (it) {
StoryPaginationType.FORWARD -> {
if (currentFeedStoryIndex == models.size - 1)
Toast.makeText(
context,
R.string.no_more_stories,
Toast.LENGTH_SHORT
).show()
else paginateStories(false, currentFeedStoryIndex == models.size - 2)
}
StoryPaginationType.BACKWARD -> {
if (currentFeedStoryIndex == 0)
Toast.makeText(
context,
R.string.no_more_stories,
Toast.LENGTH_SHORT
).show()
else paginateStories(true, false)
}
StoryPaginationType.ERROR -> {
Toast.makeText(
context,
R.string.downloader_unknown_error,
Toast.LENGTH_SHORT
).show()
}
}
}
})
if (models != null && !models.isEmpty()) {
binding.btnBackward.isEnabled = currentFeedStoryIndex != 0
binding.btnForward.isEnabled = currentFeedStoryIndex != models.size - 1
resetView()
}
})
val context = context ?: return
swipeEvent = SwipeEvent { isRightSwipe: Boolean ->
storiesViewModel.paginate(isRightSwipe)
}
gestureDetector = GestureDetectorCompat(context, SwipeGestureListener(swipeEvent))
binding.playerView.setOnTouchListener { _, event -> gestureDetector!!.onTouchEvent(event) }
val simpleOnGestureListener: SimpleOnGestureListener = object : SimpleOnGestureListener() {
override fun onFling(
e1: MotionEvent,
e2: MotionEvent,
velocityX: Float,
velocityY: Float
): Boolean {
val diffX = e2.x - e1.x
try {
if (Math.abs(diffX) > Math.abs(e2.y - e1.y) && Math.abs(diffX) > SwipeGestureListener.SWIPE_THRESHOLD && Math.abs(
velocityX
) > SwipeGestureListener.SWIPE_VELOCITY_THRESHOLD
) {
storiesViewModel.paginate(diffX > 0)
return true
}
} catch (e: Exception) {
if (BuildConfig.DEBUG) Log.e(TAG, "Error", e)
}
return false
}
}
binding.imageViewer.setTapListener(simpleOnGestureListener)
}
private fun resetView() {
val context = context ?: return
live = null
if (menuProfile != null) menuProfile!!.isVisible = false
binding.imageViewer.controller = null
releasePlayer()
val type = options!!.type
var fetchOptions: StoryViewerOptions? = null
when (type) {
StoryViewerOptions.Type.HIGHLIGHT -> {
val models = storiesViewModel.getHighlights().value
if (models == null || models.isEmpty() || currentFeedStoryIndex >= models.size || currentFeedStoryIndex < 0) {
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show()
return
}
fetchOptions = StoryViewerOptions.forHighlight(0L, models[currentFeedStoryIndex].id)
}
StoryViewerOptions.Type.FEED_STORY_POSITION -> {
val feedStoriesViewModel = listViewModel as FeedStoriesViewModel?
val models = feedStoriesViewModel!!.list.value
if (models == null || currentFeedStoryIndex >= models.size || currentFeedStoryIndex < 0) return
val (_, _, _, _, user, _, _, _, _, _, _, broadcast) = models[currentFeedStoryIndex]
currentStoryUsername = user!!.username
fetchOptions = StoryViewerOptions.forUser(user.pk, currentStoryUsername)
live = broadcast
}
StoryViewerOptions.Type.STORY_ARCHIVE -> {
val archivesViewModel = listViewModel as ArchivesViewModel?
val models = archivesViewModel!!.list.value
if (models == null || models.isEmpty() || currentFeedStoryIndex >= models.size || currentFeedStoryIndex < 0) {
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT)
.show()
return
}
val (id, _, _, _, _, _, _, _, _, title) = models[currentFeedStoryIndex]
currentStoryUsername = title
fetchOptions = StoryViewerOptions.forStoryArchive(id)
}
StoryViewerOptions.Type.USER -> {
currentStoryUsername = options!!.name
fetchOptions = StoryViewerOptions.forUser(options!!.id, currentStoryUsername)
}
}
if (type == StoryViewerOptions.Type.STORY) {
storiesViewModel.fetchSingleMedia(options!!.id)
return
}
if (live != null) {
refreshLive()
return
}
storiesViewModel.fetchStory(fetchOptions).observe(fragmentActivity, {
if (it.status == Resource.Status.ERROR) {
Toast.makeText(context, "Error: " + it.message, Toast.LENGTH_SHORT).show()
}
})
}
@Synchronized
private fun refreshLive() {
releasePlayer()
setupLive(live!!.dashPlaybackUrl ?: live!!.dashAbrPlaybackUrl ?: return)
val actionBar = fragmentActivity.supportActionBar
actionBarSubtitle = epochSecondToString(live!!.publishedTime!!)
if (actionBar != null) {
try {
actionBar.setSubtitle(actionBarSubtitle)
} catch (e: Exception) {
Log.e(TAG, "refreshLive: ", e)
}
}
}
@Synchronized
private fun refreshStory(currentStory: StoryMedia) {
val itemType = currentStory.type
val url = if (itemType === MediaItemType.MEDIA_TYPE_IMAGE) ResponseBodyUtils.getImageUrl(currentStory)
else ResponseBodyUtils.getVideoUrl(currentStory)
releasePlayer()
profileVisible = currentStory.user?.username != null
if (menuProfile != null) menuProfile!!.isVisible = profileVisible
binding.btnDownload.isEnabled = false
binding.btnShare.isEnabled = currentStory.canReshare
binding.btnReply.isEnabled = currentStory.canReply
if (itemType === MediaItemType.MEDIA_TYPE_VIDEO) setupVideo(url) else setupImage(url)
// if (Utils.settingsHelper.getBoolean(MARK_AS_SEEN)) storiesRepository!!.seen(
// csrfToken,
// userId,
// deviceId,
// currentStory!!.id!!,
// currentStory!!.takenAt,
// System.currentTimeMillis() / 1000
// )
}
private fun downloadStory() {
val context = context ?: return
val currentStory = storiesViewModel.getMedia().value
if (currentStory == null) {
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show()
return
}
download(context, currentStory)
}
private fun setupImage(url: String) {
binding.progressView.visibility = View.VISIBLE
binding.playerView.visibility = View.GONE
binding.imageViewer.visibility = View.VISIBLE
val requestBuilder = ImageRequestBuilder.newBuilderWithSource(Uri.parse(url))
.setLocalThumbnailPreviewsEnabled(true)
.setProgressiveRenderingEnabled(true)
.build()
val controller: DraweeController = Fresco.newDraweeControllerBuilder()
.setImageRequest(requestBuilder)
.setOldController(binding.imageViewer.controller)
.setControllerListener(object : BaseControllerListener<ImageInfo?>() {
override fun onFailure(id: String, throwable: Throwable) {
binding.btnDownload.isEnabled = false
binding.progressView.visibility = View.GONE
}
override fun onFinalImageSet(
id: String,
imageInfo: ImageInfo?,
animatable: Animatable?
) {
binding.btnDownload.isEnabled = true
binding.progressView.visibility = View.GONE
}
})
.build()
binding.imageViewer.controller = controller
}
private fun setupVideo(url: String) {
binding.playerView.visibility = View.VISIBLE
binding.progressView.visibility = View.GONE
binding.imageViewer.visibility = View.GONE
binding.imageViewer.controller = null
val context = context ?: return
player = SimpleExoPlayer.Builder(context).build()
binding.playerView.player = player
player!!.playWhenReady =
Utils.settingsHelper.getBoolean(PreferenceKeys.AUTOPLAY_VIDEOS_STORIES)
val uri = Uri.parse(url)
val mediaItem = MediaItem.fromUri(uri)
val mediaSource =
ProgressiveMediaSource.Factory(DefaultDataSourceFactory(context, "instagram"))
.createMediaSource(mediaItem)
mediaSource.addEventListener(Handler(), object : MediaSourceEventListener {
override fun onLoadCompleted(
windowIndex: Int,
mediaPeriodId: MediaSource.MediaPeriodId?,
loadEventInfo: LoadEventInfo,
mediaLoadData: MediaLoadData
) {
binding.btnDownload.isEnabled = true
binding.progressView.visibility = View.GONE
}
override fun onLoadStarted(
windowIndex: Int,
mediaPeriodId: MediaSource.MediaPeriodId?,
loadEventInfo: LoadEventInfo,
mediaLoadData: MediaLoadData
) {
binding.btnDownload.isEnabled = true
binding.progressView.visibility = View.VISIBLE
}
override fun onLoadCanceled(
windowIndex: Int,
mediaPeriodId: MediaSource.MediaPeriodId?,
loadEventInfo: LoadEventInfo,
mediaLoadData: MediaLoadData
) {
binding.progressView.visibility = View.GONE
}
override fun onLoadError(
windowIndex: Int,
mediaPeriodId: MediaSource.MediaPeriodId?,
loadEventInfo: LoadEventInfo,
mediaLoadData: MediaLoadData,
error: IOException,
wasCanceled: Boolean
) {
binding.btnDownload.isEnabled = false
binding.progressView.visibility = View.GONE
}
})
player!!.setMediaSource(mediaSource)
player!!.prepare()
binding.playerView.setOnClickListener { _ ->
if (player != null) {
if (player!!.playbackState == Player.STATE_ENDED) player!!.seekTo(0)
player!!.playWhenReady =
player!!.playbackState == Player.STATE_ENDED || !player!!.isPlaying
}
}
}
private fun setupLive(url: String) {
binding.playerView.visibility = View.VISIBLE
binding.progressView.visibility = View.GONE
binding.imageViewer.visibility = View.GONE
binding.imageViewer.controller = null
val context = context ?: return
player = SimpleExoPlayer.Builder(context).build()
binding.playerView.player = player
player!!.playWhenReady =
Utils.settingsHelper.getBoolean(PreferenceKeys.AUTOPLAY_VIDEOS_STORIES)
val uri = Uri.parse(url)
val mediaItem = MediaItem.fromUri(uri)
val mediaSource = DashMediaSource.Factory(DefaultDataSourceFactory(context, "instagram"))
.createMediaSource(mediaItem)
mediaSource.addEventListener(Handler(), object : MediaSourceEventListener {
override fun onLoadCompleted(
windowIndex: Int,
mediaPeriodId: MediaSource.MediaPeriodId?,
loadEventInfo: LoadEventInfo,
mediaLoadData: MediaLoadData
) {
binding.progressView.visibility = View.GONE
}
override fun onLoadStarted(
windowIndex: Int,
mediaPeriodId: MediaSource.MediaPeriodId?,
loadEventInfo: LoadEventInfo,
mediaLoadData: MediaLoadData
) {
binding.progressView.visibility = View.VISIBLE
}
override fun onLoadCanceled(
windowIndex: Int,
mediaPeriodId: MediaSource.MediaPeriodId?,
loadEventInfo: LoadEventInfo,
mediaLoadData: MediaLoadData
) {
binding.progressView.visibility = View.GONE
}
override fun onLoadError(
windowIndex: Int,
mediaPeriodId: MediaSource.MediaPeriodId?,
loadEventInfo: LoadEventInfo,
mediaLoadData: MediaLoadData,
error: IOException,
wasCanceled: Boolean
) {
binding.progressView.visibility = View.GONE
}
})
player!!.setMediaSource(mediaSource)
player!!.prepare()
binding.playerView.setOnClickListener { _ ->
if (player != null) {
if (player!!.playbackState == Player.STATE_ENDED) player!!.seekTo(0)
player!!.playWhenReady =
player!!.playbackState == Player.STATE_ENDED || !player!!.isPlaying
}
}
}
private fun openProfile(data: Pair<String?, FavoriteType>) {
val navController: NavController = NavHostFragment.findNavController(this)
val bundle = Bundle()
if (data.first == null) {
// toast
return
}
val actionBar = fragmentActivity.supportActionBar
if (actionBar != null) {
actionBar.title = null
actionBar.subtitle = null
}
when (data.second) {
FavoriteType.USER -> {
bundle.putString("username", data.first)
navController.navigate(R.id.action_global_profileFragment, bundle)
}
FavoriteType.HASHTAG -> {
bundle.putString("hashtag", data.first)
navController.navigate(R.id.action_global_hashTagFragment, bundle)
}
FavoriteType.LOCATION -> {
bundle.putLong("locationId", data.first!!.toLong())
navController.navigate(R.id.action_global_locationFragment, bundle)
}
}
}
private fun releasePlayer() {
if (player == null) return
try {
player!!.stop(true)
} catch (ignored: Exception) {
}
try {
player!!.release()
} catch (ignored: Exception) {
}
player = null
}
private fun paginateStories(
backward: Boolean,
last: Boolean
) {
binding.btnBackward.isEnabled = currentFeedStoryIndex != 1 || !backward
binding.btnForward.isEnabled = !last
currentFeedStoryIndex = if (backward) currentFeedStoryIndex - 1 else currentFeedStoryIndex + 1
resetView()
}
private fun createChoiceDialog(
title: String?,
tallies: List<Tally>,
onClickListener: OnClickListener,
viewerVote: Int?,
correctAnswer: Int?
) {
val context = context ?: return
val choices = tallies.map {
(if (viewerVote == tallies.indexOf(it)) "" else "") +
(if (correctAnswer == tallies.indexOf(it)) "*** " else "") +
it.text + " (" + it.count + ")" }
val builder = AlertDialog.Builder(context)
if (title != null) builder.setTitle(title)
if (viewerVote != null) builder.setMessage(R.string.story_quizzed)
builder.setPositiveButton(if (viewerVote == null) R.string.cancel else R.string.ok, null)
val adapter = ArrayAdapter(context, android.R.layout.simple_list_item_1, choices.toTypedArray())
builder.setAdapter(adapter, onClickListener)
builder.show()
}
private fun createMentionDialog() {
val context = context ?: return
val adapter = ArrayAdapter(context, android.R.layout.simple_list_item_1, storiesViewModel.getMentionTexts())
val builder = AlertDialog.Builder(context)
.setPositiveButton(R.string.ok, null)
.setAdapter(adapter, { _, w ->
val data = storiesViewModel.getMention(w)
if (data != null) openProfile(Pair(data.second, data.third))
})
builder.show()
}
private fun createSliderDialog() {
val slider = storiesViewModel.getSlider().value ?: return
val context = context ?: return
val percentage: NumberFormat = NumberFormat.getPercentInstance()
percentage.maximumFractionDigits = 2
val sliderView = LinearLayout(context)
sliderView.layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
sliderView.orientation = LinearLayout.VERTICAL
val tv = TextView(context)
tv.gravity = Gravity.CENTER_HORIZONTAL
val input = SeekBar(context)
val avg: Double = slider.sliderVoteAverage ?: 0.5
input.progress = (avg * 100).toInt()
var onClickListener: OnClickListener? = null
if (slider.viewerVote == null && slider.viewerCanVote == true) {
input.isEnabled = true
input.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
sliderValue = progress / 100.0
tv.text = percentage.format(sliderValue)
}
override fun onStartTrackingTouch(seekBar: SeekBar) {}
override fun onStopTrackingTouch(seekBar: SeekBar) {}
})
onClickListener = OnClickListener { _, _ -> storiesViewModel.answerSlider(sliderValue) }
}
else {
input.isEnabled = false
tv.text = getString(R.string.slider_answer, percentage.format(slider.viewerVote))
}
sliderView.addView(input)
sliderView.addView(tv)
val builder = AlertDialog.Builder(context)
.setTitle(if (slider.question.isNullOrEmpty()) slider.emoji else slider.question)
.setMessage(
resources.getQuantityString(R.plurals.slider_info,
slider.sliderVoteCount ?: 0,
slider.sliderVoteCount ?: 0,
percentage.format(avg)))
.setView(sliderView)
.setPositiveButton(R.string.ok, onClickListener)
builder.show()
}
private fun createReplyDialog(question: String?) {
val context = context ?: return
val input = TextInputEditText(context)
input.setHint(R.string.reply_hint)
val builder = AlertDialog.Builder(context)
.setTitle(question ?: context.getString(R.string.reply_story))
.setView(input)
val onClickListener = OnClickListener{ _, _ ->
val result =
if (question != null) storiesViewModel.answerQuestion(input.text.toString())
else storiesViewModel.reply(input.text.toString())
if (result == null) {
Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT)
.show()
}
else result.observe(viewLifecycleOwner, {
when (it.status) {
Resource.Status.SUCCESS -> {
Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT)
.show()
}
Resource.Status.ERROR -> {
Toast.makeText(context, "Error: " + it.message, Toast.LENGTH_SHORT)
.show()
}
Resource.Status.LOADING -> {
Toast.makeText(context, R.string.sending, Toast.LENGTH_SHORT).show()
}
}
})
}
builder.setPositiveButton(R.string.confirm, onClickListener)
builder.show()
}
private fun shareStoryViaDm() {
val actionGlobalUserSearch = UserSearchFragmentDirections.actionGlobalUserSearch().apply {
title = getString(R.string.share)
setActionLabel(getString(R.string.send))
showGroups = true
multiple = true
setSearchMode(UserSearchFragment.SearchMode.RAVEN)
}
try {
val navController = NavHostFragment.findNavController(this@StoryViewerFragment)
navController.navigate(actionGlobalUserSearch)
} catch (e: Exception) {
Log.e(TAG, "shareStoryViaDm: ", e)
}
}
private fun showStickerMenu() {
val data = storiesViewModel.getOptions().value
if (data == null) return
val themeWrapper = ContextThemeWrapper(context, R.style.popupMenuStyle)
val popupMenu = PopupMenu(themeWrapper, binding.stickers)
val menu = popupMenu.menu
data.first.map {
if (it.second != 0) menu.add(0, it.first, 0, it.second)
if (it.first == R.id.swipeUp) menu.add(0, R.id.swipeUp, 0, data.second)
if (it.first == R.id.spotify) menu.add(0, R.id.spotify, 0, data.third)
}
popupMenu.setOnMenuItemClickListener { item: MenuItem ->
val itemId = item.itemId
if (itemId == R.id.spotify) openExternalLink(storiesViewModel.getAppAttribution())
else if (itemId == R.id.swipeUp) openExternalLink(storiesViewModel.getSwipeUp())
else if (itemId == R.id.mentions) createMentionDialog()
else if (itemId == R.id.slider) createSliderDialog()
else if (itemId == R.id.question) {
val question = storiesViewModel.getQuestion().value
if (question != null) createReplyDialog(question.question)
}
else if (itemId == R.id.quiz) {
val quiz = storiesViewModel.getQuiz().value
if (quiz != null) createChoiceDialog(
quiz.question,
quiz.tallies,
{ _, w -> storiesViewModel.answerQuiz(w) },
quiz.viewerAnswer,
quiz.correctAnswer
)
}
else if (itemId == R.id.poll) {
val poll = storiesViewModel.getPoll().value
if (poll != null) createChoiceDialog(
poll.question,
poll.tallies,
{ _, w -> storiesViewModel.answerPoll(w) },
poll.viewerVote,
null
)
}
else if (itemId == R.id.viewStoryPost) {
storiesViewModel.getLinkedPost().observe(viewLifecycleOwner, {
if (it == null) Toast.makeText(context, "Error: LiveData is null", Toast.LENGTH_SHORT).show()
else when (it.status) {
Resource.Status.SUCCESS -> {
if (it.data != null) {
val actionBar = fragmentActivity.supportActionBar
if (actionBar != null) {
actionBar.title = null
actionBar.subtitle = null
}
val navController =
NavHostFragment.findNavController(this@StoryViewerFragment)
val bundle = Bundle()
bundle.putSerializable(PostViewV2Fragment.ARG_MEDIA, it.data)
try {
navController.navigate(R.id.action_global_post_view, bundle)
} catch (e: Exception) {
Log.e(TAG, "openPostDialog: ", e)
}
}
}
Resource.Status.ERROR -> {
Toast.makeText(context, "Error: " + it.message, Toast.LENGTH_SHORT)
.show()
}
Resource.Status.LOADING -> {
Toast.makeText(context, R.string.opening_post, Toast.LENGTH_SHORT)
.show()
}
}
})
}
false
}
popupMenu.show()
}
private fun openExternalLink(url: String?) {
val context = context ?: return
if (url == null) return
AlertDialog.Builder(context)
.setTitle(R.string.swipe_up_confirmation)
.setMessage(url).setPositiveButton(R.string.yes, { _, _ -> Utils.openURL(context, url) })
.setNegativeButton(R.string.no, null)
.show()
}
}

View file

@ -209,7 +209,7 @@ class ProfileFragment : Fragment(), OnRefreshListener, ConfirmDialogFragmentCall
}
}
private val onProfilePicClickListener = View.OnClickListener {
val hasStories = viewModel.userStories.value?.data?.isNotEmpty() ?: false
val hasStories = viewModel.userStories.value?.data != null
if (!hasStories) {
showProfilePicDialog()
return@OnClickListener
@ -309,15 +309,7 @@ class ProfileFragment : Fragment(), OnRefreshListener, ConfirmDialogFragmentCall
viewModel = ViewModelProvider(
this,
ProfileFragmentViewModelFactory(
csrfToken,
deviceUuid,
UserRepository.getInstance(),
FriendshipRepository.getInstance(),
StoriesRepository.getInstance(),
MediaRepository.getInstance(),
GraphQLRepository.getInstance(),
FavoriteRepository.getInstance(requireContext()),
DirectMessagesRepository.getInstance(),
if (isLoggedIn) DirectMessagesManager else null,
this,
arguments
@ -518,7 +510,7 @@ class ProfileFragment : Fragment(), OnRefreshListener, ConfirmDialogFragmentCall
highlightsAdapter?.submitList(it.data)
}
viewModel.userStories.observe(viewLifecycleOwner) {
binding.header.mainProfileImage.setStoriesBorder(if (it.data.isNullOrEmpty()) 0 else 1)
binding.header.mainProfileImage.setStoriesBorder(if (it.data == null) 0 else 1)
}
viewModel.eventLiveData.observe(viewLifecycleOwner) {
val event = it?.getContentIfNotHandled() ?: return@observe
@ -855,7 +847,7 @@ class ProfileFragment : Fragment(), OnRefreshListener, ConfirmDialogFragmentCall
private fun setupHighlights() {
val context = context ?: return
highlightsAdapter = HighlightsAdapter { model, position ->
val options = StoryViewerOptions.forHighlight(model.title).apply { currentFeedStoryIndex = position }
val options = StoryViewerOptions.forHighlight(model.user!!.pk, "").apply { currentFeedStoryIndex = position }
val action = ProfileFragmentDirections.actionToStory(options)
NavHostFragment.findNavController(this).navigate(action)
}
@ -942,7 +934,7 @@ class ProfileFragment : Fragment(), OnRefreshListener, ConfirmDialogFragmentCall
val action = ProfileFragmentDirections.actionToStory(
StoryViewerOptions.forUser(
viewModel.profile.value?.data?.pk ?: return,
viewModel.profile.value?.data?.fullName ?: return,
viewModel.profile.value?.data?.username ?: return,
)
)
findNavController().navigate(action)

View file

@ -18,7 +18,6 @@ public class PostPreferencesFragment extends BasePreferencesFragment {
// generalCategory.addPreference(getAutoPlayVideosPreference(context));
screen.addPreference(getBackgroundPlayPreference(context));
screen.addPreference(getAlwaysMuteVideosPreference(context));
screen.addPreference(getShowCaptionPreference(context));
screen.addPreference(getToggleKeywordFilterPreference(context));
screen.addPreference(getEditKeywordFilterPreference(context));
}
@ -48,15 +47,6 @@ public class PostPreferencesFragment extends BasePreferencesFragment {
return preference;
}
private Preference getShowCaptionPreference(@NonNull final Context context) {
final SwitchPreferenceCompat preference = new SwitchPreferenceCompat(context);
preference.setKey(PreferenceKeys.SHOW_CAPTIONS);
preference.setDefaultValue(true);
preference.setTitle(R.string.post_viewer_show_captions);
preference.setIconSpaceReserved(false);
return preference;
}
private Preference getToggleKeywordFilterPreference(@NonNull final Context context) {
final SwitchPreferenceCompat preference = new SwitchPreferenceCompat(context);
preference.setKey(PreferenceKeys.TOGGLE_KEYWORD_FILTER);

View file

@ -12,6 +12,7 @@ object PreferenceKeys {
const val PREF_SEARCH_FOCUS_KEYBOARD = "search_focus_keyboard"
const val PREF_AUTO_BACKUP_ENABLED = "auto_backup_enabled"
const val PREF_DISABLE_SCREEN_TRANSITIONS = "disable_screen_transitions"
const val PREF_STORY_SHOW_LIST = "story_show_list"
// string prefs
const val FOLDER_PATH = "custom_path"
@ -33,7 +34,7 @@ object PreferenceKeys {
const val PLAY_IN_BACKGROUND = "play_in_background"
const val AUTOPLAY_VIDEOS_STORIES = "autoplay_videos"
const val MUTED_VIDEOS = "muted_videos"
const val SHOW_CAPTIONS = "show_captions"
// const val SHOW_CAPTIONS = "show_captions"
const val CUSTOM_DATE_TIME_FORMAT_ENABLED = "data_time_custom_enabled"
const val SWAP_DATE_TIME_FORMAT_ENABLED = "swap_date_time_enabled"
const val MARK_AS_SEEN = "mark_as_seen"

View file

@ -19,6 +19,7 @@ public class StoriesPreferencesFragment extends BasePreferencesFragment {
screen.addPreference(getHideMutedReelsPreference(context));
screen.addPreference(getMarkStoriesSeenPreference(context));
screen.addPreference(getAutoPlayPreference(context));
screen.addPreference(getStoryListPreference(context));
}
private Preference getStorySortPreference(@NonNull final Context context) {
@ -62,4 +63,13 @@ public class StoriesPreferencesFragment extends BasePreferencesFragment {
preference.setIconSpaceReserved(false);
return preference;
}
private Preference getStoryListPreference(@NonNull final Context context) {
final SwitchPreferenceCompat preference = new SwitchPreferenceCompat(context);
preference.setKey(PreferenceKeys.PREF_STORY_SHOW_LIST);
preference.setTitle(R.string.story_list_setting);
preference.setSummary(R.string.story_list_setting_summary);
preference.setIconSpaceReserved(false);
return preference;
}
}

View file

@ -102,8 +102,8 @@ object DirectMessagesManager {
data.postValue(loading(null))
scope.launch(Dispatchers.IO) {
try {
if (itemType == BroadcastItemType.MEDIA_SHARE)
directMessagesRepository.broadcastMediaShare(
when (itemType) {
BroadcastItemType.MEDIA_SHARE -> directMessagesRepository.broadcastMediaShare(
csrfToken,
viewerId,
deviceUuid,
@ -112,8 +112,7 @@ object DirectMessagesManager {
mediaId,
secondId
)
if (itemType == BroadcastItemType.PROFILE)
directMessagesRepository.broadcastProfile(
BroadcastItemType.PROFILE -> directMessagesRepository.broadcastProfile(
csrfToken,
viewerId,
deviceUuid,
@ -121,6 +120,16 @@ object DirectMessagesManager {
ThreadIdsOrUserIds(threadIds, userIds),
mediaId
)
BroadcastItemType.STORY -> directMessagesRepository.broadcastStory(
csrfToken,
viewerId,
deviceUuid,
UUID.randomUUID().toString(),
ThreadIdsOrUserIds(threadIds, userIds),
mediaId,
secondId!!
)
}
data.postValue(success(Any()))
callback?.invoke()
} catch (e: Exception) {
@ -132,6 +141,42 @@ object DirectMessagesManager {
return data
}
fun replyToStory(
recipientId: Long?,
reelId: String?,
mediaId: String?,
text: String,
scope: CoroutineScope
): LiveData<Resource<Any?>> {
Log.d("austin_debug", "replying")
val data = MutableLiveData<Resource<Any?>>()
data.postValue(loading(null))
if (recipientId == null || reelId == null || mediaId == null) {
data.postValue(error("arguments are null", null))
return data
}
scope.launch(Dispatchers.IO) {
try {
directMessagesRepository.broadcastStoryReply(
csrfToken,
viewerId,
deviceUuid,
ThreadIdsOrUserIds.Companion.ofOneUser(recipientId.toString(10)),
text,
mediaId,
reelId
)
inboxManager.refresh(scope)
data.postValue(success(null))
}
catch (e: Exception) {
Log.e(TAG, "story reply: ", e)
data.postValue(error(e.message, null))
}
}
return data
}
init {
val cookie = Utils.settingsHelper.getString(Constants.COOKIE)
viewerId = getUserIdFromCookie(cookie)

View file

@ -0,0 +1,7 @@
package awais.instagrabber.models.enums
import java.io.Serializable
enum class StoryPaginationType : Serializable {
FORWARD, BACKWARD, DO_NOTHING, ERROR
}

View file

@ -29,12 +29,12 @@ interface StoriesService {
suspend fun getStories(@Path("type") type: String, @Path("id") id: String): ReelsResponse
@GET("/api/v1/feed/user/{id}/story/")
suspend fun getUserStories(@Path("id") id: String): ReelsResponse
suspend fun getUserStories(@Path("id") id: Long): ReelsResponse
@FormUrlEncoded
@POST("/api/v1/media/{storyId}/{stickerId}/{action}/")
suspend fun respondToSticker(
@Path("storyId") storyId: String,
@Path("storyId") storyId: Long,
@Path("stickerId") stickerId: Long,
@Path("action") action: String, // story_poll_vote, story_question_response, story_slider_vote, story_quiz_answer
@FieldMap form: Map<String, String>,

View file

@ -45,8 +45,8 @@ public class StoryViewerOptions implements Serializable {
return new StoryViewerOptions(id, name, Type.USER);
}
public static StoryViewerOptions forHighlight(final String highlight) {
return new StoryViewerOptions(highlight, Type.HIGHLIGHT);
public static StoryViewerOptions forHighlight(final long id, final String highlight) {
return new StoryViewerOptions(id, highlight, Type.HIGHLIGHT);
}
public static StoryViewerOptions forStory(final long mediaId, final String username) {

View file

@ -7,7 +7,7 @@ class StoryReplyBroadcastOptions(
threadIdsOrUserIds: ThreadIdsOrUserIds,
val text: String,
val mediaId: String,
val reelId: String // or user id, usually same
val reelId: String
) : BroadcastOptions(clientContext, threadIdsOrUserIds, BroadcastItemType.REELSHARE) {
override val formMap: Map<String, String>
get() = mapOf(

View file

@ -6,7 +6,7 @@ import awais.instagrabber.repositories.responses.Location
import awais.instagrabber.repositories.responses.User
data class QuestionSticker(
val questionType: String?,
val questionType: String,
val questionId: Long,
val question: String
) : Serializable

View file

@ -6,8 +6,8 @@ import awais.instagrabber.repositories.responses.Location
import awais.instagrabber.repositories.responses.User
data class QuizSticker(
val quizId: Long?,
val question: String?,
val quizId: Long,
val question: String,
val tallies: List<Tally>,
var viewerAnswer: Int?,
val correctAnswer: Int

View file

@ -6,11 +6,11 @@ import awais.instagrabber.repositories.responses.Location
import awais.instagrabber.repositories.responses.User
data class SliderSticker(
val sliderId: Long?,
val question: String?,
val sliderId: Long,
val question: String,
val emoji: String?,
val viewerCanVote: Boolean?,
var viewerVote: Double?,
val viewerVote: Double?,
val sliderVoteAverage: Double?,
val sliderVoteCount: Int?,
) : Serializable

View file

@ -1,9 +1,18 @@
package awais.instagrabber.repositories.responses.stories
import android.net.Uri
import java.io.Serializable
// https://github.com/austinhuang0131/barinsta/issues/1151
data class StoryAppAttribution(
val name: String?, // use name instead of app_action_text for button label
val name: String?,
val appActionText: String?,
val contentUrl: String?
) : Serializable
) : Serializable {
val url: String?
get() {
val uri = Uri.parse(contentUrl)
return if (uri.getHost().equals("open.spotify.com")) contentUrl?.split("?")?.get(0)
else contentUrl
}
}

View file

@ -10,8 +10,8 @@ import java.io.Serializable
data class StoryMedia(
// inherited from Media
val pk: String? = null,
val id: String? = null,
val pk: Long = -1,
val id: String = "",
val takenAt: Long = -1,
val user: User? = null,
val canReshare: Boolean = false,

View file

@ -6,6 +6,6 @@ import awais.instagrabber.repositories.responses.Location
import awais.instagrabber.repositories.responses.User
data class Tally(
val text: String?,
val count: Int?
val text: String,
val count: Int
) : Serializable

View file

@ -209,9 +209,7 @@ object DownloadUtils {
val extension = getFileExtensionFromUrl(displayUrl)
val usernamePrepend = if (isEmpty(username)) "" else username + "_"
val fileName = usernamePrepend + postId + sliderPostfix + extension
val mimeType = Utils.mimeTypeMap.getMimeTypeFromExtension(
if (extension.startsWith(".")) extension.substring(1) else extension
)
val mimeType = Utils.mimeTypeMap.getMimeTypeFromExtension(extension)
return Pair(fileName, mimeType!!)
}
@ -269,7 +267,7 @@ object DownloadUtils {
) {
val dotPos = filename.lastIndexOf('.')
if (0 <= dotPos) {
return filename.substring(dotPos)
return filename.substring(dotPos + 1)
}
}
}

View file

@ -121,7 +121,7 @@ class SettingsHelper(context: Context) {
PreferenceKeys.DOWNLOAD_PREPEND_USER_NAME,
PreferenceKeys.AUTOPLAY_VIDEOS_STORIES,
PreferenceKeys.MUTED_VIDEOS,
PreferenceKeys.SHOW_CAPTIONS,
// PreferenceKeys.SHOW_CAPTIONS,
PreferenceKeys.CUSTOM_DATE_TIME_FORMAT_ENABLED,
PreferenceKeys.MARK_AS_SEEN,
PreferenceKeys.DM_MARK_AS_SEEN,
@ -137,6 +137,7 @@ class SettingsHelper(context: Context) {
PreferenceKeys.PLAY_IN_BACKGROUND,
PreferenceKeys.PREF_SHOWN_COUNT_TOOLTIP,
PreferenceKeys.PREF_SEARCH_FOCUS_KEYBOARD,
PreferenceKeys.PREF_STORY_SHOW_LIST,
PreferenceKeys.PREF_AUTO_BACKUP_ENABLED
)
annotation class BooleanSettings

View file

@ -1,19 +0,0 @@
package awais.instagrabber.viewmodels;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import java.util.List;
import awais.instagrabber.repositories.responses.stories.Story;
public class HighlightsViewModel extends ViewModel {
private MutableLiveData<List<Story>> list;
public MutableLiveData<List<Story>> getList() {
if (list == null) {
list = new MutableLiveData<>();
}
return list;
}
}

View file

@ -16,10 +16,12 @@ import awais.instagrabber.repositories.responses.User
import awais.instagrabber.repositories.responses.UserProfileContextLink
import awais.instagrabber.repositories.responses.directmessages.RankedRecipient
import awais.instagrabber.repositories.responses.stories.Story
import awais.instagrabber.repositories.responses.stories.StoryMedia
import awais.instagrabber.utils.Constants
import awais.instagrabber.utils.ControlledRunner
import awais.instagrabber.utils.Event
import awais.instagrabber.utils.getCsrfTokenFromCookie
import awais.instagrabber.utils.SingleRunner
import awais.instagrabber.utils.Utils
import awais.instagrabber.utils.extensions.TAG
import awais.instagrabber.utils.extensions.isReallyPrivate
import awais.instagrabber.viewmodels.ProfileFragmentViewModel.ProfileAction.*
@ -32,18 +34,20 @@ import java.time.LocalDateTime
class ProfileFragmentViewModel(
private val state: SavedStateHandle,
private val csrfToken: String?,
private val deviceUuid: String?,
private val userRepository: UserRepository,
private val friendshipRepository: FriendshipRepository,
private val storiesRepository: StoriesRepository,
private val mediaRepository: MediaRepository,
private val graphQLRepository: GraphQLRepository,
private val favoriteRepository: FavoriteRepository,
private val directMessagesRepository: DirectMessagesRepository,
private val favoriteRepository: FavoriteRepository?,
private val messageManager: DirectMessagesManager?,
ioDispatcher: CoroutineDispatcher,
) : ViewModel() {
private val cookie: String = Utils.settingsHelper.getString(Constants.COOKIE)
private val csrfToken: String? = getCsrfTokenFromCookie(cookie)
private val deviceUuid: String = Utils.settingsHelper.getString(Constants.DEVICE_UUID)
private val userRepository: UserRepository by lazy { UserRepository.getInstance() }
private val friendshipRepository: FriendshipRepository by lazy { FriendshipRepository.getInstance() }
private val storiesRepository: StoriesRepository by lazy { StoriesRepository.getInstance() }
private val mediaRepository: MediaRepository by lazy { MediaRepository.getInstance() }
private val graphQLRepository: GraphQLRepository by lazy { GraphQLRepository.getInstance() }
private val directMessagesRepository: DirectMessagesRepository by lazy { DirectMessagesRepository.getInstance() }
private val _currentUser = MutableLiveData<Resource<User?>>(Resource.loading(null))
private val _isFavorite = MutableLiveData(false)
private val profileAction = MutableLiveData(INIT)
@ -157,9 +161,9 @@ class ProfileFragmentViewModel(
}
}
private val storyFetchControlledRunner = ControlledRunner<List<StoryMedia>?>()
val userStories: LiveData<Resource<List<StoryMedia>?>> = currentUserProfileActionLiveData.switchMap { currentUserAndProfilePair ->
liveData<Resource<List<StoryMedia>?>>(context = viewModelScope.coroutineContext + ioDispatcher) {
private val storyFetchControlledRunner = ControlledRunner<Story?>()
val userStories: LiveData<Resource<Story?>> = currentUserProfileActionLiveData.switchMap { currentUserAndProfilePair ->
liveData<Resource<Story?>>(context = viewModelScope.coroutineContext + ioDispatcher) {
val (currentUserResource, profileResource, action) = currentUserAndProfilePair
if (action != INIT && action != REFRESH) {
return@liveData
@ -235,7 +239,7 @@ class ProfileFragmentViewModel(
return graphQLRepository.fetchUser(stateUsername)
}
private suspend fun fetchUserStory(fetchedUser: User): List<StoryMedia> = storiesRepository.getStories(
private suspend fun fetchUserStory(fetchedUser: User): Story? = storiesRepository.getStories(
StoryViewerOptions.forUser(fetchedUser.pk, fetchedUser.fullName)
)
@ -243,7 +247,7 @@ class ProfileFragmentViewModel(
private suspend fun checkAndUpdateFavorite(fetchedUser: User) {
try {
val favorite = favoriteRepository.getFavorite(fetchedUser.username, FavoriteType.USER)
val favorite = favoriteRepository!!.getFavorite(fetchedUser.username, FavoriteType.USER)
if (favorite == null) {
_isFavorite.postValue(false)
return
@ -291,7 +295,7 @@ class ProfileFragmentViewModel(
viewModelScope.launch(Dispatchers.IO) {
toggleFavoriteControlledRunner.afterPrevious {
try {
val favorite = favoriteRepository.getFavorite(username, FavoriteType.USER)
val favorite = favoriteRepository!!.getFavorite(username, FavoriteType.USER)
if (favorite == null) {
// insert
favoriteRepository.insertOrUpdateFavorite(
@ -326,7 +330,7 @@ class ProfileFragmentViewModel(
val currentUserId = currentUser.value?.data?.pk ?: return@afterPrevious
val targetUserId = profile.value?.data?.pk ?: return@afterPrevious
val csrfToken = csrfToken ?: return@afterPrevious
val deviceUuid = deviceUuid ?: return@afterPrevious
val deviceUuid = deviceUuid
if (following) {
if (!confirmed) {
_eventLiveData.postValue(Event(ShowConfirmUnfollowDialog))
@ -365,7 +369,7 @@ class ProfileFragmentViewModel(
val currentUserId = currentUser.value?.data?.pk ?: return@afterPrevious
val targetUserId = profile.value?.data?.pk ?: return@afterPrevious
val csrfToken = csrfToken ?: return@afterPrevious
val deviceUuid = deviceUuid ?: return@afterPrevious
val deviceUuid = deviceUuid
val username = profile.value?.data?.username ?: return@afterPrevious
val thread = directMessagesRepository.createThread(
csrfToken,
@ -399,7 +403,7 @@ class ProfileFragmentViewModel(
val profile = profile.value?.data ?: return@afterPrevious
friendshipRepository.toggleRestrict(
csrfToken ?: return@afterPrevious,
deviceUuid ?: return@afterPrevious,
deviceUuid,
profile.pk,
!(profile.friendshipStatus?.isRestricted ?: false),
)
@ -421,7 +425,7 @@ class ProfileFragmentViewModel(
friendshipRepository.changeBlock(
csrfToken ?: return@afterPrevious,
currentUser.value?.data?.pk ?: return@afterPrevious,
deviceUuid ?: return@afterPrevious,
deviceUuid,
profile.friendshipStatus?.blocking ?: return@afterPrevious,
profile.pk
)
@ -443,7 +447,7 @@ class ProfileFragmentViewModel(
friendshipRepository.changeMute(
csrfToken ?: return@afterPrevious,
currentUser.value?.data?.pk ?: return@afterPrevious,
deviceUuid ?: return@afterPrevious,
deviceUuid,
profile.friendshipStatus?.isMutingReel ?: return@afterPrevious,
profile.pk,
true
@ -466,7 +470,7 @@ class ProfileFragmentViewModel(
friendshipRepository.changeMute(
csrfToken ?: return@afterPrevious,
currentUser.value?.data?.pk ?: return@afterPrevious,
deviceUuid ?: return@afterPrevious,
deviceUuid,
profile.friendshipStatus?.muting ?: return@afterPrevious,
profile.pk,
false
@ -488,7 +492,7 @@ class ProfileFragmentViewModel(
friendshipRepository.removeFollower(
csrfToken ?: return@afterPrevious,
currentUser.value?.data?.pk ?: return@afterPrevious,
deviceUuid ?: return@afterPrevious,
deviceUuid,
profile.value?.data?.pk ?: return@afterPrevious
)
profileAction.postValue(REFRESH_FRIENDSHIP)
@ -597,15 +601,7 @@ class ProfileFragmentViewModel(
@Suppress("UNCHECKED_CAST")
class ProfileFragmentViewModelFactory(
private val csrfToken: String?,
private val deviceUuid: String?,
private val userRepository: UserRepository,
private val friendshipRepository: FriendshipRepository,
private val storiesRepository: StoriesRepository,
private val mediaRepository: MediaRepository,
private val graphQLRepository: GraphQLRepository,
private val favoriteRepository: FavoriteRepository,
private val directMessagesRepository: DirectMessagesRepository,
private val favoriteRepository: FavoriteRepository?,
private val messageManager: DirectMessagesManager?,
owner: SavedStateRegistryOwner,
defaultArgs: Bundle? = null,
@ -617,15 +613,7 @@ class ProfileFragmentViewModelFactory(
): T {
return ProfileFragmentViewModel(
handle,
csrfToken,
deviceUuid,
userRepository,
friendshipRepository,
storiesRepository,
mediaRepository,
graphQLRepository,
favoriteRepository,
directMessagesRepository,
messageManager,
Dispatchers.IO,
) as T

View file

@ -1,19 +0,0 @@
package awais.instagrabber.viewmodels;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import java.util.List;
import awais.instagrabber.repositories.responses.stories.StoryMedia;
public class StoriesViewModel extends ViewModel {
private MutableLiveData<List<StoryMedia>> list;
public MutableLiveData<List<StoryMedia>> getList() {
if (list == null) {
list = new MutableLiveData<>();
}
return list;
}
}

View file

@ -0,0 +1,475 @@
package awais.instagrabber.viewmodels
import android.net.Uri
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import awais.instagrabber.R
import awais.instagrabber.managers.DirectMessagesManager
import awais.instagrabber.models.enums.FavoriteType
import awais.instagrabber.models.enums.MediaItemType
import awais.instagrabber.models.enums.StoryPaginationType
import awais.instagrabber.models.Resource
import awais.instagrabber.models.Resource.Companion.error
import awais.instagrabber.models.Resource.Companion.loading
import awais.instagrabber.models.Resource.Companion.success
import awais.instagrabber.models.enums.BroadcastItemType
import awais.instagrabber.repositories.requests.StoryViewerOptions
import awais.instagrabber.repositories.responses.directmessages.RankedRecipient
import awais.instagrabber.repositories.responses.stories.*
import awais.instagrabber.repositories.responses.Media
import awais.instagrabber.utils.*
import awais.instagrabber.webservices.MediaRepository
import awais.instagrabber.webservices.StoriesRepository
import com.google.common.collect.ImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class StoryFragmentViewModel : ViewModel() {
// large data
private val currentStory = MutableLiveData<Story?>()
private val currentMedia = MutableLiveData<StoryMedia>()
// small data
private val storyTitle = MutableLiveData<String>()
private val date = MutableLiveData<String>()
private val type = MutableLiveData<MediaItemType>()
private val poll = MutableLiveData<PollSticker>()
private val quiz = MutableLiveData<QuizSticker>()
private val question = MutableLiveData<QuestionSticker>()
private val slider = MutableLiveData<SliderSticker>()
private val swipeUp = MutableLiveData<String>()
private val linkedPost = MutableLiveData<String>()
private val appAttribution = MutableLiveData<StoryAppAttribution>()
private val reelMentions = MutableLiveData<List<Triple<String, String?, FavoriteType>>>()
// process
private val currentIndex = MutableLiveData<Int>()
private val pagination = MutableLiveData(StoryPaginationType.DO_NOTHING)
private val options = MutableLiveData<Triple<List<Pair<Int, Int>>, String?, String?>>()
private val seen = MutableLiveData<Triple<String, Long, Long>>()
// utils
private var messageManager: DirectMessagesManager? = null
private val cookie = Utils.settingsHelper.getString(Constants.COOKIE)
private val deviceId = Utils.settingsHelper.getString(Constants.DEVICE_UUID)
private val csrfToken = getCsrfTokenFromCookie(cookie)
private val userId = getUserIdFromCookie(cookie)
private val storiesRepository: StoriesRepository by lazy { StoriesRepository.getInstance() }
private val mediaRepository: MediaRepository by lazy { MediaRepository.getInstance() }
// for highlights ONLY
private val highlights = MutableLiveData<List<Story>?>()
/* set functions */
fun setStory(story: Story) {
if (story.items == null || story.items.size == 0) {
pagination.postValue(StoryPaginationType.ERROR)
return
}
currentStory.postValue(story)
storyTitle.postValue(story.title ?: story.user?.username)
if (story.broadcast != null) {
date.postValue(story.dateTime)
type.postValue(MediaItemType.MEDIA_TYPE_LIVE)
pagination.postValue(StoryPaginationType.DO_NOTHING)
}
}
fun setMedia(index: Int) {
if (currentStory.value?.items == null) return
if (index < 0 || index >= currentStory.value!!.items!!.size) {
pagination.postValue(if (index < 0) StoryPaginationType.BACKWARD else StoryPaginationType.FORWARD)
return
}
currentIndex.postValue(index)
val story: Story? = currentStory.value
val media = story!!.items!!.get(index)
currentMedia.postValue(media)
date.postValue(media.date)
type.postValue(media.type)
initStickers(media)
}
fun setSingleMedia(media: StoryMedia) {
currentStory.postValue(null)
currentIndex.postValue(0)
currentMedia.postValue(media)
date.postValue(media.date)
type.postValue(media.type)
}
private fun initStickers(media: StoryMedia) {
val builder = ImmutableList.builder<Pair<Int, Int>>()
var linkedText: String? = null
var appText: String? = null
if (setMentions(media)) builder.add(Pair(R.id.mentions, R.string.story_mentions))
if (setQuiz(media)) builder.add(Pair(R.id.quiz, R.string.story_quiz))
if (setQuestion(media)) builder.add(Pair(R.id.question, R.string.story_question))
if (setPoll(media)) builder.add(Pair(R.id.poll, R.string.story_poll))
if (setSlider(media)) builder.add(Pair(R.id.slider, R.string.story_slider))
if (setLinkedPost(media)) builder.add(Pair(R.id.viewStoryPost, R.string.view_post))
if (setStoryCta(media)) {
linkedText = media.linkText
builder.add(Pair(R.id.swipeUp, 0))
}
if (setStoryAppAttribution(media)) {
appText = media.storyAppAttribution!!.appActionText
builder.add(Pair(R.id.spotify, 0))
}
options.postValue(Triple(builder.build(), linkedText, appText))
}
private fun setMentions(media: StoryMedia): Boolean {
val mentions: MutableList<Triple<String, String?, FavoriteType>> = mutableListOf()
if (media.reelMentions != null)
mentions.addAll(media.reelMentions.map{
Triple("@" + it.user?.username, it.user?.username, FavoriteType.USER)
})
if (media.storyHashtags != null)
mentions.addAll(media.storyHashtags.map{
Triple("#" + it.hashtag?.name, it.hashtag?.name, FavoriteType.HASHTAG)
})
if (media.storyLocations != null)
mentions.addAll(media.storyLocations.map{
Triple(it.location?.name ?: "", it.location?.pk?.toString(10), FavoriteType.LOCATION)
})
reelMentions.postValue(mentions.filterNot { it.second.isNullOrEmpty() } .distinct())
return !mentions.isEmpty()
}
private fun setPoll(media: StoryMedia): Boolean {
poll.postValue(media.storyPolls?.get(0)?.pollSticker ?: return false)
return true
}
private fun setQuiz(media: StoryMedia): Boolean {
quiz.postValue(media.storyQuizs?.get(0)?.quizSticker ?: return false)
return true
}
private fun setQuestion(media: StoryMedia): Boolean {
val questionSticker = media.storyQuestions?.get(0)?.questionSticker ?: return false
if (questionSticker.questionType.equals("music")) return false
question.postValue(questionSticker)
return true
}
private fun setSlider(media: StoryMedia): Boolean {
slider.postValue(media.storySliders?.get(0)?.sliderSticker ?: return false)
return true
}
private fun setLinkedPost(media: StoryMedia): Boolean {
linkedPost.postValue(media.storyFeedMedia?.get(0)?.mediaId ?: return false)
return true
}
private fun setStoryCta(media: StoryMedia): Boolean {
val webUri = media.storyCta?.get(0)?.links?.get(0)?.webUri ?: return false
val parsedUri = Uri.parse(webUri)
val cleanUri = if (parsedUri.host.equals("l.instagram.com")) parsedUri.getQueryParameter("u")
else null
swipeUp.postValue(if (cleanUri != null && Uri.parse(cleanUri).scheme?.startsWith("http") == true) cleanUri
else webUri)
return true
}
private fun setStoryAppAttribution(media: StoryMedia): Boolean {
appAttribution.postValue(media.storyAppAttribution ?: return false)
return true
}
/* get functions */
fun getHighlights(): LiveData<List<Story>?> {
return highlights
}
fun getCurrentStory(): LiveData<Story?> {
return currentStory
}
fun getCurrentIndex(): LiveData<Int> {
return currentIndex
}
fun getCurrentMedia(): LiveData<StoryMedia> {
return currentMedia
}
fun getPagination(): LiveData<StoryPaginationType> {
return pagination
}
fun getDate(): LiveData<String> {
return date
}
fun getTitle(): LiveData<String> {
return storyTitle
}
fun getType(): LiveData<MediaItemType> {
return type
}
fun getMedia(): LiveData<StoryMedia> {
return currentMedia
}
fun getMention(index: Int): Triple<String, String?, FavoriteType>? {
return reelMentions.value?.get(index)
}
fun getMentionTexts(): Array<String> {
return reelMentions.value!!.map { it.first } .toTypedArray()
}
fun getPoll(): LiveData<PollSticker> {
return poll
}
fun getQuestion(): LiveData<QuestionSticker> {
return question
}
fun getQuiz(): LiveData<QuizSticker> {
return quiz
}
fun getSlider(): LiveData<SliderSticker> {
return slider
}
fun getLinkedPost(): LiveData<Resource<Media?>> {
val data = MutableLiveData<Resource<Media?>>()
data.postValue(loading(null))
val postId = linkedPost.value
if (postId == null) data.postValue(error("No post ID supplied", null))
else viewModelScope.launch(Dispatchers.IO) {
try {
val media = mediaRepository.fetch(postId.toLong())
data.postValue(success(media))
}
catch (e: Exception) {
data.postValue(error(e.message, null))
}
}
return data
}
fun getSwipeUp(): String? {
return swipeUp.value
}
fun getAppAttribution(): String? {
return appAttribution.value?.url
}
fun getOptions(): LiveData<Triple<List<Pair<Int, Int>>, String?, String?>> {
return options
}
/* action functions */
fun answerPoll(w: Int): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
data.postValue(loading(null))
viewModelScope.launch(Dispatchers.IO) {
try {
val oldPoll: PollSticker = poll.value!!
val response = storiesRepository.respondToPoll(
csrfToken!!,
userId,
deviceId,
currentMedia.value!!.pk,
oldPoll.pollId,
w
)
if (!"ok".equals(response.status))
throw Exception("Instagram returned status \"" + response.status + "\"")
val tally = oldPoll.tallies.get(w)
val newTally = tally.copy(count = tally.count + 1)
val newTallies = oldPoll.tallies.toMutableList()
newTallies.set(w, newTally)
poll.postValue(oldPoll.copy(viewerVote = w, tallies = newTallies.toList()))
data.postValue(success(null))
}
catch (e: Exception) {
data.postValue(error(e.message, null))
}
}
return data
}
fun answerQuiz(w: Int): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
data.postValue(loading(null))
viewModelScope.launch(Dispatchers.IO) {
try {
val oldQuiz = quiz.value!!
val response = storiesRepository.respondToQuiz(
csrfToken!!,
userId,
deviceId,
currentMedia.value!!.pk,
oldQuiz.quizId,
w
)
if (!"ok".equals(response.status))
throw Exception("Instagram returned status \"" + response.status + "\"")
val tally = oldQuiz.tallies.get(w)
val newTally = tally.copy(count = tally.count + 1)
val newTallies = oldQuiz.tallies.toMutableList()
newTallies.set(w, newTally)
quiz.postValue(oldQuiz.copy(viewerAnswer = w, tallies = newTallies.toList()))
data.postValue(success(null))
}
catch (e: Exception) {
data.postValue(error(e.message, null))
}
}
return data
}
fun answerQuestion(a: String): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
data.postValue(loading(null))
viewModelScope.launch(Dispatchers.IO) {
try {
val response = storiesRepository.respondToQuestion(
csrfToken!!,
userId,
deviceId,
currentMedia.value!!.pk,
question.value!!.questionId,
a
)
if (!"ok".equals(response.status))
throw Exception("Instagram returned status \"" + response.status + "\"")
data.postValue(success(null))
}
catch (e: Exception) {
data.postValue(error(e.message, null))
}
}
return data
}
fun answerSlider(a: Double): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
data.postValue(loading(null))
viewModelScope.launch(Dispatchers.IO) {
try {
val oldSlider = slider.value!!
val response = storiesRepository.respondToSlider(
csrfToken!!,
userId,
deviceId,
currentMedia.value!!.pk,
oldSlider.sliderId,
a
)
if (!"ok".equals(response.status))
throw Exception("Instagram returned status \"" + response.status + "\"")
val newVoteCount = (oldSlider.sliderVoteCount ?: 0) + 1
val newAverage = if (oldSlider.sliderVoteAverage == null) a
else (oldSlider.sliderVoteAverage * oldSlider.sliderVoteCount!! + a) / newVoteCount
slider.postValue(oldSlider.copy(viewerCanVote = false,
sliderVoteCount = newVoteCount,
viewerVote = a,
sliderVoteAverage = newAverage))
data.postValue(success(null))
}
catch (e: Exception) {
data.postValue(error(e.message, null))
}
}
return data
}
fun reply(a: String): LiveData<Resource<Any?>>? {
if (messageManager == null) {
messageManager = DirectMessagesManager
}
return messageManager?.replyToStory(
currentStory.value?.user?.pk,
currentStory.value?.id,
currentMedia.value?.id,
a,
viewModelScope
)
}
fun shareDm(result: RankedRecipient) {
if (messageManager == null) {
messageManager = DirectMessagesManager
}
val mediaId = currentMedia.value?.id ?: return
val reelId = currentStory.value?.id ?: return
messageManager?.sendMedia(result, mediaId, reelId, BroadcastItemType.STORY, viewModelScope)
}
fun shareDm(recipients: Set<RankedRecipient>) {
if (messageManager == null) {
messageManager = DirectMessagesManager
}
val mediaId = currentMedia.value?.id ?: return
val reelId = currentStory.value?.id ?: return
messageManager?.sendMedia(recipients, mediaId, reelId, BroadcastItemType.STORY, viewModelScope)
}
fun paginate(backward: Boolean) {
var index = currentIndex.value!!
index = if (backward) index - 1 else index + 1
if (index < 0 || index >= currentStory.value!!.items!!.size) skip(backward)
setMedia(index)
}
fun skip(backward: Boolean) {
pagination.postValue(if (backward) StoryPaginationType.BACKWARD else StoryPaginationType.FORWARD)
}
fun fetchStory(fetchOptions: StoryViewerOptions?): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
data.postValue(loading(null))
viewModelScope.launch(Dispatchers.IO) {
try {
val story = storiesRepository.getStories(fetchOptions!!)
setStory(story!!)
data.postValue(success(null))
} catch (e: Exception) {
data.postValue(error(e.message, null))
}
}
return data
}
fun fetchHighlights(id: Long) {
viewModelScope.launch(Dispatchers.IO) {
try {
val result = storiesRepository.fetchHighlights(id)
highlights.postValue(result)
} catch (e: Exception) {
}
}
}
fun fetchSingleMedia(mediaId: Long): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
data.postValue(loading(null))
viewModelScope.launch(Dispatchers.IO) {
try {
val storyMedia = storiesRepository.fetch(mediaId)
setSingleMedia(storyMedia!!)
data.postValue(success(null))
} catch (e: Exception) {
data.postValue(error(e.message, null))
}
}
return data
}
}

View file

@ -187,6 +187,17 @@ open class DirectMessagesRepository(private val service: DirectMessagesService)
): DirectThreadBroadcastResponse =
broadcast(csrfToken, userId, deviceUuid, ProfileBroadcastOptions(clientContext, threadIdsOrUserIds, profileId))
suspend fun broadcastStory(
csrfToken: String,
userId: Long,
deviceUuid: String,
clientContext: String,
threadIdsOrUserIds: ThreadIdsOrUserIds,
mediaId: String,
reelId: String,
): DirectThreadBroadcastResponse =
broadcast(csrfToken, userId, deviceUuid, StoryBroadcastOptions(clientContext, threadIdsOrUserIds, mediaId, reelId))
private suspend fun broadcast(
csrfToken: String,
userId: Long,

View file

@ -7,7 +7,6 @@ import awais.instagrabber.repositories.responses.stories.ArchiveResponse
import awais.instagrabber.repositories.responses.stories.Story
import awais.instagrabber.repositories.responses.stories.StoryMedia
import awais.instagrabber.repositories.responses.stories.StoryStickerResponse
import awais.instagrabber.utils.TextUtils.isEmpty
import awais.instagrabber.utils.Utils
import awais.instagrabber.webservices.RetrofitFactory.retrofit
import java.util.UUID
@ -60,35 +59,34 @@ open class StoriesRepository(private val service: StoriesService) {
"is_in_archive_home" to "true",
"include_cover" to "1",
)
if (!isEmpty(maxId)) {
if (!maxId.isNullOrEmpty()) {
form["max_id"] = maxId // NOT TESTED
}
return service.fetchArchive(form)
}
open suspend fun getStories(options: StoryViewerOptions): List<StoryMedia> {
open suspend fun getStories(options: StoryViewerOptions): Story? {
return when (options.type) {
StoryViewerOptions.Type.HIGHLIGHT,
StoryViewerOptions.Type.STORY_ARCHIVE
-> {
val response = service.getReelsMedia(options.name)
val story: Story? = response.reels?.get(options.name)
story?.items ?: emptyList()
response.reels?.get(options.name)
}
StoryViewerOptions.Type.USER -> {
val response = service.getUserStories(options.id.toString())
response.reel?.items ?: emptyList()
val response = service.getUserStories(options.id)
response.reel
}
// should not reach beyond this point
StoryViewerOptions.Type.LOCATION -> {
val response = service.getStories("locations", options.id.toString())
response.story?.items ?: emptyList()
response.story
}
StoryViewerOptions.Type.HASHTAG -> {
val response = service.getStories("tags", options.name)
response.story?.items ?: emptyList()
response.story
}
else -> emptyList()
else -> null
}
}
@ -96,7 +94,7 @@ open class StoriesRepository(private val service: StoriesService) {
csrfToken: String,
userId: Long,
deviceUuid: String,
storyId: String,
storyId: Long,
stickerId: Long,
action: String,
arg1: String,
@ -119,7 +117,7 @@ open class StoriesRepository(private val service: StoriesService) {
csrfToken: String,
userId: Long,
deviceUuid: String,
storyId: String,
storyId: Long,
stickerId: Long,
answer: String,
): StoryStickerResponse = respondToSticker(csrfToken, userId, deviceUuid, storyId, stickerId, "story_question_response", "response", answer)
@ -128,7 +126,7 @@ open class StoriesRepository(private val service: StoriesService) {
csrfToken: String,
userId: Long,
deviceUuid: String,
storyId: String,
storyId: Long,
stickerId: Long,
answer: Int,
): StoryStickerResponse {
@ -139,7 +137,7 @@ open class StoriesRepository(private val service: StoriesService) {
csrfToken: String,
userId: Long,
deviceUuid: String,
storyId: String,
storyId: Long,
stickerId: Long,
answer: Int,
): StoryStickerResponse = respondToSticker(csrfToken, userId, deviceUuid, storyId, stickerId, "story_poll_vote", "vote", answer.toString())
@ -148,7 +146,7 @@ open class StoriesRepository(private val service: StoriesService) {
csrfToken: String,
userId: Long,
deviceUuid: String,
storyId: String,
storyId: Long,
stickerId: Long,
answer: Double,
): StoryStickerResponse = respondToSticker(csrfToken, userId, deviceUuid, storyId, stickerId, "story_slider_vote", "vote", answer.toString())