package app.fedilab.fedilabtube.client.entities; /* Copyright 2020 Thomas Schneider * * This file is a part of TubeLab * * 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. * * TubeLab 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 TubeLab; if not, * see . */ import android.content.Context; import android.content.SharedPreferences; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; import android.preference.PreferenceManager; import android.text.Html; import android.text.Spannable; import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextPaint; import android.text.TextUtils; import android.text.style.ClickableSpan; import android.text.style.QuoteSpan; import android.text.style.URLSpan; import android.view.View; import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; import java.lang.ref.WeakReference; import java.net.URI; import java.net.URISyntaxException; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; import app.fedilab.fedilabtube.R; import app.fedilab.fedilabtube.asynctasks.RetrieveFeedsAsyncTask; import app.fedilab.fedilabtube.helper.CustomQuoteSpan; import app.fedilab.fedilabtube.helper.Helper; @SuppressWarnings("unused") public class Status implements Parcelable { public static final Creator CREATOR = new Creator() { @Override public Status createFromParcel(Parcel source) { return new Status(source); } @Override public Status[] newArray(int size) { return new Status[size]; } }; 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 Date created_at; private int favourites_count; private int replies_count; private boolean favourited; private boolean muted; private boolean pinned; private boolean sensitive; private boolean bookmarked; private String visibility; private List mentions; private List tags; private String language; private String content; private SpannableString contentSpan; private transient RetrieveFeedsAsyncTask.Type type; private String conversationId; private String contentType; public Status() { } protected Status(Parcel in) { this.id = in.readString(); this.uri = in.readString(); this.url = in.readString(); this.account = in.readParcelable(Account.class.getClassLoader()); this.in_reply_to_id = in.readString(); this.in_reply_to_account_id = in.readString(); long tmpCreated_at = in.readLong(); this.created_at = tmpCreated_at == -1 ? null : new Date(tmpCreated_at); this.favourites_count = in.readInt(); this.replies_count = in.readInt(); this.favourited = in.readByte() != 0; this.muted = in.readByte() != 0; this.pinned = in.readByte() != 0; this.sensitive = in.readByte() != 0; this.bookmarked = in.readByte() != 0; this.visibility = in.readString(); this.mentions = in.createTypedArrayList(Mention.CREATOR); this.tags = in.createTypedArrayList(Tag.CREATOR); this.language = in.readString(); this.content = in.readString(); this.contentSpan = (SpannableString) TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); int tmpType = in.readInt(); this.type = tmpType == -1 ? null : RetrieveFeedsAsyncTask.Type.values()[tmpType]; this.conversationId = in.readString(); this.contentType = in.readString(); } public static void fillSpan(WeakReference contextWeakReference, Status status) { Status.transform(contextWeakReference, status); } private static void transform(WeakReference contextWeakReference, Status status) { Context context = contextWeakReference.get(); if (status == null) return; SpannableString spannableStringContent, spannableStringCW; if (status.getContent() == null) return; String content = status.getContent(); Pattern aLink = Pattern.compile("]*(((?!"); Matcher matcherALink = aLink.matcher(content); int count = 0; while (matcherALink.find()) { String beforemodification; String urlText = matcherALink.group(3); assert urlText != null; urlText = urlText.substring(1); beforemodification = urlText; if (!beforemodification.startsWith("http")) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) urlText = new SpannableString(Html.fromHtml(urlText, Html.FROM_HTML_MODE_LEGACY)).toString(); else urlText = new SpannableString(Html.fromHtml(urlText)).toString(); if (urlText.startsWith("http")) { urlText = urlText.replace("http://", "").replace("https://", "").replace("www.", ""); if (urlText.length() > 31) { urlText = urlText.substring(0, 30); urlText += "…" + count; count++; } } else if (urlText.startsWith("@")) { urlText += "|" + count; count++; } content = content.replaceFirst(Pattern.quote(beforemodification), Matcher.quoteReplacement(urlText)); } } content = content.replaceAll("(<\\s?p\\s?>)>(((?!(

)|(<\\s?p\\s?>|<\\s?br\\s?/?>|<\\s?/p\\s?>$)", "
$2

