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) { 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) } } }