Use cached preview as thumbnail in ViewImageFragment, fix #1267 (#1344)

* Use cached preview as thumbnail in ViewImageFragment, fix #1267

* Use cached preview as thumbnail in ViewImageFragment, fix #1267
This commit is contained in:
Ivan Kupalov 2019-08-04 20:22:57 +02:00 committed by Konrad Pozniak
parent 70b3ce7487
commit 9805a985b2
7 changed files with 164 additions and 111 deletions

View file

@ -29,21 +29,22 @@ import android.graphics.Color
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Environment import android.os.Environment
import androidx.core.content.FileProvider import android.transition.Transition
import androidx.viewpager.widget.ViewPager
import android.util.Log import android.util.Log
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import android.widget.Toast import android.widget.Toast
import androidx.core.content.FileProvider
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.viewpager.widget.PagerAdapter
import androidx.viewpager.widget.ViewPager
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.request.FutureTarget import com.bumptech.glide.request.FutureTarget
import com.keylesspalace.tusky.BuildConfig.APPLICATION_ID import com.keylesspalace.tusky.BuildConfig.APPLICATION_ID
import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.fragment.ViewImageFragment import com.keylesspalace.tusky.fragment.ViewImageFragment
import com.keylesspalace.tusky.pager.AvatarImagePagerAdapter import com.keylesspalace.tusky.pager.AvatarImagePagerAdapter
import com.keylesspalace.tusky.pager.ImagePagerAdapter import com.keylesspalace.tusky.pager.ImagePagerAdapter
import com.keylesspalace.tusky.util.getTemporaryMediaFilename import com.keylesspalace.tusky.util.getTemporaryMediaFilename
@ -53,14 +54,14 @@ import com.uber.autodispose.autoDisposable
import io.reactivex.Single import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import kotlinx.android.synthetic.main.activity_view_media.* import kotlinx.android.synthetic.main.activity_view_media.*
import java.io.File import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.IOException import java.io.IOException
import java.util.ArrayList import java.util.*
typealias ToolbarVisibilityListener = (isVisible: Boolean) -> Unit
class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener { class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener {
companion object { companion object {
@ -84,25 +85,18 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
} }
} }
var isToolbarVisible = true
private set
private var attachments: ArrayList<AttachmentViewData>? = null private var attachments: ArrayList<AttachmentViewData>? = null
private val toolbarVisibilityListeners = mutableListOf<ToolbarVisibilityListener>()
private var toolbarVisible = true
private val toolbarVisibilityListeners = ArrayList<ToolbarVisibilityListener>()
interface ToolbarVisibilityListener {
fun onToolbarVisiblityChanged(isVisible: Boolean)
}
fun addToolbarVisibilityListener(listener: ToolbarVisibilityListener): Function0<Boolean> { fun addToolbarVisibilityListener(listener: ToolbarVisibilityListener): Function0<Boolean> {
this.toolbarVisibilityListeners.add(listener) this.toolbarVisibilityListeners.add(listener)
listener.onToolbarVisiblityChanged(toolbarVisible) listener(isToolbarVisible)
return { toolbarVisibilityListeners.remove(listener) } return { toolbarVisibilityListeners.remove(listener) }
} }
fun isToolbarVisible(): Boolean {
return toolbarVisible
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_view_media) setContentView(R.layout.activity_view_media)
@ -113,7 +107,10 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
attachments = intent.getParcelableArrayListExtra(EXTRA_ATTACHMENTS) attachments = intent.getParcelableArrayListExtra(EXTRA_ATTACHMENTS)
val initialPosition = intent.getIntExtra(EXTRA_ATTACHMENT_INDEX, 0) val initialPosition = intent.getIntExtra(EXTRA_ATTACHMENT_INDEX, 0)
val adapter = if (attachments != null) { // Adapter is actually of existential type PageAdapter & SharedElementsTransitionListener
// but it cannot be expressed and if I don't specify type explicitly compilation fails
// (probably a bug in compiler)
val adapter: PagerAdapter = if (attachments != null) {
val realAttachs = attachments!!.map(AttachmentViewData::attachment) val realAttachs = attachments!!.map(AttachmentViewData::attachment)
// Setup the view pager. // Setup the view pager.
ImagePagerAdapter(supportFragmentManager, realAttachs, initialPosition) ImagePagerAdapter(supportFragmentManager, realAttachs, initialPosition)
@ -154,6 +151,12 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LOW_PROFILE window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LOW_PROFILE
window.statusBarColor = Color.BLACK window.statusBarColor = Color.BLACK
window.sharedElementEnterTransition.addListener(object : NoopTransitionListener {
override fun onTransitionEnd(transition: Transition) {
(adapter as SharedElementTransitionListener).onTransitionEnd()
window.sharedElementEnterTransition.removeListener(this)
}
})
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
@ -178,20 +181,12 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
} }
override fun onPhotoTap() { override fun onPhotoTap() {
toolbarVisible = !toolbarVisible isToolbarVisible = !isToolbarVisible
for (listener in toolbarVisibilityListeners) { for (listener in toolbarVisibilityListeners) {
listener.onToolbarVisiblityChanged(toolbarVisible) listener(isToolbarVisible)
}
val visibility = if (toolbarVisible) {
View.VISIBLE
} else {
View.INVISIBLE
}
val alpha = if (toolbarVisible) {
1.0f
} else {
0.0f
} }
val visibility = if (isToolbarVisible) View.VISIBLE else View.INVISIBLE
val alpha = if (isToolbarVisible) 1.0f else 0.0f
toolbar.animate().alpha(alpha) toolbar.animate().alpha(alpha)
.setListener(object : AnimatorListenerAdapter() { .setListener(object : AnimatorListenerAdapter() {
@ -327,3 +322,24 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
shareFile(file, mimeType) shareFile(file, mimeType)
} }
} }
interface SharedElementTransitionListener {
fun onTransitionEnd()
}
interface NoopTransitionListener : Transition.TransitionListener {
override fun onTransitionEnd(transition: Transition) {
}
override fun onTransitionResume(transition: Transition) {
}
override fun onTransitionPause(transition: Transition) {
}
override fun onTransitionCancel(transition: Transition) {
}
override fun onTransitionStart(transition: Transition) {
}
}

View file

@ -30,14 +30,15 @@ import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target import com.bumptech.glide.request.target.Target
import com.github.chrisbanes.photoview.PhotoViewAttacher import com.github.chrisbanes.photoview.PhotoViewAttacher
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.util.visible
import io.reactivex.subjects.BehaviorSubject
import kotlinx.android.synthetic.main.activity_view_media.* import kotlinx.android.synthetic.main.activity_view_media.*
import kotlinx.android.synthetic.main.fragment_view_image.* import kotlinx.android.synthetic.main.fragment_view_image.*
import kotlin.math.abs
class ViewImageFragment : ViewMediaFragment() { class ViewImageFragment : ViewMediaFragment() {
interface PhotoActionsListener { interface PhotoActionsListener {
@ -49,35 +50,35 @@ class ViewImageFragment : ViewMediaFragment() {
private lateinit var attacher: PhotoViewAttacher private lateinit var attacher: PhotoViewAttacher
private lateinit var photoActionsListener: PhotoActionsListener private lateinit var photoActionsListener: PhotoActionsListener
private lateinit var toolbar: View private lateinit var toolbar: View
override lateinit var descriptionView: TextView private var transition = BehaviorSubject.create<Unit>()
override lateinit var descriptionView: TextView
override fun onAttach(context: Context) { override fun onAttach(context: Context) {
super.onAttach(context) super.onAttach(context)
photoActionsListener = context as PhotoActionsListener photoActionsListener = context as PhotoActionsListener
} }
override fun setupMediaView(url: String) { override fun setupMediaView(url: String, previewUrl: String?) {
descriptionView = mediaDescription descriptionView = mediaDescription
photoView.transitionName = url photoView.transitionName = url
attacher = PhotoViewAttacher(photoView) attacher = PhotoViewAttacher(photoView).apply {
// Clicking outside the photo closes the viewer.
setOnOutsidePhotoTapListener { photoActionsListener.onDismiss() }
setOnClickListener { onMediaTap() }
// Clicking outside the photo closes the viewer. /* A vertical swipe motion also closes the viewer. This is especially useful when the photo
attacher.setOnOutsidePhotoTapListener { photoActionsListener.onDismiss() } * mostly fills the screen so clicking outside is difficult. */
setOnSingleFlingListener { _, _, velocityX, velocityY ->
attacher.setOnClickListener { onMediaTap() } var result = false
if (abs(velocityY) > abs(velocityX)) {
/* A vertical swipe motion also closes the viewer. This is especially useful when the photo photoActionsListener.onDismiss()
* mostly fills the screen so clicking outside is difficult. */ result = true
attacher.setOnSingleFlingListener { _, _, velocityX, velocityY -> }
var result = false result
if (Math.abs(velocityY) > Math.abs(velocityX)) {
photoActionsListener.onDismiss()
result = true
} }
result
} }
loadImageFromNetwork(url, photoView) loadImageFromNetwork(url, previewUrl, photoView)
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
@ -103,7 +104,7 @@ class ViewImageFragment : ViewMediaFragment() {
} }
} }
finalizeViewSetup(url, description) finalizeViewSetup(url, attachment?.previewUrl, description)
} }
private fun onMediaTap() { private fun onMediaTap() {
@ -131,49 +132,71 @@ class ViewImageFragment : ViewMediaFragment() {
super.onDestroyView() super.onDestroyView()
} }
private fun loadImageFromNetwork(url: String, photoView: ImageView) = private fun loadImageFromNetwork(url: String, previewUrl: String?, photoView: ImageView) {
//Request image from the any cache val glide = Glide.with(this)
Glide.with(this) // Request image from the any cache
.load(url) glide
.dontAnimate() .load(url)
.onlyRetrieveFromCache(true) .dontAnimate()
.error( .onlyRetrieveFromCache(true)
//Request image from the network on fail load image from cache .let {
Glide.with(this) if (previewUrl != null)
.load(url) it.thumbnail(glide
.centerInside() .load(previewUrl)
.addListener(ImageRequestListener(false)) .dontAnimate()
) .onlyRetrieveFromCache(true)
.centerInside() .centerInside()
.addListener(ImageRequestListener(true)) .addListener(ImageRequestListener(true, isThumnailRequest = true)))
.into(photoView) else it
}
//Request image from the network on fail load image from cache
.error(glide.load(url)
.centerInside()
.addListener(ImageRequestListener(false, isThumnailRequest = false))
)
.centerInside()
.addListener(ImageRequestListener(true, isThumnailRequest = false))
.into(photoView)
}
/** /**
* @param isCacheRequest - is this listener for request image from cache or from the network * @param isCacheRequest - is this listener for request image from cache or from the network
*/ */
private inner class ImageRequestListener(private val isCacheRequest: Boolean) : RequestListener<Drawable> { private inner class ImageRequestListener(
override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>?, isFirstResource: Boolean): Boolean { private val isCacheRequest: Boolean,
if (isCacheRequest) //Complete the transition on failed image from cache private val isThumnailRequest: Boolean) : RequestListener<Drawable> {
completeTransition()
else override fun onLoadFailed(e: GlideException?, model: Any, target: Target<Drawable>,
progressBar?.hide() //Hide progress bar only on fail request from internet isFirstResource: Boolean): Boolean {
// If cache for full image failed, complete transition
if (isCacheRequest && !isThumnailRequest) photoActionsListener.onBringUp()
// Hide progress bar only on fail request from internet
if (!isCacheRequest) progressBar?.hide()
return false return false
} }
override fun onResourceReady(resource: Drawable?, model: Any?, target: Target<Drawable>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean { override fun onResourceReady(resource: Drawable, model: Any, target: Target<Drawable>,
progressBar?.hide() //Always hide the progress bar on success dataSource: DataSource, isFirstResource: Boolean): Boolean {
resource?.let { progressBar?.hide() // Always hide the progress bar on success
target?.onResourceReady(resource, null) if (isThumnailRequest) {
if (isCacheRequest) completeTransition() //Complete transition on cache request only, because transition already completed on Network request photoView.post {
target.onResourceReady(resource, null)
photoActionsListener.onBringUp()
}
} else {
transition
.take(1)
.subscribe {
target.onResourceReady(resource, null)
photoActionsListener.onBringUp()
}
}
return true return true
}
return false
} }
} }
private fun completeTransition() { override fun onTransitionEnd() {
attacher.update() this.transition.onNext(Unit)
photoActionsListener.onBringUp()
} }
} }

View file

@ -18,25 +18,29 @@ package com.keylesspalace.tusky.fragment
import android.os.Bundle import android.os.Bundle
import android.text.TextUtils import android.text.TextUtils
import android.widget.TextView import android.widget.TextView
import com.keylesspalace.tusky.SharedElementTransitionListener
import com.keylesspalace.tusky.ViewMediaActivity import com.keylesspalace.tusky.ViewMediaActivity
import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.util.visible
abstract class ViewMediaFragment : BaseFragment() { abstract class ViewMediaFragment : BaseFragment(), SharedElementTransitionListener {
private var toolbarVisibiltyDisposable: Function0<Boolean>? = null private var toolbarVisibiltyDisposable: Function0<Boolean>? = null
abstract fun setupMediaView(url: String) abstract fun setupMediaView(url: String, previewUrl: String?)
abstract fun onToolbarVisibilityChange(visible: Boolean) abstract fun onToolbarVisibilityChange(visible: Boolean)
abstract val descriptionView : TextView abstract val descriptionView: TextView
protected var showingDescription = false protected var showingDescription = false
protected var isDescriptionVisible = false protected var isDescriptionVisible = false
companion object { companion object {
@JvmStatic protected val ARG_START_POSTPONED_TRANSITION = "startPostponedTransition" @JvmStatic
@JvmStatic protected val ARG_ATTACHMENT = "attach" protected val ARG_START_POSTPONED_TRANSITION = "startPostponedTransition"
@JvmStatic protected val ARG_AVATAR_URL = "avatarUrl" @JvmStatic
protected val ARG_ATTACHMENT = "attach"
@JvmStatic
protected val ARG_AVATAR_URL = "avatarUrl"
@JvmStatic @JvmStatic
fun newInstance(attachment: Attachment, shouldStartPostponedTransition: Boolean): ViewMediaFragment { fun newInstance(attachment: Attachment, shouldStartPostponedTransition: Boolean): ViewMediaFragment {
@ -66,21 +70,20 @@ abstract class ViewMediaFragment : BaseFragment() {
} }
} }
protected fun finalizeViewSetup(url: String, description: String?) { protected fun finalizeViewSetup(url: String, previewUrl: String?, description: String?) {
val mediaActivity = activity as ViewMediaActivity val mediaActivity = activity as ViewMediaActivity
setupMediaView(url) setupMediaView(url, previewUrl)
descriptionView.text = description ?: "" descriptionView.text = description ?: ""
showingDescription = !TextUtils.isEmpty(description) showingDescription = !TextUtils.isEmpty(description)
isDescriptionVisible = showingDescription isDescriptionVisible = showingDescription
descriptionView.visible(showingDescription && mediaActivity.isToolbarVisible()) descriptionView.visible(showingDescription && mediaActivity.isToolbarVisible)
toolbarVisibiltyDisposable = (activity as ViewMediaActivity).addToolbarVisibilityListener(object: ViewMediaActivity.ToolbarVisibilityListener { toolbarVisibiltyDisposable = (activity as ViewMediaActivity)
override fun onToolbarVisiblityChanged(isVisible: Boolean) { .addToolbarVisibilityListener { isVisible ->
onToolbarVisibilityChange(isVisible) onToolbarVisibilityChange(isVisible)
} }
})
} }
override fun onDestroyView() { override fun onDestroyView() {

View file

@ -26,7 +26,6 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.MediaController import android.widget.MediaController
import android.widget.TextView import android.widget.TextView
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.ViewMediaActivity import com.keylesspalace.tusky.ViewMediaActivity
import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Attachment
@ -56,7 +55,7 @@ class ViewVideoFragment : ViewMediaFragment() {
} }
if (isVisibleToUser) { if (isVisibleToUser) {
if (mediaActivity.isToolbarVisible()) { if (mediaActivity.isToolbarVisible) {
handler.postDelayed(hideToolbar, TOOLBAR_HIDE_DELAY_MS) handler.postDelayed(hideToolbar, TOOLBAR_HIDE_DELAY_MS)
} }
videoPlayer.start() videoPlayer.start()
@ -68,7 +67,7 @@ class ViewVideoFragment : ViewMediaFragment() {
} }
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
override fun setupMediaView(url: String) { override fun setupMediaView(url: String, previewUrl: String?) {
descriptionView = mediaDescription descriptionView = mediaDescription
val videoView = videoPlayer val videoView = videoPlayer
videoView.transitionName = url videoView.transitionName = url
@ -114,7 +113,7 @@ class ViewVideoFragment : ViewMediaFragment() {
throw IllegalArgumentException("attachment has to be set") throw IllegalArgumentException("attachment has to be set")
} }
url = attachment.url url = attachment.url
finalizeViewSetup(url, attachment.description) finalizeViewSetup(url, attachment.previewUrl, attachment.description)
} }
override fun onToolbarVisibilityChange(visible: Boolean) { override fun onToolbarVisibilityChange(visible: Boolean) {
@ -139,4 +138,7 @@ class ViewVideoFragment : ViewMediaFragment() {
handler.removeCallbacks(hideToolbar) handler.removeCallbacks(hideToolbar)
} }
} }
override fun onTransitionEnd() {
}
} }

View file

@ -3,12 +3,10 @@ package com.keylesspalace.tusky.pager
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter import androidx.fragment.app.FragmentPagerAdapter
import com.keylesspalace.tusky.SharedElementTransitionListener
import com.keylesspalace.tusky.fragment.ViewMediaFragment import com.keylesspalace.tusky.fragment.ViewMediaFragment
import java.lang.IllegalStateException
class AvatarImagePagerAdapter(fragmentManager: FragmentManager, private val avatarUrl: String) : FragmentPagerAdapter(fragmentManager) {
class AvatarImagePagerAdapter(fragmentManager: FragmentManager, private val avatarUrl: String) : FragmentPagerAdapter(fragmentManager), SharedElementTransitionListener {
override fun getItem(position: Int): Fragment { override fun getItem(position: Int): Fragment {
return if (position == 0) { return if (position == 0) {
ViewMediaFragment.newAvatarInstance(avatarUrl) ViewMediaFragment.newAvatarInstance(avatarUrl)
@ -19,4 +17,6 @@ class AvatarImagePagerAdapter(fragmentManager: FragmentManager, private val avat
override fun getCount() = 1 override fun getCount() = 1
override fun onTransitionEnd() {
}
} }

View file

@ -1,20 +1,26 @@
package com.keylesspalace.tusky.pager package com.keylesspalace.tusky.pager
import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentStatePagerAdapter import androidx.fragment.app.FragmentStatePagerAdapter
import com.keylesspalace.tusky.SharedElementTransitionListener
import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.fragment.ViewMediaFragment import com.keylesspalace.tusky.fragment.ViewMediaFragment
import java.lang.IllegalStateException import java.util.*
import java.util.Locale
class ImagePagerAdapter( class ImagePagerAdapter(
fragmentManager: FragmentManager, fragmentManager: FragmentManager,
private val attachments: List<Attachment>, private val attachments: List<Attachment>,
private val initialPosition: Int private val initialPosition: Int
) : FragmentStatePagerAdapter(fragmentManager) { ) : FragmentStatePagerAdapter(fragmentManager), SharedElementTransitionListener {
private var primaryItem: ViewMediaFragment? = null
override fun setPrimaryItem(container: ViewGroup, position: Int, item: Any) {
super.setPrimaryItem(container, position, item)
this.primaryItem = item as ViewMediaFragment
}
override fun getItem(position: Int): Fragment { override fun getItem(position: Int): Fragment {
return if (position >= 0 && position < attachments.size) { return if (position >= 0 && position < attachments.size) {
@ -31,4 +37,8 @@ class ImagePagerAdapter(
override fun getPageTitle(position: Int): CharSequence { override fun getPageTitle(position: Int): CharSequence {
return String.format(Locale.getDefault(), "%d/%d", position + 1, attachments.size) return String.format(Locale.getDefault(), "%d/%d", position + 1, attachments.size)
} }
override fun onTransitionEnd() {
primaryItem?.onTransitionEnd()
}
} }

View file

@ -5,7 +5,6 @@ import com.google.gson.Gson
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import com.keylesspalace.tusky.db.* import com.keylesspalace.tusky.db.*
import com.keylesspalace.tusky.entity.* import com.keylesspalace.tusky.entity.*
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.repository.TimelineRequestMode.DISK import com.keylesspalace.tusky.repository.TimelineRequestMode.DISK
import com.keylesspalace.tusky.repository.TimelineRequestMode.NETWORK import com.keylesspalace.tusky.repository.TimelineRequestMode.NETWORK