fixed #779
This commit is contained in:
parent
0612336e62
commit
8359397929
|
@ -1,196 +0,0 @@
|
|||
/*
|
||||
* Twidere - Twitter client for Android
|
||||
*
|
||||
* Copyright (C) 2012-2015 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;
|
||||
|
||||
import android.graphics.Typeface;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.StyleSpan;
|
||||
import android.text.style.URLSpan;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.attoparser.ParseException;
|
||||
import org.attoparser.config.ParseConfiguration;
|
||||
import org.attoparser.simple.AbstractSimpleMarkupHandler;
|
||||
import org.attoparser.simple.SimpleMarkupParser;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Created by mariotaku on 15/11/4.
|
||||
*/
|
||||
public class HtmlSpanBuilder {
|
||||
|
||||
private static final SimpleMarkupParser PARSER = new SimpleMarkupParser(ParseConfiguration.htmlConfiguration());
|
||||
|
||||
private HtmlSpanBuilder() {
|
||||
}
|
||||
|
||||
public static Spannable fromHtml(String html) throws HtmlParseException {
|
||||
final HtmlSpanHandler handler = new HtmlSpanHandler();
|
||||
try {
|
||||
PARSER.parse(html, handler);
|
||||
} catch (ParseException e) {
|
||||
throw new HtmlParseException(e);
|
||||
}
|
||||
return handler.getText();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static CharSequence fromHtml(String html, @Nullable CharSequence fallback) {
|
||||
if (html == null) return fallback;
|
||||
try {
|
||||
return fromHtml(html);
|
||||
} catch (HtmlParseException e) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyTag(SpannableStringBuilder sb, int start, int end, TagInfo info) {
|
||||
if (info.name.equalsIgnoreCase("br")) {
|
||||
sb.append('\n');
|
||||
} else {
|
||||
final Object span = createSpan(info);
|
||||
if (span == null) return;
|
||||
sb.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
}
|
||||
|
||||
private static Object createSpan(TagInfo info) {
|
||||
switch (info.name.toLowerCase(Locale.US)) {
|
||||
case "a": {
|
||||
return new URLSpan(info.getAttribute("href"));
|
||||
}
|
||||
case "b":
|
||||
case "strong": {
|
||||
return new StyleSpan(Typeface.BOLD);
|
||||
}
|
||||
case "em":
|
||||
case "cite":
|
||||
case "dfn":
|
||||
case "i": {
|
||||
return new StyleSpan(Typeface.ITALIC);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static int lastIndexOfTag(List<TagInfo> info, String name) {
|
||||
for (int i = info.size() - 1; i >= 0; i--) {
|
||||
if (StringUtils.equals(info.get(i).name, name)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static class HtmlParseException extends RuntimeException {
|
||||
HtmlParseException() {
|
||||
super();
|
||||
}
|
||||
|
||||
HtmlParseException(String detailMessage) {
|
||||
super(detailMessage);
|
||||
}
|
||||
|
||||
HtmlParseException(String detailMessage, Throwable throwable) {
|
||||
super(detailMessage, throwable);
|
||||
}
|
||||
|
||||
HtmlParseException(Throwable throwable) {
|
||||
super(throwable);
|
||||
}
|
||||
}
|
||||
|
||||
private static class TagInfo {
|
||||
final int start;
|
||||
final String name;
|
||||
final Map<String, String> attributes;
|
||||
|
||||
TagInfo(int start, String name, Map<String, String> attributes) {
|
||||
this.start = start;
|
||||
this.name = name;
|
||||
this.attributes = attributes;
|
||||
}
|
||||
|
||||
String getAttribute(String attr) {
|
||||
if (attributes == null) return null;
|
||||
return attributes.get(attr);
|
||||
}
|
||||
}
|
||||
|
||||
private static class HtmlSpanHandler extends AbstractSimpleMarkupHandler {
|
||||
private final SpannableStringBuilder sb;
|
||||
List<TagInfo> tagInfo;
|
||||
|
||||
HtmlSpanHandler() {
|
||||
sb = new SpannableStringBuilder();
|
||||
tagInfo = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleText(char[] buffer, int offset, int len, int line, int col) {
|
||||
int cur = offset;
|
||||
while (cur < offset + len) {
|
||||
// Find first line break
|
||||
int lineBreakIndex;
|
||||
for (lineBreakIndex = cur; lineBreakIndex < offset + len; lineBreakIndex++) {
|
||||
if (buffer[lineBreakIndex] == '\n') break;
|
||||
}
|
||||
sb.append(HtmlEscapeHelper.unescape(new String(buffer, cur, lineBreakIndex - cur)));
|
||||
cur = lineBreakIndex + 1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCloseElement(String elementName, int line, int col) {
|
||||
final int lastIndex = lastIndexOfTag(tagInfo, elementName);
|
||||
if (lastIndex != -1) {
|
||||
TagInfo info = tagInfo.get(lastIndex);
|
||||
applyTag(sb, info.start, sb.length(), info);
|
||||
tagInfo.remove(lastIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleOpenElement(String elementName, Map<String, String> attributes, int line, int col) {
|
||||
tagInfo.add(new TagInfo(sb.length(), elementName, attributes));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleStandaloneElement(String elementName, Map<String, String> attributes,
|
||||
boolean minimized, int line, int col) throws ParseException {
|
||||
if (minimized) {
|
||||
final TagInfo info = new TagInfo(sb.length(), elementName, attributes);
|
||||
applyTag(sb, info.start, sb.length(), info);
|
||||
}
|
||||
}
|
||||
|
||||
public Spannable getText() {
|
||||
return sb;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -45,6 +45,24 @@ val ParcelableStatus.replyMentions: Array<ParcelableUserMention>
|
|||
return result.toTypedArray()
|
||||
}
|
||||
|
||||
inline val ParcelableStatus.user_acct: String get() = if (account_key.host == user_key.host) {
|
||||
user_screen_name
|
||||
} else {
|
||||
"$user_screen_name@${user_key.host}"
|
||||
}
|
||||
|
||||
inline val ParcelableStatus.retweeted_by_user_acct: String? get() = if (account_key.host == retweeted_by_user_key?.host) {
|
||||
retweeted_by_user_screen_name
|
||||
} else {
|
||||
"$retweeted_by_user_screen_name@${retweeted_by_user_key?.host}"
|
||||
}
|
||||
|
||||
inline val ParcelableStatus.quoted_user_acct: String? get() = if (account_key.host == quoted_user_key?.host) {
|
||||
quoted_user_screen_name
|
||||
} else {
|
||||
"$quoted_user_screen_name@${quoted_user_key?.host}"
|
||||
}
|
||||
|
||||
private fun parcelableUserMention(key: UserKey, name: String, screenName: String) = ParcelableUserMention().also {
|
||||
it.key = key
|
||||
it.name = name
|
||||
|
|
|
@ -19,13 +19,8 @@
|
|||
|
||||
package org.mariotaku.twidere.extension.model
|
||||
|
||||
import org.mariotaku.ktextension.mapToArray
|
||||
import org.mariotaku.microblog.library.twitter.model.User
|
||||
import org.mariotaku.twidere.TwidereConstants.USER_TYPE_FANFOU_COM
|
||||
import org.mariotaku.twidere.extension.model.api.toParcelable
|
||||
import org.mariotaku.twidere.extension.model.api.toParcelable
|
||||
import org.mariotaku.twidere.model.ParcelableUser
|
||||
import org.mariotaku.twidere.model.UserKey
|
||||
import org.mariotaku.twidere.util.InternalTwitterContentUtils
|
||||
import org.mariotaku.twidere.util.Utils
|
||||
|
||||
|
@ -44,4 +39,11 @@ inline val ParcelableUser.originalProfileImage: String? get() {
|
|||
?: Utils.getOriginalTwitterProfileImage(profile_image_url)
|
||||
}
|
||||
|
||||
inline val ParcelableUser.urlPreferred: String? get() = url_expanded?.takeIf(String::isNotEmpty) ?: url
|
||||
inline val ParcelableUser.urlPreferred: String? get() = url_expanded?.takeIf(String::isNotEmpty) ?: url
|
||||
|
||||
|
||||
inline val ParcelableUser.acct: String get() = if (account_key.host == key.host) {
|
||||
screen_name
|
||||
} else {
|
||||
"$screen_name@${key.host}"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,163 @@
|
|||
/*
|
||||
* Twidere - Twitter client for Android
|
||||
*
|
||||
* Copyright (C) 2012-2015 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
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import android.text.style.StyleSpan
|
||||
import android.text.style.URLSpan
|
||||
import org.attoparser.ParseException
|
||||
import org.attoparser.config.ParseConfiguration
|
||||
import org.attoparser.simple.AbstractSimpleMarkupHandler
|
||||
import org.attoparser.simple.SimpleMarkupParser
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Created by mariotaku on 15/11/4.
|
||||
*/
|
||||
object HtmlSpanBuilder {
|
||||
|
||||
private val PARSER = SimpleMarkupParser(ParseConfiguration.htmlConfiguration())
|
||||
|
||||
@Throws(HtmlParseException::class)
|
||||
fun fromHtml(html: String): Spannable {
|
||||
val handler = HtmlSpanHandler()
|
||||
try {
|
||||
PARSER.parse(html, handler)
|
||||
} catch (e: ParseException) {
|
||||
throw HtmlParseException(e)
|
||||
}
|
||||
|
||||
return handler.text
|
||||
}
|
||||
|
||||
fun fromHtml(html: String?, fallback: CharSequence?): CharSequence? {
|
||||
if (html == null) return fallback
|
||||
try {
|
||||
return fromHtml(html)
|
||||
} catch (e: HtmlParseException) {
|
||||
return fallback
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun applyTag(sb: SpannableStringBuilder, start: Int, end: Int, info: TagInfo) {
|
||||
if (info.nameLower == "br") {
|
||||
sb.append('\n')
|
||||
} else {
|
||||
val span = createSpan(info) ?: return
|
||||
sb.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSpan(info: TagInfo): Any? {
|
||||
when (info.nameLower) {
|
||||
"a" -> {
|
||||
return URLSpan(info.getAttribute("href"))
|
||||
}
|
||||
"b", "strong" -> {
|
||||
return StyleSpan(Typeface.BOLD)
|
||||
}
|
||||
"em", "cite", "dfn", "i" -> {
|
||||
return StyleSpan(Typeface.ITALIC)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun lastIndexOfTag(info: List<TagInfo>, name: String): Int {
|
||||
return info.indexOfLast { it.name.equals(name, ignoreCase = true) }
|
||||
}
|
||||
|
||||
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 data class TagInfo(val start: Int, val name: String, val attributes: Map<String, String>?) {
|
||||
|
||||
val nameLower = name.toLowerCase(Locale.US)
|
||||
|
||||
internal fun getAttribute(attr: String): String? {
|
||||
return attributes?.get(attr)
|
||||
}
|
||||
}
|
||||
|
||||
private class HtmlSpanHandler internal constructor() : AbstractSimpleMarkupHandler() {
|
||||
|
||||
private val sb = SpannableStringBuilder()
|
||||
private var tagInfo = ArrayList<TagInfo>()
|
||||
private var lastTag: TagInfo? = null
|
||||
|
||||
override fun handleText(buffer: CharArray, offset: Int, len: Int, line: Int, col: Int) {
|
||||
var cur = offset
|
||||
while (cur < offset + len) {
|
||||
// Find first line break
|
||||
var lineBreakIndex = cur
|
||||
while (lineBreakIndex < offset + len) {
|
||||
if (buffer[lineBreakIndex] == '\n') break
|
||||
lineBreakIndex++
|
||||
}
|
||||
sb.append(HtmlEscapeHelper.unescape(String(buffer, cur, lineBreakIndex - cur)))
|
||||
cur = lineBreakIndex + 1
|
||||
}
|
||||
lastTag = null
|
||||
}
|
||||
|
||||
override fun handleCloseElement(elementName: String, line: Int, col: Int) {
|
||||
val lastIndex = lastIndexOfTag(tagInfo, elementName)
|
||||
if (lastIndex == -1) return
|
||||
val info = tagInfo[lastIndex]
|
||||
applyTag(sb, info.start, sb.length, info)
|
||||
tagInfo.removeAt(lastIndex)
|
||||
lastTag = info
|
||||
}
|
||||
|
||||
override fun handleOpenElement(elementName: String, attributes: Map<String, String>?,
|
||||
line: Int, col: Int) {
|
||||
val info = TagInfo(sb.length, elementName, attributes)
|
||||
tagInfo.add(info)
|
||||
|
||||
// Mastodon case, insert 2 breaks between two <p> tag
|
||||
if ("p" == info.nameLower && "p" == lastTag?.nameLower) {
|
||||
sb.append("\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(ParseException::class)
|
||||
override fun handleStandaloneElement(elementName: String, attributes: Map<String, String>?,
|
||||
minimized: Boolean, line: Int, col: Int) {
|
||||
if (minimized) {
|
||||
val info = TagInfo(sb.length, elementName, attributes)
|
||||
applyTag(sb, info.start, sb.length, info)
|
||||
}
|
||||
}
|
||||
|
||||
val text: Spannable
|
||||
get() = sb
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
package org.mariotaku.twidere.view.holder
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.view.View
|
||||
import android.widget.CompoundButton
|
||||
|
@ -59,9 +60,10 @@ class AccountViewHolder(
|
|||
dragHandle.visibility = if (enabled) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun display(details: AccountDetails) {
|
||||
name.text = details.user.name
|
||||
screenName.text = String.format("@%s", details.user.screen_name)
|
||||
screenName.text = "@${details.user.screen_name}"
|
||||
setAccountColor(details.color)
|
||||
profileImage.visibility = View.VISIBLE
|
||||
adapter.requestManager.loadProfileImage(adapter.context, details, adapter.profileImageStyle,
|
||||
|
|
|
@ -29,6 +29,9 @@ 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.extension.model.quoted_user_acct
|
||||
import org.mariotaku.twidere.extension.model.retweeted_by_user_acct
|
||||
import org.mariotaku.twidere.extension.model.user_acct
|
||||
import org.mariotaku.twidere.graphic.like.LikeAnimationDrawable
|
||||
import org.mariotaku.twidere.model.ParcelableLocation
|
||||
import org.mariotaku.twidere.model.ParcelableMedia
|
||||
|
@ -173,7 +176,7 @@ class StatusViewHolder(private val adapter: IStatusesAdapter<*>, itemView: View)
|
|||
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)
|
||||
status.retweeted_by_user_name, status.retweeted_by_user_acct!!, nameFirst)
|
||||
statusInfoLabel.text = context.getString(R.string.name_retweeted, formatter.unicodeWrap(retweetedBy))
|
||||
statusInfoIcon.setImageResource(R.drawable.ic_activity_action_retweet)
|
||||
statusInfoLabel.visibility = View.VISIBLE
|
||||
|
@ -214,7 +217,7 @@ class StatusViewHolder(private val adapter: IStatusesAdapter<*>, itemView: View)
|
|||
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}"
|
||||
quotedNameView.screenName = "@${status.quoted_user_acct}"
|
||||
|
||||
val quotedDisplayEnd = status.extras?.quoted_display_text_range?.getOrNull(1) ?: -1
|
||||
val quotedText: CharSequence
|
||||
|
@ -303,7 +306,7 @@ class StatusViewHolder(private val adapter: IStatusesAdapter<*>, itemView: View)
|
|||
}
|
||||
|
||||
nameView.name = colorNameManager.getUserNickname(status.user_key, status.user_name)
|
||||
nameView.screenName = "@${status.user_screen_name}"
|
||||
nameView.screenName = "@${status.user_acct}"
|
||||
|
||||
if (adapter.profileImageEnabled) {
|
||||
profileImageView.visibility = View.VISIBLE
|
||||
|
|
Loading…
Reference in New Issue