"); content = content.replaceAll("^<\\s?p\\s?>(.*)<\\s?/p\\s?>$", "$1"); spannableStringContent = new SpannableString(content); if (spannableStringContent.length() > 0) status.setContentSpan(treatment(context, spannableStringContent, status)); } private static SpannableString treatment(final Context context, SpannableString spannableString, Status status) { URLSpan[] urls = spannableString.getSpans(0, spannableString.length(), URLSpan.class); for (URLSpan span : urls) spannableString.removeSpan(span); List mentions = status.getMentions(); SharedPreferences sharedpreferences = context.getSharedPreferences(Helper.APP_PREFS, Context.MODE_PRIVATE); Matcher matcher; Pattern linkPattern = Pattern.compile("]*(((?!"); matcher = linkPattern.matcher(spannableString); LinkedHashMap targetedURL = new LinkedHashMap<>(); HashMap accountsMentionUnknown = new HashMap<>(); String liveInstance = Helper.getLiveInstance(context); int i = 1; while (matcher.find()) { String key; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) key = new SpannableString(Html.fromHtml(matcher.group(3), Html.FROM_HTML_MODE_LEGACY)).toString(); else key = new SpannableString(Html.fromHtml(matcher.group(3))).toString(); key = key.substring(1); if (!key.startsWith("#") && !key.startsWith("@") && !key.trim().equals("") && !Objects.requireNonNull(matcher.group(2)).contains("search?tag=") && !Objects.requireNonNull(matcher.group(2)).contains(liveInstance + "/users/")) { String url; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { url = Html.fromHtml(matcher.group(2), Html.FROM_HTML_MODE_LEGACY).toString(); } else { url = Html.fromHtml(matcher.group(2)).toString(); } targetedURL.put(key + "|" + i, url); i++; } else if (key.startsWith("@") || Objects.requireNonNull(matcher.group(2)).contains(liveInstance + "/users/")) { String acct; String url; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { url = Html.fromHtml(matcher.group(2), Html.FROM_HTML_MODE_LEGACY).toString(); } else { url = Html.fromHtml(matcher.group(2)).toString(); } URI uri; String instance = null; try { uri = new URI(url); instance = uri.getHost(); } catch (URISyntaxException e) { if (url.contains("|")) { try { uri = new URI(url.split("\\|")[0]); instance = uri.getHost(); } catch (URISyntaxException ex) { ex.printStackTrace(); } } } if (key.startsWith("@")) acct = key.substring(1).split("\\|")[0]; else acct = key.split("\\|")[0]; Account account = new Account(); account.setAcct(acct); account.setInstance(instance); account.setUrl(url); String accountId = null; if (mentions != null) { for (Mention mention : mentions) { String[] accountMentionAcct = mention.getAcct().split("@"); //Different isntance if (accountMentionAcct.length > 1) { if (mention.getAcct().equals(account.getAcct() + "@" + account.getInstance())) { accountId = mention.getId(); break; } } else { if (mention.getAcct().equals(account.getAcct())) { accountId = mention.getId(); break; } } } } if (accountId != null) { account.setId(accountId); } accountsMentionUnknown.put(key, account); } } SpannableStringBuilder spannableStringT; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) spannableStringT = new SpannableStringBuilder(Html.fromHtml(spannableString.toString().replaceAll("[\\s]{2}", "  "), Html.FROM_HTML_MODE_LEGACY)); else spannableStringT = new SpannableStringBuilder(Html.fromHtml(spannableString.toString().replaceAll("[\\s]{2}", "  "))); replaceQuoteSpans(context, spannableStringT); URLSpan[] spans = spannableStringT.getSpans(0, spannableStringT.length(), URLSpan.class); for (URLSpan span : spans) { spannableStringT.removeSpan(span); } SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); if (accountsMentionUnknown.size() > 0) { Iterator> it = accountsMentionUnknown.entrySet().iterator(); while (it.hasNext()) { Map.Entry pair = it.next(); String key = pair.getKey(); Account account = pair.getValue(); String targetedAccount = "@" + account.getAcct(); if (spannableStringT.toString().toLowerCase().contains(targetedAccount.toLowerCase())) { int startPosition = spannableStringT.toString().toLowerCase().indexOf(key.toLowerCase()); int endPosition = startPosition + key.length(); if (key.contains("|")) { key = key.split("\\|")[0]; SpannableStringBuilder ssb = new SpannableStringBuilder(); ssb.append(spannableStringT, 0, spannableStringT.length()); if (ssb.length() >= endPosition) { ssb.replace(startPosition, endPosition, key); } spannableStringT = SpannableStringBuilder.valueOf(ssb); endPosition = startPosition + key.length(); } //Accounts can be mentioned several times so we have to loop if (startPosition >= 0 && endPosition <= spannableStringT.toString().length() && endPosition >= startPosition) spannableStringT.setSpan(new ClickableSpan() { @Override public void onClick(@NonNull View textView) { /*if (account.getId() != null) { Intent intent = new Intent(context, ShowAccountActivity.class); Bundle b = new Bundle(); b.putString("accountId", account.getId()); intent.putExtras(b); context.startActivity(intent); }*/ } @Override public void updateDrawState(@NonNull TextPaint ds) { super.updateDrawState(ds); } }, startPosition, endPosition, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } it.remove(); } } if (targetedURL.size() > 0) { Iterator> it = targetedURL.entrySet().iterator(); int endPosition = 0; while (it.hasNext()) { Map.Entry pair = it.next(); String key = (pair.getKey()).split("\\|")[0]; String url = pair.getValue(); if (spannableStringT.toString().toLowerCase().contains(key.toLowerCase())) { //Accounts can be mentioned several times so we have to loop int startPosition = spannableStringT.toString().toLowerCase().indexOf(key.toLowerCase(), endPosition); if (startPosition >= 0) { endPosition = startPosition + key.length(); if (key.contains("…") && !key.endsWith("…")) { key = key.split("…")[0] + "…"; SpannableStringBuilder ssb = new SpannableStringBuilder(); ssb.append(spannableStringT, 0, spannableStringT.length()); if (ssb.length() >= endPosition) { ssb.replace(startPosition, endPosition, key); } spannableStringT = SpannableStringBuilder.valueOf(ssb); endPosition = startPosition + key.length(); } } } it.remove(); } } matcher = Helper.hashtagPattern.matcher(spannableStringT); while (matcher.find()) { int matchStart = matcher.start(1); int matchEnd = matcher.end(); final String tag = spannableStringT.toString().substring(matchStart, matchEnd); if (matchStart >= 0 && matchEnd <= spannableStringT.toString().length() && matchEnd >= matchStart) spannableStringT.setSpan(new ClickableSpan() { @Override public void onClick(@NonNull View textView) { /* Intent intent = new Intent(context, HashTagActivity.class); Bundle b = new Bundle(); b.putString("tag", tag.substring(1)); intent.putExtras(b); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent);*/ } @Override public void updateDrawState(@NonNull TextPaint ds) { super.updateDrawState(ds); ds.setUnderlineText(false); } }, matchStart, matchEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } Pattern carriagePattern = Pattern.compile("(\\n)+$"); matcher = carriagePattern.matcher(spannableStringT); while (matcher.find()) { int matchStart = matcher.start(); int matchEnd = matcher.end(); if (matchStart >= 0 && matchEnd <= spannableStringT.toString().length() && matchEnd >= matchStart) { spannableStringT.delete(matchStart, matchEnd); } } return SpannableString.valueOf(spannableStringT); } private static void replaceQuoteSpans(Context context, Spannable spannable) { QuoteSpan[] quoteSpans = spannable.getSpans(0, spannable.length(), QuoteSpan.class); for (QuoteSpan quoteSpan : quoteSpans) { int start = spannable.getSpanStart(quoteSpan); int end = spannable.getSpanEnd(quoteSpan); int flags = spannable.getSpanFlags(quoteSpan); spannable.removeSpan(quoteSpan); spannable.setSpan(new CustomQuoteSpan( ContextCompat.getColor(context, android.R.color.transparent), R.color.colorAccent, 10, 20), start, end, flags); } } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(this.id); dest.writeString(this.uri); dest.writeString(this.url); dest.writeParcelable(this.account, flags); dest.writeString(this.in_reply_to_id); dest.writeString(this.in_reply_to_account_id); dest.writeLong(this.created_at != null ? this.created_at.getTime() : -1); dest.writeInt(this.favourites_count); dest.writeInt(this.replies_count); dest.writeByte(this.favourited ? (byte) 1 : (byte) 0); dest.writeByte(this.muted ? (byte) 1 : (byte) 0); dest.writeByte(this.pinned ? (byte) 1 : (byte) 0); dest.writeByte(this.sensitive ? (byte) 1 : (byte) 0); dest.writeByte(this.bookmarked ? (byte) 1 : (byte) 0); dest.writeString(this.visibility); dest.writeTypedList(this.mentions); dest.writeTypedList(this.tags); dest.writeString(this.language); dest.writeString(this.content); TextUtils.writeToParcel(this.contentSpan, dest, flags); dest.writeInt(this.type == null ? -1 : this.type.ordinal()); dest.writeString(this.conversationId); dest.writeString(this.contentType); } 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) { this.content = content; } 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 boolean isFavourited() { return favourited; } public void setFavourited(boolean favourited) { this.favourited = favourited; } public boolean isPinned() { return pinned; } public void setPinned(boolean pinned) { this.pinned = pinned; } public boolean isSensitive() { return sensitive; } public void setSensitive(boolean sensitive) { this.sensitive = sensitive; } 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 String getTagsString() { //iterate through tags and create comma delimited string of tag names StringBuilder tag_names = new StringBuilder(); for (Tag t : tags) { if (tag_names.toString().equals("")) { tag_names = new StringBuilder(t.getName()); } else { tag_names.append(", ").append(t.getName()); } } return tag_names.toString(); } public String getVisibility() { return visibility; } public void setVisibility(String visibility) { this.visibility = visibility; } public String getLanguage() { return language; } public void setLanguage(String language) { this.language = language; } public SpannableString getContentSpan() { return contentSpan; } public void setContentSpan(SpannableString contentSpan) { this.contentSpan = contentSpan; } @Override public boolean equals(Object otherStatus) { return otherStatus != null && (otherStatus == this || otherStatus instanceof Status && this.getId().equals(((Status) otherStatus).getId())); } 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; } public String getConversationId() { return conversationId; } public void setConversationId(String conversationId) { this.conversationId = conversationId; } @Override public int describeContents() { return 0; } public String getContentType() { return contentType; } public void setContentType(String contentType) { this.contentType = contentType; } }