diff --git a/build.gradle b/build.gradle index 8a03554d4..be0b488b4 100644 --- a/build.gradle +++ b/build.gradle @@ -30,7 +30,7 @@ subprojects { libVersions = [ Kotlin : '1.1.1', SupportLib : '25.3.1', - MariotakuCommons : '0.9.13', + MariotakuCommons : '0.9.14', RestFu : '0.9.54', ObjectCursor : '0.9.16', PlayServices : '10.2.1', diff --git a/twidere.component.common/src/main/java/org/mariotaku/twidere/model/SpanItem.java b/twidere.component.common/src/main/java/org/mariotaku/twidere/model/SpanItem.java index 4a324425e..dd06d0248 100644 --- a/twidere.component.common/src/main/java/org/mariotaku/twidere/model/SpanItem.java +++ b/twidere.component.common/src/main/java/org/mariotaku/twidere/model/SpanItem.java @@ -21,8 +21,6 @@ package org.mariotaku.twidere.model; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.IntDef; -import android.text.Spanned; -import android.text.style.URLSpan; import com.bluelinelabs.logansquare.annotation.JsonField; import com.bluelinelabs.logansquare.annotation.JsonObject; @@ -63,21 +61,23 @@ public class SpanItem implements Parcelable { @ParcelableThisPlease public String link; + @ParcelableThisPlease + @JsonField(name = "type") + @SpanType + public int type = SpanType.LINK; + @ParcelableNoThanks public int orig_start = -1; @ParcelableNoThanks public int orig_end = -1; - @ParcelableNoThanks - @SpanType - public int type = SpanType.LINK; - @Override public String toString() { return "SpanItem{" + "start=" + start + ", end=" + end + ", link='" + link + '\'' + + ", type=" + type + ", orig_start=" + orig_start + ", orig_end=" + orig_end + '}'; @@ -93,18 +93,11 @@ public class SpanItem implements Parcelable { SpanItemParcelablePlease.writeToParcel(this, dest, flags); } - public static SpanItem from(Spanned spanned, URLSpan span) { - SpanItem spanItem = new SpanItem(); - spanItem.link = span.getURL(); - spanItem.start = spanned.getSpanStart(span); - spanItem.end = spanned.getSpanEnd(span); - return spanItem; - } - - @IntDef({SpanType.HIDE, SpanType.LINK}) + @IntDef({SpanType.HIDE, SpanType.LINK, SpanType.ACCT_MENTION}) @Retention(RetentionPolicy.SOURCE) public @interface SpanType { int HIDE = -1; int LINK = 0; + int ACCT_MENTION = 1; } } diff --git a/twidere/build.gradle b/twidere/build.gradle index a9bb3935a..2e1ed9e12 100644 --- a/twidere/build.gradle +++ b/twidere/build.gradle @@ -194,6 +194,7 @@ dependencies { compile "com.github.mariotaku.CommonsLibrary:io:${libVersions['MariotakuCommons']}" compile "com.github.mariotaku.CommonsLibrary:text:${libVersions['MariotakuCommons']}" compile "com.github.mariotaku.CommonsLibrary:text-kotlin:${libVersions['MariotakuCommons']}" + compile "com.github.mariotaku.CommonsLibrary:emojione:${libVersions['MariotakuCommons']}" compile "com.github.mariotaku:KPreferences:${libVersions['KPreferences']}" compile "com.github.mariotaku:Chameleon:${libVersions['Chameleon']}" compile 'com.github.mariotaku.QR-Code-generator:core:fcab3ea7f4' diff --git a/twidere/src/main/java/org/mariotaku/twidere/util/TwidereLinkify.java b/twidere/src/main/java/org/mariotaku/twidere/util/TwidereLinkify.java index 9e990ed32..d871f612c 100644 --- a/twidere/src/main/java/org/mariotaku/twidere/util/TwidereLinkify.java +++ b/twidere/src/main/java/org/mariotaku/twidere/util/TwidereLinkify.java @@ -23,7 +23,6 @@ import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.Spannable; -import android.text.SpannableString; import android.text.Spanned; import android.text.style.URLSpan; @@ -33,6 +32,7 @@ import com.twitter.Regex; import org.mariotaku.twidere.Constants; import org.mariotaku.twidere.model.UserKey; +import org.mariotaku.twidere.text.AcctMentionSpan; import org.mariotaku.twidere.text.TwidereURLSpan; import java.lang.annotation.Retention; @@ -69,6 +69,7 @@ public final class TwidereLinkify implements Constants { public static final int LINK_TYPE_LIST = 6; public static final int LINK_TYPE_CASHTAG = 7; public static final int LINK_TYPE_USER_ID = 8; + public static final int LINK_TYPE_USER_ACCT = 9; public static final int[] ALL_LINK_TYPES = new int[]{LINK_TYPE_ENTITY_URL, LINK_TYPE_LINK_IN_TEXT, LINK_TYPE_MENTION, LINK_TYPE_HASHTAG, LINK_TYPE_CASHTAG}; @@ -133,36 +134,6 @@ public final class TwidereLinkify implements Constants { } } - public SpannableString applyUserProfileLink(@Nullable final CharSequence text, - @Nullable final UserKey accountKey, final long extraId, final long userId, - final String screenName) { - return applyUserProfileLink(text, accountKey, extraId, userId, screenName, mHighlightOption); - } - - public SpannableString applyUserProfileLink(@Nullable final CharSequence text, - @Nullable final UserKey accountKey, final long extraId, final long userId, - final String screenName, final int highlightOption) { - return applyUserProfileLink(text, accountKey, extraId, userId, screenName, highlightOption, mOnLinkClickListener); - } - - public final SpannableString applyUserProfileLink(final CharSequence text, - @Nullable final UserKey accountKey, final long extraId, final long userId, - final String screenName, final int highlightOption, final OnLinkClickListener listener) { - final SpannableString string = SpannableString.valueOf(text); - final URLSpan[] spans = string.getSpans(0, string.length(), URLSpan.class); - for (final URLSpan span : spans) { - string.removeSpan(span); - } - if (userId > 0) { - applyLink(String.valueOf(userId), null, 0, string.length(), string, accountKey, extraId, - LINK_TYPE_USER_ID, false, highlightOption, listener); - } else if (screenName != null) { - applyLink(screenName, null, 0, string.length(), string, accountKey, extraId, - LINK_TYPE_MENTION, false, highlightOption, listener); - } - return string; - } - public void setHighlightOption(@HighlightStyle final int style) { mHighlightOption = style; } @@ -218,7 +189,10 @@ public final class TwidereLinkify implements Constants { } string.removeSpan(span); String url = span.getURL(); - if (accountKey != null && USER_TYPE_FANFOU_COM.equals(accountKey.getHost())) { + int linkType = type; + if (span instanceof AcctMentionSpan) { + linkType = LINK_TYPE_USER_ACCT; + } else if (accountKey != null && USER_TYPE_FANFOU_COM.equals(accountKey.getHost())) { // Fix search path if (url.startsWith("/")) { url = "http://fanfou.com" + url; @@ -236,8 +210,8 @@ public final class TwidereLinkify implements Constants { } } applyLink(url, String.valueOf(string.subSequence(start, end)), start, end, - string, accountKey, extraId, LINK_TYPE_ENTITY_URL, sensitive, - highlightOption, listener); + string, accountKey, extraId, linkType, sensitive, highlightOption, + listener); } break; } diff --git a/twidere/src/main/kotlin/org/mariotaku/ktextension/ArrayExtensions.kt b/twidere/src/main/kotlin/org/mariotaku/ktextension/ArrayExtensions.kt index 847faef4a..5b0a331e0 100644 --- a/twidere/src/main/kotlin/org/mariotaku/ktextension/ArrayExtensions.kt +++ b/twidere/src/main/kotlin/org/mariotaku/ktextension/ArrayExtensions.kt @@ -32,4 +32,12 @@ inline fun Array.mapIndexedToArray(transform: (Int, T) -> R): inline fun LongArray.mapToArray(transform: (Long) -> R): Array { return Array(size) { transform(this[it]) } +} + +fun CharArray.indexOf(element: Char, start: Int, len: Int): Int { + @Suppress("LoopToCallChain") + for (i in rangeOfSize(start, len)) { + if (this[i] == element) return i + } + return -1 } \ No newline at end of file diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/extension/URLSpanExtensions.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/extension/URLSpanExtensions.kt new file mode 100644 index 000000000..09634709c --- /dev/null +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/extension/URLSpanExtensions.kt @@ -0,0 +1,37 @@ +/* + * Twidere - Twitter client for Android + * + * Copyright (C) 2012-2017 Mariotaku Lee + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.mariotaku.twidere.extension + +import android.text.Spanned +import android.text.style.URLSpan +import org.mariotaku.twidere.model.SpanItem + +/** + * Created by mariotaku on 2017/4/26. + */ + +fun URLSpan.toSpanItem(spanned: Spanned): SpanItem { + val spanItem = SpanItem() + spanItem.link = url + spanItem.type = SpanItem.SpanType.LINK + spanItem.start = spanned.getSpanStart(this) + spanItem.end = spanned.getSpanEnd(this) + return spanItem +} \ No newline at end of file diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/SpanItemExtensions.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/SpanItemExtensions.kt index beb1d1e78..ba9d24da7 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/SpanItemExtensions.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/SpanItemExtensions.kt @@ -23,6 +23,7 @@ import android.text.Spannable import android.text.Spanned import android.text.style.URLSpan import org.mariotaku.twidere.model.SpanItem +import org.mariotaku.twidere.text.AcctMentionSpan import org.mariotaku.twidere.text.ZeroWidthSpan val SpanItem.length: Int get() = end - start @@ -34,6 +35,10 @@ fun Array.applyTo(spannable: Spannable) { spannable.setSpan(ZeroWidthSpan(), span.start, span.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) } + SpanItem.SpanType.ACCT_MENTION -> { + spannable.setSpan(AcctMentionSpan(span.link), span.start, span.end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + } else -> { spannable.setSpan(URLSpan(span.link), span.start, span.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/UserKeyExtensions.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/UserKeyExtensions.kt index 705928565..f1ba2ce4e 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/UserKeyExtensions.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/UserKeyExtensions.kt @@ -25,10 +25,10 @@ import org.mariotaku.twidere.model.UserKey * Created by mariotaku on 2017/4/19. */ -private const val mastodonPlaceholderId = "#mastodon*placeholder#" +private const val acctPlaceholderId = "#acct*placeholder#" -val UserKey.isMastodonPlaceholder get() = mastodonPlaceholderId == id && host != null +val UserKey.isAcctPlaceholder get() = acctPlaceholderId == id && host != null -fun MastodonPlaceholderUserKey(host: String?): UserKey { - return UserKey(mastodonPlaceholderId, host) +fun AcctPlaceholderUserKey(host: String?): UserKey { + return UserKey(acctPlaceholderId, host) } \ No newline at end of file diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/api/StatusExtensions.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/api/StatusExtensions.kt index 291aa54ef..5ca03eceb 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/api/StatusExtensions.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/api/StatusExtensions.kt @@ -24,12 +24,14 @@ import android.text.style.URLSpan import org.mariotaku.ktextension.mapToArray import org.mariotaku.microblog.library.twitter.model.Status import org.mariotaku.twidere.extension.model.toParcelable +import org.mariotaku.twidere.extension.toSpanItem import org.mariotaku.twidere.model.* import org.mariotaku.twidere.model.util.ParcelableLocationUtils import org.mariotaku.twidere.model.util.ParcelableMediaUtils import org.mariotaku.twidere.model.util.ParcelableStatusUtils.addFilterFlag import org.mariotaku.twidere.model.util.ParcelableUserMentionUtils import org.mariotaku.twidere.model.util.UserKeyUtils +import org.mariotaku.twidere.text.AcctMentionSpan import org.mariotaku.twidere.util.HtmlSpanBuilder import org.mariotaku.twidere.util.InternalTwitterContentUtils @@ -183,7 +185,13 @@ fun Status.toParcelable(accountKey: UserKey, accountType: String, profileImageSi } internal inline val CharSequence.spanItems get() = (this as? Spanned)?.let { text -> - text.getSpans(0, length, URLSpan::class.java).mapToArray { SpanItem.from(text, it) } + text.getSpans(0, length, URLSpan::class.java).mapToArray { + val item = it.toSpanItem(text) + if (it is AcctMentionSpan) { + item.type = SpanItem.SpanType.ACCT_MENTION + } + return@mapToArray item + } } internal inline val String.isHtml get() = contains('<') && contains('>') diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/api/mastodon/AccountExtensions.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/api/mastodon/AccountExtensions.kt index c511f4660..cdac10764 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/api/mastodon/AccountExtensions.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/api/mastodon/AccountExtensions.kt @@ -28,6 +28,7 @@ import org.mariotaku.twidere.model.ParcelableUser import org.mariotaku.twidere.model.UserKey import org.mariotaku.twidere.util.HtmlEscapeHelper import org.mariotaku.twidere.util.HtmlSpanBuilder +import org.mariotaku.twidere.util.emoji.EmojioneTranslator /** * Created by mariotaku on 2017/4/18. @@ -49,7 +50,7 @@ fun Account.toParcelable(accountKey: UserKey, position: Long = 0): ParcelableUse obj.name = name obj.screen_name = username if (note?.isHtml ?: false) { - val descriptionHtml = HtmlSpanBuilder.fromHtml(note, note) + val descriptionHtml = HtmlSpanBuilder.fromHtml(note, note, MastodonSpanProcessor) obj.description_unescaped = descriptionHtml?.toString() obj.description_plain = obj.description_unescaped obj.description_spans = descriptionHtml?.spanItems @@ -72,6 +73,7 @@ fun Account.toParcelable(accountKey: UserKey, position: Long = 0): ParcelableUse inline val Account.host: String? get() = acct?.let(UserKey::valueOf)?.host -inline val Account.name: String? get() = displayName?.takeIf(String::isNotEmpty) ?: username +inline val Account.name: String? get() = displayName?.takeIf(String::isNotEmpty) + ?.let(EmojioneTranslator::translate) ?: username fun Account.getKey(host: String?) = UserKey(id, acct?.let(UserKey::valueOf)?.host ?: host) diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/api/mastodon/StatusExtensions.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/api/mastodon/StatusExtensions.kt index 4f31612bc..050c0c8be 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/api/mastodon/StatusExtensions.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/api/mastodon/StatusExtensions.kt @@ -19,12 +19,18 @@ package org.mariotaku.twidere.extension.model.api.mastodon +import android.net.Uri +import android.text.Editable +import android.text.Spanned import org.mariotaku.ktextension.mapToArray import org.mariotaku.microblog.library.mastodon.model.Status import org.mariotaku.twidere.extension.model.api.spanItems import org.mariotaku.twidere.model.* import org.mariotaku.twidere.model.util.ParcelableStatusUtils.addFilterFlag +import org.mariotaku.twidere.text.AcctMentionSpan +import org.mariotaku.twidere.util.HtmlEscapeHelper import org.mariotaku.twidere.util.HtmlSpanBuilder +import org.mariotaku.twidere.util.emoji.EmojioneTranslator fun Status.toParcelable(details: AccountDetails): ParcelableStatus { return toParcelable(details.key).apply { @@ -40,7 +46,7 @@ fun Status.toParcelable(accountKey: UserKey): ParcelableStatus { result.sort_id = sortId result.timestamp = createdAt?.time ?: 0 - extras.summary_text = spoilerText + extras.summary_text = spoilerText?.let(EmojioneTranslator::translate) extras.visibility = visibility extras.external_url = url @@ -83,9 +89,8 @@ fun Status.toParcelable(accountKey: UserKey): ParcelableStatus { result.user_screen_name = account.username result.user_profile_image_url = account.avatar result.user_is_protected = account.isLocked - // Twitter will escape <> to <>, so if a status contains those symbols unescaped - // We should treat this as an html - val html = HtmlSpanBuilder.fromHtml(status.content, status.content) + // Mastodon has HTML formatted content text + val html = HtmlSpanBuilder.fromHtml(status.content, status.content, MastodonSpanProcessor) result.text_unescaped = html?.toString() result.text_plain = result.text_unescaped result.spans = html?.spanItems @@ -105,4 +110,25 @@ private fun calculateDisplayTextRange(spans: Array?, media: Array media.any { span.link == it.page_url } } ?: return null return intArrayOf(0, lastMatch.start) -} \ No newline at end of file +} + +object MastodonSpanProcessor : HtmlSpanBuilder.SpanProcessor { + + override fun appendText(text: Editable, buffer: CharArray, start: Int, len: Int): Boolean { + val unescaped = HtmlEscapeHelper.unescape(String(buffer, start, len)) + text.append(EmojioneTranslator.translate(unescaped)) + return true + } + + override fun applySpan(text: Editable, start: Int, end: Int, info: HtmlSpanBuilder.TagInfo): Boolean { + val clsAttr = info.getAttribute("class") ?: return false + val hrefAttr = info.getAttribute("href") ?: return false + // Is mention or hashtag + if ("mention" !in clsAttr.split(" ")) return false + if (text[start] != '@') return false + text.setSpan(AcctMentionSpan(text.substring(start + 1, end), Uri.parse(hrefAttr).host), + start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + return true + } + +} diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/UserFragment.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/UserFragment.kt index dac83b559..fddced7ba 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/UserFragment.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/UserFragment.kt @@ -639,7 +639,7 @@ class UserFragment : BaseFragment(), OnClickListener, OnLinkClickListener, val accountKey = data.getParcelableExtra(EXTRA_ACCOUNT_KEY) var userKey = user.key if (account?.type == AccountType.MASTODON && account?.key?.host != accountKey.host) { - userKey = MastodonPlaceholderUserKey(user.key.host) + userKey = AcctPlaceholderUserKey(user.key.host) } @Referral val referral = arguments.getString(EXTRA_REFERRAL) diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/loader/ParcelableUserLoader.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/loader/ParcelableUserLoader.kt index 30f8fd70b..c22179e5a 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/loader/ParcelableUserLoader.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/loader/ParcelableUserLoader.kt @@ -41,7 +41,7 @@ import org.mariotaku.twidere.annotation.Referral import org.mariotaku.twidere.extension.api.tryShowUser import org.mariotaku.twidere.extension.model.api.mastodon.toParcelable import org.mariotaku.twidere.extension.model.api.toParcelable -import org.mariotaku.twidere.extension.model.isMastodonPlaceholder +import org.mariotaku.twidere.extension.model.isAcctPlaceholder import org.mariotaku.twidere.extension.model.newMicroBlogInstance import org.mariotaku.twidere.model.AccountDetails import org.mariotaku.twidere.model.ParcelableUser @@ -162,7 +162,7 @@ class ParcelableUserLoader( private fun showMastodonUser(details: AccountDetails): ParcelableUser { val mastodon = details.newMicroBlogInstance(context, Mastodon::class.java) if (userKey == null) throw MicroBlogException("Invalid user id") - if (!userKey.isMastodonPlaceholder) { + if (!userKey.isAcctPlaceholder) { return mastodon.getAccount(userKey.id).toParcelable(details) } if (screenName == null) throw MicroBlogException("Screen name required") diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/text/AcctMentionSpan.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/text/AcctMentionSpan.kt new file mode 100644 index 000000000..8ec010fa1 --- /dev/null +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/text/AcctMentionSpan.kt @@ -0,0 +1,31 @@ +/* + * Twidere - Twitter client for Android + * + * Copyright (C) 2012-2017 Mariotaku Lee + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.mariotaku.twidere.text + +import android.text.style.URLSpan +import org.mariotaku.twidere.model.UserKey + +/** + * Created by mariotaku on 2017/4/26. + */ + +class AcctMentionSpan(acct: String) : URLSpan(acct) { + constructor(screenName: String, host: String?) : this(UserKey(screenName, host).toString()) +} diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/util/HtmlSpanBuilder.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/util/HtmlSpanBuilder.kt index c98cc9976..2da5ebec5 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/util/HtmlSpanBuilder.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/util/HtmlSpanBuilder.kt @@ -20,6 +20,7 @@ package org.mariotaku.twidere.util import android.graphics.Typeface +import android.text.Editable import android.text.Spannable import android.text.SpannableStringBuilder import android.text.Spanned @@ -39,8 +40,8 @@ object HtmlSpanBuilder { private val PARSER = SimpleMarkupParser(ParseConfiguration.htmlConfiguration()) @Throws(HtmlParseException::class) - fun fromHtml(html: String): Spannable { - val handler = HtmlSpanHandler() + fun fromHtml(html: String, processor: SpanProcessor? = null): Spannable { + val handler = HtmlSpanHandler(processor) try { PARSER.parse(html, handler) } catch (e: ParseException) { @@ -50,17 +51,19 @@ object HtmlSpanBuilder { return handler.text } - fun fromHtml(html: String?, fallback: CharSequence?): CharSequence? { + fun fromHtml(html: String?, fallback: CharSequence?, processor: SpanProcessor? = null): CharSequence? { if (html == null) return fallback try { - return fromHtml(html) + return fromHtml(html, processor) } catch (e: HtmlParseException) { return fallback } } - private fun applyTag(sb: SpannableStringBuilder, start: Int, end: Int, info: TagInfo) { + private fun applyTag(sb: SpannableStringBuilder, start: Int, end: Int, info: TagInfo, + processor: SpanProcessor?) { + if (processor?.applySpan(sb, start, end, info) ?: false) return if (info.nameLower == "br") { sb.append('\n') } else { @@ -88,26 +91,50 @@ object HtmlSpanBuilder { return info.indexOfLast { it.name.equals(name, ignoreCase = true) } } - private class HtmlParseException : RuntimeException { - internal constructor() : super() {} + interface SpanProcessor { - internal constructor(detailMessage: String) : super(detailMessage) {} + /** + * @param text Text before content in [buffer] appended + * @param buffer Raw html buffer + * @param start Start index of text to append in [buffer] + * @param len Length of text to append in [buffer] + */ + fun appendText(text: Editable, buffer: CharArray, start: Int, len: Int): Boolean = false - internal constructor(detailMessage: String, throwable: Throwable) : super(detailMessage, throwable) {} + /** + * @param text Text to apply span from [info] + * @param start Start index for applying span + * @param end End index for applying span + * @param info Tag info + */ + fun applySpan(text: Editable, start: Int, end: Int, info: TagInfo): Boolean = false - internal constructor(throwable: Throwable) : super(throwable) {} } - private data class TagInfo(val start: Int, val name: String, val attributes: Map?) { + data class TagInfo(val start: Int, val name: String, val attributes: Map?) { val nameLower = name.toLowerCase(Locale.US) - internal fun getAttribute(attr: String): String? { + fun getAttribute(attr: String): String? { return attributes?.get(attr) } } - private class HtmlSpanHandler internal constructor() : AbstractSimpleMarkupHandler() { + private class HtmlParseException : RuntimeException { + + internal constructor() : super() + + internal constructor(detailMessage: String) : super(detailMessage) + internal constructor(detailMessage: String, throwable: Throwable) : super(detailMessage, throwable) + + + internal constructor(throwable: Throwable) : super(throwable) + + } + + private class HtmlSpanHandler( + val processor: SpanProcessor? + ) : AbstractSimpleMarkupHandler() { private val sb = SpannableStringBuilder() private var tagInfo = ArrayList() @@ -122,7 +149,9 @@ object HtmlSpanBuilder { if (buffer[lineBreakIndex] == '\n') break lineBreakIndex++ } - sb.append(HtmlEscapeHelper.unescape(String(buffer, cur, lineBreakIndex - cur))) + if (!(processor?.appendText(sb, buffer, cur, lineBreakIndex - cur) ?: false)) { + sb.append(HtmlEscapeHelper.unescape(String(buffer, cur, lineBreakIndex - cur))) + } cur = lineBreakIndex + 1 } lastTag = null @@ -132,7 +161,7 @@ object HtmlSpanBuilder { val lastIndex = lastIndexOfTag(tagInfo, elementName) if (lastIndex == -1) return val info = tagInfo[lastIndex] - applyTag(sb, info.start, sb.length, info) + applyTag(sb, info.start, sb.length, info, processor) tagInfo.removeAt(lastIndex) lastTag = info } @@ -153,11 +182,12 @@ object HtmlSpanBuilder { minimized: Boolean, line: Int, col: Int) { if (minimized) { val info = TagInfo(sb.length, elementName, attributes) - applyTag(sb, info.start, sb.length, info) + applyTag(sb, info.start, sb.length, info, processor) } } val text: Spannable get() = sb } + } diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/util/OnLinkClickHandler.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/util/OnLinkClickHandler.kt index ca6c4c8eb..1a818efaf 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/util/OnLinkClickHandler.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/util/OnLinkClickHandler.kt @@ -33,6 +33,7 @@ import org.mariotaku.twidere.app.TwidereApplication import org.mariotaku.twidere.constant.IntentConstants.EXTRA_ACCOUNT_KEY import org.mariotaku.twidere.constant.displaySensitiveContentsKey import org.mariotaku.twidere.constant.newDocumentApiKey +import org.mariotaku.twidere.extension.model.AcctPlaceholderUserKey import org.mariotaku.twidere.model.UserKey import org.mariotaku.twidere.model.util.ParcelableMediaUtils import org.mariotaku.twidere.util.TwidereLinkify.OnLinkClickListener @@ -108,6 +109,12 @@ open class OnLinkClickHandler( preferences[newDocumentApiKey], Referral.USER_MENTION, null) return true } + TwidereLinkify.LINK_TYPE_USER_ACCT -> { + val acctKey = UserKey.valueOf(link) + IntentUtils.openUserProfile(context, accountKey, AcctPlaceholderUserKey(acctKey.host), + acctKey.id, null, preferences[newDocumentApiKey], Referral.USER_MENTION, null) + return true + } } return false } diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/util/emoji/EmojioneTranslator.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/util/emoji/EmojioneTranslator.kt new file mode 100644 index 000000000..4f41910e9 --- /dev/null +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/util/emoji/EmojioneTranslator.kt @@ -0,0 +1,27 @@ +/* + * Twidere - Twitter client for Android + * + * Copyright (C) 2012-2017 Mariotaku Lee + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.mariotaku.twidere.util.emoji + +import org.mariotaku.commons.emojione.ShortnameToUnicodeTranslator + +/** + * Created by mariotaku on 2017/4/26. + */ +object EmojioneTranslator: ShortnameToUnicodeTranslator() \ No newline at end of file