SubwayTooter-Android-App/app/src/main/java/jp/juggler/subwaytooter/columnviewholder/ViewHolderHeaderProfile.kt

703 lines
24 KiB
Kotlin

package jp.juggler.subwaytooter.columnviewholder
import android.graphics.Color
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.style.ForegroundColorSpan
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import jp.juggler.subwaytooter.*
import jp.juggler.subwaytooter.action.followFromAnotherAccount
import jp.juggler.subwaytooter.action.userProfileLocal
import jp.juggler.subwaytooter.actmain.nextPosition
import jp.juggler.subwaytooter.api.MisskeyAccountDetailMap
import jp.juggler.subwaytooter.api.TootApiResult
import jp.juggler.subwaytooter.api.entity.TootAccount
import jp.juggler.subwaytooter.api.entity.TootAccountRef
import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.api.runApiTask
import jp.juggler.subwaytooter.column.*
import jp.juggler.subwaytooter.databinding.LvHeaderProfileBinding
import jp.juggler.subwaytooter.dialog.showTextInputDialog
import jp.juggler.subwaytooter.emoji.EmojiMap
import jp.juggler.subwaytooter.itemviewholder.DlgContextMenu
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.pref.PrefI
import jp.juggler.subwaytooter.span.*
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.UserRelation
import jp.juggler.subwaytooter.table.daoAcctColor
import jp.juggler.subwaytooter.table.daoUserRelation
import jp.juggler.subwaytooter.util.*
import jp.juggler.subwaytooter.view.MyLinkMovementMethod
import jp.juggler.subwaytooter.view.MyTextView
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.data.buildJsonObject
import jp.juggler.util.data.intoStringResource
import jp.juggler.util.data.notEmpty
import jp.juggler.util.data.notZero
import jp.juggler.util.log.showToast
import jp.juggler.util.network.toPostRequestBuilder
import jp.juggler.util.ui.attrColor
import jp.juggler.util.ui.setIconDrawableId
import jp.juggler.util.ui.vg
import org.jetbrains.anko.textColor
internal class ViewHolderHeaderProfile(
override val activity: ActMain,
parent: ViewGroup,
val views: LvHeaderProfileBinding =
LvHeaderProfileBinding.inflate(activity.layoutInflater, parent, false),
) : ViewHolderHeaderBase(views.root), View.OnClickListener, View.OnLongClickListener {
companion object {
private fun SpannableStringBuilder.appendSpan(text: String, span: Any) {
val start = length
append(text)
setSpan(
span,
start,
length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
private var whoRef: TootAccountRef? = null
private var movedRef: TootAccountRef? = null
private val nameInvalidator1 =
NetworkEmojiInvalidator(activity.handler, views.tvDisplayName)
private val noteInvalidator =
NetworkEmojiInvalidator(activity.handler, views.tvNote)
private val movedCaptionInvalidator =
NetworkEmojiInvalidator(activity.handler, views.tvMoved)
private val movedNameInvalidator =
NetworkEmojiInvalidator(activity.handler, views.tvMovedName)
private val density: Float
private var colorTextContent = 0
private var relation: UserRelation? = null
init {
views.root.tag = this
val holder = this
views.run {
density = tvDisplayName.resources.displayMetrics.density
for (v in arrayOf(
ivBackground,
btnFollowing,
btnFollowers,
btnStatusCount,
btnMore,
btnFollow,
tvRemoteProfileWarning,
btnPersonalNotesEdit,
btnMoved,
llMoved,
btnPersonalNotesEdit
)) {
v.setOnClickListener(holder)
}
btnMoved.setOnLongClickListener(holder)
btnFollow.setOnLongClickListener(holder)
tvNote.movementMethod = MyLinkMovementMethod
ivBackground.measureProfileBg = true
}
}
override fun getAccount(): TootAccountRef? = whoRef
override fun onViewRecycled() {
}
// fun updateRelativeTime() {
// val who = whoRef?.get()
// if(who != null) {
// tvCreated.text = TootStatus.formatTime(tvCreated.context, who.time_created_at, true)
// }
// }
override fun bindData(column: Column) {
super.bindData(column)
bindFonts()
bindColors()
views.run {
llMoved.visibility = View.GONE
tvMoved.visibility = View.GONE
llFields.visibility = View.GONE
llFields.removeAllViews()
}
val whoRef = column.whoAccount
this.whoRef = whoRef
when (val who = whoRef?.get()) {
null -> bindAccountNull()
else -> bindAccount(who, whoRef)
}
}
// カラム設定から戻った際に呼ばれる
override fun showColor() {
views.llProfile.setBackgroundColor(
when (val c = column.columnBgColor) {
0 -> activity.attrColor(R.attr.colorProfileBackgroundMask)
else -> -0x40000000 or (0x00ffffff and c)
}
)
}
// bind時に呼ばれる
private fun bindColors() {
val contentColor = column.getContentColor()
this.colorTextContent = contentColor
views.run {
tvPersonalNotes.textColor = contentColor
tvMoved.textColor = contentColor
tvMovedName.textColor = contentColor
tvDisplayName.textColor = contentColor
tvNote.textColor = contentColor
tvRemoteProfileWarning.textColor = contentColor
btnStatusCount.textColor = contentColor
btnFollowing.textColor = contentColor
btnFollowers.textColor = contentColor
tvFeaturedTags.textColor = contentColor
setIconDrawableId(
activity,
btnMore,
R.drawable.ic_more,
color = contentColor,
alphaMultiplier = stylerBoostAlpha
)
setIconDrawableId(
activity,
btnPersonalNotesEdit,
R.drawable.ic_edit,
color = contentColor,
alphaMultiplier = stylerBoostAlpha
)
val acctColor = column.getAcctColor()
tvCreated.textColor = acctColor
tvMovedAcct.textColor = acctColor
tvLastStatusAt.textColor = acctColor
showColor()
}
}
private fun bindFonts() {
views.run {
var f: Float
f = activity.timelineFontSizeSp
if (!f.isNaN()) {
tvMovedName.textSize = f
tvMoved.textSize = f
tvPersonalNotes.textSize = f
tvFeaturedTags.textSize = f
}
f = activity.acctFontSizeSp
if (!f.isNaN()) {
tvMovedAcct.textSize = f
tvCreated.textSize = f
tvLastStatusAt.textSize = f
}
val spacing = activity.timelineSpacing
if (spacing != null) {
tvMovedName.setLineSpacing(0f, spacing)
tvMoved.setLineSpacing(0f, spacing)
}
}
}
private fun bindAccountNull() {
relation = null
nameInvalidator1.register(null)
noteInvalidator.register(null)
views.run {
tvCreated.text = ""
tvLastStatusAt.vg(false)
tvFeaturedTags.vg(false)
ivBackground.setImageDrawable(null)
ivAvatar.setImageDrawable(null)
tvAcct.text = "@"
nameInvalidator1.text = ""
noteInvalidator.text = ""
tvMisskeyExtra.text = ""
btnStatusCount.text = activity.getString(R.string.statuses) + "\n" + "?"
btnFollowing.text = activity.getString(R.string.following) + "\n" + "?"
btnFollowers.text = activity.getString(R.string.followers) + "\n" + "?"
btnFollow.setImageDrawable(null)
tvRemoteProfileWarning.visibility = View.GONE
}
}
private fun bindAccount(who: TootAccount, whoRef: TootAccountRef) {
// Misskeyの場合はNote中のUserエンティティと /api/users/show の情報量がかなり異なる
val whoDetail = MisskeyAccountDetailMap.get(accessInfo, who.id)
val relation = daoUserRelation.load(accessInfo.db_id, who.id)
this.relation = relation
views.run {
tvCreated.text =
TootStatus.formatTime(tvCreated.context, (whoDetail ?: who).time_created_at, true)
who.setAccountExtra(
accessInfo,
NetworkEmojiInvalidator(activity.handler ,tvLastStatusAt),
fromProfileHeader = true
)
val featuredTagsText = formatFeaturedTags()
tvFeaturedTags.vg(featuredTagsText != null)?.let {
it.text = featuredTagsText!!
it.movementMethod = MyLinkMovementMethod
}
ivBackground.setImageUrl(0f, accessInfo.supplyBaseUrl(who.header_static))
ivAvatar.setImageUrl(
calcIconRound(ivAvatar.layoutParams),
accessInfo.supplyBaseUrl(who.avatar_static),
accessInfo.supplyBaseUrl(who.avatar)
)
val name = whoDetail?.decodeDisplayName(activity) ?: whoRef.decoded_display_name
nameInvalidator1.text = name
tvRemoteProfileWarning.vg(column.accessInfo.isRemoteUser(who))
tvAcct.text = encodeAcctText(who, whoDetail)
val note = whoRef.decoded_note
noteInvalidator.text = note
tvMisskeyExtra.text = encodeMisskeyExtra(whoDetail)
tvMisskeyExtra.vg(tvMisskeyExtra.text.isNotEmpty())
btnStatusCount.text =
"${activity.getString(R.string.statuses)}\n${
whoDetail?.statuses_count ?: who.statuses_count
}"
val hideFollowCount = PrefB.bpHideFollowCount.value
var caption = activity.getString(R.string.following)
btnFollowing.text = when {
hideFollowCount -> caption
else -> "${caption}\n${whoDetail?.following_count ?: who.following_count}"
}
caption = activity.getString(R.string.followers)
btnFollowers.text = when {
hideFollowCount -> caption
else -> "${caption}\n${whoDetail?.followers_count ?: who.followers_count}"
}
setFollowIcon(
activity,
btnFollow,
ivFollowedBy,
relation,
who,
colorTextContent,
alphaMultiplier = stylerBoostAlpha
)
tvPersonalNotes.text = relation.note ?: ""
showMoved(who, who.movedRef)
(whoDetail?.fields ?: who.fields)?.notEmpty()?.let { showFields(who, it) }
}
}
private fun showMoved(who: TootAccount, movedRef: TootAccountRef?) {
if (movedRef == null) return
this.movedRef = movedRef
val moved = movedRef.get()
views.run {
llMoved.visibility = View.VISIBLE
tvMoved.visibility = View.VISIBLE
val caption = who.decodeDisplayName(activity)
.intoStringResource(activity, R.string.account_moved_to)
movedCaptionInvalidator.text = caption
ivMoved.layoutParams.width = activity.avatarIconSize
ivMoved.setImageUrl(
calcIconRound(ivMoved.layoutParams),
accessInfo.supplyBaseUrl(moved.avatar_static)
)
movedNameInvalidator.text = movedRef.decoded_display_name
setAcct(tvMovedAcct, accessInfo, moved)
val relation = daoUserRelation.load(accessInfo.db_id, moved.id)
setFollowIcon(
activity,
btnMoved,
ivMovedBy,
relation,
moved,
colorTextContent,
alphaMultiplier = stylerBoostAlpha
)
}
}
override fun onClick(v: View) {
when (v.id) {
R.id.ivBackground, R.id.tvRemoteProfileWarning ->
activity.openCustomTab(whoRef?.get()?.url)
R.id.btnFollowing -> {
column.profileTab = ProfileTab.Following
activity.appState.saveColumnList()
column.startLoading()
}
R.id.btnFollowers -> {
column.profileTab = ProfileTab.Followers
activity.appState.saveColumnList()
column.startLoading()
}
R.id.btnStatusCount -> {
column.profileTab = ProfileTab.Status
activity.appState.saveColumnList()
column.startLoading()
}
R.id.btnMore -> whoRef?.let { whoRef ->
DlgContextMenu(activity, column, whoRef, null, null, null).show()
}
R.id.btnFollow -> whoRef?.let { whoRef ->
DlgContextMenu(activity, column, whoRef, null, null, null).show()
}
R.id.btnMoved -> movedRef?.let { movedRef ->
DlgContextMenu(activity, column, movedRef, null, null, null).show()
}
R.id.llMoved -> movedRef?.let { movedRef ->
if (accessInfo.isPseudo) {
DlgContextMenu(activity, column, movedRef, null, null, null).show()
} else {
activity.userProfileLocal(
activity.nextPosition(column),
accessInfo,
movedRef.get()
)
}
}
R.id.btnPersonalNotesEdit -> whoRef?.let { whoRef ->
val who = whoRef.get()
val relation = this.relation
val lastColumn = column
activity.launchAndShowError {
activity.showTextInputDialog(
title = daoAcctColor.getStringWithNickname(
activity,
R.string.personal_notes_of,
who.acct
),
initialText = relation?.note ?: "",
allowEmpty = true,
onEmptyText = {},
) { text ->
val result = activity.runApiTask(column.accessInfo) { client ->
when {
accessInfo.isPseudo ->
TootApiResult("Personal notes is not supported on pseudo account.")
accessInfo.isMisskey ->
TootApiResult("Personal notes is not supported on Misskey account.")
else ->
client.request(
"/api/v1/accounts/${who.id}/note",
buildJsonObject {
put("comment", text)
}.toPostRequestBuilder()
)
}
}
result ?: return@showTextInputDialog true
when (val error = result.error) {
null -> {
relation?.note = text
if (lastColumn == column) bindData(column)
true
}
else -> {
activity.showToast(true, error)
false
}
}
}
}
}
}
}
override fun onLongClick(v: View): Boolean {
when (v.id) {
R.id.btnFollow -> {
activity.followFromAnotherAccount(
activity.nextPosition(column),
accessInfo,
whoRef?.get()
)
return true
}
R.id.btnMoved -> {
activity.followFromAnotherAccount(
activity.nextPosition(column),
accessInfo,
movedRef?.get()
)
return true
}
}
return false
}
private fun setAcct(tv: TextView, accessInfo: SavedAccount, who: TootAccount) {
val ac = daoAcctColor.load(accessInfo, who)
tv.text = when {
daoAcctColor.hasNickname(ac) -> ac.nickname
PrefB.bpShortAcctLocalUser.value -> "@${who.acct.pretty}"
else -> "@${ac.nickname}"
}
tv.textColor = ac.colorFg.notZero() ?: column.getAcctColor()
tv.setBackgroundColor(ac.colorBg) // may 0
tv.setPaddingRelative(activity.acctPadLr, 0, activity.acctPadLr, 0)
}
private fun formatFeaturedTags() = column.whoFeaturedTags?.notEmpty()?.let { tagList ->
SpannableStringBuilder().apply {
append(activity.getString(R.string.featured_hashtags))
append(":")
tagList.forEach { tag ->
append(" ")
val tagWithSharp = "#" + tag.name
val start = length
append(tagWithSharp)
val end = length
tag.url?.notEmpty()?.let { url ->
val span = MyClickableSpan(
LinkInfo(url = url, tag = tag.name, caption = tagWithSharp)
)
setSpan(span, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
}
}
private fun encodeAcctText(who: TootAccount, whoDetail: TootAccount?) =
SpannableStringBuilder().apply {
append("@")
append(accessInfo.getFullAcct(who).pretty)
if (whoDetail?.locked ?: who.locked) {
append(" ")
val emoji = EmojiMap.shortNameMap["lock"]
when {
emoji == null ->
append("locked")
PrefB.bpUseTwemoji.value ->
appendSpan("locked", emoji.createSpan(activity))
else ->
append(emoji.unifiedCode)
}
}
if (who.bot) {
append(" ")
val emoji = EmojiMap.shortNameMap["robot_face"]
when {
emoji == null ->
append("bot")
PrefB.bpUseTwemoji.value ->
appendSpan("bot", emoji.createSpan(activity))
else ->
append(emoji.unifiedCode)
}
}
if (who.suspended) {
append(" ")
val emoji = EmojiMap.shortNameMap["cross_mark"]
when {
emoji == null ->
append("suspended")
PrefB.bpUseTwemoji.value ->
appendSpan("suspended", emoji.createSpan(activity))
else ->
append(emoji.unifiedCode)
}
}
}
private fun encodeMisskeyExtra(whoDetail: TootAccount?) = SpannableStringBuilder().apply {
var s = whoDetail?.location
if (s?.isNotEmpty() == true) {
if (isNotEmpty()) append('\n')
appendSpan(
activity.getString(R.string.location),
EmojiImageSpan(
activity,
R.drawable.ic_location,
useColorShader = true
)
)
append(' ')
append(s)
}
s = whoDetail?.birthday
if (s?.isNotEmpty() == true) {
if (isNotEmpty()) append('\n')
appendSpan(
activity.getString(R.string.birthday),
EmojiImageSpan(
activity,
R.drawable.ic_cake,
useColorShader = true
)
)
append(' ')
append(s)
}
}
private fun showFields(who: TootAccount, fields: List<TootAccount.Field>) {
views.llFields.visibility = View.VISIBLE
// fieldsのnameにはカスタム絵文字が適用されるようになった
// https://github.com/tootsuite/mastodon/pull/11350
// fieldsのvalueはMisskeyならMFM、MastodonならHTML
val fieldDecodeOptions = DecodeOptions(
context = activity,
decodeEmoji = true,
linkHelper = accessInfo,
short = true,
emojiMapCustom = who.custom_emojis,
emojiMapProfile = who.profile_emojis,
authorDomain = who,
emojiSizeMode = accessInfo.emojiSizeMode(),
enlargeCustomEmoji = DecodeOptions.emojiScaleUserName,
enlargeEmoji = DecodeOptions.emojiScaleUserName,
)
val nameTypeface = ActMain.timelineFontBold
val valueTypeface = ActMain.timelineFont
for (item in fields) {
//
val nameView = MyTextView(activity)
val nameLp = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
val nameText = fieldDecodeOptions.decodeEmoji(item.name)
nameLp.topMargin = (density * 6f).toInt()
nameView.layoutParams = nameLp
nameView.setTextColor(colorTextContent)
nameView.typeface = nameTypeface
nameView.movementMethod = MyLinkMovementMethod
views.llFields.addView(nameView)
val nameInvalidator = NetworkEmojiInvalidator(activity.handler, nameView)
nameInvalidator.text = nameText
// 値の方はHTMLエンコードされている
val valueView = MyTextView(activity)
val valueLp = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
val valueText = fieldDecodeOptions.decodeHTML(item.value)
if (item.verified_at > 0L) {
valueText.append('\n')
val start = valueText.length
valueText.append(activity.getString(R.string.verified_at))
valueText.append(": ")
valueText.append(TootStatus.formatTime(activity, item.verified_at, false))
val end = valueText.length
val linkFgColor = PrefI.ipVerifiedLinkFgColor.value.notZero()
?: (Color.BLACK or 0x7fbc99)
valueText.setSpan(
ForegroundColorSpan(linkFgColor),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
valueLp.startMargin = (density * 32f).toInt()
valueView.layoutParams = valueLp
valueView.setTextColor(colorTextContent)
valueView.typeface = valueTypeface
valueView.movementMethod = MyLinkMovementMethod
val valueInvalidator = NetworkEmojiInvalidator(activity.handler, valueView)
valueInvalidator.text = valueText
if (item.verified_at > 0L) {
val linkBgColor = PrefI.ipVerifiedLinkBgColor.value.notZero()
?: (0x337fbc99)
valueView.setBackgroundColor(linkBgColor)
}
views.llFields.addView(valueView)
}
}
}