/* Copyright 2017 Thomas Schneider * * This file is a part of Mastalab * * 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. * * Mastalab 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 Mastalab; if not, * see . */ package fr.gouv.etalab.mastodon.client.Entities; import android.app.Activity; import android.content.*; import android.content.Context; import android.graphics.Bitmap; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; import android.text.Html; import android.text.Spannable; import android.text.SpannableString; import android.text.Spanned; import android.text.TextPaint; import android.text.style.ClickableSpan; import android.text.style.ForegroundColorSpan; import android.text.style.ImageSpan; import android.text.style.URLSpan; import android.view.View; import com.bumptech.glide.Glide; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.engine.GlideException; import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.target.SimpleTarget; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.request.transition.Transition; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import fr.gouv.etalab.mastodon.R; import fr.gouv.etalab.mastodon.activities.HashTagActivity; import fr.gouv.etalab.mastodon.activities.MainActivity; import fr.gouv.etalab.mastodon.activities.ShowAccountActivity; import fr.gouv.etalab.mastodon.asynctasks.RetrieveFeedsAsyncTask; import fr.gouv.etalab.mastodon.helper.CrossActions; import fr.gouv.etalab.mastodon.helper.Helper; import fr.gouv.etalab.mastodon.interfaces.OnRetrieveEmojiInterface; import static fr.gouv.etalab.mastodon.helper.Helper.INTENT_ACTION; import static fr.gouv.etalab.mastodon.helper.Helper.SEARCH_REMOTE; import static fr.gouv.etalab.mastodon.helper.Helper.SEARCH_URL; /** * Created by Thomas on 23/04/2017. * Manage Status (ie: toots) */ public class Status implements Parcelable{ private String id; private String uri; private String url; private Account account; private String in_reply_to_id; private String in_reply_to_account_id; private Status reblog; private Date created_at; private int reblogs_count; private int favourites_count; private int replies_count; private boolean reblogged; private boolean favourited; private boolean muted; private boolean pinned; private boolean sensitive; private boolean bookmarked; private String visibility; private boolean attachmentShown = false; private boolean spoilerShown = false; private ArrayList media_attachments; private List mentions; private List emojis; private List tags; private Application application; private Card card; private String language; private boolean isTranslated = false; private boolean isEmojiFound = false; private boolean isEmojiTranslateFound = false; private boolean isClickable = false; private boolean isTranslationShown = false; private boolean isNew = false; private boolean isVisible = true; private boolean fetchMore = false; private Status status; private String content, contentCW, contentTranslated; private SpannableString contentSpan, displayNameSpan, contentSpanCW, contentSpanTranslated; private RetrieveFeedsAsyncTask.Type type; public Status(){ this.status = this; } protected Status(Parcel in) { id = in.readString(); uri = in.readString(); url = in.readString(); in_reply_to_id = in.readString(); in_reply_to_account_id = in.readString(); reblog = in.readParcelable(Status.class.getClassLoader()); account = in.readParcelable(Account.class.getClassLoader()); mentions = in.readArrayList(Mention.class.getClassLoader()); content = in.readString(); contentTranslated = in.readString(); reblogs_count = in.readInt(); favourites_count = in.readInt(); replies_count = in.readInt(); reblogged = in.readByte() != 0; favourited = in.readByte() != 0; muted = in.readByte() != 0; sensitive = in.readByte() != 0; contentCW = in.readString(); visibility = in.readString(); language = in.readString(); attachmentShown = in.readByte() != 0; spoilerShown = in.readByte() != 0; isTranslated = in.readByte() != 0; isTranslationShown = in.readByte() != 0; isNew = in.readByte() != 0; pinned = in.readByte() != 0; } public static final Creator CREATOR = new Creator() { @Override public Status createFromParcel(Parcel in) { return new Status(in); } @Override public Status[] newArray(int size) { return new Status[size]; } }; public String getId() { return id; } public void setId(String id) { this.id = id; } public String getUri() { return uri; } public void setUri(String uri) { this.uri = uri; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public Account getAccount() { return account; } public void setAccount(Account account) { this.account = account; } public String getIn_reply_to_id() { return in_reply_to_id; } public void setIn_reply_to_id(String in_reply_to_id) { this.in_reply_to_id = in_reply_to_id; } public String getIn_reply_to_account_id() { return in_reply_to_account_id; } public void setIn_reply_to_account_id(String in_reply_to_account_id) { this.in_reply_to_account_id = in_reply_to_account_id; } public String getContent() { return content; } public void setContent(String content) { //Remove UTM by default this.content = content.replaceAll("&utm_\\w+=[0-9a-zA-Z._-]*", ""); this.content = this.content.replaceAll("&utm_\\w+=[0-9a-zA-Z._-]*", ""); this.content = this.content.replaceAll("\\?utm_\\w+=[0-9a-zA-Z._-]*", "?"); } public Status getReblog() { return reblog; } public void setReblog(Status reblog) { this.reblog = reblog; } public int getReblogs_count() { return reblogs_count; } public void setReblogs_count(int reblogs_count) { this.reblogs_count = reblogs_count; } public Date getCreated_at() { return created_at; } public void setCreated_at(Date created_at) { this.created_at = created_at; } public int getFavourites_count() { return favourites_count; } public void setFavourites_count(int favourites_count) { this.favourites_count = favourites_count; } public SpannableString getDisplayNameSpan(){ return this.displayNameSpan; } public void setDisplayNameSpan(SpannableString displayNameSpan){ this.displayNameSpan = displayNameSpan; } public boolean isReblogged() { return reblogged; } public void setReblogged(boolean reblogged) { this.reblogged = reblogged; } public boolean isFavourited() { return favourited; } public void setFavourited(boolean favourited) { this.favourited = favourited; } public void setPinned(boolean pinned) { this.pinned = pinned; } public boolean isPinned() { return pinned; } public boolean isSensitive() { return sensitive; } public void setSensitive(boolean sensitive) { this.sensitive = sensitive; } public String getSpoiler_text() { return contentCW; } public void setSpoiler_text(String spoiler_text) { this.contentCW = spoiler_text; } public ArrayList getMedia_attachments() { return media_attachments; } public void setMedia_attachments(ArrayList media_attachments) { this.media_attachments = media_attachments; } public List getMentions() { return mentions; } public void setMentions(List mentions) { this.mentions = mentions; } public List getTags() { return tags; } public void setTags(List tags) { this.tags = tags; } public Application getApplication() { return application; } public void setApplication(Application application) { this.application = application; } public String getVisibility() { return visibility; } public void setVisibility(String visibility) { this.visibility = visibility; } public boolean isAttachmentShown() { return attachmentShown; } public void setAttachmentShown(boolean attachmentShown) { this.attachmentShown = attachmentShown; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(id); dest.writeString(uri); dest.writeString(url); dest.writeString(in_reply_to_id); dest.writeString(in_reply_to_account_id); dest.writeParcelable(reblog, flags); dest.writeParcelable(account, flags); dest.writeList(mentions); dest.writeString(content); dest.writeString(contentTranslated); dest.writeInt(reblogs_count); dest.writeInt(favourites_count); dest.writeInt(replies_count); dest.writeByte((byte) (reblogged ? 1 : 0)); dest.writeByte((byte) (favourited ? 1 : 0)); dest.writeByte((byte) (muted ? 1 : 0)); dest.writeByte((byte) (sensitive ? 1 : 0)); dest.writeString(contentCW); dest.writeString(visibility); dest.writeString(language); dest.writeByte((byte) (attachmentShown ? 1 : 0)); dest.writeByte((byte) (spoilerShown ? 1 : 0)); dest.writeByte((byte) (isTranslated ? 1 : 0)); dest.writeByte((byte) (isTranslationShown ? 1 : 0)); dest.writeByte((byte) (isNew ? 1 : 0)); dest.writeByte((byte) (pinned ? 1 : 0)); } public boolean isSpoilerShown() { return spoilerShown; } public void setSpoilerShown(boolean spoilerShown) { this.spoilerShown = spoilerShown; } public String getLanguage() { return language; } public void setLanguage(String language) { this.language = language; } public boolean isTranslated() { return isTranslated; } public void setTranslated(boolean translated) { isTranslated = translated; } public boolean isTranslationShown() { return isTranslationShown; } public void setTranslationShown(boolean translationShown) { isTranslationShown = translationShown; } public String getContentTranslated() { return contentTranslated; } public void setContentTranslated(String content_translated) { this.contentTranslated = content_translated; } public boolean isNew() { return isNew; } public void setNew(boolean aNew) { isNew = aNew; } public boolean isVisible() { return isVisible; } public void setVisible(boolean visible) { isVisible = visible; } public List getEmojis() { return emojis; } public void setEmojis(List emojis) { this.emojis = emojis; } public boolean isEmojiFound() { return isEmojiFound; } public void setEmojiFound(boolean emojiFound) { isEmojiFound = emojiFound; } public void makeClickable(Context context){ if( ((Activity)context).isFinishing() || status == null) return; SpannableString spannableStringContent, spannableStringCW; if( (status.getReblog() != null && status.getReblog().getContent() == null) || (status.getReblog() == null && status.getContent() == null)) return; SharedPreferences sharedpreferences = context.getSharedPreferences(Helper.APP_PREFS, Context.MODE_PRIVATE); boolean isCompactMode = sharedpreferences.getBoolean(Helper.SET_COMPACT_MODE, true); int mode; /*if( isCompactMode) mode = Html.FROM_HTML_MODE_COMPACT; else*/ mode = Html.FROM_HTML_MODE_LEGACY; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) spannableStringContent = new SpannableString(Html.fromHtml(status.getReblog() != null ?status.getReblog().getContent():status.getContent(), mode)); else //noinspection deprecation spannableStringContent = new SpannableString(Html.fromHtml(status.getReblog() != null ?status.getReblog().getContent():status.getContent())); spannableStringContent = new SpannableString(status.getReblog() != null ?status.getReblog().getContent():status.getContent()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) spannableStringCW = new SpannableString(Html.fromHtml(status.getReblog() != null ?status.getReblog().getSpoiler_text():status.getSpoiler_text(), mode)); else //noinspection deprecation spannableStringCW = new SpannableString(Html.fromHtml(status.getReblog() != null ?status.getReblog().getSpoiler_text():status.getSpoiler_text())); status.setContentSpan(treatment(context, spannableStringContent)); status.setContentSpanCW(treatment(context, spannableStringCW)); isClickable = true; } public void makeClickableTranslation(Context context){ if( ((Activity)context).isFinishing() || status == null) return; if( (status.getReblog() != null && status.getReblog().getContent() == null) || (status.getReblog() == null && status.getContent() == null)) return; SharedPreferences sharedpreferences = context.getSharedPreferences(Helper.APP_PREFS, Context.MODE_PRIVATE); boolean isCompactMode = sharedpreferences.getBoolean(Helper.SET_COMPACT_MODE, true); int mode; /*if( isCompactMode) mode = Html.FROM_HTML_MODE_COMPACT; else*/ mode = Html.FROM_HTML_MODE_LEGACY; SpannableString spannableStringTranslated; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) spannableStringTranslated = new SpannableString(Html.fromHtml(status.getContentTranslated(), mode)); else //noinspection deprecation spannableStringTranslated = new SpannableString(Html.fromHtml(status.getContentTranslated())); status.setContentSpanTranslated(treatment(context, spannableStringTranslated)); } public void makeEmojis(final Context context, final OnRetrieveEmojiInterface listener){ if( ((Activity)context).isFinishing() ) return; if( status.getAccount() == null) return; if( status.getReblog() != null && status.getReblog().getEmojis() == null) return; if( status.getReblog() == null && status.getEmojis() == null) return; final List emojis = status.getReblog() != null ? status.getReblog().getEmojis() : status.getEmojis(); final List emojisAccounts = status.getReblog() != null ?status.getReblog().getAccount().getEmojis():status.getAccount().getEmojis(); status.getAccount().makeEmojisAccount(context, null); String displayName; if( status.getReblog() != null){ displayName = Helper.shortnameToUnicode(status.getReblog().getAccount().getDisplay_name(), true); displayName = String.format("%s @%s",displayName,status.getReblog().getAccount().getAcct()); }else { displayName = String.format("@%s",status.getAccount().getAcct()); } displayNameSpan = new SpannableString(displayName); if( emojisAccounts != null) emojis.addAll(emojisAccounts); if( emojis != null && emojis.size() > 0 ) { final int[] i = {0}; for (final Emojis emoji : emojis) { Glide.with(context) .asBitmap() .load(emoji.getUrl()) .listener(new RequestListener() { @Override public boolean onResourceReady(Bitmap resource, Object model, Target target, DataSource dataSource, boolean isFirstResource) { return false; } @Override public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) { i[0]++; if( i[0] == (emojis.size())) { listener.onRetrieveEmoji(status,false); } return false; } }) .into(new SimpleTarget() { @Override public void onResourceReady(@NonNull Bitmap resource, Transition transition) { final String targetedEmoji = ":" + emoji.getShortcode() + ":"; if (contentSpan.toString().contains(targetedEmoji)) { //emojis can be used several times so we have to loop for (int startPosition = -1; (startPosition = contentSpan.toString().indexOf(targetedEmoji, startPosition + 1)) != -1; startPosition++) { final int endPosition = startPosition + targetedEmoji.length(); if( endPosition <= contentSpan.toString().length() && endPosition >= startPosition) contentSpan.setSpan( new ImageSpan(context, Bitmap.createScaledBitmap(resource, (int) Helper.convertDpToPixel(20, context), (int) Helper.convertDpToPixel(20, context), false)), startPosition, endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); } } if (displayNameSpan != null && displayNameSpan.toString().contains(targetedEmoji)) { //emojis can be used several times so we have to loop for (int startPosition = -1; (startPosition = displayNameSpan.toString().indexOf(targetedEmoji, startPosition + 1)) != -1; startPosition++) { final int endPosition = startPosition + targetedEmoji.length(); if(endPosition <= displayNameSpan.toString().length() && endPosition >= startPosition) displayNameSpan.setSpan( new ImageSpan(context, Bitmap.createScaledBitmap(resource, (int) Helper.convertDpToPixel(20, context), (int) Helper.convertDpToPixel(20, context), false)), startPosition, endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); } } if (contentSpanCW.toString().contains(targetedEmoji)) { //emojis can be used several times so we have to loop for (int startPosition = -1; (startPosition = contentSpanCW.toString().indexOf(targetedEmoji, startPosition + 1)) != -1; startPosition++) { final int endPosition = startPosition + targetedEmoji.length(); if( endPosition <= contentSpan.toString().length() && endPosition >= startPosition) contentSpanCW.setSpan( new ImageSpan(context, Bitmap.createScaledBitmap(resource, (int) Helper.convertDpToPixel(20, context), (int) Helper.convertDpToPixel(20, context), false)), startPosition, endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); } } i[0]++; if( i[0] == (emojis.size())) { listener.onRetrieveEmoji(status, false); } } }); } } } public void makeEmojisTranslation(final Context context, final OnRetrieveEmojiInterface listener){ if( ((Activity)context).isFinishing() ) return; SpannableString spannableStringTranslated = null; SharedPreferences sharedpreferences = context.getSharedPreferences(Helper.APP_PREFS, Context.MODE_PRIVATE); boolean isCompactMode = sharedpreferences.getBoolean(Helper.SET_COMPACT_MODE, true); int mode; /*if( isCompactMode) mode = Html.FROM_HTML_MODE_COMPACT; else*/ mode = Html.FROM_HTML_MODE_LEGACY; if( status.getContentTranslated() != null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) spannableStringTranslated = new SpannableString(Html.fromHtml(status.getContentTranslated(), mode)); else //noinspection deprecation spannableStringTranslated = new SpannableString(Html.fromHtml(status.getContentTranslated())); } final List emojis = status.getReblog() != null ? status.getReblog().getEmojis() : status.getEmojis(); if( emojis != null && emojis.size() > 0 ) { final int[] i = {0}; for (final Emojis emoji : emojis) { final SpannableString finalSpannableStringTranslated = spannableStringTranslated; Glide.with(context) .asBitmap() .load(emoji.getUrl()) .listener(new RequestListener() { @Override public boolean onResourceReady(Bitmap resource, Object model, Target target, DataSource dataSource, boolean isFirstResource) { return false; } @Override public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) { i[0]++; if( i[0] == (emojis.size())) { if( finalSpannableStringTranslated != null) status.setContentSpanTranslated(finalSpannableStringTranslated); listener.onRetrieveEmoji(status, true); } return false; } }) .into(new SimpleTarget() { @Override public void onResourceReady(@NonNull Bitmap resource, Transition transition) { final String targetedEmoji = ":" + emoji.getShortcode() + ":"; if (finalSpannableStringTranslated != null && finalSpannableStringTranslated.toString().contains(targetedEmoji)) { //emojis can be used several times so we have to loop for (int startPosition = -1; (startPosition = finalSpannableStringTranslated.toString().indexOf(targetedEmoji, startPosition + 1)) != -1; startPosition++) { final int endPosition = startPosition + targetedEmoji.length(); if( endPosition <= finalSpannableStringTranslated.toString().length() && endPosition >= startPosition) finalSpannableStringTranslated.setSpan( new ImageSpan(context, Bitmap.createScaledBitmap(resource, (int) Helper.convertDpToPixel(20, context), (int) Helper.convertDpToPixel(20, context), false)), startPosition, endPosition, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); } } i[0]++; if( i[0] == (emojis.size())) { if( finalSpannableStringTranslated != null) status.setContentSpanTranslated(finalSpannableStringTranslated); listener.onRetrieveEmoji(status, true); } } }); } } } private SpannableString treatment(final Context context, final SpannableString spannableString){ URLSpan[] urls = spannableString.getSpans(0, spannableString.length(), URLSpan.class); for(URLSpan span : urls) spannableString.removeSpan(span); List mentions = this.status.getReblog() != null ? this.status.getReblog().getMentions() : this.status.getMentions(); SharedPreferences sharedpreferences = context.getSharedPreferences(Helper.APP_PREFS, android.content.Context.MODE_PRIVATE); int theme = sharedpreferences.getInt(Helper.SET_THEME, Helper.THEME_DARK); Matcher matcher; Pattern linkPattern = Pattern.compile("]*(((?!<\\/a).)*)<\\/a>"); matcher = linkPattern.matcher(spannableString); HashMap targetedURL = new HashMap<>(); while (matcher.find()){ String key; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) key = new SpannableString(Html.fromHtml(matcher.group(2), Html.FROM_HTML_MODE_LEGACY)).toString(); else //noinspection deprecation key = new SpannableString(Html.fromHtml(matcher.group(2))).toString(); key = key.substring(1); if( !key.startsWith("#") && !key.startsWith("@")) targetedURL.put(key, matcher.group(1)); } //Get url to account that are unknown Pattern aLink = Pattern.compile("(<\\s?a\\s?href=\"https?:\\/\\/([\\da-z\\.-]+\\.[a-z\\.]{2,6})\\/(@[\\/\\w._-]*)\"\\s?[^.]*<\\s?\\/\\s?a\\s?>)"); Matcher matcherALink = aLink.matcher(spannableString.toString()); ArrayList accountsMentionUnknown = new ArrayList<>(); while (matcherALink.find()){ String acct = matcherALink.group(3).replace("@",""); String instance = matcherALink.group(2); Account account = new Account(); account.setAcct(acct); account.setInstance(instance); accountsMentionUnknown.add(account); } aLink = Pattern.compile("(<\\s?a\\s?href=\"(https?:\\/\\/[\\da-z\\.-]+\\.[a-z\\.]{2,6}[\\/]?[^\"@(\\/tags\\/)]*)\"\\s?[^.]*<\\s?\\/\\s?a\\s?>)"); matcherALink = aLink.matcher(spannableString.toString()); while (matcherALink.find()){ int matchStart = matcherALink.start(); int matchEnd = matcherALink.end(); final String url = spannableString.toString().substring(matcherALink.start(1), matcherALink.end(1)); if( matchEnd <= spannableString.toString().length() && matchEnd >= matchStart) spannableString.setSpan(new ClickableSpan() { @Override public void onClick(View textView) { String finalUrl = url; if( !url.startsWith("http://") && ! url.startsWith("https://")) finalUrl = "http://" + url; Helper.openBrowser(context, finalUrl); } @Override public void updateDrawState(TextPaint ds) { super.updateDrawState(ds); ds.setUnderlineText(false); } }, matchStart, matchEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); if( matchEnd <= spannableString.toString().length() && matchEnd >= matchStart) spannableString.setSpan(new ForegroundColorSpan(ContextCompat.getColor(context, (theme==Helper.THEME_DARK||theme==Helper.THEME_BLACK)?R.color.mastodonC2:R.color.mastodonC4)), matchStart, matchEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } boolean isCompactMode = sharedpreferences.getBoolean(Helper.SET_COMPACT_MODE, true); int mode; /*if( isCompactMode) mode = Html.FROM_HTML_MODE_COMPACT; else*/ mode = Html.FROM_HTML_MODE_LEGACY; SpannableString spannableStringT; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) spannableStringT = new SpannableString(Html.fromHtml(spannableString.toString().replaceAll("^

","").replaceAll("

","

").replaceAll("

",""), mode)); else //noinspection deprecation spannableStringT = new SpannableString(Html.fromHtml(spannableString.toString().replaceAll("^

","").replaceAll("

","

").replaceAll("

",""))); matcher = Helper.twitterPattern.matcher(spannableStringT); while (matcher.find()){ int matchStart = matcher.start(2); int matchEnd = matcher.end(); final String twittername = matcher.group(2); URLSpan[] spans = spannableStringT.getSpans(matchStart, matchEnd, URLSpan.class); for (URLSpan span : spans) { spannableStringT.removeSpan(span); } if( matchEnd <= spannableStringT.toString().length() && matchEnd >= matchStart) spannableStringT.setSpan(new ClickableSpan() { @Override public void onClick(View textView) { Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://twitter.com/"+twittername.substring(1).replace("@twitter.com",""))); context.startActivity(intent); } @Override public void updateDrawState(TextPaint ds) { super.updateDrawState(ds); ds.setUnderlineText(false); } }, matchStart, matchEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); if( matchEnd <= spannableStringT.toString().length() && matchEnd >= matchStart) spannableStringT.setSpan(new ForegroundColorSpan(ContextCompat.getColor(context, (theme==Helper.THEME_DARK||theme==Helper.THEME_BLACK)?R.color.mastodonC2:R.color.mastodonC4)), matchStart, matchEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } if( accountsMentionUnknown.size() > 0 ) { for(Account account: accountsMentionUnknown){ String targetedAccount = "@" + account.getAcct(); if (spannableStringT.toString().toLowerCase().contains(targetedAccount.toLowerCase())) { //Accounts can be mentioned several times so we have to loop for(int startPosition = -1 ; (startPosition = spannableStringT.toString().toLowerCase().indexOf(targetedAccount.toLowerCase(), startPosition + 1)) != -1 ; startPosition++){ int endPosition = startPosition + targetedAccount.length(); URLSpan[] spans = spannableStringT.getSpans(startPosition, endPosition, URLSpan.class); for (URLSpan span : spans) { spannableStringT.removeSpan(span); } if( endPosition <= spannableStringT.toString().length() && endPosition >= startPosition) spannableStringT.setSpan(new ClickableSpan() { @Override public void onClick(View textView) { CrossActions.doCrossProfile(context,account); } @Override public void updateDrawState(TextPaint ds) { super.updateDrawState(ds); ds.setUnderlineText(false); } }, startPosition, endPosition, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); if(endPosition <= spannableStringT.toString().length() && endPosition >= startPosition) spannableStringT.setSpan(new ForegroundColorSpan(ContextCompat.getColor(context, (theme==Helper.THEME_DARK||theme==Helper.THEME_BLACK)?R.color.mastodonC2:R.color.mastodonC4)), startPosition, endPosition, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } } } } if( targetedURL.size() > 0 ){ Iterator it = targetedURL.entrySet().iterator(); while (it.hasNext()) { Map.Entry pair = (Map.Entry)it.next(); String key = (String) pair.getKey(); String url = (String) pair.getValue(); if (spannableStringT.toString().toLowerCase().contains(key.toLowerCase())) { //Accounts can be mentioned several times so we have to loop for(int startPosition = -1 ; (startPosition = spannableStringT.toString().toLowerCase().indexOf(key.toLowerCase(), startPosition + 1)) != -1 ; startPosition++){ int endPosition = startPosition + key.length(); URLSpan[] spans = spannableStringT.getSpans(startPosition, endPosition, URLSpan.class); for (URLSpan span : spans) { spannableStringT.removeSpan(span); } if( endPosition <= spannableStringT.toString().length() && endPosition >= startPosition) spannableStringT.setSpan(new ClickableSpan() { @Override public void onClick(View textView) { String finalUrl = url; Pattern link = Pattern.compile("https?:\\/\\/([\\da-z\\.-]+\\.[a-z\\.]{2,6})\\/(@[\\w._-]*[0-9]*)(\\/[0-9]{1,})?$"); Matcher matcherLink = link.matcher(url); if( matcherLink.find()){ Intent intent = new Intent(context, MainActivity.class); intent.putExtra(INTENT_ACTION, SEARCH_REMOTE); intent.putExtra(SEARCH_URL, url); context.startActivity(intent); }else { if( !url.startsWith("http://") && ! url.startsWith("https://")) finalUrl = "http://" + url; Helper.openBrowser(context, finalUrl); } } @Override public void updateDrawState(TextPaint ds) { super.updateDrawState(ds); ds.setUnderlineText(false); } }, startPosition, endPosition, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); if(endPosition <= spannableStringT.toString().length() && endPosition >= startPosition) spannableStringT.setSpan(new ForegroundColorSpan(ContextCompat.getColor(context, (theme==Helper.THEME_DARK||theme==Helper.THEME_BLACK)?R.color.mastodonC2:R.color.mastodonC4)), startPosition, endPosition, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } } it.remove(); } } //Deals with mention to make them clickable if( mentions != null && mentions.size() > 0 ) { //Looping through accounts which are mentioned for (final Mention mention : mentions) { String targetedAccount = "@" + mention.getUsername(); if (spannableStringT.toString().toLowerCase().contains(targetedAccount.toLowerCase())) { //Accounts can be mentioned several times so we have to loop for(int startPosition = -1 ; (startPosition = spannableStringT.toString().toLowerCase().indexOf(targetedAccount.toLowerCase(), startPosition + 1)) != -1 ; startPosition++){ int endPosition = startPosition + targetedAccount.length(); URLSpan[] spans = spannableStringT.getSpans(startPosition, endPosition, URLSpan.class); for (URLSpan span : spans) { spannableStringT.removeSpan(span); } if( endPosition <= spannableStringT.toString().length() && endPosition >= startPosition) spannableStringT.setSpan(new ClickableSpan() { @Override public void onClick(View textView) { if( type == null || type != RetrieveFeedsAsyncTask.Type.REMOTE_INSTANCE) { Intent intent = new Intent(context, ShowAccountActivity.class); Bundle b = new Bundle(); b.putString("accountId", mention.getId()); intent.putExtras(b); context.startActivity(intent); }else { String url = mention.getUrl(); Pattern instanceHost = Pattern.compile("https?:\\/\\/([\\da-z\\.-]+\\.[a-z\\.]{2,6})\\/(@[\\/\\w._-]*)"); Matcher matcherAcct = instanceHost.matcher(url); String instance = null, acct = null; while (matcherAcct.find()){ instance = matcherAcct.group(1); acct = matcherAcct.group(2); } if( acct != null && instance != null){ Account account = new Account(); account.setInstance(instance); account.setAcct(acct.replace("@","")); CrossActions.doCrossProfile(context, account); } } } @Override public void updateDrawState(TextPaint ds) { super.updateDrawState(ds); ds.setUnderlineText(false); } }, startPosition, endPosition, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); if(endPosition <= spannableStringT.toString().length() && endPosition >= startPosition) spannableStringT.setSpan(new ForegroundColorSpan(ContextCompat.getColor(context, (theme==Helper.THEME_DARK||theme==Helper.THEME_BLACK)?R.color.mastodonC2:R.color.mastodonC4)), startPosition, endPosition, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } } } } matcher = Helper.hashtagPattern.matcher(spannableStringT); while (matcher.find()){ int matchStart = matcher.start(1); int matchEnd = matcher.end(); URLSpan[] spans = spannableStringT.getSpans(matchStart, matchEnd, URLSpan.class); for (URLSpan span : spans) { spannableStringT.removeSpan(span); } final String tag = spannableStringT.toString().substring(matchStart, matchEnd); if( matchEnd <= spannableStringT.toString().length() && matchEnd >= matchStart) spannableStringT.setSpan(new ClickableSpan() { @Override public void onClick(View textView) { Intent intent = new Intent(context, HashTagActivity.class); Bundle b = new Bundle(); b.putString("tag", tag.substring(1)); intent.putExtras(b); context.startActivity(intent); } @Override public void updateDrawState(TextPaint ds) { super.updateDrawState(ds); ds.setUnderlineText(false); } }, matchStart, matchEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); if( matchEnd <= spannableStringT.toString().length() && matchEnd >= matchStart) spannableStringT.setSpan(new ForegroundColorSpan(ContextCompat.getColor(context, (theme==Helper.THEME_DARK||theme==Helper.THEME_BLACK)?R.color.mastodonC2:R.color.mastodonC4)), matchStart, matchEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } return spannableStringT; } public SpannableString getContentSpan() { return contentSpan; } public void setContentSpan(SpannableString contentSpan) { this.contentSpan = contentSpan; } public SpannableString getContentSpanCW() { return contentSpanCW; } public void setContentSpanCW(SpannableString contentSpanCW) { this.contentSpanCW = contentSpanCW; } public SpannableString getContentSpanTranslated() { return contentSpanTranslated; } public void setContentSpanTranslated(SpannableString contentSpanTranslated) { this.contentSpanTranslated = contentSpanTranslated; } public boolean isClickable() { return isClickable; } public boolean isEmojiTranslateFound() { return isEmojiTranslateFound; } public void setEmojiTranslateFound(boolean emojiTranslateFound) { isEmojiTranslateFound = emojiTranslateFound; } public boolean isFetchMore() { return fetchMore; } public void setFetchMore(boolean fetchMore) { this.fetchMore = fetchMore; } @Override public boolean equals(Object otherStatus) { return otherStatus != null && (otherStatus == this || otherStatus instanceof Status && this.getId().equals(((Status) otherStatus).getId())); } public Card getCard() { return card; } public void setCard(Card card) { this.card = card; } public boolean isMuted() { return muted; } public void setMuted(boolean muted) { this.muted = muted; } public boolean isBookmarked() { return bookmarked; } public void setBookmarked(boolean bookmarked) { this.bookmarked = bookmarked; } public int getReplies_count() { return replies_count; } public void setReplies_count(int replies_count) { this.replies_count = replies_count; } public RetrieveFeedsAsyncTask.Type getType() { return type; } public void setType(RetrieveFeedsAsyncTask.Type type) { this.type = type; } }