fixed #792
This commit is contained in:
Mariotaku Lee 2017-04-26 23:38:59 +08:00
parent 97618595c6
commit a3b03d105c
No known key found for this signature in database
GPG Key ID: 15C10F89D7C33535
17 changed files with 230 additions and 81 deletions

View File

@ -30,7 +30,7 @@ subprojects {
libVersions = [ libVersions = [
Kotlin : '1.1.1', Kotlin : '1.1.1',
SupportLib : '25.3.1', SupportLib : '25.3.1',
MariotakuCommons : '0.9.13', MariotakuCommons : '0.9.14',
RestFu : '0.9.54', RestFu : '0.9.54',
ObjectCursor : '0.9.16', ObjectCursor : '0.9.16',
PlayServices : '10.2.1', PlayServices : '10.2.1',

View File

@ -21,8 +21,6 @@ package org.mariotaku.twidere.model;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.support.annotation.IntDef; 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.JsonField;
import com.bluelinelabs.logansquare.annotation.JsonObject; import com.bluelinelabs.logansquare.annotation.JsonObject;
@ -63,21 +61,23 @@ public class SpanItem implements Parcelable {
@ParcelableThisPlease @ParcelableThisPlease
public String link; public String link;
@ParcelableThisPlease
@JsonField(name = "type")
@SpanType
public int type = SpanType.LINK;
@ParcelableNoThanks @ParcelableNoThanks
public int orig_start = -1; public int orig_start = -1;
@ParcelableNoThanks @ParcelableNoThanks
public int orig_end = -1; public int orig_end = -1;
@ParcelableNoThanks
@SpanType
public int type = SpanType.LINK;
@Override @Override
public String toString() { public String toString() {
return "SpanItem{" + return "SpanItem{" +
"start=" + start + "start=" + start +
", end=" + end + ", end=" + end +
", link='" + link + '\'' + ", link='" + link + '\'' +
", type=" + type +
", orig_start=" + orig_start + ", orig_start=" + orig_start +
", orig_end=" + orig_end + ", orig_end=" + orig_end +
'}'; '}';
@ -93,18 +93,11 @@ public class SpanItem implements Parcelable {
SpanItemParcelablePlease.writeToParcel(this, dest, flags); SpanItemParcelablePlease.writeToParcel(this, dest, flags);
} }
public static SpanItem from(Spanned spanned, URLSpan span) { @IntDef({SpanType.HIDE, SpanType.LINK, SpanType.ACCT_MENTION})
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})
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
public @interface SpanType { public @interface SpanType {
int HIDE = -1; int HIDE = -1;
int LINK = 0; int LINK = 0;
int ACCT_MENTION = 1;
} }
} }

View File

@ -194,6 +194,7 @@ dependencies {
compile "com.github.mariotaku.CommonsLibrary:io:${libVersions['MariotakuCommons']}" compile "com.github.mariotaku.CommonsLibrary:io:${libVersions['MariotakuCommons']}"
compile "com.github.mariotaku.CommonsLibrary:text:${libVersions['MariotakuCommons']}" compile "com.github.mariotaku.CommonsLibrary:text:${libVersions['MariotakuCommons']}"
compile "com.github.mariotaku.CommonsLibrary:text-kotlin:${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:KPreferences:${libVersions['KPreferences']}"
compile "com.github.mariotaku:Chameleon:${libVersions['Chameleon']}" compile "com.github.mariotaku:Chameleon:${libVersions['Chameleon']}"
compile 'com.github.mariotaku.QR-Code-generator:core:fcab3ea7f4' compile 'com.github.mariotaku.QR-Code-generator:core:fcab3ea7f4'

View File

@ -23,7 +23,6 @@ import android.support.annotation.IntDef;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.text.Spannable; import android.text.Spannable;
import android.text.SpannableString;
import android.text.Spanned; import android.text.Spanned;
import android.text.style.URLSpan; import android.text.style.URLSpan;
@ -33,6 +32,7 @@ import com.twitter.Regex;
import org.mariotaku.twidere.Constants; import org.mariotaku.twidere.Constants;
import org.mariotaku.twidere.model.UserKey; import org.mariotaku.twidere.model.UserKey;
import org.mariotaku.twidere.text.AcctMentionSpan;
import org.mariotaku.twidere.text.TwidereURLSpan; import org.mariotaku.twidere.text.TwidereURLSpan;
import java.lang.annotation.Retention; 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_LIST = 6;
public static final int LINK_TYPE_CASHTAG = 7; public static final int LINK_TYPE_CASHTAG = 7;
public static final int LINK_TYPE_USER_ID = 8; 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, 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}; 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) { public void setHighlightOption(@HighlightStyle final int style) {
mHighlightOption = style; mHighlightOption = style;
} }
@ -218,7 +189,10 @@ public final class TwidereLinkify implements Constants {
} }
string.removeSpan(span); string.removeSpan(span);
String url = span.getURL(); 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 // Fix search path
if (url.startsWith("/")) { if (url.startsWith("/")) {
url = "http://fanfou.com" + url; 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, applyLink(url, String.valueOf(string.subSequence(start, end)), start, end,
string, accountKey, extraId, LINK_TYPE_ENTITY_URL, sensitive, string, accountKey, extraId, linkType, sensitive, highlightOption,
highlightOption, listener); listener);
} }
break; break;
} }

