Twidere-App-Android-Twitter.../twidere/src/main/kotlin/org/mariotaku/twidere/fragment/status/StatusFragment.kt

980 lines
43 KiB
Kotlin
Raw Normal View History

2017-08-28 11:56:09 +02:00
/*
* 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.status
import android.accounts.AccountManager
import android.app.Activity
import android.app.Dialog
import android.content.ContentValues
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.graphics.Color
import android.graphics.Rect
import android.nfc.NdefMessage
import android.nfc.NdefRecord
import android.nfc.NfcAdapter.CreateNdefMessageCallback
import android.os.Bundle
2020-05-13 10:36:15 +02:00
import android.text.TextUtils
import android.view.*
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
2020-01-26 08:35:15 +01:00
import androidx.loader.app.LoaderManager.LoaderCallbacks
import androidx.loader.app.hasRunningLoadersSafe
import androidx.loader.content.FixedAsyncTaskLoader
import androidx.loader.content.Loader
import androidx.recyclerview.widget.FixedLinearLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ViewHolder
2017-08-28 11:56:09 +02:00
import com.squareup.otto.Subscribe
import kotlinx.android.synthetic.main.fragment_status.*
import kotlinx.android.synthetic.main.layout_content_fragment_common.*
import nl.komponents.kovenant.combine.and
import nl.komponents.kovenant.task
import nl.komponents.kovenant.ui.alwaysUi
import nl.komponents.kovenant.ui.successUi
import org.mariotaku.abstask.library.TaskStarter
import org.mariotaku.kpreferences.get
import org.mariotaku.ktextension.*
import org.mariotaku.library.objectcursor.ObjectCursor
import org.mariotaku.microblog.library.MicroBlog
import org.mariotaku.microblog.library.MicroBlogException
import org.mariotaku.microblog.library.twitter.model.Paging
import org.mariotaku.microblog.library.twitter.model.TranslationResult
import org.mariotaku.sqliteqb.library.Expression
import org.mariotaku.twidere.Constants.*
import org.mariotaku.twidere.R
import org.mariotaku.twidere.activity.ColorPickerDialogActivity
import org.mariotaku.twidere.adapter.StatusDetailsAdapter
import org.mariotaku.twidere.adapter.decorator.ExtendedDividerItemDecoration
import org.mariotaku.twidere.adapter.iface.ILoadMoreSupportAdapter
import org.mariotaku.twidere.adapter.iface.ILoadMoreSupportAdapter.IndicatorPosition
import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.constant.KeyboardShortcutConstants.*
import org.mariotaku.twidere.constant.displaySensitiveContentsKey
import org.mariotaku.twidere.constant.newDocumentApiKey
2020-05-13 10:36:15 +02:00
import org.mariotaku.twidere.constant.yandexKeyKey
2017-08-28 11:56:09 +02:00
import org.mariotaku.twidere.extension.*
2020-05-13 10:36:15 +02:00
import org.mariotaku.twidere.extension.model.*
2017-08-28 11:56:09 +02:00
import org.mariotaku.twidere.extension.model.api.key
import org.mariotaku.twidere.extension.model.api.toParcelable
import org.mariotaku.twidere.extension.view.calculateSpaceItemHeight
import org.mariotaku.twidere.fragment.AbsStatusesFragment
import org.mariotaku.twidere.fragment.AbsStatusesFragment.Companion.handleActionClick
import org.mariotaku.twidere.fragment.BaseDialogFragment
import org.mariotaku.twidere.fragment.BaseFragment
import org.mariotaku.twidere.loader.ParcelableStatusLoader
import org.mariotaku.twidere.loader.statuses.ConversationLoader
import org.mariotaku.twidere.model.*
import org.mariotaku.twidere.model.analyzer.Share
import org.mariotaku.twidere.model.analyzer.StatusView
import org.mariotaku.twidere.model.event.FavoriteTaskEvent
import org.mariotaku.twidere.model.event.StatusListChangedEvent
import org.mariotaku.twidere.model.pagination.Pagination
import org.mariotaku.twidere.model.pagination.SinceMaxPagination
import org.mariotaku.twidere.model.util.AccountUtils
import org.mariotaku.twidere.provider.TwidereDataStore.CachedStatuses
import org.mariotaku.twidere.provider.TwidereDataStore.Statuses
import org.mariotaku.twidere.task.AbsAccountRequestTask
import org.mariotaku.twidere.util.*
import org.mariotaku.twidere.util.ContentScrollHandler.ContentListSupport
import org.mariotaku.twidere.util.KeyboardShortcutsHandler.KeyboardShortcutCallback
import org.mariotaku.twidere.util.RecyclerViewScrollHandler.RecyclerViewCallback
2020-05-13 10:36:15 +02:00
import org.mariotaku.twidere.util.dagger.DependencyHolder
2017-08-28 11:56:09 +02:00
import org.mariotaku.twidere.view.CardMediaContainer.OnMediaClickListener
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
import org.mariotaku.twidere.view.holder.iface.IStatusViewHolder.StatusClickListener
2020-05-13 10:36:15 +02:00
import org.mariotaku.yandex.YandexAPIFactory
2017-08-28 11:56:09 +02:00
import java.lang.ref.WeakReference
/**
* Displays status details
* Created by mariotaku on 14/12/5.
*/
class StatusFragment : BaseFragment(), LoaderCallbacks<SingleResponse<ParcelableStatus>>,
OnMediaClickListener, StatusClickListener, KeyboardShortcutCallback,
ContentListSupport<StatusDetailsAdapter> {
private var mItemDecoration: ExtendedDividerItemDecoration? = null
override lateinit var adapter: StatusDetailsAdapter
private lateinit var layoutManager: LinearLayoutManager
private lateinit var navigationHelper: RecyclerViewNavigationHelper
private lateinit var scrollListener: RecyclerViewScrollHandler<StatusDetailsAdapter>
private var loadTranslationTask: LoadTranslationTask? = null
2017-08-28 11:56:09 +02:00
// Data fields
private var conversationLoaderInitialized: Boolean = false
private var activityLoaderInitialized: Boolean = false
private var hasMoreConversation = true
// Listeners
private val conversationsLoaderCallback = object : LoaderCallbacks<List<ParcelableStatus>> {
2020-01-26 08:35:15 +01:00
override fun onCreateLoader(id: Int, args: Bundle?): Loader<List<ParcelableStatus>> {
2017-08-28 11:56:09 +02:00
val adapter = this@StatusFragment.adapter
adapter.isRepliesLoading = true
adapter.isConversationsLoading = true
adapter.updateItemDecoration()
2020-01-26 08:35:15 +01:00
val status: ParcelableStatus = args!!.getParcelable(EXTRA_STATUS)!!
2017-08-28 11:56:09 +02:00
val loadingMore = args.getBoolean(EXTRA_LOADING_MORE, false)
2020-01-26 08:35:15 +01:00
return ConversationLoader(activity!!, status, adapter.getData(), true, loadingMore).apply {
2017-08-28 11:56:09 +02:00
pagination = args.toPagination()
}
}
override fun onLoadFinished(loader: Loader<List<ParcelableStatus>>, data: List<ParcelableStatus>?) {
val adapter = this@StatusFragment.adapter
adapter.updateItemDecoration()
val conversationLoader = loader as ConversationLoader
var supportedPositions: Long = 0
if (data != null && !data.isEmpty()) {
val sinceSortId = (conversationLoader.pagination as? SinceMaxPagination)?.sinceSortId ?: -1
if (sinceSortId < data[data.size - 1].sort_id) {
supportedPositions = supportedPositions or ILoadMoreSupportAdapter.END
}
if (data[0].in_reply_to_status_id != null) {
supportedPositions = supportedPositions or ILoadMoreSupportAdapter.START
}
} else {
supportedPositions = supportedPositions or ILoadMoreSupportAdapter.END
val status = status
if (status?.in_reply_to_status_id != null) {
supportedPositions = supportedPositions or ILoadMoreSupportAdapter.START
}
}
adapter.loadMoreSupportedPosition = supportedPositions
setConversation(data)
adapter.isConversationsLoading = false
adapter.isRepliesLoading = false
}
override fun onLoaderReset(loader: Loader<List<ParcelableStatus>>) {
}
}
private val statusActivityLoaderCallback = object : LoaderCallbacks<StatusActivity?> {
2020-01-26 08:35:15 +01:00
override fun onCreateLoader(id: Int, args: Bundle?): Loader<StatusActivity?> {
val accountKey = args!!.getParcelable<UserKey>(EXTRA_ACCOUNT_KEY)!!
2019-11-20 07:10:24 +01:00
val statusId = args.getString(EXTRA_STATUS_ID)!!
2020-01-26 08:35:15 +01:00
return StatusActivitySummaryLoader(activity!!, accountKey, statusId)
2017-08-28 11:56:09 +02:00
}
override fun onLoadFinished(loader: Loader<StatusActivity?>, data: StatusActivity?) {
adapter.updateItemDecoration()
adapter.statusActivity = data
}
override fun onLoaderReset(loader: Loader<StatusActivity?>) {
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
val activity = activity ?: return
when (requestCode) {
REQUEST_SET_COLOR -> {
val status = adapter.status ?: return
if (resultCode == Activity.RESULT_OK) {
if (data == null) return
val color = data.getIntExtra(EXTRA_COLOR, Color.TRANSPARENT)
userColorNameManager.setUserColor(status.user_key, color)
} else if (resultCode == ColorPickerDialogActivity.RESULT_CLEARED) {
userColorNameManager.clearUserColor(status.user_key)
}
2020-01-26 08:35:15 +01:00
arguments?.let { args ->
if (args.containsKey(EXTRA_STATUS)) {
args.putParcelable(EXTRA_STATUS, status)
}
loaderManager.restartLoader(LOADER_ID_DETAIL_STATUS, args, this)
2017-08-28 11:56:09 +02:00
}
}
REQUEST_SELECT_ACCOUNT -> {
val status = adapter.status ?: return
if (resultCode == Activity.RESULT_OK) {
if (data == null || !data.hasExtra(EXTRA_ID)) return
val accountKey = data.getParcelableExtra<UserKey>(EXTRA_ACCOUNT_KEY)
IntentUtils.openStatus(activity, accountKey, status.id)
}
}
AbsStatusesFragment.REQUEST_FAVORITE_SELECT_ACCOUNT,
AbsStatusesFragment.REQUEST_RETWEET_SELECT_ACCOUNT -> {
AbsStatusesFragment.handleActionActivityResult(this, requestCode, resultCode, data)
}
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_status, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
2020-01-26 08:35:15 +01:00
val context = context ?: return
val activity = activity ?: return
2017-08-28 11:56:09 +02:00
setHasOptionsMenu(true)
Utils.setNdefPushMessageCallback(activity, CreateNdefMessageCallback {
val status = status ?: return@CreateNdefMessageCallback null
NdefMessage(arrayOf(NdefRecord.createUri(LinkCreator.getStatusWebLink(status))))
})
adapter = StatusDetailsAdapter(this)
layoutManager = StatusListLinearLayoutManager(context, recyclerView)
2020-01-26 08:35:15 +01:00
mItemDecoration = StatusDividerItemDecoration(context, adapter, layoutManager.orientation)?.apply {
recyclerView.addItemDecoration(this)
}
2017-08-28 11:56:09 +02:00
layoutManager.recycleChildrenOnDetach = true
recyclerView.layoutManager = layoutManager
recyclerView.clipToPadding = false
adapter.statusClickListener = this
recyclerView.adapter = adapter
registerForContextMenu(recyclerView)
scrollListener = RecyclerViewScrollHandler(this, RecyclerViewCallback(recyclerView))
scrollListener.touchSlop = ViewConfiguration.get(context).scaledTouchSlop
navigationHelper = RecyclerViewNavigationHelper(recyclerView, layoutManager,
adapter, null)
setState(STATE_LOADING)
loaderManager.initLoader(LOADER_ID_DETAIL_STATUS, arguments, this)
}
override fun onMediaClick(holder: IStatusViewHolder, view: View, current: ParcelableMedia, statusPosition: Int) {
2020-01-26 08:35:15 +01:00
val activity = activity ?: return
2017-08-28 11:56:09 +02:00
val status = adapter.getStatus(statusPosition)
IntentUtils.openMedia(activity, status, current, preferences[newDocumentApiKey],
preferences[displaySensitiveContentsKey])
}
override fun onQuotedMediaClick(holder: IStatusViewHolder, view: View, current: ParcelableMedia, statusPosition: Int) {
2020-01-26 08:35:15 +01:00
val activity = activity ?: return
2017-08-28 11:56:09 +02:00
val status = adapter.getStatus(statusPosition)
val quotedMedia = status.quoted_media ?: return
IntentUtils.openMedia(activity, status.account_key, status.is_possibly_sensitive, status,
current, quotedMedia, preferences[newDocumentApiKey],
preferences[displaySensitiveContentsKey])
}
override fun onGapClick(holder: GapViewHolder, position: Int) {
}
override fun onItemActionClick(holder: ViewHolder, id: Int, position: Int) {
val status = adapter.getStatus(position)
handleActionClick(this@StatusFragment, id, status, holder as StatusViewHolder)
}
override fun onItemActionLongClick(holder: RecyclerView.ViewHolder, id: Int, position: Int): Boolean {
val status = adapter.getStatus(position)
return AbsStatusesFragment.handleActionLongClick(this, status, adapter.getItemId(position), id)
}
override fun onStatusClick(holder: IStatusViewHolder, position: Int) {
2020-01-26 08:35:15 +01:00
val activity = activity ?: return
2017-08-28 11:56:09 +02:00
val status = adapter.getStatus(position)
IntentUtils.openStatus(activity, status)
}
override fun onQuotedStatusClick(holder: IStatusViewHolder, position: Int) {
2020-01-26 08:35:15 +01:00
val activity = activity ?: return
2017-08-28 11:56:09 +02:00
val status = adapter.getStatus(position)
val quotedId = status.quoted_id ?: return
IntentUtils.openStatus(activity, status.account_key, quotedId)
}
override fun onStatusLongClick(holder: IStatusViewHolder, position: Int): Boolean {
return false
}
override fun onItemMenuClick(holder: 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) {
2020-01-26 08:35:15 +01:00
val activity = activity ?: return
2017-08-28 11:56:09 +02:00
val status = adapter.getStatus(position)
IntentUtils.openUserProfile(activity, status.account_key, status.user_key,
status.user_screen_name, status.extras?.user_statusnet_profile_url,
preferences[newDocumentApiKey], null)
}
override fun onMediaClick(view: View, current: ParcelableMedia, accountKey: UserKey?, id: Long) {
2020-01-26 08:35:15 +01:00
val activity = activity ?: return
2017-08-28 11:56:09 +02:00
val status = adapter.status ?: return
if ((view.parent as View).id == R.id.quotedMediaPreview && status.quoted_media != null) {
IntentUtils.openMediaDirectly(activity, accountKey, status.quoted_media!!, current,
newDocument = preferences[newDocumentApiKey], status = status)
} else if (status.media != null) {
IntentUtils.openMediaDirectly(activity, accountKey, status.media!!, current,
newDocument = preferences[newDocumentApiKey], status = status)
}
}
override fun handleKeyboardShortcutSingle(handler: KeyboardShortcutsHandler,
keyCode: Int, event: KeyEvent,
metaState: Int): Boolean {
if (!KeyboardShortcutsHandler.isValidForHotkey(keyCode, event)) return false
val focusedChild = RecyclerViewUtils.findRecyclerViewChild(recyclerView, layoutManager.focusedChild)
val position: Int
if (focusedChild != null && focusedChild.parent === recyclerView) {
position = recyclerView.getChildLayoutPosition(focusedChild)
} else {
return false
}
if (position == -1) return false
val status = adapter.getStatus(position)
val action = handler.getKeyAction(CONTEXT_TAG_STATUS, keyCode, event, metaState) ?: return false
return AbsStatusesFragment.handleKeyboardShortcutAction(this, action, status, position)
}
override fun isKeyboardShortcutHandled(handler: KeyboardShortcutsHandler, keyCode: Int, event: KeyEvent, metaState: Int): Boolean {
val action = handler.getKeyAction(CONTEXT_TAG_STATUS, keyCode, event, metaState) ?: 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)
}
2020-01-26 08:35:15 +01:00
override fun onCreateLoader(id: Int, args: Bundle?): Loader<SingleResponse<ParcelableStatus>> {
2017-08-28 11:56:09 +02:00
val fragmentArgs = arguments
2020-01-26 08:35:15 +01:00
val accountKey = fragmentArgs!!.getParcelable<UserKey>(EXTRA_ACCOUNT_KEY)
2017-08-28 11:56:09 +02:00
val statusId = fragmentArgs.getString(EXTRA_STATUS_ID)
2020-01-26 08:35:15 +01:00
return ParcelableStatusLoader(activity!!, false, fragmentArgs, accountKey, statusId)
2017-08-28 11:56:09 +02:00
}
override fun onLoadFinished(loader: Loader<SingleResponse<ParcelableStatus>>,
2020-01-26 08:35:15 +01:00
data: SingleResponse<ParcelableStatus>) {
2017-08-28 11:56:09 +02:00
val activity = activity ?: return
2020-01-26 08:35:15 +01:00
val context = context ?: return
2017-08-28 11:56:09 +02:00
val status = data.data
if (status != null) {
val readPosition = saveReadPosition()
val dataExtra = data.extras
val details: AccountDetails? = dataExtra.getParcelable(EXTRA_ACCOUNT)
if (adapter.setStatus(status, details)) {
2020-01-26 08:35:15 +01:00
arguments?.let { args ->
if (args.containsKey(EXTRA_STATUS)) {
args.putParcelable(EXTRA_STATUS, status)
}
2017-08-28 11:56:09 +02:00
}
adapter.loadMoreSupportedPosition = ILoadMoreSupportAdapter.BOTH
adapter.setData(null)
loadConversation(status, null, null)
loadActivity(status)
val position = adapter.getFirstPositionOfItem(StatusDetailsAdapter.ITEM_IDX_STATUS)
if (position != RecyclerView.NO_POSITION) {
layoutManager.scrollToPositionWithOffset(position, 0)
}
Analyzer.log(StatusView(details?.type, status.media_type).apply {
this.type = StatusView.getStatusType(status)
this.source = status.source?.let(HtmlEscapeHelper::toPlainText)
})
} else if (readPosition != null) {
restoreReadPosition(readPosition)
}
setState(STATE_LOADED)
} else {
adapter.loadMoreSupportedPosition = ILoadMoreSupportAdapter.NONE
setState(STATE_ERROR)
val errorInfo = StatusCodeMessageUtils.getErrorInfo(context, data.exception!!)
errorText.spannable = errorInfo.message
errorIcon.setImageResource(errorInfo.icon)
}
activity.invalidateOptionsMenu()
}
override fun onLoaderReset(loader: Loader<SingleResponse<ParcelableStatus>>) {
}
override val refreshing: Boolean
get() = loaderManager.hasRunningLoadersSafe()
override fun onLoadMoreContents(@IndicatorPosition position: Long) {
if (!hasMoreConversation) return
if (ILoadMoreSupportAdapter.START in position) {
val start = adapter.getIndexStart(StatusDetailsAdapter.ITEM_IDX_CONVERSATION)
val first = adapter.getStatus(start, true)
if (first.in_reply_to_status_id == null) return
loadConversation(status, null, first.id)
} else if (ILoadMoreSupportAdapter.END in position) {
val start = adapter.getIndexStart(StatusDetailsAdapter.ITEM_IDX_CONVERSATION)
val last = adapter.getStatus(start + adapter.getStatusCount(true) - 1, true)
loadConversation(status, last.id, null)
}
adapter.loadMoreIndicatorPosition = position
}
override fun setControlVisible(visible: Boolean) {
// No-op
}
override fun onApplySystemWindowInsets(insets: Rect) {
recyclerView.setPadding(insets.left, insets.top, insets.right, insets.bottom)
}
override val reachingEnd: Boolean
get() {
val lm = layoutManager
var itemPos = lm.findLastCompletelyVisibleItemPosition()
if (itemPos == RecyclerView.NO_POSITION) {
// No completely visible item, find visible item instead
itemPos = lm.findLastVisibleItemPosition()
}
return itemPos >= lm.itemCount - 1
}
override val reachingStart: Boolean
get() {
val lm = layoutManager
var itemPos = lm.findFirstCompletelyVisibleItemPosition()
if (itemPos == RecyclerView.NO_POSITION) {
// No completely visible item, find visible item instead
itemPos = lm.findFirstVisibleItemPosition()
}
return itemPos <= 1
}
private val status: ParcelableStatus?
get() = adapter.status
private fun loadConversation(status: ParcelableStatus?, sinceId: String?, maxId: String?) {
if (status == null || activity == null) return
val args = Bundle {
this[EXTRA_ACCOUNT_KEY] = status.account_key
this[EXTRA_STATUS_ID] = status.originalId
this[EXTRA_SINCE_ID] = sinceId
this[EXTRA_MAX_ID] = maxId
this[EXTRA_STATUS] = status
}
if (conversationLoaderInitialized) {
loaderManager.restartLoader(LOADER_ID_STATUS_CONVERSATIONS, args, conversationsLoaderCallback)
return
}
loaderManager.initLoader(LOADER_ID_STATUS_CONVERSATIONS, args, conversationsLoaderCallback)
conversationLoaderInitialized = true
}
private fun loadActivity(status: ParcelableStatus?) {
if (status == null || host == null || isDetached) return
val args = Bundle {
this[EXTRA_ACCOUNT_KEY] = status.account_key
this[EXTRA_STATUS_ID] = status.originalId
}
if (activityLoaderInitialized) {
loaderManager.restartLoader(LOADER_ID_STATUS_ACTIVITY, args, statusActivityLoaderCallback)
return
}
loaderManager.initLoader(LOADER_ID_STATUS_ACTIVITY, args, statusActivityLoaderCallback)
activityLoaderInitialized = true
}
internal fun loadTranslation(status: ParcelableStatus?) {
if (status == null) return
if (loadTranslationTask?.isFinished == true) return
loadTranslationTask = run {
val task = LoadTranslationTask(this, status)
TaskStarter.execute(task)
return@run task
}
2017-08-28 11:56:09 +02:00
}
internal fun reloadTranslation() {
loadTranslationTask = null
loadTranslation(adapter.status)
}
2017-08-28 11:56:09 +02:00
private fun setConversation(data: List<ParcelableStatus>?) {
val readPosition = saveReadPosition()
val changed = adapter.setData(data)
hasMoreConversation = data != null && changed
restoreReadPosition(readPosition)
}
private fun displayTranslation(translation: TranslationResult) {
adapter.translationResult = translation
}
private fun saveReadPosition(): ReadPosition? {
val lm = layoutManager
val adapter = this.adapter
val position = lm.findFirstVisibleItemPosition()
if (position == RecyclerView.NO_POSITION) return null
val itemType = adapter.getItemType(position)
var itemId = adapter.getItemId(position)
val positionView: View?
if (itemType == StatusDetailsAdapter.ITEM_IDX_CONVERSATION_LOAD_MORE) {
// Should be next item
positionView = lm.findViewByPosition(position + 1)
itemId = adapter.getItemId(position + 1)
} else {
positionView = lm.findViewByPosition(position)
}
return ReadPosition(itemId, positionView?.top ?: 0)
}
private fun restoreReadPosition(position: ReadPosition?) {
val adapter = this.adapter
if (position == null) return
val adapterPosition = adapter.findPositionByItemId(position.statusId)
if (adapterPosition < 0) return
layoutManager.scrollToPositionWithOffset(adapterPosition, position.offsetTop)
}
private fun setState(state: Int) {
statusContent.visibility = if (state == STATE_LOADED) View.VISIBLE else View.GONE
progressContainer.visibility = if (state == STATE_LOADING) View.VISIBLE else View.GONE
errorContainer.visibility = if (state == STATE_ERROR) View.VISIBLE else View.GONE
}
override fun onStart() {
super.onStart()
bus.register(this)
recyclerView.addOnScrollListener(scrollListener)
recyclerView.setOnTouchListener(scrollListener.touchListener)
}
override fun onStop() {
recyclerView.setOnTouchListener(null)
recyclerView.removeOnScrollListener(scrollListener)
bus.unregister(this)
super.onStop()
}
override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) {
2020-01-26 08:35:15 +01:00
val context = context ?: return
2017-08-28 11:56:09 +02:00
if (!userVisibleHint) return
val contextMenuInfo = menuInfo as? ExtendedRecyclerView.ContextMenuInfo ?: return
val status = adapter.getStatus(contextMenuInfo.position)
val inflater = MenuInflater(context)
inflater.inflate(R.menu.action_status, menu)
MenuUtils.setupForStatus(context, menu, preferences, twitterWrapper, userColorNameManager,
status)
}
override fun onContextItemSelected(item: MenuItem): Boolean {
2020-01-26 08:35:15 +01:00
val activity = activity ?: return false
val fragmentManager = fragmentManager ?: return false
2017-08-28 11:56:09 +02:00
if (!userVisibleHint) return false
val contextMenuInfo = item.menuInfo as? ExtendedRecyclerView.ContextMenuInfo ?: return false
val status = adapter.getStatus(contextMenuInfo.position)
if (item.itemId == R.id.share) {
val shareIntent = Utils.createStatusShareIntent(activity, 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
}
return MenuUtils.handleStatusClick(activity, this, fragmentManager,
preferences, userColorNameManager, twitterWrapper, status, item)
}
@Subscribe
fun notifyStatusListChanged(event: StatusListChangedEvent) {
adapter.notifyDataSetChanged()
}
@Subscribe
fun notifyFavoriteTask(event: FavoriteTaskEvent) {
if (!event.isSucceeded) return
val status = adapter.findStatusById(event.accountKey, event.statusId)
if (status != null) {
when (event.action) {
FavoriteTaskEvent.Action.CREATE -> {
status.is_favorite = true
}
FavoriteTaskEvent.Action.DESTROY -> {
status.is_favorite = false
}
}
}
}
internal fun onUserClick(user: ParcelableUser) {
2020-01-26 08:35:15 +01:00
context?.let { IntentUtils.openUserProfile(it, user, true, null) }
2017-08-28 11:56:09 +02:00
}
internal fun openTranslationDestinationChooser() {
val account = adapter.statusAccount ?: return
val weakThis = WeakReference(this)
(showProgressDialog("get_language_settings") and task {
val fragment = weakThis.get() ?: throw InterruptedException()
2020-01-26 08:35:15 +01:00
val microBlog = account.newMicroBlogInstance(fragment.context!!, MicroBlog::class.java)
2017-08-28 11:56:09 +02:00
return@task Pair(microBlog.accountSettings.language,
microBlog.languages.map { TranslationDestinationDialogFragment.DisplayLanguage(it.name, it.code) })
}).successUi { (_, settings) ->
val (accountLanguage, languages) = settings
val fragment = weakThis.get() ?: return@successUi
val df = TranslationDestinationDialogFragment.create(languages, accountLanguage)
df.setTargetFragment(fragment, 0)
2020-01-26 08:35:15 +01:00
df.show(fragment.fragmentManager!!, "translation_destination_settings")
2017-08-28 11:56:09 +02:00
}.alwaysUi {
val fragment = weakThis.get() ?: return@alwaysUi
fragment.dismissProgressDialog("get_language_settings")
}
}
class LoadSensitiveImageConfirmDialogFragment : BaseDialogFragment(), DialogInterface.OnClickListener {
override fun onClick(dialog: DialogInterface, which: Int) {
when (which) {
DialogInterface.BUTTON_POSITIVE -> {
val f = parentFragment
if (f is StatusFragment) {
val adapter = f.adapter
adapter.isDetailMediaExpanded = true
}
}
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val context = activity
2020-01-26 08:35:15 +01:00
val builder = AlertDialog.Builder(context!!)
2017-08-28 11:56:09 +02:00
builder.setTitle(android.R.string.dialog_alert_title)
builder.setMessage(R.string.sensitive_content_warning)
builder.setPositiveButton(android.R.string.ok, this)
builder.setNegativeButton(android.R.string.cancel, null)
val dialog = builder.create()
dialog.onShow { it.applyTheme() }
return dialog
}
}
internal class LoadTranslationTask(fragment: StatusFragment, val status: ParcelableStatus) :
2020-04-03 11:49:35 +02:00
AbsAccountRequestTask<Any?, TranslationResult, Any?>(fragment.context!!, status.account_key) {
private val weakFragment = WeakReference(fragment)
override fun onExecute(account: AccountDetails, params: Any?): TranslationResult {
val prefDest = preferences.getString(KEY_TRANSLATION_DESTINATION, null).orEmpty()
2020-05-13 10:36:15 +02:00
val twitter = account.newMicroBlogInstance(context, MicroBlog::class.java)
val dest: String
if (TextUtils.isEmpty(prefDest)) {
dest = twitter.accountSettings.language
val editor = preferences.edit()
editor.putString(KEY_TRANSLATION_DESTINATION, dest)
editor.apply()
} else {
dest = prefDest
}
2020-05-13 10:36:15 +02:00
if (account.isOfficial(context)) {
return twitter.showTranslation(status.originalId, dest)
} else {
val holder = DependencyHolder.get(context)
val api = YandexAPIFactory(preferences[yandexKeyKey], "https://translate.yandex.net/")
.setHttpClient(holder.restHttpClient)
.build()
val result = api.search(status.text_plain, "${status.lang}-${dest.split('-').firstOrNull()}")
return TranslationResult().also {
it[TranslationResult::class.java.getDeclaredField("text")] = result.text?.firstOrNull()
it[TranslationResult::class.java.getDeclaredField("id")] = status.originalId
it[TranslationResult::class.java.getDeclaredField("translatedLang")] = result.lang?.split('-')?.lastOrNull()
it[TranslationResult::class.java.getDeclaredField("lang")] = result.lang?.split('-')?.firstOrNull()
}
}
}
override fun onSucceed(callback: Any?, result: TranslationResult) {
val fragment = weakFragment.get() ?: return
fragment.displayTranslation(result)
}
override fun onException(callback: Any?, exception: MicroBlogException) {
Toast.makeText(context, exception.getErrorMessage(context), Toast.LENGTH_SHORT).show()
}
}
2017-08-28 11:56:09 +02:00
class StatusActivitySummaryLoader(
context: Context,
private val accountKey: UserKey,
private val statusId: String
) : FixedAsyncTaskLoader<StatusActivity>(context) {
override fun loadInBackground(): StatusActivity? {
val context = context
val details = AccountUtils.getAccountDetails(AccountManager.get(context), accountKey, true) ?: return null
if (AccountType.TWITTER != details.type) {
return null
}
val twitter = MicroBlogAPIFactory.getInstance(context, accountKey) ?: return null
val paging = Paging()
paging.setCount(10)
val activitySummary = StatusActivity(statusId, emptyList())
try {
activitySummary.retweeters = twitter.getRetweets(statusId, paging)
.filterNot { DataStoreUtils.isFilteringUser(context, it.user.key) }
.distinctBy { it.user.id }
.map { it.user.toParcelable(details) }
val countValues = ContentValues()
val status = twitter.showStatus(statusId)
activitySummary.favoriteCount = status.favoriteCount
activitySummary.retweetCount = status.retweetCount
activitySummary.replyCount = status.replyCount
countValues.put(Statuses.REPLY_COUNT, activitySummary.replyCount)
countValues.put(Statuses.FAVORITE_COUNT, activitySummary.favoriteCount)
countValues.put(Statuses.RETWEET_COUNT, activitySummary.retweetCount)
val cr = context.contentResolver
val statusWhere = Expression.and(
Expression.equalsArgs(Statuses.ACCOUNT_KEY),
Expression.or(
Expression.equalsArgs(Statuses.ID),
Expression.equalsArgs(Statuses.RETWEET_ID)))
val statusWhereArgs = arrayOf(accountKey.toString(), statusId, statusId)
cr.update(Statuses.CONTENT_URI, countValues, statusWhere.sql, statusWhereArgs)
cr.updateStatusInfo(DataStoreUtils.STATUSES_ACTIVITIES_URIS, Statuses.COLUMNS,
accountKey, statusId, ParcelableStatus::class.java) { item ->
item.favorite_count = activitySummary.favoriteCount
item.reply_count = activitySummary.replyCount
item.retweet_count = activitySummary.retweetCount
return@updateStatusInfo item
}
val pStatus = status.toParcelable(details)
cr.insert(CachedStatuses.CONTENT_URI, ObjectCursor
.valuesCreatorFrom(ParcelableStatus::class.java).create(pStatus))
return activitySummary
} catch (e: MicroBlogException) {
return null
}
}
override fun onStartLoading() {
forceLoad()
}
}
data class StatusActivity(
var statusId: String,
var retweeters: List<ParcelableUser>,
var favoriteCount: Long = 0,
var replyCount: Long = -1,
var retweetCount: Long = 0
) {
fun isStatus(status: ParcelableStatus): Boolean {
return statusId == status.retweet_id ?: status.id
}
}
data class ReadPosition(var statusId: Long, var offsetTop: Int)
private class StatusListLinearLayoutManager(context: Context, private val recyclerView: RecyclerView) : FixedLinearLayoutManager(context) {
private var spaceHeight: Int = 0
init {
orientation = LinearLayoutManager.VERTICAL
}
override fun getDecoratedMeasuredHeight(child: View): Int {
if (getItemViewType(child) == StatusDetailsAdapter.VIEW_TYPE_SPACE) {
val height = calculateSpaceItemHeight(child, StatusDetailsAdapter.VIEW_TYPE_SPACE,
StatusDetailsAdapter.VIEW_TYPE_DETAIL_STATUS)
if (height >= 0) {
return height
}
}
return super.getDecoratedMeasuredHeight(child)
}
override fun setOrientation(orientation: Int) {
if (orientation != LinearLayoutManager.VERTICAL)
throw IllegalArgumentException("Only VERTICAL orientation supported")
super.setOrientation(orientation)
}
2020-01-26 08:35:15 +01:00
override fun computeVerticalScrollExtent(state: RecyclerView.State): Int {
2017-08-28 11:56:09 +02:00
val firstPosition = findFirstVisibleItemPosition()
val lastPosition = Math.min(validScrollItemCount - 1, findLastVisibleItemPosition())
if (firstPosition < 0 || lastPosition < 0) return 0
val childCount = lastPosition - firstPosition + 1
if (childCount > 0) {
if (isSmoothScrollbarEnabled) {
var extent = childCount * 100
var view = findViewByPosition(firstPosition) ?: return 0
val top = view.top
var height = view.height
if (height > 0) {
extent += top * 100 / height
}
view = findViewByPosition(lastPosition) ?: return 0
val bottom = view.bottom
height = view.height
if (height > 0) {
extent -= (bottom - getHeight()) * 100 / height
}
return extent
} else {
return 1
}
}
return 0
}
2020-01-26 08:35:15 +01:00
override fun computeVerticalScrollOffset(state: RecyclerView.State): Int {
2017-08-28 11:56:09 +02:00
val firstPosition = findFirstVisibleItemPosition()
val lastPosition = Math.min(validScrollItemCount - 1, findLastVisibleItemPosition())
if (firstPosition < 0 || lastPosition < 0) return 0
val childCount = lastPosition - firstPosition + 1
val skippedCount = skippedScrollItemCount
if (firstPosition >= skippedCount && childCount > 0) {
if (isSmoothScrollbarEnabled) {
val view = findViewByPosition(firstPosition) ?: return 0
val top = view.top
val height = view.height
if (height > 0) {
return Math.max((firstPosition - skippedCount) * 100 - top * 100 / height, 0)
}
} else {
val count = validScrollItemCount
2017-09-20 07:04:06 +02:00
val index = when {
firstPosition == 0 -> 0
firstPosition + childCount == count -> count
else -> firstPosition + childCount / 2
2017-08-28 11:56:09 +02:00
}
return (firstPosition + childCount * (index / count.toFloat())).toInt()
}
}
return 0
}
2020-01-26 08:35:15 +01:00
override fun computeVerticalScrollRange(state: RecyclerView.State): Int {
2017-09-20 07:04:06 +02:00
return if (isSmoothScrollbarEnabled) {
Math.max(validScrollItemCount * 100, 0)
2017-08-28 11:56:09 +02:00
} else {
2017-09-20 07:04:06 +02:00
validScrollItemCount
2017-08-28 11:56:09 +02:00
}
}
private val skippedScrollItemCount: Int
get() {
val adapter = recyclerView.adapter as StatusDetailsAdapter
var skipped = 0
if (!adapter.isConversationsLoading) {
skipped += adapter.getTypeCount(StatusDetailsAdapter.ITEM_IDX_CONVERSATION_LOAD_MORE)
}
return skipped
}
private val validScrollItemCount: Int
get() {
val adapter = recyclerView.adapter as StatusDetailsAdapter
var count = 0
if (adapter.isConversationsLoading) {
count += adapter.getTypeCount(StatusDetailsAdapter.ITEM_IDX_CONVERSATION_LOAD_MORE)
}
count += adapter.getTypeCount(StatusDetailsAdapter.ITEM_IDX_CONVERSATION_ERROR)
count += adapter.getTypeCount(StatusDetailsAdapter.ITEM_IDX_CONVERSATION)
count += adapter.getTypeCount(StatusDetailsAdapter.ITEM_IDX_STATUS)
count += adapter.getTypeCount(StatusDetailsAdapter.ITEM_IDX_REPLY)
count += adapter.getTypeCount(StatusDetailsAdapter.ITEM_IDX_REPLY_ERROR)
if (adapter.isRepliesLoading) {
count += adapter.getTypeCount(StatusDetailsAdapter.ITEM_IDX_REPLY_LOAD_MORE)
}
val spaceHeight = calculateSpaceHeight()
if (spaceHeight > 0) {
count += adapter.getTypeCount(StatusDetailsAdapter.ITEM_IDX_SPACE)
}
return count
}
private fun calculateSpaceHeight(): Int {
val space = findViewByPosition(itemCount - 1) ?: return spaceHeight
spaceHeight = getDecoratedMeasuredHeight(space)
return spaceHeight
}
}
private class StatusDividerItemDecoration(
context: Context,
private val statusAdapter: StatusDetailsAdapter,
orientation: Int
) : ExtendedDividerItemDecoration(context, orientation) {
override fun isDividerEnabled(childPos: Int): Boolean {
if (childPos >= statusAdapter.itemCount || childPos < 0) return false
val itemType = statusAdapter.getItemType(childPos)
when (itemType) {
StatusDetailsAdapter.ITEM_IDX_REPLY_LOAD_MORE, StatusDetailsAdapter.ITEM_IDX_REPLY_ERROR,
StatusDetailsAdapter.ITEM_IDX_SPACE -> return false
}
return true
}
}
companion object {
// Constants
private val LOADER_ID_DETAIL_STATUS = 1
private val LOADER_ID_STATUS_CONVERSATIONS = 2
private val LOADER_ID_STATUS_ACTIVITY = 3
private val STATE_LOADED = 1
private val STATE_LOADING = 2
private val STATE_ERROR = 3
fun Bundle.toPagination(): Pagination {
val maxId = getString(EXTRA_MAX_ID)
val sinceId = getString(EXTRA_SINCE_ID)
val maxSortId = getLong(EXTRA_MAX_SORT_ID)
val sinceSortId = getLong(EXTRA_SINCE_SORT_ID)
return SinceMaxPagination().apply {
this.maxId = maxId
this.sinceId = sinceId
this.maxSortId = maxSortId
this.sinceSortId = sinceSortId
}
}
}
}