287 lines
12 KiB
Kotlin
287 lines
12 KiB
Kotlin
package org.mariotaku.twidere.model.util
|
|
|
|
import android.text.Spannable
|
|
import android.text.Spanned
|
|
import android.text.style.URLSpan
|
|
import org.mariotaku.microblog.library.twitter.model.Status
|
|
import org.mariotaku.twidere.TwidereConstants.USER_TYPE_FANFOU_COM
|
|
import org.mariotaku.twidere.model.*
|
|
import org.mariotaku.twidere.model.ParcelableStatus.FilterFlags
|
|
import org.mariotaku.twidere.util.HtmlSpanBuilder
|
|
import org.mariotaku.twidere.util.InternalTwitterContentUtils
|
|
import org.mariotaku.twidere.util.TwitterContentUtils
|
|
import org.mariotaku.twidere.util.UserColorNameManager
|
|
import java.util.*
|
|
|
|
/**
|
|
* Created by mariotaku on 16/1/3.
|
|
*/
|
|
object ParcelableStatusUtils {
|
|
|
|
fun makeOriginalStatus(status: ParcelableStatus) {
|
|
if (!status.is_retweet) return
|
|
status.id = status.retweet_id
|
|
status.retweeted_by_user_key = null
|
|
status.retweeted_by_user_name = null
|
|
status.retweeted_by_user_screen_name = null
|
|
status.retweeted_by_user_profile_image = null
|
|
status.retweet_timestamp = -1
|
|
status.retweet_id = null
|
|
}
|
|
|
|
fun fromStatus(orig: Status, accountKey: UserKey,
|
|
isGap: Boolean): ParcelableStatus {
|
|
val result = ParcelableStatus()
|
|
result.is_gap = isGap
|
|
result.account_key = accountKey
|
|
result.id = orig.id
|
|
result.sort_id = orig.sortId
|
|
result.timestamp = getTime(orig.createdAt)
|
|
|
|
result.extras = ParcelableStatus.Extras()
|
|
result.extras.external_url = orig.inferExternalUrl()
|
|
result.extras.support_entities = orig.entities != null
|
|
result.extras.statusnet_conversation_id = orig.statusnetConversationId
|
|
result.is_pinned_status = orig.user.pinnedTweetIds?.contains(orig.id) ?: false
|
|
|
|
val retweetedStatus = orig.retweetedStatus
|
|
result.is_retweet = orig.isRetweet
|
|
result.retweeted = orig.wasRetweeted()
|
|
val status: Status
|
|
if (retweetedStatus != null) {
|
|
status = retweetedStatus
|
|
val retweetUser = orig.user
|
|
result.retweet_id = retweetedStatus.id
|
|
result.retweet_timestamp = getTime(retweetedStatus.createdAt)
|
|
result.retweeted_by_user_key = UserKeyUtils.fromUser(retweetUser)
|
|
result.retweeted_by_user_name = retweetUser.name
|
|
result.retweeted_by_user_screen_name = retweetUser.screenName
|
|
result.retweeted_by_user_profile_image = TwitterContentUtils.getProfileImageUrl(retweetUser)
|
|
|
|
result.extras.retweeted_external_url = retweetedStatus.inferExternalUrl()
|
|
|
|
if (retweetUser.isBlocking) {
|
|
result.addFilterFlag(FilterFlags.BLOCKING_USER)
|
|
}
|
|
if (retweetUser.isBlockedBy) {
|
|
result.addFilterFlag(FilterFlags.BLOCKED_BY_USER)
|
|
}
|
|
if (retweetedStatus.isPossiblySensitive) {
|
|
result.addFilterFlag(FilterFlags.POSSIBILITY_SENSITIVE)
|
|
}
|
|
} else {
|
|
status = orig
|
|
if (status.isPossiblySensitive) {
|
|
result.addFilterFlag(FilterFlags.POSSIBILITY_SENSITIVE)
|
|
}
|
|
}
|
|
|
|
val quoted = status.quotedStatus
|
|
result.is_quote = status.isQuoteStatus
|
|
result.quoted_id = status.quotedStatusId
|
|
if (quoted != null) {
|
|
val quotedUser = quoted.user
|
|
result.quoted_id = quoted.id
|
|
result.extras.quoted_external_url = quoted.inferExternalUrl()
|
|
|
|
val quotedText = quoted.htmlText
|
|
// Twitter will escape <> to <>, so if a status contains those symbols unescaped
|
|
// We should treat this as an html
|
|
if (isHtml(quotedText)) {
|
|
val html = HtmlSpanBuilder.fromHtml(quotedText, quoted.extendedText)
|
|
result.quoted_text_unescaped = html.toString()
|
|
result.quoted_text_plain = result.quoted_text_unescaped
|
|
result.quoted_spans = getSpanItems(html)
|
|
} else {
|
|
val textWithIndices = InternalTwitterContentUtils.formatStatusTextWithIndices(quoted)
|
|
result.quoted_text_plain = InternalTwitterContentUtils.unescapeTwitterStatusText(quotedText)
|
|
result.quoted_text_unescaped = textWithIndices.text
|
|
result.quoted_spans = textWithIndices.spans
|
|
result.extras.quoted_display_text_range = textWithIndices.range
|
|
}
|
|
|
|
result.quoted_timestamp = quoted.createdAt.time
|
|
result.quoted_source = quoted.source
|
|
result.quoted_media = ParcelableMediaUtils.fromStatus(quoted)
|
|
|
|
result.quoted_user_key = UserKeyUtils.fromUser(quotedUser)
|
|
result.quoted_user_name = quotedUser.name
|
|
result.quoted_user_screen_name = quotedUser.screenName
|
|
result.quoted_user_profile_image = TwitterContentUtils.getProfileImageUrl(quotedUser)
|
|
result.quoted_user_is_protected = quotedUser.isProtected
|
|
result.quoted_user_is_verified = quotedUser.isVerified
|
|
|
|
if (quoted.isPossiblySensitive) {
|
|
result.addFilterFlag(FilterFlags.POSSIBILITY_SENSITIVE)
|
|
}
|
|
} else if (status.isQuoteStatus) {
|
|
result.addFilterFlag(FilterFlags.QUOTE_NOT_AVAILABLE)
|
|
}
|
|
|
|
result.reply_count = status.replyCount
|
|
result.retweet_count = status.retweetCount
|
|
result.favorite_count = status.favoriteCount
|
|
|
|
result.in_reply_to_name = getInReplyToName(status)
|
|
result.in_reply_to_screen_name = status.inReplyToScreenName
|
|
result.in_reply_to_status_id = status.inReplyToStatusId
|
|
result.in_reply_to_user_key = getInReplyToUserId(status, accountKey)
|
|
|
|
val user = status.user
|
|
result.user_key = UserKeyUtils.fromUser(user)
|
|
result.user_name = user.name
|
|
result.user_screen_name = user.screenName
|
|
result.user_profile_image_url = TwitterContentUtils.getProfileImageUrl(user)
|
|
result.user_is_protected = user.isProtected
|
|
result.user_is_verified = user.isVerified
|
|
result.user_is_following = user.isFollowing
|
|
result.extras.user_profile_image_url_profile_size = user.profileImageUrlProfileSize
|
|
result.extras.user_statusnet_profile_url = user.statusnetProfileUrl
|
|
if (result.extras.user_profile_image_url_profile_size == null) {
|
|
result.extras.user_profile_image_url_profile_size = user.profileImageUrlLarge
|
|
}
|
|
val text = status.htmlText
|
|
// Twitter will escape <> to <>, so if a status contains those symbols unescaped
|
|
// We should treat this as an html
|
|
if (isHtml(text)) {
|
|
val html = HtmlSpanBuilder.fromHtml(text, status.extendedText)
|
|
result.text_unescaped = html.toString()
|
|
result.text_plain = result.text_unescaped
|
|
result.spans = getSpanItems(html)
|
|
} else {
|
|
val textWithIndices = InternalTwitterContentUtils.formatStatusTextWithIndices(status)
|
|
result.text_unescaped = textWithIndices.text
|
|
result.text_plain = InternalTwitterContentUtils.unescapeTwitterStatusText(text)
|
|
result.spans = textWithIndices.spans
|
|
result.extras.display_text_range = textWithIndices.range
|
|
}
|
|
result.media = ParcelableMediaUtils.fromStatus(status)
|
|
result.source = status.source
|
|
result.location = getLocation(status)
|
|
result.is_favorite = status.isFavorited
|
|
if (result.account_key.maybeEquals(result.retweeted_by_user_key)) {
|
|
result.my_retweet_id = result.id
|
|
} else {
|
|
result.my_retweet_id = status.currentUserRetweet
|
|
}
|
|
result.is_possibly_sensitive = status.isPossiblySensitive
|
|
result.mentions = ParcelableUserMentionUtils.fromUserMentionEntities(user,
|
|
status.userMentionEntities)
|
|
result.card = ParcelableCardEntityUtils.fromCardEntity(status.card, accountKey)
|
|
result.place_full_name = getPlaceFullName(status)
|
|
result.card_name = if (result.card != null) result.card!!.name else null
|
|
result.lang = status.lang
|
|
|
|
return result
|
|
}
|
|
|
|
private fun ParcelableStatus.addFilterFlag(@FilterFlags flags: Long) {
|
|
filter_flags = filter_flags or flags
|
|
}
|
|
|
|
private fun getSpanItems(html: CharSequence): Array<SpanItem>? {
|
|
if (html !is Spanned) return null
|
|
val spans = html.getSpans(0, html.length, URLSpan::class.java)
|
|
return Array(spans.size) { idx ->
|
|
SpanItem.from(html, spans[idx])
|
|
}
|
|
}
|
|
|
|
private fun isHtml(text: String): Boolean {
|
|
return text.contains("<") && text.contains(">")
|
|
}
|
|
|
|
private fun isFanfouStatus(accountKey: UserKey): Boolean {
|
|
return USER_TYPE_FANFOU_COM == accountKey.host
|
|
}
|
|
|
|
private fun getInReplyToUserId(status: Status, accountKey: UserKey): UserKey? {
|
|
val inReplyToUserId = status.inReplyToUserId ?: return null
|
|
val entities = status.userMentionEntities
|
|
if (entities != null) {
|
|
if (entities.any { inReplyToUserId == it.id }) {
|
|
return UserKey(inReplyToUserId, accountKey.host)
|
|
}
|
|
}
|
|
val attentions = status.attentions
|
|
if (attentions != null) {
|
|
attentions.firstOrNull { inReplyToUserId == it.id }?.let {
|
|
val host = UserKeyUtils.getUserHost(it.ostatusUri,
|
|
accountKey.host)
|
|
return UserKey(inReplyToUserId, host)
|
|
}
|
|
}
|
|
return UserKey(inReplyToUserId, accountKey.host)
|
|
}
|
|
|
|
fun fromStatuses(statuses: Array<Status>?, accountKey: UserKey): Array<ParcelableStatus>? {
|
|
if (statuses == null) return null
|
|
return Array(statuses.size) { i ->
|
|
fromStatus(statuses[i], accountKey, false)
|
|
}
|
|
}
|
|
|
|
private fun getPlaceFullName(status: Status): String? {
|
|
val place = status.place
|
|
if (place != null) return place.fullName
|
|
val location = status.location ?: return null
|
|
if (ParcelableLocation.valueOf(location) == null) {
|
|
return location
|
|
}
|
|
return null
|
|
}
|
|
|
|
private fun getLocation(status: Status): ParcelableLocation? {
|
|
val geoLocation = status.geoLocation
|
|
if (geoLocation != null) {
|
|
return ParcelableLocationUtils.fromGeoLocation(geoLocation)
|
|
}
|
|
val locationString = status.location ?: return null
|
|
val location = ParcelableLocation.valueOf(locationString)
|
|
if (location != null) {
|
|
return location
|
|
}
|
|
return null
|
|
}
|
|
|
|
private fun getTime(date: Date?): Long {
|
|
return date?.time ?: 0
|
|
}
|
|
|
|
fun getInReplyToName(status: Status): String? {
|
|
val inReplyToUserId = status.inReplyToUserId
|
|
status.userMentionEntities?.firstOrNull { inReplyToUserId == it.id }?.let {
|
|
return it.name
|
|
}
|
|
status.attentions?.firstOrNull { inReplyToUserId == it.id }?.let {
|
|
return it.fullName
|
|
}
|
|
return status.inReplyToScreenName
|
|
}
|
|
|
|
fun applySpans(text: Spannable, spans: Array<SpanItem>?) {
|
|
spans?.forEach { span ->
|
|
text.setSpan(URLSpan(span.link), span.start, span.end,
|
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
}
|
|
}
|
|
|
|
fun updateExtraInformation(status: ParcelableStatus, details: AccountDetails, manager: UserColorNameManager) {
|
|
status.account_color = details.color
|
|
}
|
|
|
|
fun Status.inferExternalUrl(): String? {
|
|
if (externalUrl != null) {
|
|
return externalUrl
|
|
}
|
|
if (uri != null) {
|
|
val r = Regex("tag:([\\w\\d\\.]+),(\\d{4}\\-\\d{2}\\-\\d{2}):noticeId=(\\d+):objectType=(\\w+)")
|
|
r.matchEntire(uri)?.let { result: MatchResult ->
|
|
return "https://%s/notice/%s".format(Locale.ROOT, result.groups[1]?.value, result.groups[3]?.value)
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
}
|
|
|