View File

@ -32,4 +32,12 @@ inline fun <T, reified R> Array<T>.mapIndexedToArray(transform: (Int, T) -> R):
inline fun <reified R> LongArray.mapToArray(transform: (Long) -> R): Array<R> { inline fun <reified R> LongArray.mapToArray(transform: (Long) -> R): Array<R> {
return Array(size) { transform(this[it]) } 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
} }

View File

@ -0,0 +1,37 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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
}

View File

@ -23,6 +23,7 @@ import android.text.Spannable
import android.text.Spanned import android.text.Spanned
import android.text.style.URLSpan import android.text.style.URLSpan
import org.mariotaku.twidere.model.SpanItem import org.mariotaku.twidere.model.SpanItem
import org.mariotaku.twidere.text.AcctMentionSpan
import org.mariotaku.twidere.text.ZeroWidthSpan import org.mariotaku.twidere.text.ZeroWidthSpan
val SpanItem.length: Int get() = end - start val SpanItem.length: Int get() = end - start
@ -34,6 +35,10 @@ fun Array<SpanItem>.applyTo(spannable: Spannable) {
spannable.setSpan(ZeroWidthSpan(), span.start, span.end, spannable.setSpan(ZeroWidthSpan(), span.start, span.end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
} }
SpanItem.SpanType.ACCT_MENTION -> {
spannable.setSpan(AcctMentionSpan(span.link), span.start, span.end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
else -> { else -> {
spannable.setSpan(URLSpan(span.link), span.start, span.end, spannable.setSpan(URLSpan(span.link), span.start, span.end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)

View File

@ -25,10 +25,10 @@ import org.mariotaku.twidere.model.UserKey
* Created by mariotaku on 2017/4/19. * 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 { fun AcctPlaceholderUserKey(host: String?): UserKey {
return UserKey(mastodonPlaceholderId, host) return UserKey(acctPlaceholderId, host)
} }

View File

@ -24,12 +24,14 @@ import android.text.style.URLSpan
import org.mariotaku.ktextension.mapToArray import org.mariotaku.ktextension.mapToArray
import org.mariotaku.microblog.library.twitter.model.Status import org.mariotaku.microblog.library.twitter.model.Status
import org.mariotaku.twidere.extension.model.toParcelable import org.mariotaku.twidere.extension.model.toParcelable
import org.mariotaku.twidere.extension.toSpanItem
import org.mariotaku.twidere.model.* import org.mariotaku.twidere.model.*
import org.mariotaku.twidere.model.util.ParcelableLocationUtils import org.mariotaku.twidere.model.util.ParcelableLocationUtils
import org.mariotaku.twidere.model.util.ParcelableMediaUtils import org.mariotaku.twidere.model.util.ParcelableMediaUtils
import org.mariotaku.twidere.model.util.ParcelableStatusUtils.addFilterFlag import org.mariotaku.twidere.model.util.ParcelableStatusUtils.addFilterFlag
import org.mariotaku.twidere.model.util.ParcelableUserMentionUtils import org.mariotaku.twidere.model.util.ParcelableUserMentionUtils
import org.mariotaku.twidere.model.util.UserKeyUtils import org.mariotaku.twidere.model.util.UserKeyUtils
import org.mariotaku.twidere.text.AcctMentionSpan
import org.mariotaku.twidere.util.HtmlSpanBuilder import org.mariotaku.twidere.util.HtmlSpanBuilder
import org.mariotaku.twidere.util.InternalTwitterContentUtils 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 -> 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('>') internal inline val String.isHtml get() = contains('<') && contains('>')

View File

@ -28,6 +28,7 @@ import org.mariotaku.twidere.model.ParcelableUser
import org.mariotaku.twidere.model.UserKey import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.util.HtmlEscapeHelper import org.mariotaku.twidere.util.HtmlEscapeHelper
import org.mariotaku.twidere.util.HtmlSpanBuilder import org.mariotaku.twidere.util.HtmlSpanBuilder
import org.mariotaku.twidere.util.emoji.EmojioneTranslator
/** /**
* Created by mariotaku on 2017/4/18. * Created by mariotaku on 2017/4/18.
@ -49,7 +50,7 @@ fun Account.toParcelable(accountKey: UserKey, position: Long = 0): ParcelableUse
obj.name = name obj.name = name
obj.screen_name = username obj.screen_name = username
if (note?.isHtml ?: false) { if (note?.isHtml ?: false) {
val descriptionHtml = HtmlSpanBuilder.fromHtml(note, note) val descriptionHtml = HtmlSpanBuilder.fromHtml(note, note, MastodonSpanProcessor)
obj.description_unescaped = descriptionHtml?.toString() obj.description_unescaped = descriptionHtml?.toString()
obj.description_plain = obj.description_unescaped obj.description_plain = obj.description_unescaped
obj.description_spans = descriptionHtml?.spanItems 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.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) fun Account.getKey(host: String?) = UserKey(id, acct?.let(UserKey::valueOf)?.host ?: host)

View File

@ -19,12 +19,18 @@
package org.mariotaku.twidere.extension.model.api.mastodon 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.ktextension.mapToArray
import org.mariotaku.microblog.library.mastodon.model.Status import org.mariotaku.microblog.library.mastodon.model.Status
import org.mariotaku.twidere.extension.model.api.spanItems import org.mariotaku.twidere.extension.model.api.spanItems
import org.mariotaku.twidere.model.* import org.mariotaku.twidere.model.*
import org.mariotaku.twidere.model.util.ParcelableStatusUtils.addFilterFlag 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.HtmlSpanBuilder
import org.mariotaku.twidere.util.emoji.EmojioneTranslator
fun Status.toParcelable(details: AccountDetails): ParcelableStatus { fun Status.toParcelable(details: AccountDetails): ParcelableStatus {
return toParcelable(details.key).apply { return toParcelable(details.key).apply {
@ -40,7 +46,7 @@ fun Status.toParcelable(accountKey: UserKey): ParcelableStatus {
result.sort_id = sortId result.sort_id = sortId
result.timestamp = createdAt?.time ?: 0 result.timestamp = createdAt?.time ?: 0
extras.summary_text = spoilerText extras.summary_text = spoilerText?.let(EmojioneTranslator::translate)
extras.visibility = visibility extras.visibility = visibility
extras.external_url = url extras.external_url = url
@ -83,9 +89,8 @@ fun Status.toParcelable(accountKey: UserKey): ParcelableStatus {
result.user_screen_name = account.username result.user_screen_name = account.username
result.user_profile_image_url = account.avatar result.user_profile_image_url = account.avatar
result.user_is_protected = account.isLocked result.user_is_protected = account.isLocked
// Twitter will escape <> to &lt;&gt;, so if a status contains those symbols unescaped // Mastodon has HTML formatted content text
// We should treat this as an html val html = HtmlSpanBuilder.fromHtml(status.content, status.content, MastodonSpanProcessor)
val html = HtmlSpanBuilder.fromHtml(status.content, status.content)
result.text_unescaped = html?.toString() result.text_unescaped = html?.toString()
result.text_plain = result.text_unescaped result.text_plain = result.text_unescaped
result.spans = html?.spanItems result.spans = html?.spanItems
@ -105,4 +110,25 @@ private fun calculateDisplayTextRange(spans: Array<SpanItem>?, media: Array<Parc
if (spans == null || media == null) return null if (spans == null || media == null) return null
val lastMatch = spans.lastOrNull { span -> media.any { span.link == it.page_url } } ?: return null val lastMatch = spans.lastOrNull { span -> media.any { span.link == it.page_url } } ?: return null
return intArrayOf(0, lastMatch.start) return intArrayOf(0, lastMatch.start)
} }
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
}
}

View File

@ -639,7 +639,7 @@ class UserFragment : BaseFragment(), OnClickListener, OnLinkClickListener,
val accountKey = data.getParcelableExtra<UserKey>(EXTRA_ACCOUNT_KEY) val accountKey = data.getParcelableExtra<UserKey>(EXTRA_ACCOUNT_KEY)
var userKey = user.key var userKey = user.key
if (account?.type == AccountType.MASTODON && account?.key?.host != accountKey.host) { if (account?.type == AccountType.MASTODON && account?.key?.host != accountKey.host) {
userKey = MastodonPlaceholderUserKey(user.key.host) userKey = AcctPlaceholderUserKey(user.key.host)
} }
@Referral @Referral
val referral = arguments.getString(EXTRA_REFERRAL) val referral = arguments.getString(EXTRA_REFERRAL)

View File

@ -41,7 +41,7 @@ import org.mariotaku.twidere.annotation.Referral
import org.mariotaku.twidere.extension.api.tryShowUser import org.mariotaku.twidere.extension.api.tryShowUser
import org.mariotaku.twidere.extension.model.api.mastodon.toParcelable import org.mariotaku.twidere.extension.model.api.mastodon.toParcelable
import org.mariotaku.twidere.extension.model.api.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.extension.model.newMicroBlogInstance
import org.mariotaku.twidere.model.AccountDetails import org.mariotaku.twidere.model.AccountDetails
import org.mariotaku.twidere.model.ParcelableUser import org.mariotaku.twidere.model.ParcelableUser
@ -162,7 +162,7 @@ class ParcelableUserLoader(
private fun showMastodonUser(details: AccountDetails): ParcelableUser { private fun showMastodonUser(details: AccountDetails): ParcelableUser {
val mastodon = details.newMicroBlogInstance(context, Mastodon::class.java) val mastodon = details.newMicroBlogInstance(context, Mastodon::class.java)
if (userKey == null) throw MicroBlogException("Invalid user id") if (userKey == null) throw MicroBlogException("Invalid user id")
if (!userKey.isMastodonPlaceholder) { if (!userKey.isAcctPlaceholder) {
return mastodon.getAccount(userKey.id).toParcelable(details) return mastodon.getAccount(userKey.id).toParcelable(details)
} }
if (screenName == null) throw MicroBlogException("Screen name required") if (screenName == null) throw MicroBlogException("Screen name required")

View File

@ -0,0 +1,31 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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())
}

View File

@ -20,6 +20,7 @@
package org.mariotaku.twidere.util package org.mariotaku.twidere.util
import android.graphics.Typeface import android.graphics.Typeface
import android.text.Editable
import android.text.Spannable import android.text.Spannable
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.text.Spanned import android.text.Spanned
@ -39,8 +40,8 @@ object HtmlSpanBuilder {
private val PARSER = SimpleMarkupParser(ParseConfiguration.htmlConfiguration()) private val PARSER = SimpleMarkupParser(ParseConfiguration.htmlConfiguration())
@Throws(HtmlParseException::class) @Throws(HtmlParseException::class)
fun fromHtml(html: String): Spannable { fun fromHtml(html: String, processor: SpanProcessor? = null): Spannable {
val handler = HtmlSpanHandler() val handler = HtmlSpanHandler(processor)
try { try {
PARSER.parse(html, handler) PARSER.parse(html, handler)
} catch (e: ParseException) { } catch (e: ParseException) {
@ -50,17 +51,19 @@ object HtmlSpanBuilder {
return handler.text 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 if (html == null) return fallback
try { try {
return fromHtml(html) return fromHtml(html, processor)
} catch (e: HtmlParseException) { } catch (e: HtmlParseException) {
return fallback 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") { if (info.nameLower == "br") {
sb.append('\n') sb.append('\n')
} else { } else {
@ -88,26 +91,50 @@ object HtmlSpanBuilder {
return info.indexOfLast { it.name.equals(name, ignoreCase = true) } return info.indexOfLast { it.name.equals(name, ignoreCase = true) }
} }
private class HtmlParseException : RuntimeException { interface SpanProcessor {
internal constructor() : super() {}
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<String, String>?) { data class TagInfo(val start: Int, val name: String, val attributes: Map<String, String>?) {
val nameLower = name.toLowerCase(Locale.US) val nameLower = name.toLowerCase(Locale.US)
internal fun getAttribute(attr: String): String? { fun getAttribute(attr: String): String? {
return attributes?.get(attr) 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 val sb = SpannableStringBuilder()
private var tagInfo = ArrayList<TagInfo>() private var tagInfo = ArrayList<TagInfo>()
@ -122,7 +149,9 @@ object HtmlSpanBuilder {
if (buffer[lineBreakIndex] == '\n') break if (buffer[lineBreakIndex] == '\n') break
lineBreakIndex++ 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 cur = lineBreakIndex + 1
} }
lastTag = null lastTag = null
@ -132,7 +161,7 @@ object HtmlSpanBuilder {
val lastIndex = lastIndexOfTag(tagInfo, elementName) val lastIndex = lastIndexOfTag(tagInfo, elementName)
if (lastIndex == -1) return if (lastIndex == -1) return
val info = tagInfo[lastIndex] val info = tagInfo[lastIndex]
applyTag(sb, info.start, sb.length, info) applyTag(sb, info.start, sb.length, info, processor)
tagInfo.removeAt(lastIndex) tagInfo.removeAt(lastIndex)
lastTag = info lastTag = info
} }
@ -153,11 +182,12 @@ object HtmlSpanBuilder {
minimized: Boolean, line: Int, col: Int) { minimized: Boolean, line: Int, col: Int) {
if (minimized) { if (minimized) {
val info = TagInfo(sb.length, elementName, attributes) 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 val text: Spannable
get() = sb get() = sb
} }
} }

View File

@ -33,6 +33,7 @@ import org.mariotaku.twidere.app.TwidereApplication
import org.mariotaku.twidere.constant.IntentConstants.EXTRA_ACCOUNT_KEY import org.mariotaku.twidere.constant.IntentConstants.EXTRA_ACCOUNT_KEY
import org.mariotaku.twidere.constant.displaySensitiveContentsKey import org.mariotaku.twidere.constant.displaySensitiveContentsKey
import org.mariotaku.twidere.constant.newDocumentApiKey import org.mariotaku.twidere.constant.newDocumentApiKey
import org.mariotaku.twidere.extension.model.AcctPlaceholderUserKey
import org.mariotaku.twidere.model.UserKey import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.model.util.ParcelableMediaUtils import org.mariotaku.twidere.model.util.ParcelableMediaUtils
import org.mariotaku.twidere.util.TwidereLinkify.OnLinkClickListener import org.mariotaku.twidere.util.TwidereLinkify.OnLinkClickListener
@ -108,6 +109,12 @@ open class OnLinkClickHandler(
preferences[newDocumentApiKey], Referral.USER_MENTION, null) preferences[newDocumentApiKey], Referral.USER_MENTION, null)
return true 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 return false
} }

View File

@ -0,0 +1,27 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.util.emoji
import org.mariotaku.commons.emojione.ShortnameToUnicodeTranslator
/**
* Created by mariotaku on 2017/4/26.
*/
object EmojioneTranslator: ShortnameToUnicodeTranslator()