755 lines
32 KiB
Kotlin
755 lines
32 KiB
Kotlin
/*
|
|
* Twidere - Twitter client for Android
|
|
*
|
|
* Copyright (C) 2012-2015 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
|
|
|
|
import android.accounts.AccountManager
|
|
import android.app.Activity
|
|
import android.content.ContentValues
|
|
import android.content.Context
|
|
import android.content.Intent
|
|
import android.graphics.Rect
|
|
import android.net.Uri
|
|
import android.os.Bundle
|
|
import androidx.annotation.CallSuper
|
|
import androidx.fragment.app.Fragment
|
|
import androidx.loader.app.LoaderManager.LoaderCallbacks
|
|
import androidx.loader.content.Loader
|
|
import androidx.recyclerview.widget.LinearLayoutManager
|
|
import androidx.recyclerview.widget.RecyclerView
|
|
import androidx.recyclerview.widget.RecyclerView.OnScrollListener
|
|
import android.view.*
|
|
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.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.ILoadMoreSupportAdapter
|
|
import org.mariotaku.twidere.annotation.ReadPositionTag
|
|
import org.mariotaku.twidere.constant.*
|
|
import org.mariotaku.twidere.constant.IntentConstants.*
|
|
import org.mariotaku.twidere.constant.KeyboardShortcutConstants.*
|
|
import org.mariotaku.twidere.extension.model.getAccountType
|
|
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.loader.iface.IExtendedLoader
|
|
import org.mariotaku.twidere.model.*
|
|
import org.mariotaku.twidere.model.analyzer.Share
|
|
import org.mariotaku.twidere.model.event.StatusListChangedEvent
|
|
import org.mariotaku.twidere.model.pagination.SinceMaxPagination
|
|
import org.mariotaku.twidere.model.timeline.TimelineFilter
|
|
import org.mariotaku.twidere.model.util.AccountUtils
|
|
import org.mariotaku.twidere.provider.TwidereDataStore.Statuses
|
|
import org.mariotaku.twidere.util.*
|
|
import org.mariotaku.twidere.util.KeyboardShortcutsHandler.KeyboardShortcutCallback
|
|
import org.mariotaku.twidere.util.glide.PauseRecyclerViewOnScrollListener
|
|
import org.mariotaku.twidere.util.sync.SyncTaskRunner
|
|
import org.mariotaku.twidere.view.ExtendedRecyclerView
|
|
import org.mariotaku.twidere.view.holder.GapViewHolder
|
|
import org.mariotaku.twidere.view.holder.StatusViewHolder
|
|
import org.mariotaku.twidere.view.holder.iface.IStatusViewHolder
|
|
|
|
/**
|
|
* Created by mariotaku on 14/11/5.
|
|
*/
|
|
abstract class AbsStatusesFragment : AbsContentListRecyclerViewFragment<ParcelableStatusesAdapter>(),
|
|
LoaderCallbacks<List<ParcelableStatus>?>, IStatusViewHolder.StatusClickListener,
|
|
KeyboardShortcutCallback {
|
|
|
|
private lateinit var statusesBusCallback: Any
|
|
private lateinit var navigationHelper: RecyclerViewNavigationHelper
|
|
private var pauseOnScrollListener: OnScrollListener? = null
|
|
var loaderInitialized: Boolean = false
|
|
private set
|
|
|
|
private val onScrollListener = object : OnScrollListener() {
|
|
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
|
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
|
|
val layoutManager = layoutManager
|
|
saveReadPosition(layoutManager.findFirstVisibleItemPosition())
|
|
}
|
|
}
|
|
}
|
|
|
|
protected abstract val accountKeys: Array<UserKey>
|
|
|
|
protected var adapterData: List<ParcelableStatus>?
|
|
get() = adapter.getData()
|
|
set(data) {
|
|
adapter.setData(data)
|
|
}
|
|
|
|
@ReadPositionTag
|
|
protected open val readPositionTag: String?
|
|
get() = null
|
|
|
|
protected open val timelineSyncTag: String?
|
|
get() = null
|
|
|
|
protected open val readPositionTagWithArguments: String?
|
|
get() = readPositionTag
|
|
|
|
protected open val useSortIdAsReadPosition: Boolean = true
|
|
|
|
/**
|
|
* Used for 'restore position' feature
|
|
*/
|
|
protected open val currentReadPositionTag: String?
|
|
get() {
|
|
val positionTag = readPositionTagWithArguments ?: readPositionTag ?: return null
|
|
return if (tabId < 0) null else "${positionTag}_${tabId}_current"
|
|
}
|
|
|
|
override val extraContentPadding: Rect
|
|
get() {
|
|
val paddingVertical = resources.getDimensionPixelSize(R.dimen.element_spacing_small)
|
|
return Rect(0, paddingVertical, 0, paddingVertical)
|
|
}
|
|
|
|
val shouldInitLoader: Boolean
|
|
get() = (parentFragment as? StatusesFragmentDelegate)?.shouldInitLoader ?: true
|
|
|
|
|
|
protected open val enableTimelineFilter: Boolean = false
|
|
|
|
protected open val timelineFilter: TimelineFilter? = null
|
|
|
|
protected open val loaderId: Int
|
|
get() = tabId.toInt().coerceIn(0..Int.MAX_VALUE)
|
|
|
|
// Fragment life cycles
|
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
|
super.onActivityCreated(savedInstanceState)
|
|
statusesBusCallback = createMessageBusCallback()
|
|
scrollListener.reversed = preferences[readFromBottomKey]
|
|
adapter.statusClickListener = this
|
|
registerForContextMenu(recyclerView)
|
|
navigationHelper = RecyclerViewNavigationHelper(recyclerView, layoutManager, adapter, this)
|
|
pauseOnScrollListener = PauseRecyclerViewOnScrollListener(
|
|
pauseOnScroll = false, pauseOnFling = false,
|
|
requestManager = requestManager
|
|
)
|
|
|
|
if (shouldInitLoader) {
|
|
initLoaderIfNeeded()
|
|
}
|
|
showProgress()
|
|
}
|
|
|
|
override fun onStart() {
|
|
super.onStart()
|
|
recyclerView.addOnScrollListener(onScrollListener)
|
|
pauseOnScrollListener?.let { recyclerView.addOnScrollListener(it) }
|
|
bus.register(statusesBusCallback)
|
|
}
|
|
|
|
override fun onStop() {
|
|
bus.unregister(statusesBusCallback)
|
|
pauseOnScrollListener?.let { recyclerView.removeOnScrollListener(it) }
|
|
recyclerView.removeOnScrollListener(onScrollListener)
|
|
if (userVisibleHint) {
|
|
saveReadPosition()
|
|
}
|
|
super.onStop()
|
|
}
|
|
|
|
override fun onDestroy() {
|
|
adapter.statusClickListener = null
|
|
super.onDestroy()
|
|
}
|
|
|
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
when (requestCode) {
|
|
REQUEST_FAVORITE_SELECT_ACCOUNT, REQUEST_RETWEET_SELECT_ACCOUNT -> {
|
|
handleActionActivityResult(this, requestCode, resultCode, data)
|
|
}
|
|
}
|
|
}
|
|
|
|
abstract fun getStatuses(param: RefreshTaskParam): Boolean
|
|
|
|
override fun handleKeyboardShortcutSingle(handler: KeyboardShortcutsHandler, keyCode: Int, event: KeyEvent, metaState: Int): Boolean {
|
|
var action = handler.getKeyAction(CONTEXT_TAG_NAVIGATION, keyCode, event, metaState)
|
|
if (ACTION_NAVIGATION_REFRESH == action) {
|
|
triggerRefresh()
|
|
return true
|
|
}
|
|
val focusedChild = RecyclerViewUtils.findRecyclerViewChild(recyclerView,
|
|
layoutManager.focusedChild)
|
|
var position = -1
|
|
if (focusedChild != null && focusedChild.parent === recyclerView) {
|
|
position = recyclerView.getChildLayoutPosition(focusedChild)
|
|
}
|
|
if (position != -1) {
|
|
val status = adapter.getStatus(position)
|
|
if (keyCode == KeyEvent.KEYCODE_ENTER) {
|
|
activity?.let {
|
|
IntentUtils.openStatus(it, status, null)
|
|
}
|
|
return true
|
|
}
|
|
if (action == null) {
|
|
action = handler.getKeyAction(CONTEXT_TAG_STATUS, keyCode, event, metaState)
|
|
}
|
|
if (action == null) return false
|
|
return handleKeyboardShortcutAction(this, action, status, position)
|
|
}
|
|
return navigationHelper.handleKeyboardShortcutSingle(handler, keyCode, event, metaState)
|
|
}
|
|
|
|
override fun isKeyboardShortcutHandled(handler: KeyboardShortcutsHandler, keyCode: Int, event: KeyEvent, metaState: Int): Boolean {
|
|
var action = handler.getKeyAction(CONTEXT_TAG_NAVIGATION, keyCode, event, metaState)
|
|
if (ACTION_NAVIGATION_REFRESH == action) {
|
|
return true
|
|
}
|
|
if (action == null) {
|
|
action = handler.getKeyAction(CONTEXT_TAG_STATUS, keyCode, event, metaState)
|
|
}
|
|
if (action == null) return false
|
|
when (action) {
|
|
ACTION_STATUS_REPLY, ACTION_STATUS_RETWEET, ACTION_STATUS_FAVORITE -> return true
|
|
}
|
|
return navigationHelper.isKeyboardShortcutHandled(handler, keyCode, event, metaState)
|
|
}
|
|
|
|
override fun handleKeyboardShortcutRepeat(handler: KeyboardShortcutsHandler, keyCode: Int, repeatCount: Int,
|
|
event: KeyEvent, metaState: Int): Boolean {
|
|
return navigationHelper.handleKeyboardShortcutRepeat(handler, keyCode, repeatCount, event, metaState)
|
|
}
|
|
|
|
override fun onCreateLoader(id: Int, args: Bundle?): Loader<List<ParcelableStatus>?> {
|
|
val fromUser = args?.getBoolean(EXTRA_FROM_USER)
|
|
args?.remove(EXTRA_FROM_USER)
|
|
return onCreateStatusesLoader(requireActivity(), args!!, fromUser!!)
|
|
}
|
|
|
|
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
|
|
if (userVisibleHint && !isVisibleToUser && host != null) {
|
|
saveReadPosition()
|
|
}
|
|
super.setUserVisibleHint(isVisibleToUser)
|
|
}
|
|
|
|
/**
|
|
* Statuses loaded, update adapter data & restore load position
|
|
*
|
|
* Steps:
|
|
* 1. Save current read position if not first load (adapter data is not empty)
|
|
* 1.1 If readFromBottom is true, save position on screen bottom
|
|
* 2. Change adapter data
|
|
* 3. Restore adapter data
|
|
* 3.1 If lastVisible was last item, keep lastVisibleItem position (load more)
|
|
* 3.2 Else, if readFromBottom is true:
|
|
* 3.1.1 If position was first, keep lastVisibleItem position (pull refresh)
|
|
* 3.1.2 Else, keep lastVisibleItem position
|
|
* 3.2 If readFromBottom is false:
|
|
* 3.2.1 If position was first, set new position to 0 (pull refresh)
|
|
* 3.2.2 Else, keep firstVisibleItem position (gap clicked)
|
|
*/
|
|
override fun onLoadFinished(loader: Loader<List<ParcelableStatus>?>, data: List<ParcelableStatus>?) {
|
|
val rememberPosition = preferences[rememberPositionKey]
|
|
val currentReadPositionTag = currentReadPositionTag
|
|
val readFromBottom = preferences[readFromBottomKey]
|
|
val firstLoad = adapterData.isNullOrEmpty()
|
|
|
|
var lastReadId: Long = -1
|
|
var lastReadViewTop = 0
|
|
var loadMore = false
|
|
var wasAtTop = false
|
|
// 1. Save current read position if not first load
|
|
if (!firstLoad) {
|
|
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
|
|
val lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition()
|
|
wasAtTop = firstVisibleItemPosition == 0
|
|
// Get display range of statuses
|
|
val statusRange = rangeOfSize(adapter.statusStartIndex, adapter.getStatusCount(raw = false))
|
|
val lastReadPosition = if (loadMore || readFromBottom) {
|
|
lastVisibleItemPosition
|
|
} else {
|
|
firstVisibleItemPosition
|
|
}.coerceInOr(statusRange, -1)
|
|
lastReadId = when {
|
|
lastReadPosition < 0 -> {
|
|
-1
|
|
}
|
|
useSortIdAsReadPosition -> {
|
|
adapter.getStatusSortId(lastReadPosition, false)
|
|
}
|
|
else -> {
|
|
adapter.getStatusPositionKey(lastReadPosition)
|
|
}
|
|
}
|
|
lastReadViewTop = layoutManager.findViewByPosition(lastReadPosition)?.top ?: 0
|
|
loadMore = statusRange.last in 0..lastVisibleItemPosition
|
|
} else if (rememberPosition) {
|
|
val syncManager = timelineSyncManager
|
|
val positionTag = this.readPositionTag
|
|
val syncTag = this.timelineSyncTag
|
|
val currentTag = this.currentReadPositionTag
|
|
|
|
if (syncManager != null && positionTag != null && syncTag != null &&
|
|
syncPreferences.isSyncEnabled(SyncTaskRunner.SYNC_TYPE_TIMELINE_POSITIONS)) {
|
|
lastReadId = syncManager.peekPosition(positionTag, syncTag)
|
|
}
|
|
if (lastReadId <= 0 && currentTag != null) {
|
|
lastReadId = readStateManager.getPosition(currentTag)
|
|
}
|
|
|
|
lastReadViewTop = 0
|
|
}
|
|
// 2. Change adapter data
|
|
adapterData = data
|
|
adapter.timelineFilter = timelineFilter
|
|
|
|
refreshEnabled = true
|
|
|
|
var restorePosition = -1
|
|
|
|
if (loader !is IExtendedLoader || loader.fromUser) {
|
|
if (hasMoreData(loader, data)) {
|
|
adapter.loadMoreSupportedPosition = ILoadMoreSupportAdapter.END
|
|
onHasMoreDataChanged(true)
|
|
} else {
|
|
adapter.loadMoreSupportedPosition = ILoadMoreSupportAdapter.NONE
|
|
onHasMoreDataChanged(false)
|
|
}
|
|
restorePosition = if (useSortIdAsReadPosition) {
|
|
adapter.findPositionBySortId(lastReadId)
|
|
} else {
|
|
adapter.findPositionByPositionKey(lastReadId)
|
|
}
|
|
} else {
|
|
onHasMoreDataChanged(false)
|
|
}
|
|
if (loadMore) {
|
|
restorePosition += 1
|
|
restorePosition.coerceInOr(0 until layoutManager.itemCount, -1)
|
|
}
|
|
if (restorePosition != -1 && adapter.isStatus(restorePosition) && (loadMore || !wasAtTop
|
|
|| readFromBottom || (rememberPosition && firstLoad))) {
|
|
if (layoutManager.height == 0) {
|
|
// RecyclerView has not currently laid out, ignore padding.
|
|
layoutManager.scrollToPositionWithOffset(restorePosition, lastReadViewTop)
|
|
} else {
|
|
layoutManager.scrollToPositionWithOffset(restorePosition, lastReadViewTop - layoutManager.paddingTop)
|
|
}
|
|
}
|
|
|
|
if (loader is IExtendedLoader) {
|
|
loader.fromUser = false
|
|
}
|
|
onStatusesLoaded(loader, data)
|
|
}
|
|
|
|
override fun onLoaderReset(loader: Loader<List<ParcelableStatus>?>) {
|
|
if (loader is IExtendedLoader) {
|
|
loader.fromUser = false
|
|
}
|
|
}
|
|
|
|
|
|
override fun onGapClick(holder: GapViewHolder, position: Int) {
|
|
val status = adapter.getStatus(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(BaseRefreshTaskParam(accountKeys, pagination))
|
|
}
|
|
|
|
override fun onMediaClick(holder: IStatusViewHolder, view: View, current: ParcelableMedia,
|
|
statusPosition: Int) {
|
|
val status = adapter.getStatus(statusPosition)
|
|
activity?.let {
|
|
IntentUtils.openMedia(it, status, current, preferences[newDocumentApiKey],
|
|
preferences[displaySensitiveContentsKey])
|
|
}
|
|
}
|
|
|
|
override fun onQuotedMediaClick(holder: IStatusViewHolder, view: View, current: ParcelableMedia,
|
|
statusPosition: Int) {
|
|
val status = adapter.getStatus(statusPosition)
|
|
val quotedMedia = status.quoted_media ?: return
|
|
activity?.let {
|
|
IntentUtils.openMedia(it, status.account_key, status.is_possibly_sensitive, status,
|
|
current, quotedMedia, preferences[newDocumentApiKey], preferences[displaySensitiveContentsKey])
|
|
}
|
|
}
|
|
|
|
override fun onItemActionClick(holder: RecyclerView.ViewHolder, id: Int, position: Int) {
|
|
val status = getFullStatus(position) ?: return
|
|
handleActionClick(this@AbsStatusesFragment, id, status, holder as StatusViewHolder)
|
|
}
|
|
|
|
override fun onItemActionLongClick(holder: RecyclerView.ViewHolder, id: Int, position: Int): Boolean {
|
|
val status = getFullStatus(position) ?: return false
|
|
return handleActionLongClick(this, status, adapter.getItemId(position), id)
|
|
}
|
|
|
|
override fun onCreateItemDecoration(context: Context, recyclerView: RecyclerView, layoutManager: LinearLayoutManager): RecyclerView.ItemDecoration? {
|
|
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) == ParcelableStatusesAdapter.VIEW_TYPE_STATUS
|
|
}
|
|
if (nextItemIsStatus && itemViewType == ParcelableStatusesAdapter.VIEW_TYPE_STATUS) {
|
|
rect.left = decorPaddingLeft
|
|
} else {
|
|
rect.left = 0
|
|
}
|
|
true
|
|
}
|
|
}
|
|
itemDecoration.setDecorationEndOffset(1)
|
|
return itemDecoration
|
|
}
|
|
|
|
protected fun saveReadPosition() {
|
|
saveReadPosition(layoutManager.findFirstVisibleItemPosition())
|
|
}
|
|
|
|
protected open fun onHasMoreDataChanged(hasMoreData: Boolean) {
|
|
}
|
|
|
|
override fun onStatusClick(holder: IStatusViewHolder, position: Int) {
|
|
val status = getFullStatus(position) ?: return
|
|
activity?.let {
|
|
IntentUtils.openStatus(it, status, null)
|
|
}
|
|
}
|
|
|
|
override fun onQuotedStatusClick(holder: IStatusViewHolder, position: Int) {
|
|
val status = adapter.getStatus(position)
|
|
val quotedId = status.quoted_id ?: return
|
|
activity?.let {
|
|
IntentUtils.openStatus(it, status.account_key, quotedId)
|
|
}
|
|
}
|
|
|
|
override fun onStatusLongClick(holder: IStatusViewHolder, position: Int): Boolean {
|
|
//TODO handle long click event
|
|
return true
|
|
}
|
|
|
|
override fun onItemMenuClick(holder: RecyclerView.ViewHolder, menuView: View, position: Int) {
|
|
if (activity == null) return
|
|
val view = layoutManager.findViewByPosition(position) ?: return
|
|
recyclerView.showContextMenuForChild(view)
|
|
}
|
|
|
|
override fun onUserProfileClick(holder: IStatusViewHolder, position: Int) {
|
|
val status = adapter.getStatus(position)
|
|
val intent = IntentUtils.userProfile(status.account_key, status.user_key,
|
|
status.user_screen_name, status.extras?.user_statusnet_profile_url)
|
|
IntentUtils.applyNewDocument(intent, preferences[newDocumentApiKey])
|
|
startActivity(intent)
|
|
}
|
|
|
|
override fun onLinkClick(holder: IStatusViewHolder, position: Int) {
|
|
val status = adapter.getStatus(position)
|
|
val url = status.extras?.entities_url?.firstOrNull()
|
|
OnLinkClickHandler.openLink(requireContext(), preferences, Uri.parse(url))
|
|
}
|
|
|
|
override fun scrollToStart(): Boolean {
|
|
val result = super.scrollToStart()
|
|
if (result) {
|
|
saveReadPosition(0)
|
|
}
|
|
return result
|
|
}
|
|
|
|
fun initLoaderIfNeeded() {
|
|
if (isDetached || host == null || loaderInitialized) return
|
|
val loaderArgs = Bundle(arguments)
|
|
loaderArgs.putBoolean(EXTRA_FROM_USER, true)
|
|
loaderManager.initLoader(loaderId, loaderArgs, this)
|
|
loaderInitialized = true
|
|
}
|
|
|
|
protected open fun createMessageBusCallback(): Any {
|
|
return StatusesBusCallback()
|
|
}
|
|
|
|
@CallSuper
|
|
protected open fun saveReadPosition(position: Int) {
|
|
if (host == null) return
|
|
if (position == RecyclerView.NO_POSITION || adapter.getStatusCount(false) <= 0) return
|
|
val status = adapter.getStatus(position.coerceIn(rangeOfSize(adapter.statusStartIndex,
|
|
adapter.getStatusCount(false))))
|
|
val readPosition = if (useSortIdAsReadPosition) {
|
|
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)
|
|
|
|
}
|
|
}
|
|
timelineSyncTag?.let { syncTag ->
|
|
timelineSyncManager?.setPosition(positionTag, syncTag, status.position_key)
|
|
}
|
|
currentReadPositionTag?.let {
|
|
readStateManager.setPosition(it, readPosition, true)
|
|
}
|
|
}
|
|
|
|
protected open fun getFullStatus(position: Int): ParcelableStatus? {
|
|
return adapter.getStatus(position)
|
|
}
|
|
|
|
protected abstract fun hasMoreData(loader: Loader<List<ParcelableStatus>?>,
|
|
data: List<ParcelableStatus>?): Boolean
|
|
|
|
protected abstract fun onCreateStatusesLoader(context: Context, args: Bundle,
|
|
fromUser: Boolean): Loader<List<ParcelableStatus>?>
|
|
|
|
protected abstract fun onStatusesLoaded(loader: Loader<List<ParcelableStatus>?>, data: List<ParcelableStatus>?)
|
|
|
|
// Context Menu functions
|
|
|
|
override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) {
|
|
if (!userVisibleHint || menuInfo == null) return
|
|
val inflater = MenuInflater(context)
|
|
val contextMenuInfo = menuInfo as ExtendedRecyclerView.ContextMenuInfo?
|
|
val status = adapter.getStatus(contextMenuInfo!!.position)
|
|
inflater.inflate(R.menu.action_status, menu)
|
|
context?.let {
|
|
MenuUtils.setupForStatus(it, menu, preferences, twitterWrapper, userColorNameManager,
|
|
status)
|
|
}
|
|
}
|
|
|
|
override fun onContextItemSelected(item: MenuItem): Boolean {
|
|
if (!userVisibleHint) return false
|
|
val contextMenuInfo = item.menuInfo as ExtendedRecyclerView.ContextMenuInfo
|
|
val status = adapter.getStatus(contextMenuInfo.position)
|
|
when (item.itemId) {
|
|
R.id.share -> {
|
|
val shareIntent = activity?.let { Utils.createStatusShareIntent(it, status) }
|
|
val chooser = Intent.createChooser(shareIntent, getString(R.string.share_status))
|
|
startActivity(chooser)
|
|
|
|
val am = AccountManager.get(context)
|
|
val accountType = AccountUtils.findByAccountKey(am, status.account_key)?.getAccountType(am)
|
|
Analyzer.log(Share.status(accountType, status))
|
|
return true
|
|
}
|
|
R.id.make_gap -> {
|
|
if (this !is CursorStatusesFragment) 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(requireActivity(), this, requireFragmentManager(),
|
|
preferences, userColorNameManager, twitterWrapper, status, item)
|
|
}
|
|
}
|
|
|
|
class DefaultOnLikedListener(
|
|
private val twitter: AsyncTwitterWrapper,
|
|
private val status: ParcelableStatus,
|
|
private val accountKey: UserKey? = null
|
|
) : LikeAnimationDrawable.OnLikedListener {
|
|
|
|
override fun onLiked(): Boolean {
|
|
if (status.is_favorite) return false
|
|
twitter.createFavoriteAsync(accountKey ?: status.account_key, status)
|
|
return true
|
|
}
|
|
}
|
|
|
|
protected inner class StatusesBusCallback {
|
|
|
|
@Subscribe
|
|
fun notifyStatusListChanged(event: StatusListChangedEvent) {
|
|
adapter.notifyDataSetChanged()
|
|
}
|
|
|
|
}
|
|
|
|
interface StatusesFragmentDelegate {
|
|
val shouldInitLoader: Boolean
|
|
}
|
|
|
|
companion object {
|
|
|
|
const val REQUEST_FAVORITE_SELECT_ACCOUNT = 101
|
|
const val REQUEST_RETWEET_SELECT_ACCOUNT = 102
|
|
|
|
fun handleActionClick(fragment: BaseFragment, id: Int, status: ParcelableStatus,
|
|
holder: StatusViewHolder) {
|
|
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 { fragment ->
|
|
RetweetQuoteDialogFragment.show(fragment.childFragmentManager,
|
|
status.account_key, status.id, status)
|
|
}
|
|
}
|
|
R.id.favorite -> {
|
|
when {
|
|
fragment.preferences[favoriteConfirmationKey] -> {
|
|
fragment.executeAfterFragmentResumed {
|
|
FavoriteConfirmDialogFragment.show(it.childFragmentManager,
|
|
status.account_key, status.id, status)
|
|
}
|
|
}
|
|
status.is_favorite -> {
|
|
fragment.twitterWrapper.destroyFavoriteAsync(status.account_key, status.id)
|
|
}
|
|
else -> {
|
|
holder.playLikeAnimation(DefaultOnLikedListener(fragment.twitterWrapper, status))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fun handleActionLongClick(fragment: Fragment, status: ParcelableStatus, itemId: Long, id: Int): Boolean {
|
|
when (id) {
|
|
R.id.favorite -> {
|
|
val intent = fragment.context?.let { selectAccountIntent(it, status, itemId) }
|
|
fragment.startActivityForResult(intent, REQUEST_FAVORITE_SELECT_ACCOUNT)
|
|
return true
|
|
}
|
|
R.id.retweet -> {
|
|
val intent = fragment.context?.let { selectAccountIntent(it, 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.getParcelable<ParcelableStatus>(EXTRA_STATUS)!!
|
|
if (fragment.preferences[favoriteConfirmationKey]) {
|
|
fragment.executeAfterFragmentResumed {
|
|
FavoriteConfirmDialogFragment.show(it.childFragmentManager,
|
|
accountKey, status.id, status)
|
|
}
|
|
} else {
|
|
fragment.twitterWrapper.createFavoriteAsync(accountKey, status)
|
|
}
|
|
}
|
|
REQUEST_RETWEET_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.getParcelable<ParcelableStatus>(EXTRA_STATUS)!!
|
|
if (status.account_key.host != accountKey.host) {
|
|
val composeIntent = Intent(fragment.context, ComposeActivity::class.java)
|
|
composeIntent.putExtra(Intent.EXTRA_TEXT, "${status.text_plain} ${LinkCreator.getStatusWebLink(status)}")
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
|
|
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 -> {
|
|
when {
|
|
fragment.preferences[favoriteConfirmationKey] -> {
|
|
fragment.executeAfterFragmentResumed {
|
|
FavoriteConfirmDialogFragment.show(it.childFragmentManager,
|
|
status.account_key, status.id, status)
|
|
}
|
|
}
|
|
status.is_favorite -> {
|
|
fragment.twitterWrapper.destroyFavoriteAsync(status.account_key, status.id)
|
|
}
|
|
else -> {
|
|
val holder = fragment.recyclerView.findViewHolderForLayoutPosition(position) as StatusViewHolder
|
|
holder.playLikeAnimation(DefaultOnLikedListener(fragment.twitterWrapper, status))
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
}
|
|
}
|