Twidere-App-Android-Twitter.../twidere/src/main/kotlin/org/mariotaku/twidere/adapter/StatusDetailsAdapter.kt

570 lines
21 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.adapter
import android.text.TextUtils
import android.text.method.LinkMovementMethod
import android.util.SparseBooleanArray
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Space
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import org.mariotaku.kpreferences.get
import org.mariotaku.ktextension.contains
import org.mariotaku.microblog.library.twitter.model.TranslationResult
import org.mariotaku.twidere.R
import org.mariotaku.twidere.adapter.iface.IGapSupportedAdapter
import org.mariotaku.twidere.adapter.iface.IItemCountsAdapter
import org.mariotaku.twidere.adapter.iface.ILoadMoreSupportAdapter
import org.mariotaku.twidere.adapter.iface.IStatusesAdapter
import org.mariotaku.twidere.constant.*
import org.mariotaku.twidere.extension.model.originalId
import org.mariotaku.twidere.extension.model.retweet_sort_id
import org.mariotaku.twidere.fragment.status.StatusFragment
import org.mariotaku.twidere.model.*
import org.mariotaku.twidere.util.StatusAdapterLinkClickHandler
import org.mariotaku.twidere.util.ThemeUtils
import org.mariotaku.twidere.util.TwidereLinkify
import org.mariotaku.twidere.view.holder.EmptyViewHolder
import org.mariotaku.twidere.view.holder.LoadIndicatorViewHolder
import org.mariotaku.twidere.view.holder.iface.IStatusViewHolder
import org.mariotaku.twidere.view.holder.status.DetailStatusViewHolder
class StatusDetailsAdapter(
val fragment: StatusFragment
) : LoadMoreSupportAdapter<RecyclerView.ViewHolder>(fragment.requireContext(), fragment.requestManager),
IStatusesAdapter<List<ParcelableStatus>>, IItemCountsAdapter {
override val twidereLinkify: TwidereLinkify
override var statusClickListener: IStatusViewHolder.StatusClickListener? = null
override val itemCounts = ItemCounts(ITEM_TYPES_SUM)
override val nameFirst = preferences[nameFirstKey]
override val mediaPreviewStyle = preferences[mediaPreviewStyleKey]
override val linkHighlightingStyle = preferences[linkHighlightOptionKey]
override val lightFont = preferences[lightFontKey]
override val mediaPreviewEnabled = preferences[mediaPreviewKey]
override val sensitiveContentEnabled = preferences[displaySensitiveContentsKey]
override val useStarsForLikes = preferences[iWantMyStarsBackKey]
private val inflater: LayoutInflater
private val cardBackgroundColor: Int
private val showCardActions = !preferences[hideCardActionsKey]
private val showCardNumbers = !preferences[hideCardNumbersKey]
private val showLinkPreview = preferences[showLinkPreviewKey]
private var recyclerView: RecyclerView? = null
private var detailMediaExpanded: Boolean = false
var status: ParcelableStatus? = null
internal set
var translationResult: TranslationResult? = null
internal set(translation) {
field = if (translation == null || status?.originalId != translation.id) {
null
} else {
translation
}
notifyDataSetChanged()
}
var statusActivity: StatusFragment.StatusActivity? = null
internal set(value) {
val status = status ?: return
if (value != null && !value.isStatus(status)) {
return
}
field = value
val statusIndex = getIndexStart(ITEM_IDX_STATUS)
notifyItemChanged(statusIndex, value)
}
var statusAccount: AccountDetails? = null
internal set
private var data: List<ParcelableStatus>? = null
private var replyError: CharSequence? = null
private var conversationError: CharSequence? = null
private var replyStart: Int = 0
private var showingActionCardPosition = RecyclerView.NO_POSITION
private val showingFullTextStates = SparseBooleanArray()
init {
setHasStableIds(true)
val context = fragment.activity
// There's always a space at the end of the list
itemCounts[ITEM_IDX_SPACE] = 1
itemCounts[ITEM_IDX_STATUS] = 1
itemCounts[ITEM_IDX_CONVERSATION_LOAD_MORE] = 1
itemCounts[ITEM_IDX_REPLY_LOAD_MORE] = 1
inflater = LayoutInflater.from(context)
cardBackgroundColor = ThemeUtils.getCardBackgroundColor(context!!,
preferences[themeBackgroundOptionKey], preferences[themeBackgroundAlphaKey])
val listener = StatusAdapterLinkClickHandler<List<ParcelableStatus>>(context, preferences)
listener.setAdapter(this)
twidereLinkify = TwidereLinkify(listener)
}
override fun getStatus(position: Int, raw: Boolean): ParcelableStatus {
when (getItemCountIndex(position, raw)) {
ITEM_IDX_CONVERSATION -> {
data?.let { data ->
var idx = position - getIndexStart(ITEM_IDX_CONVERSATION)
if (idx in data.indices) {
if (data[idx].is_filtered) {
idx++
}
return data[idx]
}
}
}
ITEM_IDX_REPLY -> {
data?.let { data ->
var idx = position - getIndexStart(ITEM_IDX_CONVERSATION) -
getTypeCount(ITEM_IDX_CONVERSATION) - getTypeCount(ITEM_IDX_STATUS) +
replyStart
if (idx in data.indices) {
if (data[idx].is_filtered) {
idx++
}
return data[idx]
}
}
}
ITEM_IDX_STATUS -> {
return status!!
}
}
throw IndexOutOfBoundsException("index: $position")
}
fun getIndexStart(index: Int): Int {
if (index == 0) return 0
return itemCounts.getItemStartPosition(index)
}
override fun getStatusId(position: Int, raw: Boolean): String {
return getStatus(position, raw).id
}
override fun getStatusTimestamp(position: Int, raw: Boolean): Long {
return getStatus(position, raw).timestamp
}
override fun getStatusPositionKey(position: Int, raw: Boolean): Long {
val status = getStatus(position, raw)
return if (status.position_key > 0) status.timestamp else getStatusTimestamp(position, raw)
}
override fun getAccountKey(position: Int, raw: Boolean) = getStatus(position, raw).account_key
override fun findStatusById(accountKey: UserKey, statusId: String): ParcelableStatus? {
if (status != null && accountKey == status!!.account_key && TextUtils.equals(statusId, status!!.id)) {
return status
}
return data?.firstOrNull { accountKey == it.account_key && TextUtils.equals(it.id, statusId) }
}
override fun getStatusCount(raw: Boolean): Int {
return getTypeCount(ITEM_IDX_CONVERSATION) + getTypeCount(ITEM_IDX_STATUS) + getTypeCount(ITEM_IDX_REPLY)
}
override fun isCardNumbersShown(position: Int): Boolean {
if (position == RecyclerView.NO_POSITION) return showCardNumbers
return showCardNumbers || showingActionCardPosition == position
}
override fun isLinkPreviewShown(position: Int): Boolean {
return showLinkPreview
}
override fun isCardActionsShown(position: Int): Boolean {
if (position == RecyclerView.NO_POSITION) return showCardActions
return showCardActions || showingActionCardPosition == position
}
override fun showCardActions(position: Int) {
if (showingActionCardPosition != RecyclerView.NO_POSITION) {
notifyItemChanged(showingActionCardPosition)
}
showingActionCardPosition = position
if (position != RecyclerView.NO_POSITION) {
notifyItemChanged(position)
}
}
override fun isFullTextVisible(position: Int): Boolean {
return showingFullTextStates.get(position)
}
override fun setFullTextVisible(position: Int, visible: Boolean) {
showingFullTextStates.put(position, visible)
if (position != RecyclerView.NO_POSITION) {
notifyItemChanged(position)
}
}
override fun setData(data: List<ParcelableStatus>?): Boolean {
val status = this.status ?: return false
val changed = this.data != data
this.data = data
if (data == null || data.isEmpty()) {
setTypeCount(ITEM_IDX_CONVERSATION, 0)
setTypeCount(ITEM_IDX_REPLY, 0)
replyStart = -1
} else {
val sortId = if (status.is_retweet) {
status.retweet_sort_id
} else {
status.sort_id
}
var conversationCount = 0
var replyCount = 0
var replyStart = -1
data.forEachIndexed { i, item ->
if (item.sort_id < sortId) {
if (!item.is_filtered) {
conversationCount++
}
} else if (status.id == item.id) {
this.status = item
} else if (item.sort_id > sortId) {
if (replyStart < 0) {
replyStart = i
}
if (!item.is_filtered) {
replyCount++
}
}
}
setTypeCount(ITEM_IDX_CONVERSATION, conversationCount)
setTypeCount(ITEM_IDX_REPLY, replyCount)
this.replyStart = replyStart
}
notifyDataSetChanged()
updateItemDecoration()
return changed
}
override val showAccountsColor: Boolean
get() = false
var isDetailMediaExpanded: Boolean
get() {
if (detailMediaExpanded) return true
if (mediaPreviewEnabled) {
val status = this.status
return status != null && (sensitiveContentEnabled || !status.is_possibly_sensitive)
}
return false
}
set(expanded) {
detailMediaExpanded = expanded
notifyDataSetChanged()
updateItemDecoration()
}
override fun isGapItem(position: Int): Boolean {
return false
}
override val gapClickListener: IGapSupportedAdapter.GapClickListener?
get() = statusClickListener
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
when (viewType) {
VIEW_TYPE_DETAIL_STATUS -> {
val view = inflater.inflate(R.layout.header_status, parent, false)
view.setBackgroundColor(cardBackgroundColor)
return DetailStatusViewHolder(this, view)
}
VIEW_TYPE_LIST_STATUS -> {
return ListParcelableStatusesAdapter.createStatusViewHolder(this, inflater, parent)
}
VIEW_TYPE_CONVERSATION_LOAD_INDICATOR, VIEW_TYPE_REPLIES_LOAD_INDICATOR -> {
val view = inflater.inflate(R.layout.list_item_load_indicator, parent,
false)
return LoadIndicatorViewHolder(view)
}
VIEW_TYPE_SPACE -> {
return EmptyViewHolder(Space(context))
}
VIEW_TYPE_REPLY_ERROR -> {
val view = inflater.inflate(R.layout.adapter_item_status_error, parent,
false)
return StatusErrorItemViewHolder(view)
}
}
return EmptyViewHolder(View(context))
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: List<Any>) {
var handled = false
when (holder.itemViewType) {
VIEW_TYPE_DETAIL_STATUS -> {
holder as DetailStatusViewHolder
payloads.forEach {
when (it) {
is StatusFragment.StatusActivity -> {
holder.updateStatusActivity(it)
}
is ParcelableStatus -> {
holder.displayStatus(statusAccount, status, statusActivity,
translationResult)
}
}
handled = true
}
}
}
if (handled) return
super.onBindViewHolder(holder, position, payloads)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder.itemViewType) {
VIEW_TYPE_DETAIL_STATUS -> {
val status = getStatus(position)
val detailHolder = holder as DetailStatusViewHolder
detailHolder.displayStatus(statusAccount, status, statusActivity, translationResult)
}
VIEW_TYPE_LIST_STATUS -> {
val status = getStatus(position)
val statusHolder = holder as IStatusViewHolder
// Display 'in reply to' for first item
// useful to indicate whether first tweet has reply or not
// We only display that indicator for first conversation item
val itemType = getItemType(position)
val displayInReplyTo = itemType == ITEM_IDX_CONVERSATION && position - getItemTypeStart(position) == 0
statusHolder.display(status = status, displayInReplyTo = displayInReplyTo)
}
VIEW_TYPE_REPLY_ERROR -> {
val errorHolder = holder as StatusErrorItemViewHolder
errorHolder.showError(replyError!!)
}
VIEW_TYPE_CONVERSATION_ERROR -> {
val errorHolder = holder as StatusErrorItemViewHolder
errorHolder.showError(conversationError!!)
}
VIEW_TYPE_CONVERSATION_LOAD_INDICATOR -> {
val indicatorHolder = holder as LoadIndicatorViewHolder
indicatorHolder.setLoadProgressVisible(isConversationsLoading)
}
VIEW_TYPE_REPLIES_LOAD_INDICATOR -> {
val indicatorHolder = holder as LoadIndicatorViewHolder
indicatorHolder.setLoadProgressVisible(isRepliesLoading)
}
}
}
override fun getItemViewType(position: Int): Int {
return getItemViewTypeByItemType(getItemType(position))
}
override fun addGapLoadingId(id: ObjectId) {
}
override fun removeGapLoadingId(id: ObjectId) {
}
private fun getItemViewTypeByItemType(type: Int): Int {
when (type) {
ITEM_IDX_CONVERSATION, ITEM_IDX_REPLY -> return VIEW_TYPE_LIST_STATUS
ITEM_IDX_CONVERSATION_LOAD_MORE -> return VIEW_TYPE_CONVERSATION_LOAD_INDICATOR
ITEM_IDX_REPLY_LOAD_MORE -> return VIEW_TYPE_REPLIES_LOAD_INDICATOR
ITEM_IDX_STATUS -> return VIEW_TYPE_DETAIL_STATUS
ITEM_IDX_SPACE -> return VIEW_TYPE_SPACE
ITEM_IDX_REPLY_ERROR -> return VIEW_TYPE_REPLY_ERROR
ITEM_IDX_CONVERSATION_ERROR -> return VIEW_TYPE_CONVERSATION_ERROR
}
throw IllegalStateException()
}
private fun getItemCountIndex(position: Int, raw: Boolean): Int {
return itemCounts.getItemCountIndex(position)
}
fun getItemType(position: Int): Int {
var typeStart = 0
for (type in 0 until ITEM_TYPES_SUM) {
val typeCount = getTypeCount(type)
val typeEnd = typeStart + typeCount
if (position in typeStart until typeEnd) return type
typeStart = typeEnd
}
throw IllegalStateException("Unknown position $position")
}
fun getItemTypeStart(position: Int): Int {
var typeStart = 0
for (type in 0 until ITEM_TYPES_SUM) {
val typeCount = getTypeCount(type)
val typeEnd = typeStart + typeCount
if (position in typeStart until typeEnd) return typeStart
typeStart = typeEnd
}
throw IllegalStateException()
}
override fun getItemId(position: Int): Long {
val countIndex = getItemCountIndex(position)
when (countIndex) {
ITEM_IDX_CONVERSATION, ITEM_IDX_STATUS, ITEM_IDX_REPLY -> {
val status = getStatus(position)
val hashCode = ParcelableStatus.calculateHashCode(status.account_key, status.id)
return (countIndex.toLong() shl 32) or hashCode.toLong()
}
}
val countPos = (position - getItemStartPosition(countIndex)).toLong()
return (countIndex.toLong() shl 32) or countPos
}
override fun getItemCount(): Int {
if (status == null) return 0
return itemCounts.itemCount
}
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
super.onAttachedToRecyclerView(recyclerView)
this.recyclerView = recyclerView
}
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
super.onDetachedFromRecyclerView(recyclerView)
this.recyclerView = null
}
private fun setTypeCount(idx: Int, size: Int) {
itemCounts[idx] = size
notifyDataSetChanged()
}
fun getTypeCount(idx: Int): Int {
return itemCounts[idx]
}
fun setReplyError(error: CharSequence?) {
replyError = error
setTypeCount(ITEM_IDX_REPLY_ERROR, if (error != null) 1 else 0)
updateItemDecoration()
}
fun setConversationError(error: CharSequence?) {
conversationError = error
setTypeCount(ITEM_IDX_CONVERSATION_ERROR, if (error != null) 1 else 0)
updateItemDecoration()
}
fun setStatus(status: ParcelableStatus, account: AccountDetails?): Boolean {
val oldStatus = this.status
val oldAccount = this.statusAccount
val changed = oldStatus != status && oldAccount != account
this.status = status
this.statusAccount = account
if (changed) {
notifyDataSetChanged()
updateItemDecoration()
} else {
val statusIndex = getIndexStart(ITEM_IDX_STATUS)
notifyItemChanged(statusIndex, status)
}
return changed
}
fun updateItemDecoration() {
if (recyclerView == null) return
}
fun getFirstPositionOfItem(itemIdx: Int): Int {
var position = 0
for (i in 0 until ITEM_TYPES_SUM) {
if (itemIdx == i) return position
position += getTypeCount(i)
}
return RecyclerView.NO_POSITION
}
fun getData(): List<ParcelableStatus>? {
return data
}
var isConversationsLoading: Boolean
get() = ILoadMoreSupportAdapter.START in loadMoreIndicatorPosition
set(loading) {
loadMoreIndicatorPosition = if (loading) {
loadMoreIndicatorPosition or ILoadMoreSupportAdapter.START
} else {
loadMoreIndicatorPosition and ILoadMoreSupportAdapter.START.inv()
}
updateItemDecoration()
}
var isRepliesLoading: Boolean
get() = ILoadMoreSupportAdapter.END in loadMoreIndicatorPosition
set(loading) {
loadMoreIndicatorPosition = if (loading) {
loadMoreIndicatorPosition or ILoadMoreSupportAdapter.END
} else {
loadMoreIndicatorPosition and ILoadMoreSupportAdapter.END.inv()
}
updateItemDecoration()
}
class StatusErrorItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val textView = itemView.findViewById<TextView>(android.R.id.text1)
init {
textView.movementMethod = LinkMovementMethod.getInstance()
textView.linksClickable = true
}
fun showError(text: CharSequence) {
textView.text = text
}
}
companion object {
const val VIEW_TYPE_LIST_STATUS = 0
const val VIEW_TYPE_DETAIL_STATUS = 1
const val VIEW_TYPE_CONVERSATION_LOAD_INDICATOR = 2
const val VIEW_TYPE_REPLIES_LOAD_INDICATOR = 3
const val VIEW_TYPE_REPLY_ERROR = 4
const val VIEW_TYPE_CONVERSATION_ERROR = 5
const val VIEW_TYPE_SPACE = 6
const val VIEW_TYPE_EMPTY = 7
const val ITEM_IDX_CONVERSATION_LOAD_MORE = 0
const val ITEM_IDX_CONVERSATION_ERROR = 1
const val ITEM_IDX_CONVERSATION = 2
const val ITEM_IDX_STATUS = 3
const val ITEM_IDX_REPLY = 4
const val ITEM_IDX_REPLY_ERROR = 5
const val ITEM_IDX_REPLY_LOAD_MORE = 6
const val ITEM_IDX_SPACE = 7
const val ITEM_TYPES_SUM = 8
}
}