refactor: Convert Java viewholders to Kotlin (#200)
This commit is contained in:
parent
0598c0e667
commit
2f3851acee
|
@ -740,7 +740,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="335"
|
||||
line="337"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
|
@ -751,7 +751,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="392"
|
||||
line="394"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
|
@ -762,7 +762,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="565"
|
||||
line="567"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
|
@ -773,7 +773,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="780"
|
||||
line="782"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1415,17 +1415,6 @@
|
|||
column="1"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedResources"
|
||||
message="The resource `R.color.seed` appears to be unused"
|
||||
errorLine1=" <color name="seed">#006382</color>"
|
||||
errorLine2=" ~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/colors.xml"
|
||||
line="4"
|
||||
column="12"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedResources"
|
||||
message="The resource `R.color.md_theme_light_shadow` appears to be unused"
|
||||
|
@ -1796,7 +1785,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="165"
|
||||
line="167"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1807,7 +1796,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="228"
|
||||
line="230"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1818,7 +1807,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="268"
|
||||
line="270"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1829,7 +1818,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="269"
|
||||
line="271"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1840,7 +1829,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="294"
|
||||
line="296"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1851,7 +1840,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="325"
|
||||
line="327"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1862,7 +1851,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="405"
|
||||
line="407"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1873,7 +1862,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="462"
|
||||
line="464"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1884,7 +1873,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="513"
|
||||
line="515"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1895,7 +1884,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="517"
|
||||
line="519"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1906,7 +1895,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="518"
|
||||
line="520"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1917,7 +1906,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="519"
|
||||
line="521"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1928,7 +1917,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="520"
|
||||
line="522"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1939,7 +1928,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="521"
|
||||
line="523"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1950,7 +1939,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="522"
|
||||
line="524"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1961,7 +1950,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="523"
|
||||
line="525"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1972,7 +1961,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="524"
|
||||
line="526"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1983,7 +1972,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="526"
|
||||
line="528"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1994,7 +1983,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="604"
|
||||
line="606"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -2005,7 +1994,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="605"
|
||||
line="607"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -2016,7 +2005,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="620"
|
||||
line="622"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -2027,7 +2016,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="622"
|
||||
line="624"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -2038,7 +2027,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="623"
|
||||
line="625"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -2049,7 +2038,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="626"
|
||||
line="628"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -2060,7 +2049,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="671"
|
||||
line="673"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -2071,7 +2060,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="719"
|
||||
line="721"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -2082,7 +2071,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="725"
|
||||
line="727"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -2093,7 +2082,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="768"
|
||||
line="770"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -2104,7 +2093,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="802"
|
||||
line="804"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -3703,6 +3692,39 @@
|
|||
column="48"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="SyntheticAccessor"
|
||||
message="Access to `private` method `getContentWarningDescription` of class `Companion` requires synthetic accessor"
|
||||
errorLine1=" getContentWarningDescription(context, status),"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/adapter/StatusBaseViewHolder.kt"
|
||||
line="772"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="SyntheticAccessor"
|
||||
message="Access to `private` method `getReblogDescription` of class `Companion` requires synthetic accessor"
|
||||
errorLine1=" getReblogDescription(context, status),"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/adapter/StatusBaseViewHolder.kt"
|
||||
line="776"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="SyntheticAccessor"
|
||||
message="Access to `private` method `getMediaDescription` of class `Companion` requires synthetic accessor"
|
||||
errorLine1=" getMediaDescription(context, status),"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/adapter/StatusBaseViewHolder.kt"
|
||||
line="781"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="SyntheticAccessor"
|
||||
message="Access to `private` method `viewdata` of class `StatusViewHolder` requires synthetic accessor"
|
||||
|
@ -3886,18 +3908,18 @@
|
|||
errorLine2=" ~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/fragment/ViewVideoFragment.kt"
|
||||
line="140"
|
||||
line="152"
|
||||
column="32"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="SyntheticAccessor"
|
||||
message="Access to `private` method `hideToolbarAfterDelay` of class `ViewVideoFragment` requires synthetic accessor"
|
||||
errorLine1=" hideToolbarAfterDelay(TOOLBAR_HIDE_DELAY_MS)"
|
||||
errorLine1=" hideToolbarAfterDelay()"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/fragment/ViewVideoFragment.kt"
|
||||
line="221"
|
||||
line="232"
|
||||
column="21"/>
|
||||
</issue>
|
||||
|
||||
|
@ -4036,7 +4058,7 @@
|
|||
errorLine2=" ~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/layout/activity_account.xml"
|
||||
line="131"
|
||||
line="129"
|
||||
column="22"/>
|
||||
</issue>
|
||||
|
||||
|
@ -4047,7 +4069,7 @@
|
|||
errorLine2=" ~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/layout/activity_account.xml"
|
||||
line="246"
|
||||
line="242"
|
||||
column="22"/>
|
||||
</issue>
|
||||
|
||||
|
@ -4058,7 +4080,7 @@
|
|||
errorLine2=" ~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/layout/activity_account.xml"
|
||||
line="282"
|
||||
line="278"
|
||||
column="26"/>
|
||||
</issue>
|
||||
|
||||
|
@ -4069,7 +4091,7 @@
|
|||
errorLine2=" ~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/layout/activity_account.xml"
|
||||
line="309"
|
||||
line="305"
|
||||
column="26"/>
|
||||
</issue>
|
||||
|
||||
|
@ -4080,7 +4102,7 @@
|
|||
errorLine2=" ~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/layout/activity_account.xml"
|
||||
line="324"
|
||||
line="320"
|
||||
column="26"/>
|
||||
</issue>
|
||||
|
||||
|
@ -4091,7 +4113,7 @@
|
|||
errorLine2=" ~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/layout/activity_account.xml"
|
||||
line="351"
|
||||
line="347"
|
||||
column="26"/>
|
||||
</issue>
|
||||
|
||||
|
@ -4102,7 +4124,7 @@
|
|||
errorLine2=" ~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/layout/activity_account.xml"
|
||||
line="382"
|
||||
line="378"
|
||||
column="26"/>
|
||||
</issue>
|
||||
|
||||
|
@ -4113,7 +4135,7 @@
|
|||
errorLine2=" ~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/layout/activity_account.xml"
|
||||
line="412"
|
||||
line="408"
|
||||
column="26"/>
|
||||
</issue>
|
||||
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* Copyright 2023 Pachli Association
|
||||
*
|
||||
* This file is a part of Pachli.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* Pachli 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 Pachli; if not,
|
||||
* see <http://www.gnu.org/licenses>.
|
||||
*/
|
||||
|
||||
package app.pachli.adapter
|
||||
|
||||
import android.view.View
|
||||
import app.pachli.R
|
||||
import app.pachli.databinding.ItemStatusWrapperBinding
|
||||
import app.pachli.entity.Filter
|
||||
import app.pachli.interfaces.StatusActionListener
|
||||
import app.pachli.util.StatusDisplayOptions
|
||||
import app.pachli.viewdata.StatusViewData
|
||||
|
||||
open class FilterableStatusViewHolder(
|
||||
private val binding: ItemStatusWrapperBinding,
|
||||
) : StatusViewHolder(binding.statusContainer, binding.root) {
|
||||
|
||||
override fun setupWithStatus(
|
||||
status: StatusViewData,
|
||||
listener: StatusActionListener,
|
||||
statusDisplayOptions: StatusDisplayOptions,
|
||||
payloads: Any?,
|
||||
) {
|
||||
super.setupWithStatus(status, listener, statusDisplayOptions, payloads)
|
||||
setupFilterPlaceholder(status, listener)
|
||||
}
|
||||
|
||||
private fun setupFilterPlaceholder(
|
||||
status: StatusViewData,
|
||||
listener: StatusActionListener,
|
||||
) {
|
||||
if (status.filterAction !== Filter.Action.WARN) {
|
||||
showFilteredPlaceholder(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Shouldn't be necessary given the previous test against getFilterAction(),
|
||||
// but guards against a possible NPE. See the TODO in StatusViewData.filterAction
|
||||
// for more details.
|
||||
val filterResults = status.actionable.filtered
|
||||
if (filterResults.isNullOrEmpty()) {
|
||||
showFilteredPlaceholder(false)
|
||||
return
|
||||
}
|
||||
var matchedFilter: Filter? = null
|
||||
for ((filter) in filterResults) {
|
||||
if (filter.action === Filter.Action.WARN) {
|
||||
matchedFilter = filter
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Guard against a possible NPE
|
||||
if (matchedFilter == null) {
|
||||
showFilteredPlaceholder(false)
|
||||
return
|
||||
}
|
||||
showFilteredPlaceholder(true)
|
||||
binding.statusFilteredPlaceholder.statusFilterLabel.text = context.getString(
|
||||
R.string.status_filter_placeholder_label_format,
|
||||
matchedFilter.title,
|
||||
)
|
||||
binding.statusFilteredPlaceholder.statusFilterShowAnyway.setOnClickListener {
|
||||
listener.clearWarningAction(
|
||||
bindingAdapterPosition,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showFilteredPlaceholder(show: Boolean) {
|
||||
binding.statusContainer.root.visibility = if (show) View.GONE else View.VISIBLE
|
||||
binding.statusFilteredPlaceholder.root.visibility = if (show) View.VISIBLE else View.GONE
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,915 @@
|
|||
package app.pachli.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.text.TextUtils
|
||||
import android.text.format.DateUtils
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import app.pachli.R
|
||||
import app.pachli.ViewMediaActivity.Companion.newSingleImageIntent
|
||||
import app.pachli.entity.Attachment
|
||||
import app.pachli.entity.Emoji
|
||||
import app.pachli.entity.PreviewCardKind
|
||||
import app.pachli.entity.Status
|
||||
import app.pachli.entity.description
|
||||
import app.pachli.interfaces.StatusActionListener
|
||||
import app.pachli.util.AbsoluteTimeFormatter
|
||||
import app.pachli.util.CardViewMode
|
||||
import app.pachli.util.CompositeWithOpaqueBackground
|
||||
import app.pachli.util.StatusDisplayOptions
|
||||
import app.pachli.util.aspectRatios
|
||||
import app.pachli.util.decodeBlurHash
|
||||
import app.pachli.util.emojify
|
||||
import app.pachli.util.expandTouchSizeToFillRow
|
||||
import app.pachli.util.formatNumber
|
||||
import app.pachli.util.getFormattedDescription
|
||||
import app.pachli.util.getRelativeTimeSpanString
|
||||
import app.pachli.util.hide
|
||||
import app.pachli.util.loadAvatar
|
||||
import app.pachli.util.setClickableMentions
|
||||
import app.pachli.util.setClickableText
|
||||
import app.pachli.view.MediaPreviewImageView
|
||||
import app.pachli.view.MediaPreviewLayout
|
||||
import app.pachli.view.PollView
|
||||
import app.pachli.view.PreviewCardView
|
||||
import app.pachli.viewdata.PollViewData.Companion.from
|
||||
import app.pachli.viewdata.StatusViewData
|
||||
import at.connyduck.sparkbutton.SparkButton
|
||||
import at.connyduck.sparkbutton.helpers.Utils
|
||||
import com.bumptech.glide.Glide
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import java.text.NumberFormat
|
||||
import java.util.Date
|
||||
|
||||
abstract class StatusBaseViewHolder protected constructor(itemView: View) :
|
||||
RecyclerView.ViewHolder(itemView) {
|
||||
object Key {
|
||||
const val KEY_CREATED = "created"
|
||||
}
|
||||
|
||||
protected val context: Context
|
||||
private val displayName: TextView
|
||||
private val username: TextView
|
||||
private val replyButton: ImageButton
|
||||
private val replyCountLabel: TextView?
|
||||
private val reblogButton: SparkButton?
|
||||
private val favouriteButton: SparkButton
|
||||
private val bookmarkButton: SparkButton
|
||||
private val moreButton: ImageButton
|
||||
private val mediaContainer: ConstraintLayout
|
||||
protected val mediaPreview: MediaPreviewLayout
|
||||
private val sensitiveMediaWarning: TextView
|
||||
private val sensitiveMediaShow: View
|
||||
protected val mediaLabels: Array<TextView>
|
||||
private val mediaDescriptions: Array<CharSequence?>
|
||||
private val contentWarningButton: MaterialButton
|
||||
private val avatarInset: ImageView
|
||||
val avatar: ImageView
|
||||
val metaInfo: TextView
|
||||
val content: TextView
|
||||
private val contentWarningDescription: TextView
|
||||
private val pollView: PollView
|
||||
private val cardView: PreviewCardView?
|
||||
private val filteredPlaceholder: LinearLayout?
|
||||
private val filteredPlaceholderLabel: TextView?
|
||||
private val filteredPlaceholderShowButton: Button?
|
||||
private val statusContainer: ConstraintLayout?
|
||||
private val numberFormat = NumberFormat.getNumberInstance()
|
||||
private val absoluteTimeFormatter = AbsoluteTimeFormatter()
|
||||
protected val avatarRadius48dp: Int
|
||||
private val avatarRadius36dp: Int
|
||||
private val avatarRadius24dp: Int
|
||||
private val mediaPreviewUnloaded: Drawable
|
||||
|
||||
init {
|
||||
context = itemView.context
|
||||
displayName = itemView.findViewById(R.id.status_display_name)
|
||||
username = itemView.findViewById(R.id.status_username)
|
||||
metaInfo = itemView.findViewById(R.id.status_meta_info)
|
||||
content = itemView.findViewById(R.id.status_content)
|
||||
avatar = itemView.findViewById(R.id.status_avatar)
|
||||
replyButton = itemView.findViewById(R.id.status_reply)
|
||||
replyCountLabel = itemView.findViewById(R.id.status_replies)
|
||||
reblogButton = itemView.findViewById(R.id.status_inset)
|
||||
favouriteButton = itemView.findViewById(R.id.status_favourite)
|
||||
bookmarkButton = itemView.findViewById(R.id.status_bookmark)
|
||||
moreButton = itemView.findViewById(R.id.status_more)
|
||||
mediaContainer = itemView.findViewById(R.id.status_media_preview_container)
|
||||
mediaContainer.clipToOutline = true
|
||||
mediaPreview = itemView.findViewById(R.id.status_media_preview)
|
||||
sensitiveMediaWarning = itemView.findViewById(R.id.status_sensitive_media_warning)
|
||||
sensitiveMediaShow = itemView.findViewById(R.id.status_sensitive_media_button)
|
||||
mediaLabels = arrayOf(
|
||||
itemView.findViewById(R.id.status_media_label_0),
|
||||
itemView.findViewById(R.id.status_media_label_1),
|
||||
itemView.findViewById(R.id.status_media_label_2),
|
||||
itemView.findViewById(R.id.status_media_label_3),
|
||||
)
|
||||
mediaDescriptions = arrayOfNulls(mediaLabels.size)
|
||||
contentWarningDescription = itemView.findViewById(R.id.status_content_warning_description)
|
||||
contentWarningButton = itemView.findViewById(R.id.status_content_warning_button)
|
||||
avatarInset = itemView.findViewById(R.id.status_avatar_inset)
|
||||
pollView = itemView.findViewById(R.id.status_poll)
|
||||
cardView = itemView.findViewById(R.id.status_card_view)
|
||||
filteredPlaceholder = itemView.findViewById(R.id.status_filtered_placeholder)
|
||||
filteredPlaceholderLabel = itemView.findViewById(R.id.status_filter_label)
|
||||
filteredPlaceholderShowButton = itemView.findViewById(R.id.status_filter_show_anyway)
|
||||
statusContainer = itemView.findViewById(R.id.status_container)
|
||||
avatarRadius48dp = context.resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp)
|
||||
avatarRadius36dp = context.resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp)
|
||||
avatarRadius24dp = context.resources.getDimensionPixelSize(R.dimen.avatar_radius_24dp)
|
||||
mediaPreviewUnloaded =
|
||||
ColorDrawable(MaterialColors.getColor(itemView, android.R.attr.textColorLink))
|
||||
(itemView as ViewGroup).expandTouchSizeToFillRow(
|
||||
listOfNotNull(
|
||||
replyButton,
|
||||
reblogButton,
|
||||
favouriteButton,
|
||||
bookmarkButton,
|
||||
moreButton,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
protected fun setDisplayName(
|
||||
name: String,
|
||||
customEmojis: List<Emoji>?,
|
||||
statusDisplayOptions: StatusDisplayOptions,
|
||||
) {
|
||||
displayName.text = name.emojify(customEmojis, displayName, statusDisplayOptions.animateEmojis)
|
||||
}
|
||||
|
||||
protected fun setUsername(name: String) {
|
||||
username.text = context.getString(R.string.post_username_format, name)
|
||||
}
|
||||
|
||||
fun toggleContentWarning() {
|
||||
contentWarningButton.performClick()
|
||||
}
|
||||
|
||||
protected fun setSpoilerAndContent(
|
||||
status: StatusViewData,
|
||||
statusDisplayOptions: StatusDisplayOptions,
|
||||
listener: StatusActionListener,
|
||||
) {
|
||||
val (_, _, _, _, _, _, _, _, _, emojis) = status.actionable
|
||||
val spoilerText = status.spoilerText
|
||||
val sensitive = !TextUtils.isEmpty(spoilerText)
|
||||
val expanded = status.isExpanded
|
||||
if (sensitive) {
|
||||
val emojiSpoiler = spoilerText.emojify(
|
||||
emojis,
|
||||
contentWarningDescription,
|
||||
statusDisplayOptions.animateEmojis,
|
||||
)
|
||||
contentWarningDescription.text = emojiSpoiler
|
||||
contentWarningDescription.visibility = View.VISIBLE
|
||||
contentWarningButton.visibility = View.VISIBLE
|
||||
setContentWarningButtonText(expanded)
|
||||
contentWarningButton.setOnClickListener {
|
||||
toggleExpandedState(
|
||||
true,
|
||||
!expanded,
|
||||
status,
|
||||
statusDisplayOptions,
|
||||
listener,
|
||||
)
|
||||
}
|
||||
setTextVisible(true, expanded, status, statusDisplayOptions, listener)
|
||||
return
|
||||
}
|
||||
|
||||
contentWarningDescription.visibility = View.GONE
|
||||
contentWarningButton.visibility = View.GONE
|
||||
setTextVisible(
|
||||
sensitive = false,
|
||||
expanded = true,
|
||||
status = status,
|
||||
statusDisplayOptions = statusDisplayOptions,
|
||||
listener = listener,
|
||||
)
|
||||
}
|
||||
|
||||
private fun setContentWarningButtonText(expanded: Boolean) {
|
||||
if (expanded) {
|
||||
contentWarningButton.setText(R.string.post_content_warning_show_less)
|
||||
} else {
|
||||
contentWarningButton.setText(R.string.post_content_warning_show_more)
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun toggleExpandedState(
|
||||
sensitive: Boolean,
|
||||
expanded: Boolean,
|
||||
status: StatusViewData,
|
||||
statusDisplayOptions: StatusDisplayOptions,
|
||||
listener: StatusActionListener,
|
||||
) {
|
||||
contentWarningDescription.invalidate()
|
||||
val adapterPosition = bindingAdapterPosition
|
||||
if (adapterPosition != RecyclerView.NO_POSITION) {
|
||||
listener.onExpandedChange(expanded, adapterPosition)
|
||||
}
|
||||
setContentWarningButtonText(expanded)
|
||||
setTextVisible(sensitive, expanded, status, statusDisplayOptions, listener)
|
||||
setupCard(
|
||||
status,
|
||||
expanded,
|
||||
statusDisplayOptions.cardViewMode,
|
||||
statusDisplayOptions,
|
||||
listener,
|
||||
)
|
||||
}
|
||||
|
||||
private fun setTextVisible(
|
||||
sensitive: Boolean,
|
||||
expanded: Boolean,
|
||||
status: StatusViewData,
|
||||
statusDisplayOptions: StatusDisplayOptions,
|
||||
listener: StatusActionListener,
|
||||
) {
|
||||
val (_, _, _, _, _, _, _, _, _, emojis, _, _, _, _, _, _, _, _, _, _, mentions, tags, _, _, _, poll) = status.actionable
|
||||
val content = status.content
|
||||
if (expanded) {
|
||||
val emojifiedText =
|
||||
content.emojify(emojis, this.content, statusDisplayOptions.animateEmojis)
|
||||
setClickableText(this.content, emojifiedText, mentions, tags, listener)
|
||||
for (i in mediaLabels.indices) {
|
||||
updateMediaLabel(i, sensitive, true)
|
||||
}
|
||||
|
||||
poll?.let {
|
||||
pollView.bind(
|
||||
from(it),
|
||||
emojis,
|
||||
statusDisplayOptions,
|
||||
numberFormat,
|
||||
absoluteTimeFormatter,
|
||||
) { choices ->
|
||||
val position = bindingAdapterPosition
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
choices?.let { listener.onVoteInPoll(position, it) }
|
||||
?: listener.onViewThread(position)
|
||||
}
|
||||
}
|
||||
} ?: pollView.hide()
|
||||
} else {
|
||||
pollView.hide()
|
||||
setClickableMentions(this.content, mentions, listener)
|
||||
}
|
||||
if (TextUtils.isEmpty(this.content.text)) {
|
||||
this.content.visibility = View.GONE
|
||||
} else {
|
||||
this.content.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
private fun setAvatar(
|
||||
url: String,
|
||||
rebloggedUrl: String?,
|
||||
isBot: Boolean,
|
||||
statusDisplayOptions: StatusDisplayOptions,
|
||||
) {
|
||||
val avatarRadius: Int
|
||||
if (TextUtils.isEmpty(rebloggedUrl)) {
|
||||
avatar.setPaddingRelative(0, 0, 0, 0)
|
||||
if (statusDisplayOptions.showBotOverlay && isBot) {
|
||||
avatarInset.visibility = View.VISIBLE
|
||||
Glide.with(avatarInset)
|
||||
.load(R.drawable.bot_badge)
|
||||
.into(avatarInset)
|
||||
} else {
|
||||
avatarInset.visibility = View.GONE
|
||||
}
|
||||
avatarRadius = avatarRadius48dp
|
||||
} else {
|
||||
val padding = Utils.dpToPx(context, 12)
|
||||
avatar.setPaddingRelative(0, 0, padding, padding)
|
||||
avatarInset.visibility = View.VISIBLE
|
||||
avatarInset.background = null
|
||||
loadAvatar(
|
||||
rebloggedUrl,
|
||||
avatarInset,
|
||||
avatarRadius24dp,
|
||||
statusDisplayOptions.animateAvatars,
|
||||
null,
|
||||
)
|
||||
avatarRadius = avatarRadius36dp
|
||||
}
|
||||
loadAvatar(
|
||||
url,
|
||||
avatar,
|
||||
avatarRadius,
|
||||
statusDisplayOptions.animateAvatars,
|
||||
listOf(CompositeWithOpaqueBackground(avatar)),
|
||||
)
|
||||
}
|
||||
|
||||
protected open fun setMetaData(
|
||||
statusViewData: StatusViewData,
|
||||
statusDisplayOptions: StatusDisplayOptions,
|
||||
listener: StatusActionListener,
|
||||
) {
|
||||
val (_, _, _, _, _, _, _, createdAt, editedAt) = statusViewData.actionable
|
||||
var timestampText: String
|
||||
timestampText = if (statusDisplayOptions.useAbsoluteTime) {
|
||||
absoluteTimeFormatter.format(createdAt, true)
|
||||
} else {
|
||||
val then = createdAt.time
|
||||
val now = System.currentTimeMillis()
|
||||
getRelativeTimeSpanString(context, then, now)
|
||||
}
|
||||
editedAt?.also {
|
||||
timestampText = context.getString(
|
||||
R.string.post_timestamp_with_edited_indicator,
|
||||
timestampText,
|
||||
)
|
||||
}
|
||||
metaInfo.text = timestampText
|
||||
}
|
||||
|
||||
private fun getCreatedAtDescription(
|
||||
createdAt: Date?,
|
||||
statusDisplayOptions: StatusDisplayOptions,
|
||||
): CharSequence {
|
||||
return if (statusDisplayOptions.useAbsoluteTime) {
|
||||
absoluteTimeFormatter.format(createdAt, true)
|
||||
} else {
|
||||
/* This one is for screen-readers. Frequently, they would mispronounce timestamps like "17m"
|
||||
* as 17 meters instead of minutes. */
|
||||
createdAt?.let {
|
||||
val then = createdAt.time
|
||||
val now = System.currentTimeMillis()
|
||||
DateUtils.getRelativeTimeSpanString(
|
||||
then,
|
||||
now,
|
||||
DateUtils.SECOND_IN_MILLIS,
|
||||
DateUtils.FORMAT_ABBREV_RELATIVE,
|
||||
)
|
||||
} ?: "? minutes"
|
||||
}
|
||||
}
|
||||
|
||||
protected fun setIsReply(isReply: Boolean) {
|
||||
val drawable = if (isReply) R.drawable.ic_reply_all_24dp else R.drawable.ic_reply_24dp
|
||||
replyButton.setImageResource(drawable)
|
||||
}
|
||||
|
||||
private fun setReplyCount(repliesCount: Int, fullStats: Boolean) {
|
||||
// This label only exists in the non-detailed view (to match the web ui)
|
||||
replyCountLabel ?: return
|
||||
|
||||
if (fullStats) {
|
||||
replyCountLabel.text = formatNumber(repliesCount.toLong(), 1000)
|
||||
return
|
||||
}
|
||||
|
||||
// Show "0", "1", or "1+" for replies otherwise, so the user knows if there is a thread
|
||||
// that they can click through to read.
|
||||
replyCountLabel.text =
|
||||
if (repliesCount > 1) context.getString(R.string.status_count_one_plus) else repliesCount.toString()
|
||||
}
|
||||
|
||||
private fun setReblogged(reblogged: Boolean) {
|
||||
reblogButton!!.isChecked = reblogged
|
||||
}
|
||||
|
||||
// This should only be called after setReblogged, in order to override the tint correctly.
|
||||
private fun setRebloggingEnabled(enabled: Boolean, visibility: Status.Visibility) {
|
||||
reblogButton!!.isEnabled = enabled && visibility !== Status.Visibility.PRIVATE
|
||||
if (enabled) {
|
||||
val inactiveId: Int
|
||||
val activeId: Int
|
||||
if (visibility === Status.Visibility.PRIVATE) {
|
||||
inactiveId = R.drawable.ic_reblog_private_24dp
|
||||
activeId = R.drawable.ic_reblog_private_active_24dp
|
||||
} else {
|
||||
inactiveId = R.drawable.ic_reblog_24dp
|
||||
activeId = R.drawable.ic_reblog_active_24dp
|
||||
}
|
||||
reblogButton.setInactiveImage(inactiveId)
|
||||
reblogButton.setActiveImage(activeId)
|
||||
return
|
||||
}
|
||||
|
||||
val disabledId: Int = if (visibility === Status.Visibility.DIRECT) {
|
||||
R.drawable.ic_reblog_direct_24dp
|
||||
} else {
|
||||
R.drawable.ic_reblog_private_24dp
|
||||
}
|
||||
reblogButton.setInactiveImage(disabledId)
|
||||
reblogButton.setActiveImage(disabledId)
|
||||
}
|
||||
|
||||
protected fun setFavourited(favourited: Boolean) {
|
||||
favouriteButton.isChecked = favourited
|
||||
}
|
||||
|
||||
protected fun setBookmarked(bookmarked: Boolean) {
|
||||
bookmarkButton.isChecked = bookmarked
|
||||
}
|
||||
|
||||
private fun decodeBlurHash(blurhash: String): BitmapDrawable {
|
||||
return decodeBlurHash(context, blurhash)
|
||||
}
|
||||
|
||||
private fun loadImage(
|
||||
imageView: MediaPreviewImageView,
|
||||
previewUrl: String?,
|
||||
focus: Attachment.Focus?,
|
||||
blurhash: String?,
|
||||
) {
|
||||
val placeholder = blurhash?.let { decodeBlurHash(it) } ?: mediaPreviewUnloaded
|
||||
if (TextUtils.isEmpty(previewUrl)) {
|
||||
imageView.removeFocalPoint()
|
||||
Glide.with(imageView)
|
||||
.load(placeholder)
|
||||
.centerInside()
|
||||
.into(imageView)
|
||||
return
|
||||
}
|
||||
|
||||
if (focus != null) { // If there is a focal point for this attachment:
|
||||
imageView.setFocalPoint(focus)
|
||||
Glide.with(context)
|
||||
.load(previewUrl)
|
||||
.placeholder(placeholder)
|
||||
.centerInside()
|
||||
.addListener(imageView)
|
||||
.into(imageView)
|
||||
} else {
|
||||
imageView.removeFocalPoint()
|
||||
Glide.with(imageView)
|
||||
.load(previewUrl)
|
||||
.placeholder(placeholder)
|
||||
.centerInside()
|
||||
.into(imageView)
|
||||
}
|
||||
}
|
||||
|
||||
protected fun setMediaPreviews(
|
||||
attachments: List<Attachment>,
|
||||
sensitive: Boolean,
|
||||
listener: StatusActionListener,
|
||||
showingContent: Boolean,
|
||||
useBlurhash: Boolean,
|
||||
) {
|
||||
mediaPreview.visibility = View.VISIBLE
|
||||
mediaPreview.aspectRatios = attachments.aspectRatios()
|
||||
mediaPreview.forEachIndexed { i: Int, imageView: MediaPreviewImageView, descriptionIndicator: TextView ->
|
||||
val attachment = attachments[i]
|
||||
val previewUrl = attachment.previewUrl
|
||||
val description = attachment.description
|
||||
val hasDescription = !TextUtils.isEmpty(description)
|
||||
if (hasDescription) {
|
||||
imageView.contentDescription = description
|
||||
} else {
|
||||
imageView.contentDescription = context.getString(R.string.action_view_media)
|
||||
}
|
||||
loadImage(
|
||||
imageView,
|
||||
if (showingContent) previewUrl else null,
|
||||
attachment.meta?.focus,
|
||||
if (useBlurhash) attachment.blurhash else null,
|
||||
)
|
||||
val type = attachment.type
|
||||
if (showingContent && (type === Attachment.Type.VIDEO || type === Attachment.Type.GIFV)) {
|
||||
imageView.foreground =
|
||||
ContextCompat.getDrawable(context, R.drawable.play_indicator_overlay)
|
||||
} else {
|
||||
imageView.foreground = null
|
||||
}
|
||||
setAttachmentClickListener(imageView, listener, i, attachment, true)
|
||||
if (sensitive) {
|
||||
sensitiveMediaWarning.setText(R.string.post_sensitive_media_title)
|
||||
} else {
|
||||
sensitiveMediaWarning.setText(R.string.post_media_hidden_title)
|
||||
}
|
||||
sensitiveMediaWarning.visibility = if (showingContent) View.GONE else View.VISIBLE
|
||||
sensitiveMediaShow.visibility = if (showingContent) View.VISIBLE else View.GONE
|
||||
descriptionIndicator.visibility =
|
||||
if (hasDescription && showingContent) View.VISIBLE else View.GONE
|
||||
sensitiveMediaShow.setOnClickListener { v: View ->
|
||||
if (bindingAdapterPosition != RecyclerView.NO_POSITION) {
|
||||
listener.onContentHiddenChange(false, bindingAdapterPosition)
|
||||
}
|
||||
v.visibility = View.GONE
|
||||
sensitiveMediaWarning.visibility = View.VISIBLE
|
||||
descriptionIndicator.visibility = View.GONE
|
||||
}
|
||||
sensitiveMediaWarning.setOnClickListener { v: View ->
|
||||
if (bindingAdapterPosition != RecyclerView.NO_POSITION) {
|
||||
listener.onContentHiddenChange(true, bindingAdapterPosition)
|
||||
}
|
||||
v.visibility = View.GONE
|
||||
sensitiveMediaShow.visibility = View.VISIBLE
|
||||
descriptionIndicator.visibility = if (hasDescription) View.VISIBLE else View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateMediaLabel(index: Int, sensitive: Boolean, showingContent: Boolean) {
|
||||
val label =
|
||||
if (sensitive && !showingContent) context.getString(R.string.post_sensitive_media_title) else mediaDescriptions[index]
|
||||
mediaLabels[index].text = label
|
||||
}
|
||||
|
||||
protected fun setMediaLabel(
|
||||
attachments: List<Attachment>,
|
||||
sensitive: Boolean,
|
||||
listener: StatusActionListener,
|
||||
showingContent: Boolean,
|
||||
) {
|
||||
for (i in mediaLabels.indices) {
|
||||
val mediaLabel = mediaLabels[i]
|
||||
if (i < attachments.size) {
|
||||
val attachment = attachments[i]
|
||||
mediaLabel.visibility = View.VISIBLE
|
||||
mediaDescriptions[i] = attachment.getFormattedDescription(context)
|
||||
updateMediaLabel(i, sensitive, showingContent)
|
||||
|
||||
// Set the icon next to the label.
|
||||
val drawableId = attachments[0].iconResource()
|
||||
mediaLabel.setCompoundDrawablesWithIntrinsicBounds(drawableId, 0, 0, 0)
|
||||
setAttachmentClickListener(mediaLabel, listener, i, attachment, false)
|
||||
} else {
|
||||
mediaLabel.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setAttachmentClickListener(
|
||||
view: View,
|
||||
listener: StatusActionListener,
|
||||
index: Int,
|
||||
attachment: Attachment,
|
||||
animateTransition: Boolean,
|
||||
) {
|
||||
view.setOnClickListener { v: View? ->
|
||||
val position = bindingAdapterPosition.takeIf { it != RecyclerView.NO_POSITION } ?: return@setOnClickListener
|
||||
if (sensitiveMediaWarning.visibility == View.VISIBLE) {
|
||||
listener.onContentHiddenChange(true, bindingAdapterPosition)
|
||||
} else {
|
||||
listener.onViewMedia(position, index, if (animateTransition) v else null)
|
||||
}
|
||||
}
|
||||
view.setOnLongClickListener {
|
||||
val description = attachment.getFormattedDescription(view.context)
|
||||
Toast.makeText(view.context, description, Toast.LENGTH_LONG).show()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
protected fun hideSensitiveMediaWarning() {
|
||||
sensitiveMediaWarning.hide()
|
||||
sensitiveMediaShow.hide()
|
||||
}
|
||||
|
||||
protected fun setupButtons(
|
||||
listener: StatusActionListener,
|
||||
accountId: String,
|
||||
statusDisplayOptions: StatusDisplayOptions,
|
||||
) {
|
||||
val profileButtonClickListener = View.OnClickListener { listener.onViewAccount(accountId) }
|
||||
avatar.setOnClickListener(profileButtonClickListener)
|
||||
displayName.setOnClickListener(profileButtonClickListener)
|
||||
replyButton.setOnClickListener {
|
||||
val position = bindingAdapterPosition.takeIf { it != RecyclerView.NO_POSITION } ?: return@setOnClickListener
|
||||
listener.onReply(position)
|
||||
}
|
||||
reblogButton?.setEventListener { _: SparkButton?, buttonState: Boolean ->
|
||||
// return true to play animation
|
||||
val position = bindingAdapterPosition.takeIf { it != RecyclerView.NO_POSITION } ?: return@setEventListener false
|
||||
return@setEventListener if (statusDisplayOptions.confirmReblogs) {
|
||||
showConfirmReblog(listener, buttonState, position)
|
||||
false
|
||||
} else {
|
||||
listener.onReblog(!buttonState, position)
|
||||
true
|
||||
}
|
||||
}
|
||||
favouriteButton.setEventListener { _: SparkButton?, buttonState: Boolean ->
|
||||
// return true to play animation
|
||||
val position = bindingAdapterPosition.takeIf { it != RecyclerView.NO_POSITION } ?: return@setEventListener true
|
||||
return@setEventListener if (statusDisplayOptions.confirmFavourites) {
|
||||
showConfirmFavourite(listener, buttonState, position)
|
||||
false
|
||||
} else {
|
||||
listener.onFavourite(!buttonState, position)
|
||||
true
|
||||
}
|
||||
}
|
||||
bookmarkButton.setEventListener { _: SparkButton?, buttonState: Boolean ->
|
||||
val position = bindingAdapterPosition.takeIf { it != RecyclerView.NO_POSITION } ?: return@setEventListener true
|
||||
listener.onBookmark(!buttonState, position)
|
||||
true
|
||||
}
|
||||
moreButton.setOnClickListener { v: View? ->
|
||||
val position = bindingAdapterPosition.takeIf { it != RecyclerView.NO_POSITION } ?: return@setOnClickListener
|
||||
listener.onMore(v!!, position)
|
||||
}
|
||||
|
||||
/* Even though the content TextView is a child of the container, it won't respond to clicks
|
||||
* if it contains URLSpans without also setting its listener. The surrounding spans will
|
||||
* just eat the clicks instead of deferring to the parent listener, but WILL respond to a
|
||||
* listener directly on the TextView, for whatever reason. */
|
||||
val viewThreadListener = View.OnClickListener {
|
||||
val position = bindingAdapterPosition.takeIf { it != RecyclerView.NO_POSITION } ?: return@OnClickListener
|
||||
listener.onViewThread(position)
|
||||
}
|
||||
|
||||
content.setOnClickListener(viewThreadListener)
|
||||
itemView.setOnClickListener(viewThreadListener)
|
||||
}
|
||||
|
||||
private fun showConfirmReblog(
|
||||
listener: StatusActionListener,
|
||||
buttonState: Boolean,
|
||||
position: Int,
|
||||
) {
|
||||
val popup = PopupMenu(context, reblogButton!!)
|
||||
popup.inflate(R.menu.status_reblog)
|
||||
val menu = popup.menu
|
||||
if (buttonState) {
|
||||
menu.findItem(R.id.menu_action_reblog).isVisible = false
|
||||
} else {
|
||||
menu.findItem(R.id.menu_action_unreblog).isVisible = false
|
||||
}
|
||||
popup.setOnMenuItemClickListener {
|
||||
listener.onReblog(!buttonState, position)
|
||||
if (!buttonState) {
|
||||
reblogButton.playAnimation()
|
||||
}
|
||||
true
|
||||
}
|
||||
popup.show()
|
||||
}
|
||||
|
||||
private fun showConfirmFavourite(
|
||||
listener: StatusActionListener,
|
||||
buttonState: Boolean,
|
||||
position: Int,
|
||||
) {
|
||||
val popup = PopupMenu(context, favouriteButton)
|
||||
popup.inflate(R.menu.status_favourite)
|
||||
val menu = popup.menu
|
||||
if (buttonState) {
|
||||
menu.findItem(R.id.menu_action_favourite).isVisible = false
|
||||
} else {
|
||||
menu.findItem(R.id.menu_action_unfavourite).isVisible = false
|
||||
}
|
||||
popup.setOnMenuItemClickListener {
|
||||
listener.onFavourite(!buttonState, position)
|
||||
if (!buttonState) {
|
||||
favouriteButton.playAnimation()
|
||||
}
|
||||
true
|
||||
}
|
||||
popup.show()
|
||||
}
|
||||
|
||||
open fun setupWithStatus(
|
||||
status: StatusViewData,
|
||||
listener: StatusActionListener,
|
||||
statusDisplayOptions: StatusDisplayOptions,
|
||||
payloads: Any? = null,
|
||||
) {
|
||||
if (payloads == null) {
|
||||
val actionable = status.actionable
|
||||
setDisplayName(actionable.account.name, actionable.account.emojis, statusDisplayOptions)
|
||||
setUsername(status.username)
|
||||
setMetaData(status, statusDisplayOptions, listener)
|
||||
setIsReply(actionable.inReplyToId != null)
|
||||
setReplyCount(actionable.repliesCount, statusDisplayOptions.showStatsInline)
|
||||
setAvatar(
|
||||
actionable.account.avatar,
|
||||
status.rebloggedAvatar,
|
||||
actionable.account.bot,
|
||||
statusDisplayOptions,
|
||||
)
|
||||
setReblogged(actionable.reblogged)
|
||||
setFavourited(actionable.favourited)
|
||||
setBookmarked(actionable.bookmarked)
|
||||
val attachments = actionable.attachments
|
||||
val sensitive = actionable.sensitive
|
||||
if (statusDisplayOptions.mediaPreviewEnabled && hasPreviewableAttachment(attachments)) {
|
||||
setMediaPreviews(
|
||||
attachments,
|
||||
sensitive,
|
||||
listener,
|
||||
status.isShowingContent,
|
||||
statusDisplayOptions.useBlurhash,
|
||||
)
|
||||
if (attachments.isEmpty()) {
|
||||
hideSensitiveMediaWarning()
|
||||
}
|
||||
// Hide the unused label.
|
||||
for (mediaLabel in mediaLabels) {
|
||||
mediaLabel.visibility = View.GONE
|
||||
}
|
||||
} else {
|
||||
setMediaLabel(attachments, sensitive, listener, status.isShowingContent)
|
||||
// Hide all unused views.
|
||||
mediaPreview.visibility = View.GONE
|
||||
hideSensitiveMediaWarning()
|
||||
}
|
||||
setupCard(
|
||||
status,
|
||||
status.isExpanded,
|
||||
statusDisplayOptions.cardViewMode,
|
||||
statusDisplayOptions,
|
||||
listener,
|
||||
)
|
||||
setupButtons(
|
||||
listener,
|
||||
actionable.account.id,
|
||||
statusDisplayOptions,
|
||||
)
|
||||
setRebloggingEnabled(actionable.rebloggingAllowed(), actionable.visibility)
|
||||
setSpoilerAndContent(status, statusDisplayOptions, listener)
|
||||
setDescriptionForStatus(status, statusDisplayOptions)
|
||||
|
||||
// Workaround for RecyclerView 1.0.0 / androidx.core 1.0.0
|
||||
// RecyclerView tries to set AccessibilityDelegateCompat to null
|
||||
// but ViewCompat code replaces is with the default one. RecyclerView never
|
||||
// fetches another one from its delegate because it checks that it's set so we remove it
|
||||
// and let RecyclerView ask for a new delegate.
|
||||
itemView.accessibilityDelegate = null
|
||||
} else {
|
||||
if (payloads is List<*>) {
|
||||
for (item in payloads) {
|
||||
if (Key.KEY_CREATED == item) {
|
||||
setMetaData(status, statusDisplayOptions, listener)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setDescriptionForStatus(
|
||||
status: StatusViewData,
|
||||
statusDisplayOptions: StatusDisplayOptions,
|
||||
) {
|
||||
val (_, _, account, _, _, _, _, createdAt, editedAt, _, reblogsCount, favouritesCount, _, reblogged, favourited, bookmarked, sensitive, _, visibility) = status.actionable
|
||||
val description = context.getString(
|
||||
R.string.description_status,
|
||||
account.displayName,
|
||||
getContentWarningDescription(context, status),
|
||||
if (TextUtils.isEmpty(status.spoilerText) || !sensitive || status.isExpanded) status.content else "",
|
||||
getCreatedAtDescription(createdAt, statusDisplayOptions),
|
||||
editedAt?.let { context.getString(R.string.description_post_edited) } ?: "",
|
||||
getReblogDescription(context, status),
|
||||
status.username,
|
||||
if (reblogged) context.getString(R.string.description_post_reblogged) else "",
|
||||
if (favourited) context.getString(R.string.description_post_favourited) else "",
|
||||
if (bookmarked) context.getString(R.string.description_post_bookmarked) else "",
|
||||
getMediaDescription(context, status),
|
||||
visibility.description(context),
|
||||
getFavsText(favouritesCount),
|
||||
getReblogsText(reblogsCount),
|
||||
status.actionable.poll?.let {
|
||||
pollView.getPollDescription(
|
||||
from(it),
|
||||
statusDisplayOptions,
|
||||
numberFormat,
|
||||
absoluteTimeFormatter,
|
||||
)
|
||||
} ?: "",
|
||||
)
|
||||
itemView.contentDescription = description
|
||||
}
|
||||
|
||||
protected fun getFavsText(count: Int): CharSequence {
|
||||
if (count <= 0) return ""
|
||||
|
||||
val countString = numberFormat.format(count.toLong())
|
||||
return HtmlCompat.fromHtml(
|
||||
context.resources.getQuantityString(R.plurals.favs, count, countString),
|
||||
HtmlCompat.FROM_HTML_MODE_LEGACY,
|
||||
)
|
||||
}
|
||||
|
||||
protected fun getReblogsText(count: Int): CharSequence {
|
||||
if (count <= 0) return ""
|
||||
|
||||
val countString = numberFormat.format(count.toLong())
|
||||
return HtmlCompat.fromHtml(
|
||||
context.resources.getQuantityString(
|
||||
R.plurals.reblogs,
|
||||
count,
|
||||
countString,
|
||||
),
|
||||
HtmlCompat.FROM_HTML_MODE_LEGACY,
|
||||
)
|
||||
}
|
||||
|
||||
protected fun setupCard(
|
||||
status: StatusViewData,
|
||||
expanded: Boolean,
|
||||
cardViewMode: CardViewMode,
|
||||
statusDisplayOptions: StatusDisplayOptions,
|
||||
listener: StatusActionListener,
|
||||
) {
|
||||
cardView ?: return
|
||||
|
||||
val (_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, sensitive, _, _, attachments, _, _, _, _, _, poll, card) = status.actionable
|
||||
if (cardViewMode !== CardViewMode.NONE && attachments.isEmpty() && poll == null && card != null &&
|
||||
!TextUtils.isEmpty(card.url) &&
|
||||
(!sensitive || expanded) &&
|
||||
(!status.isCollapsible || !status.isCollapsed)
|
||||
) {
|
||||
cardView.visibility = View.VISIBLE
|
||||
cardView.bind(card, status.actionable.sensitive, statusDisplayOptions) { target ->
|
||||
if (card.kind == PreviewCardKind.PHOTO && card.embedUrl.isNotEmpty() && target == PreviewCardView.Target.IMAGE) {
|
||||
context.startActivity(
|
||||
newSingleImageIntent(context, card.embedUrl),
|
||||
)
|
||||
} else {
|
||||
listener.onViewUrl(card.url)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cardView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
open fun showStatusContent(show: Boolean) {
|
||||
val visibility = if (show) View.VISIBLE else View.GONE
|
||||
avatar.visibility = visibility
|
||||
avatarInset.visibility = visibility
|
||||
displayName.visibility = visibility
|
||||
username.visibility = visibility
|
||||
metaInfo.visibility = visibility
|
||||
contentWarningDescription.visibility = visibility
|
||||
contentWarningButton.visibility = visibility
|
||||
content.visibility = visibility
|
||||
cardView!!.visibility = visibility
|
||||
mediaContainer.visibility = visibility
|
||||
pollView.visibility = visibility
|
||||
replyButton.visibility = visibility
|
||||
reblogButton!!.visibility = visibility
|
||||
favouriteButton.visibility = visibility
|
||||
bookmarkButton.visibility = visibility
|
||||
moreButton.visibility = visibility
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "StatusBaseViewHolder"
|
||||
|
||||
@JvmStatic
|
||||
protected fun hasPreviewableAttachment(attachments: List<Attachment>): Boolean {
|
||||
for ((_, _, _, _, type) in attachments) {
|
||||
if (type === Attachment.Type.AUDIO || type === Attachment.Type.UNKNOWN) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun getReblogDescription(context: Context, status: StatusViewData): CharSequence {
|
||||
return status.rebloggingStatus?.let {
|
||||
context.getString(R.string.post_boosted_format, it.account.username)
|
||||
} ?: ""
|
||||
}
|
||||
|
||||
private fun getMediaDescription(context: Context, status: StatusViewData): CharSequence {
|
||||
if (status.actionable.attachments.isEmpty()) return ""
|
||||
|
||||
val mediaDescriptions =
|
||||
status.actionable.attachments.fold(StringBuilder()) { builder: StringBuilder, (_, _, _, _, _, description): Attachment ->
|
||||
if (description == null) {
|
||||
val placeholder =
|
||||
context.getString(R.string.description_post_media_no_description_placeholder)
|
||||
return@fold builder.append(placeholder)
|
||||
} else {
|
||||
builder.append("; ")
|
||||
return@fold builder.append(description)
|
||||
}
|
||||
}
|
||||
return context.getString(R.string.description_post_media, mediaDescriptions)
|
||||
}
|
||||
|
||||
private fun getContentWarningDescription(context: Context, status: StatusViewData): CharSequence {
|
||||
return if (!TextUtils.isEmpty(status.spoilerText)) {
|
||||
context.getString(R.string.description_post_cw, status.spoilerText)
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,216 +0,0 @@
|
|||
package app.pachli.adapter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.text.style.DynamicDrawableSpan;
|
||||
import android.text.style.ImageSpan;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
import app.pachli.R;
|
||||
import app.pachli.entity.Status;
|
||||
import app.pachli.interfaces.StatusActionListener;
|
||||
import app.pachli.util.CardViewMode;
|
||||
import app.pachli.util.LinkHelper;
|
||||
import app.pachli.util.NoUnderlineURLSpan;
|
||||
import app.pachli.util.StatusDisplayOptions;
|
||||
import app.pachli.viewdata.StatusViewData;
|
||||
|
||||
public class StatusDetailedViewHolder extends StatusBaseViewHolder {
|
||||
private final TextView reblogs;
|
||||
private final TextView favourites;
|
||||
private final View infoDivider;
|
||||
|
||||
private static final DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.SHORT);
|
||||
|
||||
public StatusDetailedViewHolder(@NonNull View view) {
|
||||
super(view);
|
||||
reblogs = view.findViewById(R.id.status_reblogs);
|
||||
favourites = view.findViewById(R.id.status_favourites);
|
||||
infoDivider = view.findViewById(R.id.status_info_divider);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setMetaData(@NonNull StatusViewData statusViewData, @NonNull StatusDisplayOptions statusDisplayOptions, @NonNull StatusActionListener listener) {
|
||||
|
||||
Status status = statusViewData.getActionable();
|
||||
|
||||
Status.Visibility visibility = status.getVisibility();
|
||||
Context context = metaInfo.getContext();
|
||||
|
||||
Drawable visibilityIcon = getVisibilityIcon(visibility);
|
||||
CharSequence visibilityString = getVisibilityDescription(context, visibility);
|
||||
|
||||
SpannableStringBuilder sb = new SpannableStringBuilder(visibilityString);
|
||||
|
||||
if (visibilityIcon != null) {
|
||||
ImageSpan visibilityIconSpan = new ImageSpan(
|
||||
visibilityIcon,
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ? DynamicDrawableSpan.ALIGN_CENTER : DynamicDrawableSpan.ALIGN_BASELINE
|
||||
);
|
||||
sb.setSpan(visibilityIconSpan, 0, visibilityString.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
|
||||
String metadataJoiner = context.getString(R.string.metadata_joiner);
|
||||
|
||||
Date createdAt = status.getCreatedAt();
|
||||
if (createdAt != null) {
|
||||
|
||||
sb.append(" ");
|
||||
sb.append(dateFormat.format(createdAt));
|
||||
}
|
||||
|
||||
Date editedAt = status.getEditedAt();
|
||||
|
||||
if (editedAt != null) {
|
||||
String editedAtString = context.getString(R.string.post_edited, dateFormat.format(editedAt));
|
||||
|
||||
sb.append(metadataJoiner);
|
||||
int spanStart = sb.length();
|
||||
int spanEnd = spanStart + editedAtString.length();
|
||||
|
||||
sb.append(editedAtString);
|
||||
|
||||
if (statusViewData.getStatus().getEditedAt() != null) {
|
||||
NoUnderlineURLSpan editedClickSpan = new NoUnderlineURLSpan("") {
|
||||
@Override
|
||||
public void onClick(@NonNull View view) {
|
||||
listener.onShowEdits(getBindingAdapterPosition());
|
||||
}
|
||||
};
|
||||
|
||||
sb.setSpan(editedClickSpan, spanStart, spanEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
}
|
||||
|
||||
Status.Application app = status.getApplication();
|
||||
|
||||
if (app != null) {
|
||||
|
||||
sb.append(metadataJoiner);
|
||||
|
||||
if (app.getWebsite() != null) {
|
||||
CharSequence text = LinkHelper.createClickableText(app.getName(), app.getWebsite());
|
||||
sb.append(text);
|
||||
} else {
|
||||
sb.append(app.getName());
|
||||
}
|
||||
}
|
||||
|
||||
metaInfo.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
metaInfo.setText(sb);
|
||||
}
|
||||
|
||||
private void setReblogAndFavCount(int reblogCount, int favCount, @NonNull StatusActionListener listener) {
|
||||
|
||||
if (reblogCount > 0) {
|
||||
reblogs.setText(getReblogsText(reblogs.getContext(), reblogCount));
|
||||
reblogs.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
reblogs.setVisibility(View.GONE);
|
||||
}
|
||||
if (favCount > 0) {
|
||||
favourites.setText(getFavsText(favourites.getContext(), favCount));
|
||||
favourites.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
favourites.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (reblogs.getVisibility() == View.GONE && favourites.getVisibility() == View.GONE) {
|
||||
infoDivider.setVisibility(View.GONE);
|
||||
} else {
|
||||
infoDivider.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
reblogs.setOnClickListener(v -> {
|
||||
int position = getBindingAdapterPosition();
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
listener.onShowReblogs(position);
|
||||
}
|
||||
});
|
||||
favourites.setOnClickListener(v -> {
|
||||
int position = getBindingAdapterPosition();
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
listener.onShowFavs(position);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setupWithStatus(@NonNull final StatusViewData status,
|
||||
@NonNull final StatusActionListener listener,
|
||||
@NonNull StatusDisplayOptions statusDisplayOptions,
|
||||
@Nullable Object payloads) {
|
||||
// We never collapse statuses in the detail view
|
||||
StatusViewData uncollapsedStatus = (status.isCollapsible() && status.isCollapsed()) ?
|
||||
status.copyWithCollapsed(false) :
|
||||
status;
|
||||
|
||||
super.setupWithStatus(uncollapsedStatus, listener, statusDisplayOptions, payloads);
|
||||
setupCard(uncollapsedStatus, status.isExpanded(), CardViewMode.FULL_WIDTH, statusDisplayOptions, listener); // Always show card for detailed status
|
||||
if (payloads == null) {
|
||||
Status actionable = uncollapsedStatus.getActionable();
|
||||
|
||||
if (!statusDisplayOptions.hideStats()) {
|
||||
setReblogAndFavCount(actionable.getReblogsCount(),
|
||||
actionable.getFavouritesCount(), listener);
|
||||
} else {
|
||||
hideQuantitativeStats();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable Drawable getVisibilityIcon(@Nullable Status.Visibility visibility) {
|
||||
|
||||
if (visibility == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int visibilityIcon;
|
||||
switch (visibility) {
|
||||
case PUBLIC -> visibilityIcon = R.drawable.ic_public_24dp;
|
||||
case UNLISTED -> visibilityIcon = R.drawable.ic_lock_open_24dp;
|
||||
case PRIVATE -> visibilityIcon = R.drawable.ic_lock_outline_24dp;
|
||||
case DIRECT -> visibilityIcon = R.drawable.ic_email_24dp;
|
||||
default -> {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
final Drawable visibilityDrawable = AppCompatResources.getDrawable(
|
||||
this.metaInfo.getContext(), visibilityIcon
|
||||
);
|
||||
if (visibilityDrawable == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final int size = (int) this.metaInfo.getTextSize();
|
||||
visibilityDrawable.setBounds(
|
||||
0,
|
||||
0,
|
||||
size,
|
||||
size
|
||||
);
|
||||
visibilityDrawable.setTint(this.metaInfo.getCurrentTextColor());
|
||||
|
||||
return visibilityDrawable;
|
||||
}
|
||||
|
||||
private void hideQuantitativeStats() {
|
||||
reblogs.setVisibility(View.GONE);
|
||||
favourites.setVisibility(View.GONE);
|
||||
infoDivider.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
package app.pachli.adapter
|
||||
|
||||
import android.os.Build
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.text.style.DynamicDrawableSpan
|
||||
import android.text.style.ImageSpan
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import app.pachli.R
|
||||
import app.pachli.databinding.ItemStatusDetailedBinding
|
||||
import app.pachli.entity.description
|
||||
import app.pachli.entity.icon
|
||||
import app.pachli.interfaces.StatusActionListener
|
||||
import app.pachli.util.CardViewMode
|
||||
import app.pachli.util.NoUnderlineURLSpan
|
||||
import app.pachli.util.StatusDisplayOptions
|
||||
import app.pachli.util.createClickableText
|
||||
import app.pachli.util.hide
|
||||
import app.pachli.util.show
|
||||
import app.pachli.viewdata.StatusViewData
|
||||
import java.text.DateFormat
|
||||
|
||||
class StatusDetailedViewHolder(
|
||||
private val binding: ItemStatusDetailedBinding,
|
||||
) : StatusBaseViewHolder(binding.root) {
|
||||
|
||||
override fun setMetaData(
|
||||
statusViewData: StatusViewData,
|
||||
statusDisplayOptions: StatusDisplayOptions,
|
||||
listener: StatusActionListener,
|
||||
) {
|
||||
val (_, _, _, _, _, _, _, createdAt, editedAt, _, _, _, _, _, _, _, _, _, visibility, _, _, _, app) = statusViewData.actionable
|
||||
val visibilityIcon = visibility.icon(metaInfo)
|
||||
val visibilityString = visibility.description(context)
|
||||
val sb = SpannableStringBuilder(visibilityString)
|
||||
visibilityIcon?.also {
|
||||
val alignment = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) DynamicDrawableSpan.ALIGN_CENTER else DynamicDrawableSpan.ALIGN_BASELINE
|
||||
val visibilityIconSpan = ImageSpan(it, alignment)
|
||||
sb.setSpan(visibilityIconSpan, 0, visibilityString.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
val metadataJoiner = context.getString(R.string.metadata_joiner)
|
||||
sb.append(" ")
|
||||
sb.append(dateFormat.format(createdAt))
|
||||
|
||||
editedAt?.also {
|
||||
val editedAtString = context.getString(R.string.post_edited, dateFormat.format(it))
|
||||
sb.append(metadataJoiner)
|
||||
val spanStart = sb.length
|
||||
val spanEnd = spanStart + editedAtString.length
|
||||
sb.append(editedAtString)
|
||||
statusViewData.status.editedAt?.also {
|
||||
val editedClickSpan: NoUnderlineURLSpan = object : NoUnderlineURLSpan("") {
|
||||
override fun onClick(view: View) {
|
||||
listener.onShowEdits(bindingAdapterPosition)
|
||||
}
|
||||
}
|
||||
sb.setSpan(editedClickSpan, spanStart, spanEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
}
|
||||
|
||||
app?.also { application ->
|
||||
val (name, website) = application
|
||||
sb.append(metadataJoiner)
|
||||
website?.also { sb.append(createClickableText(name, it)) ?: sb.append(name) }
|
||||
}
|
||||
|
||||
metaInfo.movementMethod = LinkMovementMethod.getInstance()
|
||||
metaInfo.text = sb
|
||||
}
|
||||
|
||||
private fun setReblogAndFavCount(
|
||||
reblogCount: Int,
|
||||
favCount: Int,
|
||||
listener: StatusActionListener,
|
||||
) {
|
||||
if (reblogCount > 0) {
|
||||
binding.statusReblogs.text = getReblogsText(reblogCount)
|
||||
binding.statusReblogs.show()
|
||||
} else {
|
||||
binding.statusReblogs.hide()
|
||||
}
|
||||
if (favCount > 0) {
|
||||
binding.statusFavourites.text = getFavsText(favCount)
|
||||
binding.statusFavourites.show()
|
||||
} else {
|
||||
binding.statusFavourites.hide()
|
||||
}
|
||||
if (binding.statusReblogs.visibility == View.GONE && binding.statusFavourites.visibility == View.GONE) {
|
||||
binding.statusInfoDivider.hide()
|
||||
} else {
|
||||
binding.statusInfoDivider.show()
|
||||
}
|
||||
binding.statusReblogs.setOnClickListener {
|
||||
val position = bindingAdapterPosition.takeIf { it != RecyclerView.NO_POSITION } ?: return@setOnClickListener
|
||||
listener.onShowReblogs(position)
|
||||
}
|
||||
binding.statusFavourites.setOnClickListener {
|
||||
val position = bindingAdapterPosition.takeIf { it != RecyclerView.NO_POSITION } ?: return@setOnClickListener
|
||||
listener.onShowFavs(position)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setupWithStatus(
|
||||
status: StatusViewData,
|
||||
listener: StatusActionListener,
|
||||
statusDisplayOptions: StatusDisplayOptions,
|
||||
payloads: Any?,
|
||||
) {
|
||||
// We never collapse statuses in the detail view
|
||||
val uncollapsedStatus =
|
||||
if (status.isCollapsible && status.isCollapsed) status.copyWithCollapsed(false) else status
|
||||
super.setupWithStatus(uncollapsedStatus, listener, statusDisplayOptions, payloads)
|
||||
setupCard(
|
||||
uncollapsedStatus,
|
||||
status.isExpanded,
|
||||
CardViewMode.FULL_WIDTH,
|
||||
statusDisplayOptions,
|
||||
listener,
|
||||
) // Always show card for detailed status
|
||||
if (payloads == null) {
|
||||
val (_, _, _, _, _, _, _, _, _, _, reblogsCount, favouritesCount) = uncollapsedStatus.actionable
|
||||
if (!statusDisplayOptions.hideStats) {
|
||||
setReblogAndFavCount(reblogsCount, favouritesCount, listener)
|
||||
} else {
|
||||
hideQuantitativeStats()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideQuantitativeStats() {
|
||||
binding.statusReblogs.hide()
|
||||
binding.statusFavourites.hide()
|
||||
binding.statusInfoDivider.hide()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val dateFormat =
|
||||
DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.SHORT)
|
||||
}
|
||||
}
|
|
@ -1,169 +0,0 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* This file is a part of Pachli.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* Pachli 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 Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package app.pachli.adapter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.InputFilter;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import app.pachli.R;
|
||||
import app.pachli.entity.Emoji;
|
||||
import app.pachli.entity.Filter;
|
||||
import app.pachli.entity.Status;
|
||||
import app.pachli.interfaces.StatusActionListener;
|
||||
import app.pachli.util.CustomEmojiHelper;
|
||||
import app.pachli.util.NumberUtils;
|
||||
import app.pachli.util.SmartLengthInputFilter;
|
||||
import app.pachli.util.StatusDisplayOptions;
|
||||
import app.pachli.util.StringUtils;
|
||||
import app.pachli.viewdata.StatusViewData;
|
||||
import at.connyduck.sparkbutton.helpers.Utils;
|
||||
|
||||
public class StatusViewHolder extends StatusBaseViewHolder {
|
||||
private static final InputFilter[] COLLAPSE_INPUT_FILTER = new InputFilter[]{SmartLengthInputFilter.INSTANCE};
|
||||
private static final InputFilter[] NO_INPUT_FILTER = new InputFilter[0];
|
||||
|
||||
private final TextView statusInfo;
|
||||
private final Button contentCollapseButton;
|
||||
private final TextView favouritedCountLabel;
|
||||
private final TextView reblogsCountLabel;
|
||||
|
||||
public StatusViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
statusInfo = itemView.findViewById(R.id.status_info);
|
||||
contentCollapseButton = itemView.findViewById(R.id.button_toggle_content);
|
||||
favouritedCountLabel = itemView.findViewById(R.id.status_favourites_count);
|
||||
reblogsCountLabel = itemView.findViewById(R.id.status_insets);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setupWithStatus(@NonNull StatusViewData status,
|
||||
@NonNull final StatusActionListener listener,
|
||||
@NonNull StatusDisplayOptions statusDisplayOptions,
|
||||
@Nullable Object payloads) {
|
||||
if (payloads == null) {
|
||||
|
||||
boolean sensitive = !TextUtils.isEmpty(status.getActionable().getSpoilerText());
|
||||
boolean expanded = status.isExpanded();
|
||||
|
||||
setupCollapsedState(sensitive, expanded, status, listener);
|
||||
|
||||
Status reblogging = status.getRebloggingStatus();
|
||||
if (reblogging == null || status.getFilterAction() == Filter.Action.WARN) {
|
||||
hideStatusInfo();
|
||||
} else {
|
||||
String rebloggedByDisplayName = reblogging.getAccount().getName();
|
||||
setRebloggedByDisplayName(rebloggedByDisplayName,
|
||||
reblogging.getAccount().getEmojis(), statusDisplayOptions);
|
||||
statusInfo.setOnClickListener(v -> listener.onOpenReblog(getBindingAdapterPosition()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
reblogsCountLabel.setVisibility(statusDisplayOptions.showStatsInline() ? View.VISIBLE : View.INVISIBLE);
|
||||
favouritedCountLabel.setVisibility(statusDisplayOptions.showStatsInline() ? View.VISIBLE : View.INVISIBLE);
|
||||
setFavouritedCount(status.getActionable().getFavouritesCount());
|
||||
setReblogsCount(status.getActionable().getReblogsCount());
|
||||
|
||||
super.setupWithStatus(status, listener, statusDisplayOptions, payloads);
|
||||
}
|
||||
|
||||
private void setRebloggedByDisplayName(@NonNull final CharSequence name,
|
||||
final List<Emoji> accountEmoji,
|
||||
@NonNull final StatusDisplayOptions statusDisplayOptions) {
|
||||
Context context = statusInfo.getContext();
|
||||
CharSequence wrappedName = StringUtils.unicodeWrap(name);
|
||||
CharSequence boostedText = context.getString(R.string.post_boosted_format, wrappedName);
|
||||
CharSequence emojifiedText = CustomEmojiHelper.emojify(
|
||||
boostedText, accountEmoji, statusInfo, statusDisplayOptions.animateEmojis()
|
||||
);
|
||||
statusInfo.setText(emojifiedText);
|
||||
statusInfo.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
// don't use this on the same ViewHolder as setRebloggedByDisplayName, will cause recycling issues as paddings are changed
|
||||
protected void setPollInfo(final boolean ownPoll) {
|
||||
statusInfo.setText(ownPoll ? R.string.poll_ended_created : R.string.poll_ended_voted);
|
||||
statusInfo.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_poll_24dp, 0, 0, 0);
|
||||
statusInfo.setCompoundDrawablePadding(Utils.dpToPx(statusInfo.getContext(), 10));
|
||||
statusInfo.setPaddingRelative(Utils.dpToPx(statusInfo.getContext(), 28), 0, 0, 0);
|
||||
statusInfo.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
protected void setReblogsCount(int reblogsCount) {
|
||||
reblogsCountLabel.setText(NumberUtils.formatNumber(reblogsCount, 1000));
|
||||
}
|
||||
|
||||
protected void setFavouritedCount(int favouritedCount) {
|
||||
favouritedCountLabel.setText(NumberUtils.formatNumber(favouritedCount, 1000));
|
||||
}
|
||||
|
||||
protected void hideStatusInfo() {
|
||||
statusInfo.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
private void setupCollapsedState(boolean sensitive,
|
||||
boolean expanded,
|
||||
@NonNull final StatusViewData status,
|
||||
@NonNull final StatusActionListener listener) {
|
||||
/* input filter for TextViews have to be set before text */
|
||||
if (status.isCollapsible() && (!sensitive || expanded)) {
|
||||
contentCollapseButton.setOnClickListener(view -> {
|
||||
int position = getBindingAdapterPosition();
|
||||
if (position != RecyclerView.NO_POSITION)
|
||||
listener.onContentCollapsedChange(!status.isCollapsed(), position);
|
||||
});
|
||||
|
||||
contentCollapseButton.setVisibility(View.VISIBLE);
|
||||
if (status.isCollapsed()) {
|
||||
contentCollapseButton.setText(R.string.post_content_warning_show_more);
|
||||
content.setFilters(COLLAPSE_INPUT_FILTER);
|
||||
} else {
|
||||
contentCollapseButton.setText(R.string.post_content_warning_show_less);
|
||||
content.setFilters(NO_INPUT_FILTER);
|
||||
}
|
||||
} else {
|
||||
contentCollapseButton.setVisibility(View.GONE);
|
||||
content.setFilters(NO_INPUT_FILTER);
|
||||
}
|
||||
}
|
||||
|
||||
public void showStatusContent(boolean show) {
|
||||
super.showStatusContent(show);
|
||||
contentCollapseButton.setVisibility(show ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void toggleExpandedState(boolean sensitive,
|
||||
boolean expanded,
|
||||
@NonNull StatusViewData status,
|
||||
@NonNull StatusDisplayOptions statusDisplayOptions,
|
||||
@NonNull final StatusActionListener listener) {
|
||||
|
||||
setupCollapsedState(sensitive, expanded, status, listener);
|
||||
|
||||
super.toggleExpandedState(sensitive, expanded, status, statusDisplayOptions, listener);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,160 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* This file is a part of Pachli.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* Pachli 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 Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>.
|
||||
*/
|
||||
|
||||
package app.pachli.adapter
|
||||
|
||||
import android.text.InputFilter
|
||||
import android.text.TextUtils
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import app.pachli.R
|
||||
import app.pachli.databinding.ItemStatusBinding
|
||||
import app.pachli.entity.Emoji
|
||||
import app.pachli.entity.Filter
|
||||
import app.pachli.interfaces.StatusActionListener
|
||||
import app.pachli.util.SmartLengthInputFilter
|
||||
import app.pachli.util.StatusDisplayOptions
|
||||
import app.pachli.util.emojify
|
||||
import app.pachli.util.formatNumber
|
||||
import app.pachli.util.hide
|
||||
import app.pachli.util.show
|
||||
import app.pachli.util.unicodeWrap
|
||||
import app.pachli.util.visible
|
||||
import app.pachli.viewdata.StatusViewData
|
||||
import at.connyduck.sparkbutton.helpers.Utils
|
||||
|
||||
open class StatusViewHolder(
|
||||
private val binding: ItemStatusBinding,
|
||||
root: View? = null,
|
||||
) : StatusBaseViewHolder(root ?: binding.root) {
|
||||
|
||||
override fun setupWithStatus(
|
||||
status: StatusViewData,
|
||||
listener: StatusActionListener,
|
||||
statusDisplayOptions: StatusDisplayOptions,
|
||||
payloads: Any?,
|
||||
) = with(binding) {
|
||||
if (payloads == null) {
|
||||
val sensitive = !TextUtils.isEmpty(status.actionable.spoilerText)
|
||||
val expanded = status.isExpanded
|
||||
setupCollapsedState(sensitive, expanded, status, listener)
|
||||
val reblogging = status.rebloggingStatus
|
||||
if (reblogging == null || status.filterAction === Filter.Action.WARN) {
|
||||
statusInfo.hide()
|
||||
} else {
|
||||
val rebloggedByDisplayName = reblogging.account.name
|
||||
setRebloggedByDisplayName(
|
||||
rebloggedByDisplayName,
|
||||
reblogging.account.emojis,
|
||||
statusDisplayOptions,
|
||||
)
|
||||
statusInfo.setOnClickListener {
|
||||
listener.onOpenReblog(bindingAdapterPosition)
|
||||
}
|
||||
}
|
||||
}
|
||||
statusReblogsCount.visible(statusDisplayOptions.showStatsInline)
|
||||
statusFavouritesCount.visible(statusDisplayOptions.showStatsInline)
|
||||
setFavouritedCount(status.actionable.favouritesCount)
|
||||
setReblogsCount(status.actionable.reblogsCount)
|
||||
super.setupWithStatus(status, listener, statusDisplayOptions, payloads)
|
||||
}
|
||||
|
||||
private fun setRebloggedByDisplayName(
|
||||
name: CharSequence,
|
||||
accountEmoji: List<Emoji>?,
|
||||
statusDisplayOptions: StatusDisplayOptions,
|
||||
) = with(binding) {
|
||||
val wrappedName: CharSequence = name.unicodeWrap()
|
||||
val boostedText: CharSequence = context.getString(R.string.post_boosted_format, wrappedName)
|
||||
val emojifiedText =
|
||||
boostedText.emojify(accountEmoji, statusInfo, statusDisplayOptions.animateEmojis)
|
||||
statusInfo.text = emojifiedText
|
||||
statusInfo.show()
|
||||
}
|
||||
|
||||
// don't use this on the same ViewHolder as setRebloggedByDisplayName, will cause recycling issues as paddings are changed
|
||||
protected fun setPollInfo(ownPoll: Boolean) = with(binding) {
|
||||
statusInfo.setText(if (ownPoll) R.string.poll_ended_created else R.string.poll_ended_voted)
|
||||
statusInfo.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_poll_24dp, 0, 0, 0)
|
||||
statusInfo.compoundDrawablePadding =
|
||||
Utils.dpToPx(context, 10)
|
||||
statusInfo.setPaddingRelative(Utils.dpToPx(context, 28), 0, 0, 0)
|
||||
statusInfo.show()
|
||||
}
|
||||
|
||||
private fun setReblogsCount(reblogsCount: Int) = with(binding) {
|
||||
statusReblogsCount.text = formatNumber(reblogsCount.toLong(), 1000)
|
||||
}
|
||||
|
||||
private fun setFavouritedCount(favouritedCount: Int) = with(binding) {
|
||||
statusFavouritesCount.text = formatNumber(favouritedCount.toLong(), 1000)
|
||||
}
|
||||
|
||||
protected fun hideStatusInfo() = with(binding) {
|
||||
statusInfo.hide()
|
||||
}
|
||||
|
||||
private fun setupCollapsedState(
|
||||
sensitive: Boolean,
|
||||
expanded: Boolean,
|
||||
status: StatusViewData,
|
||||
listener: StatusActionListener,
|
||||
) = with(binding) {
|
||||
/* input filter for TextViews have to be set before text */
|
||||
if (status.isCollapsible && (!sensitive || expanded)) {
|
||||
buttonToggleContent.setOnClickListener {
|
||||
val position = bindingAdapterPosition.takeIf { it != RecyclerView.NO_POSITION } ?: return@setOnClickListener
|
||||
listener.onContentCollapsedChange(
|
||||
!status.isCollapsed,
|
||||
position,
|
||||
)
|
||||
}
|
||||
buttonToggleContent.show()
|
||||
if (status.isCollapsed) {
|
||||
buttonToggleContent.setText(R.string.post_content_warning_show_more)
|
||||
content.filters = COLLAPSE_INPUT_FILTER
|
||||
} else {
|
||||
buttonToggleContent.setText(R.string.post_content_warning_show_less)
|
||||
content.filters = NO_INPUT_FILTER
|
||||
}
|
||||
} else {
|
||||
buttonToggleContent.hide()
|
||||
content.filters = NO_INPUT_FILTER
|
||||
}
|
||||
}
|
||||
|
||||
override fun showStatusContent(show: Boolean) = with(binding) {
|
||||
super.showStatusContent(show)
|
||||
buttonToggleContent.visibility = if (show) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
override fun toggleExpandedState(
|
||||
sensitive: Boolean,
|
||||
expanded: Boolean,
|
||||
status: StatusViewData,
|
||||
statusDisplayOptions: StatusDisplayOptions,
|
||||
listener: StatusActionListener,
|
||||
) {
|
||||
setupCollapsedState(sensitive, expanded, status, listener)
|
||||
super.toggleExpandedState(sensitive, expanded, status, statusDisplayOptions, listener)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val COLLAPSE_INPUT_FILTER = arrayOf<InputFilter>(SmartLengthInputFilter)
|
||||
private val NO_INPUT_FILTER = arrayOfNulls<InputFilter>(0)
|
||||
}
|
||||
}
|
|
@ -1,178 +0,0 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* This file is a part of Pachli.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* Pachli 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 Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package app.pachli.components.conversation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.InputFilter;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import app.pachli.R;
|
||||
import app.pachli.adapter.StatusBaseViewHolder;
|
||||
import app.pachli.entity.Attachment;
|
||||
import app.pachli.entity.Status;
|
||||
import app.pachli.entity.TimelineAccount;
|
||||
import app.pachli.interfaces.StatusActionListener;
|
||||
import app.pachli.util.ImageLoadingHelper;
|
||||
import app.pachli.util.SmartLengthInputFilter;
|
||||
import app.pachli.util.StatusDisplayOptions;
|
||||
import app.pachli.viewdata.StatusViewData;
|
||||
|
||||
public class ConversationViewHolder extends StatusBaseViewHolder {
|
||||
private static final InputFilter[] COLLAPSE_INPUT_FILTER = new InputFilter[]{SmartLengthInputFilter.INSTANCE};
|
||||
private static final InputFilter[] NO_INPUT_FILTER = new InputFilter[0];
|
||||
|
||||
private final TextView conversationNameTextView;
|
||||
private final Button contentCollapseButton;
|
||||
@NonNull
|
||||
private final ImageView[] avatars;
|
||||
|
||||
private final StatusDisplayOptions statusDisplayOptions;
|
||||
private final StatusActionListener listener;
|
||||
|
||||
ConversationViewHolder(@NonNull View itemView,
|
||||
StatusDisplayOptions statusDisplayOptions,
|
||||
StatusActionListener listener) {
|
||||
super(itemView);
|
||||
conversationNameTextView = itemView.findViewById(R.id.conversation_name);
|
||||
contentCollapseButton = itemView.findViewById(R.id.button_toggle_content);
|
||||
avatars = new ImageView[]{
|
||||
avatar,
|
||||
itemView.findViewById(R.id.status_avatar_1),
|
||||
itemView.findViewById(R.id.status_avatar_2)
|
||||
};
|
||||
this.statusDisplayOptions = statusDisplayOptions;
|
||||
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
void setupWithConversation(
|
||||
@NonNull ConversationViewData conversation,
|
||||
@Nullable Object payloads
|
||||
) {
|
||||
|
||||
StatusViewData statusViewData = conversation.getLastStatus();
|
||||
Status status = statusViewData.getStatus();
|
||||
|
||||
if (payloads == null) {
|
||||
TimelineAccount account = status.getAccount();
|
||||
|
||||
setupCollapsedState(statusViewData.isCollapsible(), statusViewData.isCollapsed(), statusViewData.isExpanded(), statusViewData.getSpoilerText(), listener);
|
||||
|
||||
setDisplayName(account.getName(), account.getEmojis(), statusDisplayOptions);
|
||||
setUsername(account.getUsername());
|
||||
setMetaData(statusViewData, statusDisplayOptions, listener);
|
||||
setIsReply(status.getInReplyToId() != null);
|
||||
setFavourited(status.getFavourited());
|
||||
setBookmarked(status.getBookmarked());
|
||||
List<Attachment> attachments = status.getAttachments();
|
||||
boolean sensitive = status.getSensitive();
|
||||
if (statusDisplayOptions.mediaPreviewEnabled() && hasPreviewableAttachment(attachments)) {
|
||||
setMediaPreviews(attachments, sensitive, listener, statusViewData.isShowingContent(),
|
||||
statusDisplayOptions.useBlurhash());
|
||||
|
||||
if (attachments.size() == 0) {
|
||||
hideSensitiveMediaWarning();
|
||||
}
|
||||
// Hide the unused label.
|
||||
for (TextView mediaLabel : mediaLabels) {
|
||||
mediaLabel.setVisibility(View.GONE);
|
||||
}
|
||||
} else {
|
||||
setMediaLabel(attachments, sensitive, listener, statusViewData.isShowingContent());
|
||||
// Hide all unused views.
|
||||
mediaPreview.setVisibility(View.GONE);
|
||||
hideSensitiveMediaWarning();
|
||||
}
|
||||
|
||||
setupButtons(listener, account.getId(), statusViewData.getContent().toString(),
|
||||
statusDisplayOptions);
|
||||
|
||||
setSpoilerAndContent(statusViewData, statusDisplayOptions, listener);
|
||||
|
||||
setConversationName(conversation.getAccounts());
|
||||
|
||||
setAvatars(conversation.getAccounts());
|
||||
} else {
|
||||
if (payloads instanceof List) {
|
||||
for (Object item : (List<?>) payloads) {
|
||||
if (Key.KEY_CREATED.equals(item)) {
|
||||
setMetaData(statusViewData, statusDisplayOptions, listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setConversationName(@NonNull List<ConversationAccountEntity> accounts) {
|
||||
Context context = conversationNameTextView.getContext();
|
||||
String conversationName = "";
|
||||
if (accounts.size() == 1) {
|
||||
conversationName = context.getString(R.string.conversation_1_recipients, accounts.get(0).getUsername());
|
||||
} else if (accounts.size() == 2) {
|
||||
conversationName = context.getString(R.string.conversation_2_recipients, accounts.get(0).getUsername(), accounts.get(1).getUsername());
|
||||
} else if (accounts.size() > 2) {
|
||||
conversationName = context.getString(R.string.conversation_more_recipients, accounts.get(0).getUsername(), accounts.get(1).getUsername(), accounts.size() - 2);
|
||||
}
|
||||
|
||||
conversationNameTextView.setText(conversationName);
|
||||
}
|
||||
|
||||
private void setAvatars(@NonNull List<ConversationAccountEntity> accounts) {
|
||||
for (int i = 0; i < avatars.length; i++) {
|
||||
ImageView avatarView = avatars[i];
|
||||
if (i < accounts.size()) {
|
||||
ImageLoadingHelper.loadAvatar(accounts.get(i).getAvatar(), avatarView,
|
||||
avatarRadius48dp, statusDisplayOptions.animateAvatars(), null);
|
||||
avatarView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
avatarView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setupCollapsedState(boolean collapsible, boolean collapsed, boolean expanded, String spoilerText, @NonNull final StatusActionListener listener) {
|
||||
/* input filter for TextViews have to be set before text */
|
||||
if (collapsible && (expanded || TextUtils.isEmpty(spoilerText))) {
|
||||
contentCollapseButton.setOnClickListener(view -> {
|
||||
int position = getBindingAdapterPosition();
|
||||
if (position != RecyclerView.NO_POSITION)
|
||||
listener.onContentCollapsedChange(!collapsed, position);
|
||||
});
|
||||
|
||||
contentCollapseButton.setVisibility(View.VISIBLE);
|
||||
if (collapsed) {
|
||||
contentCollapseButton.setText(R.string.post_content_warning_show_more);
|
||||
content.setFilters(COLLAPSE_INPUT_FILTER);
|
||||
} else {
|
||||
contentCollapseButton.setText(R.string.post_content_warning_show_less);
|
||||
content.setFilters(NO_INPUT_FILTER);
|
||||
}
|
||||
} else {
|
||||
contentCollapseButton.setVisibility(View.GONE);
|
||||
content.setFilters(NO_INPUT_FILTER);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,179 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* This file is a part of Pachli.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* Pachli 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 Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>.
|
||||
*/
|
||||
|
||||
package app.pachli.components.conversation
|
||||
|
||||
import android.text.InputFilter
|
||||
import android.text.TextUtils
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import app.pachli.R
|
||||
import app.pachli.adapter.StatusBaseViewHolder
|
||||
import app.pachli.interfaces.StatusActionListener
|
||||
import app.pachli.util.SmartLengthInputFilter
|
||||
import app.pachli.util.StatusDisplayOptions
|
||||
import app.pachli.util.hide
|
||||
import app.pachli.util.loadAvatar
|
||||
import app.pachli.util.show
|
||||
|
||||
class ConversationViewHolder internal constructor(
|
||||
itemView: View,
|
||||
private val statusDisplayOptions: StatusDisplayOptions,
|
||||
private val listener: StatusActionListener,
|
||||
) : StatusBaseViewHolder(itemView) {
|
||||
private val conversationNameTextView: TextView
|
||||
private val contentCollapseButton: Button
|
||||
private val avatars: Array<ImageView>
|
||||
|
||||
init {
|
||||
conversationNameTextView = itemView.findViewById(R.id.conversation_name)
|
||||
contentCollapseButton = itemView.findViewById(R.id.button_toggle_content)
|
||||
avatars = arrayOf(
|
||||
avatar,
|
||||
itemView.findViewById(R.id.status_avatar_1),
|
||||
itemView.findViewById(R.id.status_avatar_2),
|
||||
)
|
||||
}
|
||||
|
||||
fun setupWithConversation(
|
||||
conversation: ConversationViewData,
|
||||
payloads: Any?,
|
||||
) {
|
||||
val statusViewData = conversation.lastStatus
|
||||
val (_, _, account, inReplyToId, _, _, _, _, _, _, _, _, _, _, favourited, bookmarked, sensitive, _, _, attachments) = statusViewData.status
|
||||
if (payloads == null) {
|
||||
setupCollapsedState(
|
||||
statusViewData.isCollapsible,
|
||||
statusViewData.isCollapsed,
|
||||
statusViewData.isExpanded,
|
||||
statusViewData.spoilerText,
|
||||
listener,
|
||||
)
|
||||
setDisplayName(account.name, account.emojis, statusDisplayOptions)
|
||||
setUsername(account.username)
|
||||
setMetaData(statusViewData, statusDisplayOptions, listener)
|
||||
setIsReply(inReplyToId != null)
|
||||
setFavourited(favourited)
|
||||
setBookmarked(bookmarked)
|
||||
if (statusDisplayOptions.mediaPreviewEnabled && hasPreviewableAttachment(attachments)) {
|
||||
setMediaPreviews(
|
||||
attachments,
|
||||
sensitive,
|
||||
listener,
|
||||
statusViewData.isShowingContent,
|
||||
statusDisplayOptions.useBlurhash,
|
||||
)
|
||||
if (attachments.isEmpty()) {
|
||||
hideSensitiveMediaWarning()
|
||||
}
|
||||
// Hide the unused label.
|
||||
for (mediaLabel in mediaLabels) {
|
||||
mediaLabel.visibility = View.GONE
|
||||
}
|
||||
} else {
|
||||
setMediaLabel(attachments, sensitive, listener, statusViewData.isShowingContent)
|
||||
// Hide all unused views.
|
||||
mediaPreview.visibility = View.GONE
|
||||
hideSensitiveMediaWarning()
|
||||
}
|
||||
setupButtons(
|
||||
listener,
|
||||
account.id,
|
||||
statusDisplayOptions,
|
||||
)
|
||||
setSpoilerAndContent(statusViewData, statusDisplayOptions, listener)
|
||||
setConversationName(conversation.accounts)
|
||||
setAvatars(conversation.accounts)
|
||||
} else {
|
||||
if (payloads is List<*>) {
|
||||
for (item in payloads) {
|
||||
if (Key.KEY_CREATED == item) {
|
||||
setMetaData(statusViewData, statusDisplayOptions, listener)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setConversationName(accounts: List<ConversationAccountEntity>) {
|
||||
conversationNameTextView.text = when (accounts.size) {
|
||||
1 -> context.getString(
|
||||
R.string.conversation_1_recipients,
|
||||
accounts[0].username,
|
||||
)
|
||||
2 -> context.getString(
|
||||
R.string.conversation_2_recipients,
|
||||
accounts[0].username,
|
||||
accounts[1].username,
|
||||
)
|
||||
else -> context.getString(
|
||||
R.string.conversation_more_recipients,
|
||||
accounts[0].username,
|
||||
accounts[1].username,
|
||||
accounts.size - 2,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setAvatars(accounts: List<ConversationAccountEntity>) {
|
||||
avatars.withIndex().forEach { views ->
|
||||
accounts.getOrNull(views.index)?.also { account ->
|
||||
loadAvatar(
|
||||
account.avatar,
|
||||
views.value,
|
||||
avatarRadius48dp,
|
||||
statusDisplayOptions.animateAvatars,
|
||||
)
|
||||
views.value.show()
|
||||
} ?: views.value.hide()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupCollapsedState(
|
||||
collapsible: Boolean,
|
||||
collapsed: Boolean,
|
||||
expanded: Boolean,
|
||||
spoilerText: String,
|
||||
listener: StatusActionListener,
|
||||
) {
|
||||
/* input filter for TextViews have to be set before text */
|
||||
if (collapsible && (expanded || TextUtils.isEmpty(spoilerText))) {
|
||||
contentCollapseButton.setOnClickListener {
|
||||
val position = bindingAdapterPosition.takeIf { it != RecyclerView.NO_POSITION } ?: return@setOnClickListener
|
||||
listener.onContentCollapsedChange(!collapsed, position)
|
||||
}
|
||||
contentCollapseButton.show()
|
||||
if (collapsed) {
|
||||
contentCollapseButton.setText(R.string.post_content_warning_show_more)
|
||||
content.filters = COLLAPSE_INPUT_FILTER
|
||||
} else {
|
||||
contentCollapseButton.setText(R.string.post_content_warning_show_less)
|
||||
content.filters = NO_INPUT_FILTER
|
||||
}
|
||||
} else {
|
||||
contentCollapseButton.visibility = View.GONE
|
||||
content.filters = NO_INPUT_FILTER
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val COLLAPSE_INPUT_FILTER = arrayOf<InputFilter>(SmartLengthInputFilter)
|
||||
private val NO_INPUT_FILTER = arrayOfNulls<InputFilter>(0)
|
||||
}
|
||||
}
|
|
@ -145,7 +145,7 @@ class NotificationsPagingAdapter(
|
|||
)
|
||||
}
|
||||
NotificationViewKind.STATUS_FILTERED -> {
|
||||
StatusViewHolder(
|
||||
FilterableStatusViewHolder(
|
||||
ItemStatusWrapperBinding.inflate(inflater, parent, false),
|
||||
statusActionListener,
|
||||
accountId,
|
||||
|
|
|
@ -17,18 +17,20 @@
|
|||
|
||||
package app.pachli.components.notifications
|
||||
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import app.pachli.adapter.FilterableStatusViewHolder
|
||||
import app.pachli.adapter.StatusViewHolder
|
||||
import app.pachli.databinding.ItemStatusBinding
|
||||
import app.pachli.databinding.ItemStatusWrapperBinding
|
||||
import app.pachli.entity.Notification
|
||||
import app.pachli.interfaces.StatusActionListener
|
||||
import app.pachli.util.StatusDisplayOptions
|
||||
import app.pachli.viewdata.NotificationViewData
|
||||
|
||||
internal class StatusViewHolder(
|
||||
binding: ViewBinding,
|
||||
binding: ItemStatusBinding,
|
||||
private val statusActionListener: StatusActionListener,
|
||||
private val accountId: String,
|
||||
) : NotificationsPagingAdapter.ViewHolder, StatusViewHolder(binding.root) {
|
||||
) : NotificationsPagingAdapter.ViewHolder, StatusViewHolder(binding) {
|
||||
|
||||
override fun bind(
|
||||
viewData: NotificationViewData,
|
||||
|
@ -58,3 +60,38 @@ internal class StatusViewHolder(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FilterableStatusViewHolder(
|
||||
binding: ItemStatusWrapperBinding,
|
||||
private val statusActionListener: StatusActionListener,
|
||||
private val accountId: String,
|
||||
) : NotificationsPagingAdapter.ViewHolder, FilterableStatusViewHolder(binding) {
|
||||
// Note: Identical to bind() in StatusViewHolder above
|
||||
override fun bind(
|
||||
viewData: NotificationViewData,
|
||||
payloads: List<*>?,
|
||||
statusDisplayOptions: StatusDisplayOptions,
|
||||
) {
|
||||
val statusViewData = viewData.statusViewData
|
||||
if (statusViewData == null) {
|
||||
// Hide null statuses. Shouldn't happen according to the spec, but some servers
|
||||
// have been seen to do this (https://github.com/tuskyapp/Tusky/issues/2252)
|
||||
showStatusContent(false)
|
||||
} else {
|
||||
if (payloads.isNullOrEmpty()) {
|
||||
showStatusContent(true)
|
||||
}
|
||||
setupWithStatus(
|
||||
statusViewData,
|
||||
statusActionListener,
|
||||
statusDisplayOptions,
|
||||
payloads?.firstOrNull(),
|
||||
)
|
||||
}
|
||||
if (viewData.type == Notification.Type.POLL) {
|
||||
setPollInfo(accountId == viewData.account.id)
|
||||
} else {
|
||||
hideStatusInfo()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ import app.pachli.viewdata.PollViewData
|
|||
import app.pachli.viewdata.StatusViewData
|
||||
import java.util.Date
|
||||
|
||||
class StatusViewHolder(
|
||||
open class StatusViewHolder(
|
||||
private val binding: ItemReportStatusBinding,
|
||||
private val statusDisplayOptions: StatusDisplayOptions,
|
||||
private val viewState: StatusViewState,
|
||||
|
|
|
@ -19,8 +19,8 @@ import android.view.LayoutInflater
|
|||
import android.view.ViewGroup
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import app.pachli.R
|
||||
import app.pachli.adapter.StatusViewHolder
|
||||
import app.pachli.databinding.ItemStatusBinding
|
||||
import app.pachli.interfaces.StatusActionListener
|
||||
import app.pachli.util.StatusDisplayOptions
|
||||
import app.pachli.viewdata.StatusViewData
|
||||
|
@ -31,9 +31,9 @@ class SearchStatusesAdapter(
|
|||
) : PagingDataAdapter<StatusViewData, StatusViewHolder>(STATUS_COMPARATOR) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StatusViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_status, parent, false)
|
||||
return StatusViewHolder(view)
|
||||
return StatusViewHolder(
|
||||
ItemStatusBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: StatusViewHolder, position: Int) {
|
||||
|
|
|
@ -21,8 +21,11 @@ import androidx.paging.PagingDataAdapter
|
|||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import app.pachli.R
|
||||
import app.pachli.adapter.FilterableStatusViewHolder
|
||||
import app.pachli.adapter.StatusBaseViewHolder
|
||||
import app.pachli.adapter.StatusViewHolder
|
||||
import app.pachli.databinding.ItemStatusBinding
|
||||
import app.pachli.databinding.ItemStatusWrapperBinding
|
||||
import app.pachli.entity.Filter
|
||||
import app.pachli.interfaces.StatusActionListener
|
||||
import app.pachli.util.StatusDisplayOptions
|
||||
|
@ -36,10 +39,10 @@ class TimelinePagingAdapter(
|
|||
val inflater = LayoutInflater.from(viewGroup.context)
|
||||
return when (viewType) {
|
||||
VIEW_TYPE_STATUS_FILTERED -> {
|
||||
StatusViewHolder(inflater.inflate(R.layout.item_status_wrapper, viewGroup, false))
|
||||
FilterableStatusViewHolder(ItemStatusWrapperBinding.inflate(inflater, viewGroup, false))
|
||||
}
|
||||
VIEW_TYPE_STATUS -> {
|
||||
StatusViewHolder(inflater.inflate(R.layout.item_status, viewGroup, false))
|
||||
StatusViewHolder(ItemStatusBinding.inflate(inflater, viewGroup, false))
|
||||
}
|
||||
else -> return object : RecyclerView.ViewHolder(inflater.inflate(R.layout.item_placeholder, viewGroup, false)) {}
|
||||
}
|
||||
|
|
|
@ -19,10 +19,13 @@ import android.view.LayoutInflater
|
|||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import app.pachli.R
|
||||
import app.pachli.adapter.FilterableStatusViewHolder
|
||||
import app.pachli.adapter.StatusBaseViewHolder
|
||||
import app.pachli.adapter.StatusDetailedViewHolder
|
||||
import app.pachli.adapter.StatusViewHolder
|
||||
import app.pachli.databinding.ItemStatusBinding
|
||||
import app.pachli.databinding.ItemStatusDetailedBinding
|
||||
import app.pachli.databinding.ItemStatusWrapperBinding
|
||||
import app.pachli.entity.Filter
|
||||
import app.pachli.interfaces.StatusActionListener
|
||||
import app.pachli.util.StatusDisplayOptions
|
||||
|
@ -37,13 +40,13 @@ class ThreadAdapter(
|
|||
val inflater = LayoutInflater.from(parent.context)
|
||||
return when (viewType) {
|
||||
VIEW_TYPE_STATUS -> {
|
||||
StatusViewHolder(inflater.inflate(R.layout.item_status, parent, false))
|
||||
StatusViewHolder(ItemStatusBinding.inflate(inflater, parent, false))
|
||||
}
|
||||
VIEW_TYPE_STATUS_FILTERED -> {
|
||||
StatusViewHolder(inflater.inflate(R.layout.item_status_wrapper, parent, false))
|
||||
FilterableStatusViewHolder(ItemStatusWrapperBinding.inflate(inflater, parent, false))
|
||||
}
|
||||
VIEW_TYPE_STATUS_DETAILED -> {
|
||||
StatusDetailedViewHolder(inflater.inflate(R.layout.item_status_detailed, parent, false))
|
||||
StatusDetailedViewHolder(ItemStatusDetailedBinding.inflate(inflater, parent, false))
|
||||
}
|
||||
else -> error("Unknown item type: $viewType")
|
||||
}
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
package app.pachli.entity
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.DrawableRes
|
||||
import app.pachli.R
|
||||
import com.google.gson.JsonDeserializationContext
|
||||
import com.google.gson.JsonDeserializer
|
||||
import com.google.gson.JsonElement
|
||||
|
@ -53,6 +55,15 @@ data class Attachment(
|
|||
UNKNOWN,
|
||||
}
|
||||
|
||||
/** @return a drawable resource for an icon to indicate the attachment type */
|
||||
@DrawableRes
|
||||
fun iconResource() = when (this.type) {
|
||||
Type.IMAGE -> R.drawable.ic_photo_24dp
|
||||
Type.GIFV, Type.VIDEO -> R.drawable.ic_videocam_24dp
|
||||
Type.AUDIO -> R.drawable.ic_music_box_24dp
|
||||
Type.UNKNOWN -> R.drawable.ic_attach_file_24dp
|
||||
}
|
||||
|
||||
class MediaTypeDeserializer : JsonDeserializer<Type> {
|
||||
@Throws(JsonParseException::class)
|
||||
override fun deserialize(json: JsonElement, classOfT: java.lang.reflect.Type, context: JsonDeserializationContext): Type {
|
||||
|
|
|
@ -15,8 +15,13 @@
|
|||
|
||||
package app.pachli.entity
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.style.URLSpan
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import app.pachli.R
|
||||
import app.pachli.util.parseAsMastodonHtml
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import java.util.Date
|
||||
|
@ -59,13 +64,6 @@ data class Status(
|
|||
val actionableStatus: Status
|
||||
get() = reblog ?: this
|
||||
|
||||
/** Helpers for Java */
|
||||
fun copyWithFavourited(favourited: Boolean): Status = copy(favourited = favourited)
|
||||
fun copyWithReblogged(reblogged: Boolean): Status = copy(reblogged = reblogged)
|
||||
fun copyWithBookmarked(bookmarked: Boolean): Status = copy(bookmarked = bookmarked)
|
||||
fun copyWithPoll(poll: Poll?): Status = copy(poll = poll)
|
||||
fun copyWithPinned(pinned: Boolean): Status = copy(pinned = pinned)
|
||||
|
||||
enum class Visibility(val num: Int) {
|
||||
UNKNOWN(0),
|
||||
|
||||
|
@ -178,3 +176,43 @@ data class Status(
|
|||
const val MAX_POLL_OPTIONS = 4
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A description for this visibility, or "" if it's null or [Status.Visibility.UNKNOWN].
|
||||
*/
|
||||
fun Status.Visibility?.description(context: Context): CharSequence {
|
||||
this ?: return ""
|
||||
|
||||
val resource: Int = when (this) {
|
||||
Status.Visibility.PUBLIC -> R.string.description_visibility_public
|
||||
Status.Visibility.UNLISTED -> R.string.description_visibility_unlisted
|
||||
Status.Visibility.PRIVATE -> R.string.description_visibility_private
|
||||
Status.Visibility.DIRECT -> R.string.description_visibility_direct
|
||||
Status.Visibility.UNKNOWN -> return ""
|
||||
}
|
||||
return context.getString(resource)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return An icon for this visibility scaled and coloured to match the text on [textView].
|
||||
* Returns null if visibility is [Status.Visibility.UNKNOWN].
|
||||
*/
|
||||
fun Status.Visibility?.icon(textView: TextView): Drawable? {
|
||||
this ?: return null
|
||||
|
||||
val resource: Int = when (this) {
|
||||
Status.Visibility.PUBLIC -> R.drawable.ic_public_24dp
|
||||
Status.Visibility.UNLISTED -> R.drawable.ic_lock_open_24dp
|
||||
Status.Visibility.PRIVATE -> R.drawable.ic_lock_outline_24dp
|
||||
Status.Visibility.DIRECT -> R.drawable.ic_email_24dp
|
||||
Status.Visibility.UNKNOWN -> return null
|
||||
}
|
||||
val visibilityDrawable = AppCompatResources.getDrawable(
|
||||
textView.context,
|
||||
resource,
|
||||
) ?: return null
|
||||
val size = textView.textSize.toInt()
|
||||
visibilityDrawable.setBounds(0, 0, size, size)
|
||||
visibilityDrawable.setTint(textView.currentTextColor)
|
||||
return visibilityDrawable
|
||||
}
|
||||
|
|
|
@ -62,7 +62,7 @@ class PollView @JvmOverloads constructor(
|
|||
* should be treated as a navigation click. If non-null the user has voted,
|
||||
* and [choices] contains the option(s) they voted for.
|
||||
*/
|
||||
fun onClick(choices: List<Int>?): Unit
|
||||
fun onClick(choices: List<Int>?)
|
||||
}
|
||||
|
||||
val binding: StatusPollBinding
|
||||
|
|
|
@ -259,7 +259,7 @@
|
|||
sparkbutton:secondaryColor="?colorPrimaryContainer" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/status_insets"
|
||||
android:id="@+id/status_reblogs_count"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="45dp"
|
||||
|
|
|
@ -4,10 +4,12 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<include layout="@layout/item_status" />
|
||||
<include
|
||||
android:id="@+id/status_container"
|
||||
layout="@layout/item_status" />
|
||||
|
||||
<include
|
||||
android:id="@+id/status_filtered_placeholder"
|
||||
layout="@layout/item_status_filtered"
|
||||
android:visibility="gone"
|
||||
/>
|
||||
</FrameLayout>
|
||||
android:visibility="gone" />
|
||||
</FrameLayout>
|
||||
|
|
Loading…
Reference in New Issue