850 lines
37 KiB
Kotlin
850 lines
37 KiB
Kotlin
/*
|
|
* Twidere - Twitter client for Android
|
|
*
|
|
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
package org.mariotaku.twidere.fragment.timeline
|
|
|
|
import android.app.Activity
|
|
import android.arch.lifecycle.LiveData
|
|
import android.arch.lifecycle.MediatorLiveData
|
|
import android.arch.lifecycle.MutableLiveData
|
|
import android.arch.paging.LivePagedListBuilder
|
|
import android.arch.paging.PagedList
|
|
import android.content.ContentValues
|
|
import android.content.Context
|
|
import android.content.Intent
|
|
import android.net.Uri
|
|
import android.os.Bundle
|
|
import android.support.annotation.CallSuper
|
|
import android.support.v4.app.Fragment
|
|
import android.support.v7.widget.FixedLinearLayoutManager
|
|
import android.support.v7.widget.LinearLayoutManager
|
|
import android.support.v7.widget.RecyclerView
|
|
import android.support.v7.widget.RecyclerView.LayoutManager
|
|
import android.support.v7.widget.StaggeredGridLayoutManager
|
|
import android.view.ContextMenu
|
|
import android.view.MenuInflater
|
|
import android.view.MenuItem
|
|
import android.view.View
|
|
import android.widget.Toast
|
|
import com.bumptech.glide.RequestManager
|
|
import com.squareup.otto.Subscribe
|
|
import kotlinx.android.synthetic.main.fragment_content_recyclerview.*
|
|
import org.mariotaku.kpreferences.get
|
|
import org.mariotaku.ktextension.*
|
|
import org.mariotaku.sqliteqb.library.Expression
|
|
import org.mariotaku.twidere.BuildConfig
|
|
import org.mariotaku.twidere.R
|
|
import org.mariotaku.twidere.activity.AccountSelectorActivity
|
|
import org.mariotaku.twidere.activity.ComposeActivity
|
|
import org.mariotaku.twidere.adapter.ParcelableStatusesAdapter
|
|
import org.mariotaku.twidere.adapter.decorator.ExtendedDividerItemDecoration
|
|
import org.mariotaku.twidere.adapter.iface.IContentAdapter
|
|
import org.mariotaku.twidere.annotation.FilterScope
|
|
import org.mariotaku.twidere.annotation.LoadMorePosition
|
|
import org.mariotaku.twidere.annotation.ReadPositionTag
|
|
import org.mariotaku.twidere.annotation.TimelineStyle
|
|
import org.mariotaku.twidere.constant.*
|
|
import org.mariotaku.twidere.constant.IntentConstants.*
|
|
import org.mariotaku.twidere.constant.KeyboardShortcutConstants.*
|
|
import org.mariotaku.twidere.data.CursorObjectDataSourceFactory
|
|
import org.mariotaku.twidere.data.ExceptionLiveData
|
|
import org.mariotaku.twidere.data.StatusesDataSourceFactory
|
|
import org.mariotaku.twidere.data.fetcher.StatusesFetcher
|
|
import org.mariotaku.twidere.data.processor.ParcelableStatusDisplayProcessor
|
|
import org.mariotaku.twidere.extension.*
|
|
import org.mariotaku.twidere.extension.adapter.removeStatuses
|
|
import org.mariotaku.twidere.extension.data.observe
|
|
import org.mariotaku.twidere.extension.model.quoted
|
|
import org.mariotaku.twidere.extension.view.PositionWithOffset
|
|
import org.mariotaku.twidere.extension.view.firstVisibleItemPosition
|
|
import org.mariotaku.twidere.extension.view.firstVisibleItemPositionWithOffset
|
|
import org.mariotaku.twidere.extension.view.lastVisibleItemPosition
|
|
import org.mariotaku.twidere.fragment.AbsContentRecyclerViewFragment
|
|
import org.mariotaku.twidere.fragment.BaseFragment
|
|
import org.mariotaku.twidere.fragment.iface.IFloatingActionButtonFragment
|
|
import org.mariotaku.twidere.fragment.status.FavoriteConfirmDialogFragment
|
|
import org.mariotaku.twidere.fragment.status.RetweetQuoteDialogFragment
|
|
import org.mariotaku.twidere.graphic.like.LikeAnimationDrawable
|
|
import org.mariotaku.twidere.model.*
|
|
import org.mariotaku.twidere.model.event.FavoriteTaskEvent
|
|
import org.mariotaku.twidere.model.event.GetStatusesTaskEvent
|
|
import org.mariotaku.twidere.model.event.StatusDestroyedEvent
|
|
import org.mariotaku.twidere.model.event.StatusRetweetedEvent
|
|
import org.mariotaku.twidere.model.pagination.SinceMaxPagination
|
|
import org.mariotaku.twidere.model.refresh.BaseContentRefreshParam
|
|
import org.mariotaku.twidere.model.refresh.ContentRefreshParam
|
|
import org.mariotaku.twidere.model.tab.extra.TimelineTabExtras
|
|
import org.mariotaku.twidere.model.timeline.TimelineFilter
|
|
import org.mariotaku.twidere.promise.StatusPromises
|
|
import org.mariotaku.twidere.provider.TwidereDataStore.Statuses
|
|
import org.mariotaku.twidere.singleton.BusSingleton
|
|
import org.mariotaku.twidere.singleton.PreferencesSingleton
|
|
import org.mariotaku.twidere.task.CreateFavoriteTask
|
|
import org.mariotaku.twidere.task.statuses.GetStatusesTask
|
|
import org.mariotaku.twidere.util.*
|
|
import org.mariotaku.twidere.view.ExtendedRecyclerView
|
|
import org.mariotaku.twidere.view.holder.GapViewHolder
|
|
import org.mariotaku.twidere.view.holder.TimelineFilterHeaderViewHolder
|
|
import org.mariotaku.twidere.view.holder.iface.IStatusViewHolder
|
|
import org.mariotaku.twidere.view.holder.status.StatusViewHolder
|
|
import java.util.concurrent.atomic.AtomicReference
|
|
|
|
abstract class AbsTimelineFragment : AbsContentRecyclerViewFragment<ParcelableStatusesAdapter, LayoutManager>(),
|
|
IFloatingActionButtonFragment {
|
|
|
|
override val reachingStart: Boolean
|
|
get() = recyclerView.layoutManager.firstVisibleItemPosition <= 0
|
|
|
|
override val reachingEnd: Boolean
|
|
get() = recyclerView.layoutManager.lastVisibleItemPosition >= recyclerView.layoutManager.itemCount - 1
|
|
|
|
@TimelineStyle
|
|
protected open val timelineStyle: Int
|
|
get() {
|
|
val extras = arguments?.getParcelable<TimelineTabExtras>(EXTRA_EXTRAS)
|
|
if (extras != null) return extras.timelineStyle
|
|
return arguments!!.getInt(EXTRA_TIMELINE_STYLE, TimelineStyle.PLAIN)
|
|
}
|
|
|
|
protected open val isStandalone: Boolean
|
|
get() = tabId <= 0
|
|
|
|
protected open val filtersEnabled: Boolean
|
|
get() = true
|
|
|
|
protected open val timelineFilter: TimelineFilter? = null
|
|
|
|
protected open val readPositionTag: String? = null
|
|
|
|
protected open val readPositionTagWithArguments: String?
|
|
get() = readPositionTag
|
|
|
|
@FilterScope
|
|
protected abstract val filterScope: Int
|
|
|
|
/**
|
|
* Content Uri for in-database data source
|
|
*/
|
|
protected abstract val contentUri: Uri
|
|
|
|
protected var statuses: LiveData<SingleResponse<PagedList<ParcelableStatus>?>>? = null
|
|
private set(value) {
|
|
field?.removeObservers(this)
|
|
field = value
|
|
}
|
|
|
|
protected val accountKeys: Array<UserKey>
|
|
get() = Utils.getAccountKeys(context!!, arguments) ?: if (isStandalone) {
|
|
emptyArray()
|
|
} else {
|
|
DataStoreUtils.getActivatedAccountKeys(context!!)
|
|
}
|
|
|
|
private val busEventHandler = BusEventHandler()
|
|
private val scrollHandler = ScrollHandler()
|
|
private val timelineBoundaryCallback = StatusesBoundaryCallback()
|
|
private val positionBackup: AtomicReference<PositionWithOffset> = AtomicReference()
|
|
|
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
|
super.onActivityCreated(savedInstanceState)
|
|
registerForContextMenu(recyclerView)
|
|
adapter.statusClickListener = StatusClickHandler()
|
|
adapter.pagedListListener = this::onPagedListChanged
|
|
adapter.loadMoreSupportedPosition = if (isStandalone) {
|
|
LoadMorePosition.NONE
|
|
} else {
|
|
LoadMorePosition.END
|
|
}
|
|
setupLiveData()
|
|
showProgress()
|
|
}
|
|
|
|
override fun onStart() {
|
|
super.onStart()
|
|
recyclerView.addOnScrollListener(scrollHandler)
|
|
BusSingleton.register(busEventHandler)
|
|
}
|
|
|
|
override fun onStop() {
|
|
BusSingleton.unregister(busEventHandler)
|
|
recyclerView.removeOnScrollListener(scrollHandler)
|
|
if (userVisibleHint) {
|
|
saveReadPosition(layoutManager.firstVisibleItemPosition)
|
|
}
|
|
super.onStop()
|
|
}
|
|
|
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
handleActionActivityResult(this, requestCode, resultCode, data)
|
|
}
|
|
|
|
override fun onCreateLayoutManager(context: Context): LayoutManager = when (timelineStyle) {
|
|
TimelineStyle.STAGGERED -> StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
|
|
else -> FixedLinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
|
}
|
|
|
|
override fun onCreateAdapter(context: Context, requestManager: RequestManager): ParcelableStatusesAdapter {
|
|
return ParcelableStatusesAdapter(context, requestManager, timelineStyle)
|
|
}
|
|
|
|
override fun onCreateItemDecoration(context: Context, recyclerView: RecyclerView,
|
|
layoutManager: LayoutManager): RecyclerView.ItemDecoration? {
|
|
return when (timelineStyle) {
|
|
TimelineStyle.PLAIN -> createStatusesListItemDecoration(context, recyclerView, adapter)
|
|
TimelineStyle.GALLERY -> createStatusesListGalleryDecoration(context, recyclerView)
|
|
else -> super.onCreateItemDecoration(context, recyclerView, layoutManager)
|
|
}
|
|
}
|
|
|
|
override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) {
|
|
if (!userVisibleHint || menuInfo == null) return
|
|
val context = this.context!!
|
|
val inflater = MenuInflater(context)
|
|
val contextMenuInfo = menuInfo as ExtendedRecyclerView.ContextMenuInfo
|
|
val status = adapter.getStatus(contextMenuInfo.position)
|
|
inflater.inflate(R.menu.action_status, menu)
|
|
MenuUtils.setupForStatus(context, menu, PreferencesSingleton.get(this.context!!), UserColorNameManager.get(this.context!!), status)
|
|
}
|
|
|
|
override fun onContextItemSelected(item: MenuItem): Boolean {
|
|
if (!userVisibleHint) return false
|
|
val context = this.context!!
|
|
val contextMenuInfo = item.menuInfo as ExtendedRecyclerView.ContextMenuInfo
|
|
val status = adapter.getStatus(contextMenuInfo.position)
|
|
when (item.itemId) {
|
|
R.id.share -> {
|
|
val shareIntent = Utils.createStatusShareIntent(context, status)
|
|
val chooser = Intent.createChooser(shareIntent, getString(R.string.share_status))
|
|
startActivity(chooser)
|
|
return true
|
|
}
|
|
R.id.make_gap -> {
|
|
if (isStandalone) return true
|
|
val resolver = context.contentResolver
|
|
val values = ContentValues()
|
|
values.put(Statuses.IS_GAP, 1)
|
|
val where = Expression.equals(Statuses._ID, status._id).sql
|
|
resolver.update(contentUri, values, where, null)
|
|
return true
|
|
}
|
|
else -> return MenuUtils.handleStatusClick(context, this, fragmentManager!!,
|
|
PreferencesSingleton.get(this.context!!), UserColorNameManager.get(this.context!!), status, item)
|
|
}
|
|
}
|
|
|
|
|
|
override fun triggerRefresh(): Boolean {
|
|
if (isStandalone) {
|
|
return false
|
|
}
|
|
return getStatuses(object : ContentRefreshParam {
|
|
override val accountKeys: Array<UserKey> by lazy {
|
|
this@AbsTimelineFragment.accountKeys
|
|
}
|
|
|
|
override val pagination by lazy {
|
|
val context = context!!
|
|
val keys = accountKeys.toNulls()
|
|
val sinceIds = DataStoreUtils.getNewestStatusIds(context, contentUri, keys)
|
|
val sinceSortIds = DataStoreUtils.getNewestStatusSortIds(context, contentUri, keys)
|
|
return@lazy Array(keys.size) { idx ->
|
|
SinceMaxPagination.sinceId(sinceIds[idx], sinceSortIds[idx])
|
|
}
|
|
}
|
|
|
|
override val tabId: Long
|
|
get() = this@AbsTimelineFragment.tabId
|
|
|
|
override val shouldAbort: Boolean
|
|
get() = context == null
|
|
|
|
override val hasMaxIds: Boolean
|
|
get() = false
|
|
})
|
|
}
|
|
|
|
override fun onLoadMoreContents(position: Int) {
|
|
// No-op
|
|
}
|
|
|
|
override fun scrollToPositionWithOffset(position: Int, offset: Int) {
|
|
val layoutManager = this.layoutManager
|
|
when (layoutManager) {
|
|
is StaggeredGridLayoutManager -> {
|
|
layoutManager.scrollToPositionWithOffset(position, offset)
|
|
}
|
|
is LinearLayoutManager -> {
|
|
layoutManager.scrollToPositionWithOffset(position, offset)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Scroll to start of the timeline. This also updates read position
|
|
*/
|
|
override fun scrollToStart(): Boolean {
|
|
val result = super.scrollToStart()
|
|
if (result) saveReadPosition(0)
|
|
return result
|
|
}
|
|
|
|
override fun onActionClick(tag: String): Boolean {
|
|
when (tag) {
|
|
"home" -> {
|
|
val intent = Intent(INTENT_ACTION_COMPOSE).setPackage(BuildConfig.APPLICATION_ID)
|
|
val accountKeys = Utils.getAccountKeys(context!!, arguments)
|
|
if (accountKeys != null) {
|
|
intent.putExtra(EXTRA_ACCOUNT_KEYS, accountKeys)
|
|
}
|
|
startActivity(intent)
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
fun reloadAll() {
|
|
adapter.statuses = null
|
|
setupLiveData()
|
|
showProgress()
|
|
}
|
|
|
|
protected open fun onDataLoaded(data: PagedList<ParcelableStatus>?) {
|
|
val firstVisiblePosition = layoutManager.firstVisibleItemPositionWithOffset
|
|
adapter.showAccountsColor = accountKeys.size > 1
|
|
adapter.statuses = data
|
|
adapter.timelineFilter = timelineFilter
|
|
when {
|
|
data == null || data.isEmpty() -> {
|
|
showEmpty(R.drawable.ic_info_refresh, getString(R.string.swipe_down_to_refresh))
|
|
}
|
|
else -> {
|
|
showContent()
|
|
}
|
|
}
|
|
positionBackup.set(firstVisiblePosition)
|
|
}
|
|
|
|
protected open fun onPagedListChanged(data: PagedList<ParcelableStatus>?) {
|
|
val context = context ?: return
|
|
val firstVisiblePosition = positionBackup.getAndSet(null) ?: return
|
|
if (firstVisiblePosition.position == 0 && !PreferencesSingleton.get(context)[readFromBottomKey]) {
|
|
scrollToPositionWithOffset(0, 0)
|
|
} else {
|
|
scrollToPositionWithOffset(firstVisiblePosition.position, firstVisiblePosition.offset)
|
|
}
|
|
}
|
|
|
|
protected abstract fun getStatuses(param: ContentRefreshParam): Boolean
|
|
|
|
protected abstract fun onCreateStatusesFetcher(): StatusesFetcher
|
|
|
|
protected open fun getExtraSelection(): Pair<Expression, Array<String>?>? {
|
|
return null
|
|
}
|
|
|
|
protected open fun getMaxLoadItemLimit(forAccount: UserKey): Int {
|
|
return 200
|
|
}
|
|
|
|
protected open fun onFavoriteTaskEvent(event: FavoriteTaskEvent) {
|
|
val status = event.status
|
|
if (event.isSucceeded && status != null) {
|
|
replaceStatusStates(status)
|
|
}
|
|
}
|
|
|
|
protected open fun onStatusRetweetedEvent(event: StatusRetweetedEvent) {
|
|
replaceStatusStates(event.status)
|
|
}
|
|
|
|
protected open fun onStatusDestroyedEvent(event: StatusDestroyedEvent) {
|
|
if (!isStandalone) return
|
|
adapter.removeStatuses { it != null && it.id == event.status.id }
|
|
}
|
|
|
|
protected open fun onTimelineFilterClick() {
|
|
|
|
}
|
|
|
|
@CallSuper
|
|
protected open fun saveReadPosition(position: Int) {
|
|
if (host == null) return
|
|
if (position == RecyclerView.NO_POSITION || adapter.getStatusCount() <= 0) return
|
|
val status = adapter.getStatus(position.coerceIn(rangeOfSize(adapter.statusStartIndex,
|
|
adapter.getStatusCount())))
|
|
val readPosition = if (isStandalone) {
|
|
status.sort_id
|
|
} else {
|
|
status.position_key
|
|
}
|
|
val positionTag = readPositionTag ?: ReadPositionTag.CUSTOM_TIMELINE
|
|
readPositionTagWithArguments?.let {
|
|
accountKeys.forEach { accountKey ->
|
|
val tag = Utils.getReadPositionTagWithAccount(it, accountKey)
|
|
readStateManager.setPosition(tag, readPosition)
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun setupLiveData() {
|
|
statuses = if (isStandalone) onCreateStandaloneLiveData() else onCreateDatabaseLiveData()
|
|
statuses?.observe(this, success = this::onDataLoaded, fail = {
|
|
showError(R.drawable.ic_info_error_generic, it.getErrorMessage(context!!))
|
|
})
|
|
}
|
|
|
|
private fun onCreateStandaloneLiveData(): LiveData<SingleResponse<PagedList<ParcelableStatus>?>> {
|
|
val merger = MediatorLiveData<SingleResponse<PagedList<ParcelableStatus>?>>()
|
|
val context = context!!
|
|
val accountKey = accountKeys.singleOrNull()!!
|
|
|
|
val errorLiveData = MutableLiveData<SingleResponse<PagedList<ParcelableStatus>?>>()
|
|
val factory = StatusesDataSourceFactory(context.applicationContext,
|
|
onCreateStatusesFetcher(), accountKey, timelineFilter) {
|
|
errorLiveData.postValue(SingleResponse(it))
|
|
}
|
|
val maxLoadLimit = getMaxLoadItemLimit(accountKey)
|
|
val loadLimit = PreferencesSingleton.get(this.context!!)[loadItemLimitKey]
|
|
val apiLiveData = ExceptionLiveData.wrap(LivePagedListBuilder(factory, PagedList.Config.Builder()
|
|
.setPageSize(loadLimit.coerceAtMost(maxLoadLimit))
|
|
.setInitialLoadSizeHint(loadLimit.coerceAtMost(maxLoadLimit))
|
|
.build()).build())
|
|
merger.addSource(errorLiveData) {
|
|
merger.removeSource(apiLiveData)
|
|
merger.removeSource(errorLiveData)
|
|
merger.value = it
|
|
}
|
|
merger.addSource(apiLiveData) {
|
|
merger.value = it
|
|
}
|
|
return merger
|
|
}
|
|
|
|
|
|
private fun onCreateDatabaseLiveData(): LiveData<SingleResponse<PagedList<ParcelableStatus>?>> {
|
|
val context = context!!
|
|
val table = DataStoreUtils.getTableNameByUri(contentUri)!!
|
|
val accountKeys = accountKeys
|
|
val expressions = mutableListOf(Expression.inArgs(Statuses.ACCOUNT_KEY, accountKeys.size))
|
|
val expressionArgs = mutableListOf(*accountKeys.mapToArray(UserKey::toString))
|
|
if (filtersEnabled) {
|
|
expressions.add(DataStoreUtils.buildStatusFilterWhereClause(PreferencesSingleton.get(this.context!!), table,
|
|
null, filterScope))
|
|
}
|
|
val extraSelection = getExtraSelection()
|
|
if (extraSelection != null) {
|
|
extraSelection.first.addTo(expressions)
|
|
extraSelection.second?.addAllTo(expressionArgs)
|
|
}
|
|
val processor = ParcelableStatusDisplayProcessor(context)
|
|
val factory = CursorObjectDataSourceFactory(context.contentResolver, contentUri,
|
|
statusColumnsLite, Expression.and(*expressions.toTypedArray()).sql,
|
|
expressionArgs.toTypedArray(), Statuses.DEFAULT_SORT_ORDER,
|
|
ParcelableStatus::class.java, processor)
|
|
// dataController = factory.obtainDataController()
|
|
return ExceptionLiveData.wrap(LivePagedListBuilder(factory, databasePagedListConfig)
|
|
.setBoundaryCallback(timelineBoundaryCallback).build())
|
|
}
|
|
|
|
private fun getFullStatus(position: Int): ParcelableStatus {
|
|
if (isStandalone) {
|
|
return adapter.getStatus(position)
|
|
}
|
|
val context = context!!
|
|
val rowId = adapter.getRowId(position)
|
|
return context.contentResolver.queryOne(contentUri, Statuses.COLUMNS, rowId, ParcelableStatus::class.java)!!
|
|
}
|
|
|
|
private fun replaceStatusStates(status: ParcelableStatus) {
|
|
val statuses = adapter.statuses?.snapshot() ?: return
|
|
val lm = layoutManager
|
|
val range = lm.firstVisibleItemPosition..lm.lastVisibleItemPosition
|
|
statuses.forEachIndexed { index, item ->
|
|
if (item?.id != status.id) return@forEachIndexed
|
|
item.favorite_count = status.favorite_count
|
|
item.retweet_count = status.retweet_count
|
|
item.reply_count = status.reply_count
|
|
|
|
item.my_retweet_id = status.my_retweet_id
|
|
item.is_favorite = status.is_favorite
|
|
if (index in range) {
|
|
adapter.notifyItemRangeChanged(index, 1)
|
|
}
|
|
}
|
|
}
|
|
|
|
class DefaultOnLikedListener(
|
|
private val context: Context,
|
|
private val status: ParcelableStatus,
|
|
private val accountKey: UserKey? = null
|
|
) : LikeAnimationDrawable.OnLikedListener {
|
|
|
|
override fun onLiked(): Boolean {
|
|
if (status.is_favorite) return false
|
|
CreateFavoriteTask(context, accountKey ?: status.account_key, status).promise()
|
|
return true
|
|
}
|
|
}
|
|
|
|
internal inner class BusEventHandler {
|
|
|
|
@Subscribe
|
|
fun notifyGetStatusesTaskChanged(event: GetStatusesTaskEvent) {
|
|
val context = context ?: return
|
|
if (event.uri != contentUri) return
|
|
refreshing = event.running
|
|
if (!event.running) {
|
|
setLoadMoreIndicatorPosition(LoadMorePosition.NONE)
|
|
refreshEnabled = true
|
|
// TODO: showContentOrError()
|
|
|
|
val exception = event.exception
|
|
if (exception is GetStatusesTask.GetTimelineException && userVisibleHint) {
|
|
Toast.makeText(context, exception.getToastMessage(context), Toast.LENGTH_SHORT).show()
|
|
}
|
|
}
|
|
}
|
|
|
|
@Subscribe
|
|
fun notifyStatusDestroyed(event: StatusDestroyedEvent) {
|
|
onStatusDestroyedEvent(event)
|
|
}
|
|
|
|
@Subscribe
|
|
fun notifyFavoriteTask(event: FavoriteTaskEvent) {
|
|
onFavoriteTaskEvent(event)
|
|
}
|
|
|
|
@Subscribe
|
|
fun notifyRetweetTask(event: StatusRetweetedEvent) {
|
|
onStatusRetweetedEvent(event)
|
|
}
|
|
|
|
}
|
|
|
|
private inner class StatusClickHandler : IStatusViewHolder.StatusClickListener {
|
|
override fun onStatusClick(holder: IStatusViewHolder, position: Int) {
|
|
val context = context ?: return
|
|
val status = getFullStatus(position)
|
|
IntentUtils.openStatus(context, status, null)
|
|
}
|
|
|
|
override fun onStatusLongClick(holder: IStatusViewHolder, position: Int): Boolean {
|
|
return false
|
|
}
|
|
|
|
override fun onItemActionClick(holder: RecyclerView.ViewHolder, id: Int, position: Int) {
|
|
val status = getFullStatus(position)
|
|
handleActionClick(this@AbsTimelineFragment, id, status,
|
|
holder as IStatusViewHolder)
|
|
}
|
|
|
|
override fun onItemActionLongClick(holder: RecyclerView.ViewHolder, id: Int, position: Int): Boolean {
|
|
val status = getFullStatus(position)
|
|
return handleActionLongClick(this@AbsTimelineFragment, status,
|
|
adapter.getItemId(position), id)
|
|
}
|
|
|
|
override fun onFilterClick(holder: TimelineFilterHeaderViewHolder) {
|
|
onTimelineFilterClick()
|
|
}
|
|
|
|
override fun onMediaClick(holder: IStatusViewHolder, view: View, current: ParcelableMedia, statusPosition: Int) {
|
|
val context = context ?: return
|
|
val status = getFullStatus(statusPosition)
|
|
IntentUtils.openMedia(context, status, current, PreferencesSingleton.get(this@AbsTimelineFragment.context!!)[newDocumentApiKey],
|
|
PreferencesSingleton.get(this@AbsTimelineFragment.context!!)[displaySensitiveContentsKey])
|
|
}
|
|
|
|
override fun onQuotedMediaClick(holder: IStatusViewHolder, view: View, current: ParcelableMedia, statusPosition: Int) {
|
|
val context = context ?: return
|
|
val status = getFullStatus(statusPosition)
|
|
val quotedMedia = status.quoted?.media ?: return
|
|
IntentUtils.openMedia(context, status.account_key, status.is_possibly_sensitive, status,
|
|
current, quotedMedia, PreferencesSingleton.get(this@AbsTimelineFragment.context!!)[newDocumentApiKey], PreferencesSingleton.get(this@AbsTimelineFragment.context!!)[displaySensitiveContentsKey])
|
|
}
|
|
|
|
override fun onQuotedStatusClick(holder: IStatusViewHolder, position: Int) {
|
|
val context = context ?: return
|
|
val status = getFullStatus(position)
|
|
val quotedId = status.quoted?.id ?: return
|
|
IntentUtils.openStatus(context, status.account_key, quotedId)
|
|
}
|
|
|
|
override fun onUserProfileClick(holder: IStatusViewHolder, position: Int) {
|
|
val status = getFullStatus(position)
|
|
val intent = IntentUtils.userProfile(status.account_key, status.user_key,
|
|
status.user_screen_name, status.extras?.user_statusnet_profile_url)
|
|
IntentUtils.applyNewDocument(intent, PreferencesSingleton.get(context!!)[newDocumentApiKey])
|
|
startActivity(intent)
|
|
}
|
|
|
|
override fun onItemMenuClick(holder: RecyclerView.ViewHolder, menuView: View, position: Int) {
|
|
if (activity == null) return
|
|
val view = layoutManager.findViewByPosition(position) ?: return
|
|
recyclerView.showContextMenuForChild(view, menuView)
|
|
}
|
|
|
|
override fun onGapClick(holder: GapViewHolder, position: Int) {
|
|
val status = getFullStatus(position)
|
|
DebugLog.v(msg = "Load activity gap $status")
|
|
adapter.addGapLoadingId(ObjectId(status.account_key, status.id))
|
|
val accountKeys = arrayOf(status.account_key)
|
|
val pagination = arrayOf(SinceMaxPagination.maxId(status.id, status.sort_id))
|
|
getStatuses(BaseContentRefreshParam(accountKeys, pagination).also {
|
|
it.tabId = tabId
|
|
})
|
|
}
|
|
}
|
|
|
|
private inner class ScrollHandler : RecyclerView.OnScrollListener() {
|
|
|
|
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
|
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
|
|
val layoutManager = layoutManager
|
|
saveReadPosition(layoutManager.firstVisibleItemPosition)
|
|
}
|
|
}
|
|
}
|
|
|
|
private inner class StatusesBoundaryCallback : PagedList.BoundaryCallback<ParcelableStatus>() {
|
|
override fun onItemAtEndLoaded(itemAtEnd: ParcelableStatus) {
|
|
val started = getStatuses(object : ContentRefreshParam {
|
|
override val accountKeys by lazy {
|
|
this@AbsTimelineFragment.accountKeys
|
|
}
|
|
override val pagination by lazy {
|
|
val context = context!!
|
|
val keys = accountKeys.toNulls()
|
|
val maxIds = DataStoreUtils.getOldestStatusIds(context, contentUri, keys)
|
|
val maxSortIds = DataStoreUtils.getOldestStatusSortIds(context, contentUri, keys)
|
|
return@lazy Array(keys.size) { idx ->
|
|
SinceMaxPagination.maxId(maxIds[idx], maxSortIds[idx])
|
|
}
|
|
}
|
|
|
|
override val tabId: Long
|
|
get() = this@AbsTimelineFragment.tabId
|
|
|
|
})
|
|
if (started) {
|
|
adapter.loadMoreIndicatorPosition = LoadMorePosition.END
|
|
}
|
|
}
|
|
}
|
|
|
|
companion object {
|
|
|
|
const val REQUEST_OPEN_SELECT_ACCOUNT = 101
|
|
const val REQUEST_RETWEET_SELECT_ACCOUNT = 102
|
|
const val REQUEST_FAVORITE_SELECT_ACCOUNT = 103
|
|
|
|
|
|
val RANGE_REQUEST_CODES = 100 until 110
|
|
|
|
val statusColumnsLite = Statuses.COLUMNS - arrayOf(Statuses.MENTIONS_JSON,
|
|
Statuses.CARD, Statuses.FILTER_FLAGS, Statuses.FILTER_USERS, Statuses.FILTER_LINKS,
|
|
Statuses.FILTER_SOURCES, Statuses.FILTER_NAMES, Statuses.FILTER_TEXTS,
|
|
Statuses.FILTER_DESCRIPTIONS)
|
|
|
|
fun handleActionClick(fragment: BaseFragment, id: Int, status: ParcelableStatus,
|
|
holder: IStatusViewHolder) {
|
|
when (id) {
|
|
R.id.reply -> {
|
|
val intent = Intent(INTENT_ACTION_REPLY)
|
|
intent.`package` = fragment.context!!.packageName
|
|
intent.putExtra(EXTRA_STATUS, status)
|
|
fragment.startActivity(intent)
|
|
}
|
|
R.id.retweet -> {
|
|
fragment.executeAfterFragmentResumed { f ->
|
|
RetweetQuoteDialogFragment.show(f.childFragmentManager,
|
|
status.account_key, status.id, status)
|
|
}
|
|
}
|
|
R.id.favorite -> {
|
|
when {
|
|
PreferencesSingleton.get(fragment.context!!)[favoriteConfirmationKey] -> fragment.executeAfterFragmentResumed {
|
|
FavoriteConfirmDialogFragment.show(it.childFragmentManager,
|
|
status.account_key, status.id, status)
|
|
}
|
|
status.is_favorite -> StatusPromises.get(fragment.context!!).unfavorite(status.account_key, status.id)
|
|
else -> holder.playLikeAnimation(DefaultOnLikedListener(fragment.context!!, status))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fun handleActionLongClick(fragment: Fragment, status: ParcelableStatus, itemId: Long, id: Int): Boolean {
|
|
val context = fragment.context ?: return false
|
|
when (id) {
|
|
R.id.favorite -> {
|
|
val intent = selectAccountIntent(context, status, itemId)
|
|
fragment.startActivityForResult(intent, REQUEST_FAVORITE_SELECT_ACCOUNT)
|
|
return true
|
|
}
|
|
R.id.retweet -> {
|
|
val intent = selectAccountIntent(context, status, itemId, false)
|
|
fragment.startActivityForResult(intent, REQUEST_RETWEET_SELECT_ACCOUNT)
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
fun handleActionActivityResult(fragment: BaseFragment, requestCode: Int, resultCode: Int, data: Intent?) {
|
|
when (requestCode) {
|
|
REQUEST_FAVORITE_SELECT_ACCOUNT -> {
|
|
if (resultCode != Activity.RESULT_OK || data == null) return
|
|
val accountKey = data.getParcelableExtra<UserKey>(EXTRA_ACCOUNT_KEY)
|
|
val extras = data.getBundleExtra(EXTRA_EXTRAS)
|
|
val status = extras.status!!
|
|
if (PreferencesSingleton.get(fragment.context!!)[favoriteConfirmationKey]) {
|
|
fragment.executeAfterFragmentResumed {
|
|
FavoriteConfirmDialogFragment.show(it.childFragmentManager,
|
|
accountKey, status.id, status)
|
|
}
|
|
} else {
|
|
StatusPromises.get(fragment.context!!).favorite(accountKey, status)
|
|
}
|
|
}
|
|
REQUEST_RETWEET_SELECT_ACCOUNT -> {
|
|
if (resultCode != Activity.RESULT_OK || data == null) return
|
|
val accountKey = data.extras!!.accountKey!!
|
|
val extras = data.getBundleExtra(EXTRA_EXTRAS)
|
|
val status = extras.status!!
|
|
if (status.account_key.host != accountKey.host) {
|
|
val composeIntent = Intent(fragment.context, ComposeActivity::class.java)
|
|
val link = LinkCreator.getStatusWebLink(status)
|
|
if (link == null) {
|
|
Toast.makeText(fragment.context, R.string.message_toast_retweet_not_supported, Toast.LENGTH_SHORT).show()
|
|
return
|
|
}
|
|
composeIntent.putExtra(Intent.EXTRA_TEXT, "${status.text_plain} $link")
|
|
composeIntent.putExtra(EXTRA_ACCOUNT_KEY, accountKey)
|
|
composeIntent.putExtra(EXTRA_SELECTION, 0)
|
|
fragment.startActivity(composeIntent)
|
|
} else fragment.executeAfterFragmentResumed {
|
|
RetweetQuoteDialogFragment.show(it.childFragmentManager, accountKey,
|
|
status.id, status)
|
|
}
|
|
}
|
|
REQUEST_OPEN_SELECT_ACCOUNT -> {
|
|
if (resultCode != Activity.RESULT_OK || data == null) return
|
|
val accountKey = data.extras!!.accountKey
|
|
val extras = data.getBundleExtra(EXTRA_EXTRAS)
|
|
val status = extras.status!!
|
|
IntentUtils.openStatus(fragment.context!!, accountKey, status.id)
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
fun handleKeyboardShortcutAction(fragment: BaseFragment, action: String,
|
|
status: ParcelableStatus, position: Int): Boolean {
|
|
when (action) {
|
|
ACTION_STATUS_REPLY -> {
|
|
val intent = Intent(INTENT_ACTION_REPLY)
|
|
intent.putExtra(EXTRA_STATUS, status)
|
|
fragment.startActivity(intent)
|
|
return true
|
|
}
|
|
ACTION_STATUS_RETWEET -> {
|
|
fragment.executeAfterFragmentResumed {
|
|
RetweetQuoteDialogFragment.show(it.childFragmentManager,
|
|
status.account_key, status.id, status)
|
|
}
|
|
return true
|
|
}
|
|
ACTION_STATUS_FAVORITE -> {
|
|
if (PreferencesSingleton.get(fragment.context!!)[favoriteConfirmationKey]) {
|
|
fragment.executeAfterFragmentResumed {
|
|
FavoriteConfirmDialogFragment.show(it.childFragmentManager,
|
|
status.account_key, status.id, status)
|
|
}
|
|
} else if (status.is_favorite) {
|
|
StatusPromises.get(fragment.context!!).unfavorite(status.account_key, status.id)
|
|
} else {
|
|
val holder = fragment.recyclerView.findViewHolderForLayoutPosition(position) as StatusViewHolder
|
|
holder.playLikeAnimation(DefaultOnLikedListener(fragment.context!!, status))
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
fun createStatusesListItemDecoration(context: Context, recyclerView: RecyclerView,
|
|
adapter: IContentAdapter): RecyclerView.ItemDecoration {
|
|
adapter as RecyclerView.Adapter<*>
|
|
val itemDecoration = ExtendedDividerItemDecoration(context, (recyclerView.layoutManager as LinearLayoutManager).orientation)
|
|
val res = context.resources
|
|
if (adapter.profileImageEnabled) {
|
|
val decorPaddingLeft = res.getDimensionPixelSize(R.dimen.element_spacing_normal) * 2 + res.getDimensionPixelSize(R.dimen.icon_size_status_profile_image)
|
|
itemDecoration.setPadding { position, rect ->
|
|
val itemViewType = adapter.getItemViewType(position)
|
|
var nextItemIsStatus = false
|
|
if (position < adapter.itemCount - 1) {
|
|
nextItemIsStatus = adapter.getItemViewType(position + 1) in RecyclerViewTypes.STATUS_TYPES
|
|
}
|
|
if (nextItemIsStatus && itemViewType in RecyclerViewTypes.STATUS_TYPES) {
|
|
rect.left = decorPaddingLeft
|
|
} else {
|
|
rect.left = 0
|
|
}
|
|
true
|
|
}
|
|
}
|
|
itemDecoration.setDecorationEndOffset(1)
|
|
return itemDecoration
|
|
}
|
|
|
|
fun createStatusesListGalleryDecoration(context: Context, recyclerView: RecyclerView): RecyclerView.ItemDecoration {
|
|
val itemDecoration = ExtendedDividerItemDecoration(context, (recyclerView.layoutManager as LinearLayoutManager).orientation)
|
|
itemDecoration.setDecorationEndOffset(1)
|
|
return itemDecoration
|
|
}
|
|
|
|
fun selectAccountIntent(context: Context, status: ParcelableStatus, itemId: Long,
|
|
sameHostOnly: Boolean = true): Intent {
|
|
val intent = Intent(context, AccountSelectorActivity::class.java)
|
|
intent.putExtra(EXTRA_SELECT_ONLY_ITEM_AUTOMATICALLY, true)
|
|
if (sameHostOnly) {
|
|
intent.putExtra(EXTRA_ACCOUNT_HOST, status.account_key.host)
|
|
}
|
|
intent.putExtra(EXTRA_SINGLE_SELECTION, true)
|
|
intent.putExtra(EXTRA_EXTRAS, Bundle {
|
|
this[EXTRA_STATUS] = status
|
|
this[EXTRA_ID] = itemId
|
|
})
|
|
return intent
|
|
}
|
|
|
|
val databasePagedListConfig: PagedList.Config = PagedList.Config.Builder()
|
|
.setPageSize(50)
|
|
.setInitialLoadSizeHint(50)
|
|
.setPrefetchDistance(15)
|
|
.setEnablePlaceholders(true)
|
|
.build()
|
|
|
|
|
|
}
|
|
}
|