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.emoji.EmojiMap import jp.juggler.subwaytooter.action.Action_Follow import jp.juggler.subwaytooter.action.Action_User 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 { 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 name_invalidator : NetworkEmojiInvalidator private val note_invalidator : 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 moved_caption_invalidator : NetworkEmojiInvalidator private val moved_name_invalidator : NetworkEmojiInvalidator private val density : Float private val btnMore : ImageButton private val tvPersonalNotes : TextView private val btnPersonalNotesEdit : ImageButton 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 name_invalidator = NetworkEmojiInvalidator(activity.handler, tvDisplayName) note_invalidator = NetworkEmojiInvalidator(activity.handler, tvNote) moved_caption_invalidator = NetworkEmojiInvalidator(activity.handler, tvMoved) moved_name_invalidator = NetworkEmojiInvalidator(activity.handler, tvMovedName) ivBackground.measureProfileBg = true } override fun showColor() { llProfile.setBackgroundColor( when(val c = column.column_bg_color) { 0 -> activity.attrColor(R.attr.colorProfileBackgroundMask) else -> - 0x40000000 or (0x00ffffff and c) } ) } private var contentColor = 0 private var relation : UserRelation? = null override fun bindData(column : Column) { super.bindData(column) var f : Float f = activity.timeline_font_size_sp if(! f.isNaN()) { tvMovedName.textSize = f tvMoved.textSize = f tvPersonalNotes.textSize = f tvFeaturedTags.textSize = f } f = activity.acct_font_size_sp if(! f.isNaN()) { tvMovedAcct.textSize = f tvCreated.textSize = f tvLastStatusAt.textSize = f } val spacing = activity.timeline_spacing if(spacing != null) { tvMovedName.setLineSpacing(0f, spacing) tvMoved.setLineSpacing(0f, spacing) } 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.boost_alpha ) setIconDrawableId( activity, btnPersonalNotesEdit, R.drawable.ic_edit, color = contentColor, alphaMultiplier = Styler.boost_alpha ) val acctColor = column.getAcctColor() tvCreated.textColor = acctColor tvMovedAcct.textColor = acctColor tvLastStatusAt.textColor = acctColor val whoRef = column.who_account this.whoRef = whoRef val who = whoRef?.get() // Misskeyの場合はNote中のUserエンティティと /api/users/show の情報量がかなり異なる val whoDetail = if(who == null) { null } else { MisskeyAccountDetailMap.get(access_info, who.id) } showColor() llMoved.visibility = View.GONE tvMoved.visibility = View.GONE llFields.visibility = View.GONE llFields.removeAllViews() if(who == null) { relation = null tvCreated.text = "" tvLastStatusAt.vg(false) tvFeaturedTags.vg(false) ivBackground.setImageDrawable(null) ivAvatar.setImageDrawable(null) tvAcct.text = "@" tvDisplayName.text = "" name_invalidator.register(null) tvNote.text = "" tvMisskeyExtra.text = "" note_invalidator.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 } else { tvCreated.text = TootStatus.formatTime(tvCreated.context, (whoDetail ?: who).time_created_at, true) who.setAccountExtra( access_info, tvLastStatusAt, invalidator = null, fromProfileHeader = true ) val featuredTagsText = column.who_featured_tags?.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) } } } } tvFeaturedTags.vg( featuredTagsText !=null)?.let{ it.text = featuredTagsText!! it.movementMethod = MyLinkMovementMethod } ivBackground.setImageUrl( activity.pref, 0f, access_info.supplyBaseUrl(who.header_static) ) ivAvatar.setImageUrl( activity.pref, Styler.calcIconRound(ivAvatar.layoutParams), access_info.supplyBaseUrl(who.avatar_static), access_info.supplyBaseUrl(who.avatar) ) val name = whoDetail?.decodeDisplayName(activity) ?: whoRef.decoded_display_name tvDisplayName.text = name name_invalidator.register(name) tvRemoteProfileWarning.visibility = if(column.access_info.isRemoteUser(who)) View.VISIBLE else View.GONE fun SpannableStringBuilder.appendSpan(text : String, span : Any) { val start = length append(text) setSpan( span, start, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE ) } tvAcct.text = SpannableStringBuilder().apply { append("@") append(access_info.getFullAcct(who).pretty) if(whoDetail?.locked ?: who.locked) { append(" ") val info = EmojiMap.sShortNameToEmojiInfo["lock"] if(info != null) { appendSpan("locked", info.er.createSpan(activity)) } else { append("locked") } } if(who.bot) { append(" ") val info = EmojiMap.sShortNameToEmojiInfo["robot_face"] if(info != null) { appendSpan("bot", info.er.createSpan(activity)) } else { append("bot") } } if(who.suspended) { append(" ") val info = EmojiMap.sShortNameToEmojiInfo["x"] if(info != null) { appendSpan("suspended", info.er.createSpan(activity)) } else { append("suspended") } } } val note = whoRef.decoded_note tvNote.text = note note_invalidator.register(note) tvMisskeyExtra.text = 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) } } tvMisskeyExtra.vg(tvMisskeyExtra.text.isNotEmpty()) btnStatusCount.text = "${activity.getString(R.string.statuses)}\n${ whoDetail?.statuses_count ?: who.statuses_count }" if(Pref.bpHideFollowCount(activity.pref)) { btnFollowing.text = activity.getString(R.string.following) btnFollowers.text = activity.getString(R.string.followers) } else { btnFollowing.text = "${activity.getString(R.string.following)}\n${ whoDetail?.following_count ?: who.following_count }" btnFollowers.text = "${activity.getString(R.string.followers)}\n${ whoDetail?.followers_count ?: who.followers_count }" } val relation = UserRelation.load(access_info.db_id, who.id) this.relation = relation Styler.setFollowIcon( activity, btnFollow, ivFollowedBy, relation, who, contentColor, alphaMultiplier = Styler.boost_alpha ) tvPersonalNotes.text = relation.note ?: "" showMoved(who, who.movedRef) val fields = whoDetail?.fields ?: who.fields if(fields != null) { 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 = access_info, short = true, emojiMapCustom = who.custom_emojis, emojiMapProfile = who.profile_emojis, mentionDefaultHostDomain = who ) val nameTypeface = ActMain.timeline_font_bold val valueTypeface = ActMain.timeline_font 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 = Pref.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 = Pref.ipVerifiedLinkBgColor(activity.pref).notZero() ?: (0x337fbc99) valueView.setBackgroundColor(linkBgColor) } llFields.addView(valueView) } } } } 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 moved_caption_invalidator.register(caption) ivMoved.layoutParams.width = activity.avatarIconSize ivMoved.setImageUrl( activity.pref, Styler.calcIconRound(ivMoved.layoutParams), access_info.supplyBaseUrl(moved.avatar_static) ) tvMovedName.text = movedRef.decoded_display_name moved_name_invalidator.register(movedRef.decoded_display_name) setAcct(tvMovedAcct, access_info, moved) val relation = UserRelation.load(access_info.db_id, moved.id) Styler.setFollowIcon( activity, btnMoved, ivMovedBy, relation, moved, contentColor, alphaMultiplier = Styler.boost_alpha ) } private fun setAcct(tv : TextView, accessInfo : SavedAccount, who : TootAccount) { val ac = AcctColor.load(accessInfo, who) tv.text = when { AcctColor.hasNickname(ac) -> ac.nickname Pref.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.acct_pad_lr, 0, activity.acct_pad_lr, 0) } override fun onClick(v : View) { when(v.id) { R.id.ivBackground, R.id.tvRemoteProfileWarning -> activity.openCustomTab(whoRef?.get()?.url) R.id.btnFollowing -> { column.profile_tab = ProfileTab.Following activity.app_state.saveColumnList() column.startLoading() } R.id.btnFollowers -> { column.profile_tab = ProfileTab.Followers activity.app_state.saveColumnList() column.startLoading() } R.id.btnStatusCount -> { column.profile_tab = ProfileTab.Status activity.app_state.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(access_info.isPseudo) { DlgContextMenu(activity, column, movedRef, null, null, null).show() } else { Action_User.profileLocal( activity, activity.nextPosition(column), access_info, 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) { TootTaskRunner(activity).run(column.access_info, object : TootTask { override suspend fun background(client : TootApiClient) : TootApiResult? { if(access_info.isPseudo) return TootApiResult("Personal notes is not supported on pseudo account.") if(access_info.isMisskey) return TootApiResult("Personal notes is not supported on Misskey account.") return client.request( "/api/v1/accounts/${who.id}/note", jsonObject { put("comment", text) }.toPostRequestBuilder() ) } override suspend fun handleResult(result : TootApiResult?) { if(result == null) return if(result.error != null) activity.showToast(true, result.error) else { relation?.note = text dialog.dismissSafe() if(lastColumn == column) bindData(column) } } }) } } ) } } } override fun onLongClick(v : View) : Boolean { when(v.id) { R.id.btnFollow -> { Action_Follow.followFromAnotherAccount( activity, activity.nextPosition(column), access_info, whoRef?.get() ) return true } R.id.btnMoved -> { Action_Follow.followFromAnotherAccount( activity, activity.nextPosition(column), access_info, movedRef?.get() ) return true } } return false } 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 getAccount() : TootAccountRef? = whoRef }