package jp.juggler.subwaytooter import android.app.Dialog 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.widget.* import jp.juggler.subwaytooter.emoji.EmojiMap import jp.juggler.subwaytooter.action.* import jp.juggler.subwaytooter.api.* import jp.juggler.subwaytooter.api.entity.* import jp.juggler.subwaytooter.dialog.DlgTextInput import jp.juggler.subwaytooter.span.EmojiImageSpan import jp.juggler.subwaytooter.span.LinkInfo import jp.juggler.subwaytooter.span.MyClickableSpan import jp.juggler.subwaytooter.span.createSpan import jp.juggler.subwaytooter.table.AcctColor import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.UserRelation import jp.juggler.subwaytooter.util.* import jp.juggler.subwaytooter.view.MyLinkMovementMethod import jp.juggler.subwaytooter.view.MyNetworkImageView import jp.juggler.subwaytooter.view.MyTextView import jp.juggler.util.* import org.jetbrains.anko.textColor internal class ViewHolderHeaderProfile( activity: ActMain, viewRoot: View, ) : ViewHolderHeaderBase(activity, viewRoot), 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 val ivBackground: MyNetworkImageView private val tvCreated: TextView private val tvLastStatusAt: TextView private val tvFeaturedTags: TextView private val ivAvatar: MyNetworkImageView private val tvDisplayName: TextView private val tvAcct: TextView private val btnFollowing: Button private val btnFollowers: Button private val btnStatusCount: Button private val tvNote: TextView private val tvMisskeyExtra: TextView private val btnFollow: ImageButton private val ivFollowedBy: ImageView private val llProfile: View private val tvRemoteProfileWarning: TextView private val nameInvalidator1: NetworkEmojiInvalidator private val noteInvalidator: NetworkEmojiInvalidator private val llFields: LinearLayout private var whoRef: TootAccountRef? = null private var movedRef: TootAccountRef? = null private val llMoved: View private val tvMoved: TextView private val ivMoved: MyNetworkImageView private val tvMovedName: TextView private val tvMovedAcct: TextView private val btnMoved: ImageButton private val ivMovedBy: ImageView private val movedCaptionInvalidator: NetworkEmojiInvalidator private val movedNameInvalidator: NetworkEmojiInvalidator private val density: Float private val btnMore: ImageButton private val tvPersonalNotes: TextView private val btnPersonalNotesEdit: ImageButton private var contentColor = 0 private var relation: UserRelation? = null init { ivBackground = viewRoot.findViewById(R.id.ivBackground) llProfile = viewRoot.findViewById(R.id.llProfile) tvCreated = viewRoot.findViewById(R.id.tvCreated) tvLastStatusAt = viewRoot.findViewById(R.id.tvLastStatusAt) tvFeaturedTags = viewRoot.findViewById(R.id.tvFeaturedTags) ivAvatar = viewRoot.findViewById(R.id.ivAvatar) tvDisplayName = viewRoot.findViewById(R.id.tvDisplayName) tvAcct = viewRoot.findViewById(R.id.tvAcct) btnFollowing = viewRoot.findViewById(R.id.btnFollowing) btnFollowers = viewRoot.findViewById(R.id.btnFollowers) btnStatusCount = viewRoot.findViewById(R.id.btnStatusCount) tvNote = viewRoot.findViewById(R.id.tvNote) tvMisskeyExtra = viewRoot.findViewById(R.id.tvMisskeyExtra) btnMore = viewRoot.findViewById(R.id.btnMore) btnFollow = viewRoot.findViewById(R.id.btnFollow) ivFollowedBy = viewRoot.findViewById(R.id.ivFollowedBy) tvRemoteProfileWarning = viewRoot.findViewById(R.id.tvRemoteProfileWarning) llMoved = viewRoot.findViewById(R.id.llMoved) tvMoved = viewRoot.findViewById(R.id.tvMoved) ivMoved = viewRoot.findViewById(R.id.ivMoved) tvMovedName = viewRoot.findViewById(R.id.tvMovedName) tvMovedAcct = viewRoot.findViewById(R.id.tvMovedAcct) btnMoved = viewRoot.findViewById(R.id.btnMoved) ivMovedBy = viewRoot.findViewById(R.id.ivMovedBy) llFields = viewRoot.findViewById(R.id.llFields) tvPersonalNotes = viewRoot.findViewById(R.id.tvPersonalNotes) btnPersonalNotesEdit = viewRoot.findViewById(R.id.btnPersonalNotesEdit) density = tvDisplayName.resources.displayMetrics.density for (v in arrayOf( ivBackground, btnFollowing, btnFollowers, btnStatusCount, btnMore, btnFollow, tvRemoteProfileWarning, btnPersonalNotesEdit, btnMoved, llMoved, btnPersonalNotesEdit )) { v.setOnClickListener(this) } btnMoved.setOnLongClickListener(this) btnFollow.setOnLongClickListener(this) tvNote.movementMethod = MyLinkMovementMethod nameInvalidator1 = NetworkEmojiInvalidator(activity.handler, tvDisplayName) noteInvalidator = NetworkEmojiInvalidator(activity.handler, tvNote) movedCaptionInvalidator = NetworkEmojiInvalidator(activity.handler, tvMoved) movedNameInvalidator = NetworkEmojiInvalidator(activity.handler, tvMovedName) 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() 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() { 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.contentColor = contentColor 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 = Styler.boostAlpha ) setIconDrawableId( activity, btnPersonalNotesEdit, R.drawable.ic_edit, color = contentColor, alphaMultiplier = Styler.boostAlpha ) val acctColor = column.getAcctColor() tvCreated.textColor = acctColor tvMovedAcct.textColor = acctColor tvLastStatusAt.textColor = acctColor showColor() } private fun bindFonts() { 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 tvCreated.text = "" tvLastStatusAt.vg(false) tvFeaturedTags.vg(false) ivBackground.setImageDrawable(null) ivAvatar.setImageDrawable(null) tvAcct.text = "@" tvDisplayName.text = "" nameInvalidator1.register(null) tvNote.text = "" tvMisskeyExtra.text = "" noteInvalidator.register(null) 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) tvCreated.text = TootStatus.formatTime(tvCreated.context, (whoDetail ?: who).time_created_at, true) who.setAccountExtra( accessInfo, tvLastStatusAt, invalidator = null, fromProfileHeader = true ) val featuredTagsText = formatFeaturedTags() tvFeaturedTags.vg(featuredTagsText != null)?.let { it.text = featuredTagsText!! it.movementMethod = MyLinkMovementMethod } ivBackground.setImageUrl(activity.pref, 0f, accessInfo.supplyBaseUrl(who.header_static)) ivAvatar.setImageUrl( activity.pref, Styler.calcIconRound(ivAvatar.layoutParams), accessInfo.supplyBaseUrl(who.avatar_static), accessInfo.supplyBaseUrl(who.avatar) ) val name = whoDetail?.decodeDisplayName(activity) ?: whoRef.decoded_display_name tvDisplayName.text = name nameInvalidator1.register(name) tvRemoteProfileWarning.vg(column.accessInfo.isRemoteUser(who)) tvAcct.text = encodeAcctText(who, whoDetail) val note = whoRef.decoded_note tvNote.text = note noteInvalidator.register(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(activity.pref) 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}" } val relation = UserRelation.load(accessInfo.db_id, who.id) this.relation = relation Styler.setFollowIcon( activity, btnFollow, ivFollowedBy, relation, who, contentColor, alphaMultiplier = Styler.boostAlpha ) 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() llMoved.visibility = View.VISIBLE tvMoved.visibility = View.VISIBLE val caption = who.decodeDisplayName(activity) .intoStringResource(activity, R.string.account_moved_to) tvMoved.text = caption movedCaptionInvalidator.register(caption) ivMoved.layoutParams.width = activity.avatarIconSize ivMoved.setImageUrl( activity.pref, Styler.calcIconRound(ivMoved.layoutParams), accessInfo.supplyBaseUrl(moved.avatar_static) ) tvMovedName.text = movedRef.decoded_display_name movedNameInvalidator.register(movedRef.decoded_display_name) setAcct(tvMovedAcct, accessInfo, moved) val relation = UserRelation.load(accessInfo.db_id, moved.id) Styler.setFollowIcon( activity, btnMoved, ivMovedBy, relation, moved, contentColor, alphaMultiplier = Styler.boostAlpha ) } 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 DlgTextInput.show( activity, AcctColor.getStringWithNickname(activity, R.string.personal_notes_of, who.acct), relation?.note ?: "", allowEmpty = true, callback = object : DlgTextInput.Callback { override fun onEmptyError() { } override fun onOK(dialog: Dialog, text: String) { launchMain { 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", jsonObject { put("comment", text) }.toPostRequestBuilder() ) } }?.let { result -> when (val error = result.error) { null -> { relation?.note = text dialog.dismissSafe() if (lastColumn == column) bindData(column) } else -> activity.showToast(true, error) } } } } } ) } } } 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 = AcctColor.load(accessInfo, who) tv.text = when { AcctColor.hasNickname(ac) -> ac.nickname PrefB.bpShortAcctLocalUser(App1.pref) -> "@${who.acct.pretty}" else -> "@${ac.nickname}" } tv.textColor = ac.color_fg.notZero() ?: column.getAcctColor() tv.setBackgroundColor(ac.color_bg) // 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"] if (emoji != null) { appendSpan("locked", emoji.createSpan(activity)) } else { append("locked") } } if (who.bot) { append(" ") val emoji = EmojiMap.shortNameMap["robot_face"] if (emoji != null) { appendSpan("bot", emoji.createSpan(activity)) } else { append("bot") } } if (who.suspended) { append(" ") val emoji = EmojiMap.shortNameMap["cross_mark"] if (emoji != null) { appendSpan("suspended", emoji.createSpan(activity)) } else { append("suspended") } } } 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) { 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, mentionDefaultHostDomain = who ) val nameTypeface = ActMain.timeline_font_bold 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) val nameInvalidator = NetworkEmojiInvalidator(activity.handler, nameView) nameInvalidator.register(nameText) nameLp.topMargin = (density * 6f).toInt() nameView.layoutParams = nameLp nameView.text = nameText nameView.setTextColor(contentColor) nameView.typeface = nameTypeface nameView.movementMethod = MyLinkMovementMethod llFields.addView(nameView) // 値の方は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(activity.pref).notZero() ?: (Color.BLACK or 0x7fbc99) valueText.setSpan( ForegroundColorSpan(linkFgColor), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE ) } val valueInvalidator = NetworkEmojiInvalidator(activity.handler, valueView) valueInvalidator.register(valueText) valueLp.startMargin = (density * 32f).toInt() valueView.layoutParams = valueLp valueView.text = valueText valueView.setTextColor(contentColor) valueView.typeface = valueTypeface valueView.movementMethod = MyLinkMovementMethod if (item.verified_at > 0L) { val linkBgColor = PrefI.ipVerifiedLinkBgColor(activity.pref).notZero() ?: (0x337fbc99) valueView.setBackgroundColor(linkBgColor) } llFields.addView(valueView) } } }