package org.mariotaku.twidere.view.holder import android.support.v4.content.ContextCompat import android.support.v4.widget.TextViewCompat import android.support.v7.widget.RecyclerView import android.support.v7.widget.RecyclerView.ViewHolder import android.text.SpannableString import android.text.SpannableStringBuilder import android.text.Spanned import android.text.style.ForegroundColorSpan import android.view.View import android.view.View.OnClickListener import android.view.View.OnLongClickListener import android.widget.ImageView import android.widget.TextView import com.bumptech.glide.RequestManager import kotlinx.android.synthetic.main.list_item_status.view.* import org.mariotaku.ktextension.applyFontFamily import org.mariotaku.ktextension.isNotNullOrEmpty import org.mariotaku.twidere.Constants.* import org.mariotaku.twidere.R import org.mariotaku.twidere.TwidereConstants.USER_TYPE_FANFOU_COM import org.mariotaku.twidere.adapter.iface.IStatusesAdapter import org.mariotaku.twidere.constant.SharedPreferenceConstants.VALUE_LINK_HIGHLIGHT_OPTION_CODE_NONE import org.mariotaku.twidere.extension.loadProfileImage import org.mariotaku.twidere.extension.model.applyTo import org.mariotaku.twidere.graphic.like.LikeAnimationDrawable import org.mariotaku.twidere.model.ParcelableLocation import org.mariotaku.twidere.model.ParcelableMedia import org.mariotaku.twidere.model.ParcelableStatus import org.mariotaku.twidere.model.UserKey import org.mariotaku.twidere.task.CreateFavoriteTask import org.mariotaku.twidere.task.DestroyFavoriteTask import org.mariotaku.twidere.task.RetweetStatusTask import org.mariotaku.twidere.util.* import org.mariotaku.twidere.util.HtmlEscapeHelper.toPlainText import org.mariotaku.twidere.util.Utils.getUserTypeIconRes import org.mariotaku.twidere.view.ShapedImageView import org.mariotaku.twidere.view.holder.iface.IStatusViewHolder import java.lang.ref.WeakReference /** * IDE gives me warning if I don't change default comment, so I wrote this XD * * * Created by mariotaku on 14/11/19. */ class StatusViewHolder(private val adapter: IStatusesAdapter<*>, itemView: View) : ViewHolder(itemView), IStatusViewHolder { override val profileImageView: ShapedImageView by lazy { itemView.profileImage } override val profileTypeView: ImageView by lazy { itemView.profileType } private val itemContent by lazy { itemView.itemContent } private val mediaPreview by lazy { itemView.mediaPreview } private val statusContentUpperSpace by lazy { itemView.statusContentUpperSpace } private val textView by lazy { itemView.text } private val nameView by lazy { itemView.name } private val itemMenu by lazy { itemView.itemMenu } private val statusInfoLabel by lazy { itemView.statusInfoLabel } private val statusInfoIcon by lazy { itemView.statusInfoIcon } private val quotedNameView by lazy { itemView.quotedName } private val timeView by lazy { itemView.time } private val replyCountView by lazy { itemView.replyCount } private val retweetCountView by lazy { itemView.retweetCount } private val quotedView by lazy { itemView.quotedView } private val quotedTextView by lazy { itemView.quotedText } private val actionButtons by lazy { itemView.actionButtons } private val mediaLabel by lazy { itemView.mediaLabel } private val quotedMediaLabel by lazy { itemView.quotedMediaLabel } private val statusContentLowerSpace by lazy { itemView.statusContentLowerSpace } private val quotedMediaPreview by lazy { itemView.quotedMediaPreview } private val favoriteIcon by lazy { itemView.favoriteIcon } private val retweetIcon by lazy { itemView.retweetIcon } private val favoriteCountView by lazy { itemView.favoriteCount } private val replyButton by lazy { itemView.reply } private val retweetButton by lazy { itemView.retweet } private val favoriteButton by lazy { itemView.favorite } private val eventListener: EventListener private var statusClickListener: IStatusViewHolder.StatusClickListener? = null init { this.eventListener = EventListener(this) if (adapter.mediaPreviewEnabled) { View.inflate(mediaPreview.context, R.layout.layout_card_media_preview, itemView.mediaPreview) View.inflate(quotedMediaPreview.context, R.layout.layout_card_media_preview, itemView.quotedMediaPreview) } profileImageView.style = adapter.profileImageStyle } fun displaySampleStatus() { val profileImageEnabled = adapter.profileImageEnabled profileImageView.visibility = if (profileImageEnabled) View.VISIBLE else View.GONE statusContentUpperSpace.visibility = View.VISIBLE adapter.requestManager.loadProfileImage(itemView.context, R.drawable.ic_profile_image_twidere, adapter.profileImageStyle, profileImageView.cornerRadius, profileImageView.cornerRadiusRatio).into(profileImageView) nameView.name = TWIDERE_PREVIEW_NAME nameView.screenName = "@" + TWIDERE_PREVIEW_SCREEN_NAME nameView.updateText(adapter.bidiFormatter) if (adapter.linkHighlightingStyle == VALUE_LINK_HIGHLIGHT_OPTION_CODE_NONE) { textView.text = toPlainText(TWIDERE_PREVIEW_TEXT_HTML) } else { val linkify = adapter.twidereLinkify val text = HtmlSpanBuilder.fromHtml(TWIDERE_PREVIEW_TEXT_HTML) linkify.applyAllLinks(text, null, -1, false, adapter.linkHighlightingStyle, true) textView.text = text } timeView.time = System.currentTimeMillis() val showCardActions = isCardActionsShown if (adapter.mediaPreviewEnabled) { mediaPreview.visibility = View.VISIBLE mediaLabel.visibility = View.GONE } else { mediaPreview.visibility = View.GONE mediaLabel.visibility = View.VISIBLE } actionButtons.visibility = if (showCardActions) View.VISIBLE else View.GONE itemMenu.visibility = if (showCardActions) View.VISIBLE else View.GONE statusContentLowerSpace.visibility = if (showCardActions) View.GONE else View.VISIBLE quotedMediaPreview.visibility = View.GONE quotedMediaLabel.visibility = View.GONE mediaPreview.displayMedia(R.drawable.featured_graphics) } override fun displayStatus(status: ParcelableStatus, displayInReplyTo: Boolean, displayPinned: Boolean) { val context = itemView.context val requestManager = adapter.requestManager val twitter = adapter.twitterWrapper val linkify = adapter.twidereLinkify val formatter = adapter.bidiFormatter val colorNameManager = adapter.userColorNameManager val nameFirst = adapter.nameFirst val showCardActions = isCardActionsShown actionButtons.visibility = if (showCardActions) View.VISIBLE else View.GONE itemMenu.visibility = if (showCardActions) View.VISIBLE else View.GONE statusContentLowerSpace.visibility = if (showCardActions) View.GONE else View.VISIBLE val replyCount = status.reply_count val retweetCount: Long val favoriteCount: Long if (displayPinned && status.is_pinned_status) { statusInfoLabel.setText(R.string.pinned_status) statusInfoIcon.setImageResource(R.drawable.ic_activity_action_pinned) statusInfoLabel.visibility = View.VISIBLE statusInfoIcon.visibility = View.VISIBLE statusContentUpperSpace.visibility = View.GONE } else if (TwitterCardUtils.isPoll(status)) { statusInfoLabel.setText(R.string.label_poll) statusInfoIcon.setImageResource(R.drawable.ic_activity_action_poll) statusInfoLabel.visibility = View.VISIBLE statusInfoIcon.visibility = View.VISIBLE statusContentUpperSpace.visibility = View.GONE } else if (status.retweet_id != null) { val retweetedBy = colorNameManager.getDisplayName(status.retweeted_by_user_key!!, status.retweeted_by_user_name, status.retweeted_by_user_screen_name, nameFirst) statusInfoLabel.text = context.getString(R.string.name_retweeted, formatter.unicodeWrap(retweetedBy)) statusInfoIcon.setImageResource(R.drawable.ic_activity_action_retweet) statusInfoLabel.visibility = View.VISIBLE statusInfoIcon.visibility = View.VISIBLE statusContentUpperSpace.visibility = View.GONE } else if (status.in_reply_to_status_id != null && status.in_reply_to_user_key != null && displayInReplyTo) { val inReplyTo = colorNameManager.getDisplayName(status.in_reply_to_user_key!!, status.in_reply_to_name, status.in_reply_to_screen_name, nameFirst) statusInfoLabel.text = context.getString(R.string.in_reply_to_name, formatter.unicodeWrap(inReplyTo)) statusInfoIcon.setImageResource(R.drawable.ic_activity_action_reply) statusInfoLabel.visibility = View.VISIBLE statusInfoIcon.visibility = View.VISIBLE statusContentUpperSpace.visibility = View.GONE } else { statusInfoLabel.visibility = View.GONE statusInfoIcon.visibility = View.GONE statusContentUpperSpace.visibility = View.VISIBLE } val skipLinksInText = status.extras != null && status.extras.support_entities if (status.is_quote) { quotedView.visibility = View.VISIBLE val quoteContentAvailable = status.quoted_text_plain != null && status.quoted_text_unescaped != null val isFanfouStatus = status.account_key.host == USER_TYPE_FANFOU_COM if (quoteContentAvailable && !isFanfouStatus) { quotedNameView.visibility = View.VISIBLE quotedTextView.visibility = View.VISIBLE val quoted_user_key = status.quoted_user_key!! quotedNameView.name = colorNameManager.getUserNickname(quoted_user_key, status.quoted_user_name) quotedNameView.screenName = "@${status.quoted_user_screen_name}" var quotedDisplayEnd = -1 if (status.extras.quoted_display_text_range != null) { quotedDisplayEnd = status.extras.quoted_display_text_range!![1] } val quotedText: CharSequence if (adapter.linkHighlightingStyle != VALUE_LINK_HIGHLIGHT_OPTION_CODE_NONE) { quotedText = SpannableStringBuilder.valueOf(status.quoted_text_unescaped) status.quoted_spans?.applyTo(quotedText) linkify.applyAllLinks(quotedText, status.account_key, layoutPosition.toLong(), status.is_possibly_sensitive, adapter.linkHighlightingStyle, skipLinksInText) } else { quotedText = status.quoted_text_unescaped } if (quotedDisplayEnd != -1 && quotedDisplayEnd <= quotedText.length) { quotedTextView.text = quotedText.subSequence(0, quotedDisplayEnd) } else { quotedTextView.text = quotedText } if (quotedTextView.length() == 0) { // No text quotedTextView.visibility = View.GONE } else { quotedTextView.visibility = View.VISIBLE } val quoted_user_color = colorNameManager.getUserColor(quoted_user_key) if (quoted_user_color != 0) { quotedView.drawStart(quoted_user_color) } else { quotedView.drawStart(ThemeUtils.getColorFromAttribute(context, R.attr.quoteIndicatorBackgroundColor, 0)) } displayQuotedMedia(requestManager, status) } else { quotedNameView.visibility = View.GONE quotedTextView.visibility = View.VISIBLE if (quoteContentAvailable) { displayQuotedMedia(requestManager, status) } else { quotedMediaPreview.visibility = View.GONE quotedMediaLabel.visibility = View.GONE } quotedTextView.text = if (!quoteContentAvailable) { // Display 'not available' label SpannableString.valueOf(context.getString(R.string.label_status_not_available)).apply { setSpan(ForegroundColorSpan(ThemeUtils.getColorFromAttribute(context, android.R.attr.textColorTertiary, textView.currentTextColor)), 0, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) } } else { // Display 'original status' label context.getString(R.string.label_original_status) } quotedView.drawStart(ThemeUtils.getColorFromAttribute(context, R.attr.quoteIndicatorBackgroundColor, 0)) } itemContent.drawStart(colorNameManager.getUserColor(status.user_key)) } else { quotedView.visibility = View.GONE val userColor = colorNameManager.getUserColor(status.user_key) if (status.is_retweet) { val retweetUserColor = colorNameManager.getUserColor(status.retweeted_by_user_key!!) if (retweetUserColor == 0) { itemContent.drawStart(userColor) } else if (userColor == 0) { itemContent.drawStart(retweetUserColor) } else { itemContent.drawStart(retweetUserColor, userColor) } } else { itemContent.drawStart(userColor) } } timeView.time = if (status.is_retweet) { status.retweet_timestamp } else { status.timestamp } nameView.name = colorNameManager.getUserNickname(status.user_key, status.user_name) nameView.screenName = "@${status.user_screen_name}" if (adapter.profileImageEnabled) { profileImageView.visibility = View.VISIBLE requestManager.loadProfileImage(context, status, adapter.profileImageStyle, profileImageView.cornerRadius, profileImageView.cornerRadiusRatio) .into(profileImageView) profileTypeView.setImageResource(getUserTypeIconRes(status.user_is_verified, status.user_is_protected)) profileTypeView.visibility = View.VISIBLE } else { profileImageView.visibility = View.GONE profileTypeView.setImageDrawable(null) profileTypeView.visibility = View.GONE } if (adapter.showAccountsColor) { itemContent.drawEnd(status.account_color) } else { itemContent.drawEnd() } if (status.media.isNotNullOrEmpty()) { mediaLabel.displayMediaLabel(status.card_name, status.media, status.location, status.place_full_name, status.is_possibly_sensitive) if (!adapter.sensitiveContentEnabled && status.is_possibly_sensitive) { // Sensitive content, show label instead of media view mediaLabel.visibility = View.VISIBLE mediaLabel.contentDescription = status.media?.firstOrNull()?.alt_text mediaPreview.visibility = View.GONE } else if (!adapter.mediaPreviewEnabled) { // Media preview disabled, just show label mediaLabel.visibility = View.VISIBLE mediaLabel.contentDescription = status.media?.firstOrNull()?.alt_text mediaPreview.visibility = View.GONE } else { // Show media mediaLabel.visibility = View.GONE mediaPreview.visibility = View.VISIBLE mediaPreview.displayMedia(requestManager = requestManager, media = status.media, accountId = status.account_key, mediaClickListener = this) } } else { // No media, hide all related views mediaLabel.visibility = View.GONE mediaPreview.visibility = View.GONE } var displayEnd = -1 if (status.extras.display_text_range != null) { displayEnd = status.extras.display_text_range!![1] } val text: CharSequence if (adapter.linkHighlightingStyle != VALUE_LINK_HIGHLIGHT_OPTION_CODE_NONE) { text = SpannableStringBuilder.valueOf(status.text_unescaped).apply { status.spans?.applyTo(this) linkify.applyAllLinks(this, status.account_key, layoutPosition.toLong(), status.is_possibly_sensitive, adapter.linkHighlightingStyle, skipLinksInText) } } else { text = status.text_unescaped } if (displayEnd != -1 && displayEnd <= text.length) { textView.text = text.subSequence(0, displayEnd) } else { textView.text = text } if (textView.length() == 0) { // No text textView.visibility = View.GONE } else { textView.visibility = View.VISIBLE } if (replyCount > 0) { replyCountView.text = UnitConvertUtils.calculateProperCount(replyCount) replyCountView.visibility = View.VISIBLE } else { replyCountView.text = null replyCountView.visibility = View.GONE } if (twitter.isDestroyingStatus(status.account_key, status.my_retweet_id)) { retweetIcon.isActivated = false retweetCount = Math.max(0, status.retweet_count - 1) } else { val creatingRetweet = RetweetStatusTask.isCreatingRetweet(status.account_key, status.id) retweetIcon.isActivated = creatingRetweet || status.retweeted || Utils.isMyRetweet(status.account_key, status.retweeted_by_user_key, status.my_retweet_id) retweetCount = status.retweet_count + if (creatingRetweet) 1 else 0 } if (retweetCount > 0) { retweetCountView.text = UnitConvertUtils.calculateProperCount(retweetCount) retweetCountView.visibility = View.VISIBLE } else { retweetCountView.text = null retweetCountView.visibility = View.GONE } if (DestroyFavoriteTask.isDestroyingFavorite(status.account_key, status.id)) { favoriteIcon.isActivated = false favoriteCount = Math.max(0, status.favorite_count - 1) } else { val creatingFavorite = CreateFavoriteTask.isCreatingFavorite(status.account_key, status.id) favoriteIcon.isActivated = creatingFavorite || status.is_favorite favoriteCount = status.favorite_count + if (creatingFavorite) 1 else 0 } if (favoriteCount > 0) { favoriteCountView.text = UnitConvertUtils.calculateProperCount(favoriteCount) favoriteCountView.visibility = View.VISIBLE } else { favoriteCountView.text = null favoriteCountView.visibility = View.GONE } nameView.updateText(formatter) quotedNameView.updateText(formatter) } private fun displayQuotedMedia(requestManager: RequestManager, status: ParcelableStatus) { if (status.quoted_media?.isNotEmpty() ?: false) { quotedMediaLabel.displayMediaLabel(null, status.quoted_media, null, null, status.is_possibly_sensitive) if (!adapter.sensitiveContentEnabled && status.is_possibly_sensitive) { // Sensitive content, show label instead of media view quotedMediaPreview.visibility = View.GONE quotedMediaLabel.visibility = View.VISIBLE } else if (!adapter.mediaPreviewEnabled) { // Media preview disabled, just show label quotedMediaPreview.visibility = View.GONE quotedMediaLabel.visibility = View.VISIBLE } else { // Show media quotedMediaPreview.visibility = View.VISIBLE quotedMediaLabel.visibility = View.GONE quotedMediaPreview.displayMedia(requestManager = requestManager, media = status.quoted_media, accountId = status.account_key, mediaClickListener = this) } } else { // No media, hide all related views quotedMediaPreview.visibility = View.GONE quotedMediaLabel.visibility = View.GONE } } override fun onMediaClick(view: View, media: ParcelableMedia, accountKey: UserKey?, id: Long) { if (view.parent == quotedMediaPreview) { statusClickListener?.onQuotedMediaClick(this, view, media, layoutPosition) } else { statusClickListener?.onMediaClick(this, view, media, layoutPosition) } } fun setOnClickListeners() { setStatusClickListener(adapter.statusClickListener) } override fun setStatusClickListener(listener: IStatusViewHolder.StatusClickListener?) { statusClickListener = listener itemContent.setOnClickListener(eventListener) itemContent.setOnLongClickListener(eventListener) itemMenu.setOnClickListener(eventListener) profileImageView.setOnClickListener(eventListener) replyButton.setOnClickListener(eventListener) retweetButton.setOnClickListener(eventListener) favoriteButton.setOnClickListener(eventListener) retweetButton.setOnLongClickListener(eventListener) favoriteButton.setOnLongClickListener(eventListener) mediaLabel.setOnClickListener(eventListener) quotedView.setOnClickListener(eventListener) } override fun setTextSize(textSize: Float) { nameView.setPrimaryTextSize(textSize) quotedNameView.setPrimaryTextSize(textSize) textView.textSize = textSize quotedTextView.textSize = textSize nameView.setSecondaryTextSize(textSize * 0.85f) quotedNameView.setSecondaryTextSize(textSize * 0.85f) timeView.textSize = textSize * 0.85f statusInfoLabel.textSize = textSize * 0.75f mediaLabel.textSize = textSize * 0.95f quotedMediaLabel.textSize = textSize * 0.95f replyCountView.textSize = textSize retweetCountView.textSize = textSize favoriteCountView.textSize = textSize } fun setupViewOptions() { setTextSize(adapter.textSize) mediaPreview.style = adapter.mediaPreviewStyle quotedMediaPreview.style = adapter.mediaPreviewStyle // profileImageView.setStyle(adapter.getProfileImageStyle()); val nameFirst = adapter.nameFirst nameView.nameFirst = nameFirst quotedNameView.nameFirst = nameFirst val favIcon: Int val favStyle: Int val favColor: Int val context = itemView.context if (adapter.useStarsForLikes) { favIcon = R.drawable.ic_action_star favStyle = LikeAnimationDrawable.Style.FAVORITE favColor = ContextCompat.getColor(context, R.color.highlight_favorite) } else { favIcon = R.drawable.ic_action_heart favStyle = LikeAnimationDrawable.Style.LIKE favColor = ContextCompat.getColor(context, R.color.highlight_like) } val icon = ContextCompat.getDrawable(context, favIcon) val drawable = LikeAnimationDrawable(icon, favoriteCountView.textColors.defaultColor, favColor, favStyle) drawable.mutate() favoriteIcon.setImageDrawable(drawable) timeView.showAbsoluteTime = adapter.showAbsoluteTime favoriteIcon.activatedColor = favColor nameView.applyFontFamily(adapter.lightFont) timeView.applyFontFamily(adapter.lightFont) textView.applyFontFamily(adapter.lightFont) mediaLabel.applyFontFamily(adapter.lightFont) quotedNameView.applyFontFamily(adapter.lightFont) quotedTextView.applyFontFamily(adapter.lightFont) quotedMediaLabel.applyFontFamily(adapter.lightFont) } override fun playLikeAnimation(listener: LikeAnimationDrawable.OnLikedListener) { var handled = false val drawable = favoriteIcon.drawable if (drawable is LikeAnimationDrawable) { drawable.setOnLikedListener(listener) drawable.start() handled = true } if (!handled) { listener.onLiked() } } private val isCardActionsShown: Boolean get() = adapter.isCardActionsShown(layoutPosition) private fun showCardActions() { adapter.showCardActions(layoutPosition) } private fun hideTempCardActions(): Boolean { adapter.showCardActions(RecyclerView.NO_POSITION) return !adapter.isCardActionsShown(RecyclerView.NO_POSITION) } private fun TextView.displayMediaLabel(cardName: String?, media: Array?, location: ParcelableLocation?, placeFullName: String?, sensitive: Boolean) { if (sensitive) { TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(this, R.drawable.ic_label_warning, 0, 0, 0) setText(R.string.label_sensitive_content) } else if (media != null && media.isNotEmpty()) { val type = media.type if (type in videoTypes) { TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(this, R.drawable.ic_label_video, 0, 0, 0) setText(R.string.label_video) } else if (media.size > 1) { TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(this, R.drawable.ic_label_gallery, 0, 0, 0) setText(R.string.label_photos) } else { TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(this, R.drawable.ic_label_gallery, 0, 0, 0) setText(R.string.label_photo) } } else { TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(this, R.drawable.ic_label_gallery, 0, 0, 0) setText(R.string.label_media) } refreshDrawableState() } private val Array.type: Int get() { forEach { if (it != null) return it.type } return 0 } private fun hasVideo(media: Array?): Boolean { if (media == null) return false return media.any { item -> if (item == null) return@any false return@any videoTypes.contains(item.type) } } internal class EventListener(holder: StatusViewHolder) : OnClickListener, OnLongClickListener { private val holderRef = WeakReference(holder) override fun onClick(v: View) { val holder = holderRef.get() ?: return val listener = holder.statusClickListener ?: return val position = holder.layoutPosition when (v) { holder.itemContent -> { listener.onStatusClick(holder, position) } holder.quotedView -> { listener.onQuotedStatusClick(holder, position) } holder.itemMenu -> { listener.onItemMenuClick(holder, v, position) } holder.profileImageView -> { listener.onUserProfileClick(holder, position) } holder.replyButton -> { listener.onItemActionClick(holder, R.id.reply, position) } holder.retweetButton -> { listener.onItemActionClick(holder, R.id.retweet, position) } holder.favoriteButton -> { listener.onItemActionClick(holder, R.id.favorite, position) } holder.mediaLabel -> { val firstMedia = holder.adapter.getStatus(position)?.media?.firstOrNull() ?: return listener.onMediaClick(holder, v, firstMedia, position) } } } override fun onLongClick(v: View): Boolean { val holder = holderRef.get() ?: return false val listener = holder.statusClickListener ?: return false val position = holder.layoutPosition when (v) { holder.itemContent -> { if (!holder.isCardActionsShown) { holder.showCardActions() return true } else if (holder.hideTempCardActions()) { return true } return listener.onStatusLongClick(holder, position) } holder.favoriteButton -> { return listener.onItemActionLongClick(holder, R.id.favorite, position) } holder.retweetButton -> { return listener.onItemActionLongClick(holder, R.id.retweet, position) } } return false } } companion object { const val layoutResource = R.layout.list_item_status private val videoTypes = intArrayOf(ParcelableMedia.Type.VIDEO, ParcelableMedia.Type.ANIMATED_GIF, ParcelableMedia.Type.EXTERNAL_PLAYER) } }