package com.ioabsoftware.gameraven.views.rowdata; import android.graphics.PorterDuff; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.style.CharacterStyle; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.text.style.TypefaceSpan; import android.text.style.UnderlineSpan; import android.text.util.Linkify; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.Button; import android.widget.LinearLayout; import android.widget.TextView; import com.ioabsoftware.gameraven.AllInOneV2; import com.ioabsoftware.gameraven.BuildConfig; import com.ioabsoftware.gameraven.R; import com.ioabsoftware.gameraven.networking.NetDesc; import com.ioabsoftware.gameraven.networking.Session; import com.ioabsoftware.gameraven.util.MyLinkifier; import com.ioabsoftware.gameraven.util.Theming; import com.ioabsoftware.gameraven.views.BaseRowData; import com.ioabsoftware.gameraven.views.GRQuoteSpan; import com.ioabsoftware.gameraven.views.RowType; import com.ioabsoftware.gameraven.views.SpoilerBackgroundSpan; import com.ioabsoftware.gameraven.views.SpoilerClickSpan; import com.ioabsoftware.gameraven.views.rowview.HeaderRowView; import org.apache.commons.lang3.StringEscapeUtils; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; public class MessageRowData extends BaseRowData { private String username, userTitles, avatarUrl, postNum, postTime, messageID, boardID, topicID; private final String unprocessedMessageText; private LinearLayout poll = null; private Spannable spannedMessage; private int hlColor; private boolean topClickable = true; private boolean isDeleted = false; private boolean canReport = false, canDelete = false, canEdit = false, canQuote = false; @Override public String toString() { return "username: " + username + "\nuserTitles: " + userTitles + "\nhlColor: " + hlColor + "\npostNum: " + postNum + "\npostTime: " + postTime + "\nmessageID: " + messageID + "\nboardID: " + boardID + "\ntopicID: " + topicID + "\nhasPoll: " + hasPoll() + "\nunprocessedMessageText: " + unprocessedMessageText + "\nspannedMessage: " + spannedMessage; } public void disableTopClick() { topClickable = false; } public boolean topClickable() { return topClickable; } public boolean isDeleted() { return isDeleted; } public String getUser() { return username; } public String getUserTitles() { return userTitles; } public boolean hasTitles() { return userTitles != null; } public String getAvatarUrl() { return avatarUrl; } public String getPostNum() { return postNum; } public String getPostTime() { return postTime; } public String getMessageID() { return messageID; } public boolean hasMsgID() { return messageID != null; } public String getTopicID() { return topicID; } public String getBoardID() { return boardID; } public boolean canReport() { return canReport; } public boolean canDelete() { return canDelete; } public boolean canEdit() { return canEdit; } public boolean canQuote() { return canQuote; } public String getUnprocessedMessageText() { return unprocessedMessageText; } public LinearLayout getPoll() { if (poll.getParent() != null) ((ViewGroup) poll.getParent()).removeView(poll); return poll; } public boolean hasPoll() { return poll != null; } public Spannable getSpannedMessage() { return spannedMessage; } public int getHLColor() { return hlColor; } public String getMessageDetailLink() { return Session.ROOT + "/boards/" + boardID + "/" + topicID + "/" + messageID; } public String getUserDetailLink() { return Session.ROOT + "/community/" + username.replace(' ', '+') + "/boards"; } private static AllInOneV2 aio = null; @Override public RowType getRowType() { return RowType.MESSAGE; } public MessageRowData(boolean isDeletedIn, String postNumIn) { isDeleted = isDeletedIn; postNum = postNumIn; unprocessedMessageText = ""; } public MessageRowData(String userIn, String userTitlesIn, String avatarUrlIn, String postNumIn, String postTimeIn, Element messageIn, String BID, String TID, String MID, int hlColorIn, boolean cReport, boolean cDelete, boolean cEdit, boolean cQuote) { if (aio == null || aio != AllInOneV2.get()) aio = AllInOneV2.get(); if (BuildConfig.DEBUG) AllInOneV2.wtl("setting values"); username = userIn; userTitles = userTitlesIn; avatarUrl = avatarUrlIn; postNum = postNumIn; postTime = postTimeIn.replace('\u00A0', ' '); boardID = BID; topicID = TID; messageID = MID; hlColor = hlColorIn; canReport = cReport; canDelete = cDelete; canEdit = cEdit; canQuote = cQuote; String sigHtml = ""; Element sig = messageIn.select("div.sig_text").first(); if (sig != null) { sigHtml = "<br />---<br />" + sig.html(); messageIn.select("div.signature").remove(); } if (!Session.isLoggedIn()) messageIn.select("div.message_mpu").remove(); if (BuildConfig.DEBUG) AllInOneV2.wtl("checking for poll"); if (!messageIn.getElementsByClass("board_poll").isEmpty()) { if (BuildConfig.DEBUG) AllInOneV2.wtl("there is a poll"); Element pollElem = messageIn.getElementsByClass("board_poll").first(); poll = new LinearLayout(aio); poll.setOrientation(LinearLayout.VERTICAL); LinearLayout pollInnerWrapper = new LinearLayout(aio); pollInnerWrapper.setPadding(15, 0, 15, 15); pollInnerWrapper.setOrientation(LinearLayout.VERTICAL); Drawable s = aio.getResources().getDrawable(R.drawable.item_background); s.setColorFilter(Theming.colorPrimary(), PorterDuff.Mode.SRC_ATOP); poll.setBackgroundDrawable(s); HeaderRowView h = new HeaderRowView(aio); h.showView(new HeaderRowData(pollElem.getElementsByClass("poll_head").first().text())); poll.addView(h); poll.addView(pollInnerWrapper); if (pollElem.getElementsByTag("form").isEmpty()) { // poll has been voted in // poll_foot_left TextView t; for (Element e : pollElem.select("div.row")) { Elements c = e.children(); t = new TextView(aio); String text = c.get(0).text() + ": " + c.get(1).text(); if (!c.get(0).children().isEmpty()) { SpannableStringBuilder votedFor = new SpannableStringBuilder(text); votedFor.setSpan(new StyleSpan(Typeface.BOLD), 0, text.length(), 0); votedFor.setSpan(new ForegroundColorSpan(Theming.colorPrimary()), 0, text.length(), 0); t.setText(votedFor); } else t.setText(text); pollInnerWrapper.addView(t); } String foot = pollElem.getElementsByClass("poll_foot_left").text(); if (foot.length() > 0) { t = new TextView(aio); t.setText(foot); pollInnerWrapper.addView(t); } } else { // poll has not been voted in final String action = "/boards/" + boardID + "/" + topicID; String key = pollElem.getElementsByAttributeValue("name", "key").attr("value"); LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, Theming.convertDPtoPX(aio, 1)); int x = 0; for (Element e : pollElem.getElementsByAttributeValue("name", "poll_vote")) { if (x > 0) { View v = new View(aio); v.setLayoutParams(lp); v.setBackgroundColor(Theming.colorPrimary()); pollInnerWrapper.addView(v); } x++; Button b = new Button(aio); b.setBackgroundDrawable(Theming.selectableItemBackground()); b.setText(e.nextElementSibling().text()); final HashMap<String, List<String>> data = new HashMap<String, List<String>>(); data.put("key", Collections.singletonList(key)); data.put("poll_vote", Collections.singletonList(Integer.toString(x))); data.put("submit", Collections.singletonList("Vote")); b.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { aio.getSession().post(NetDesc.TOPIC, action, data); } }); pollInnerWrapper.addView(b); } View v = new View(aio); v.setLayoutParams(lp); v.setBackgroundColor(Theming.colorPrimary()); pollInnerWrapper.addView(v); Button b = new Button(aio); b.setBackgroundDrawable(Theming.selectableItemBackground()); b.setText(R.string.view_results); b.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { aio.getSession().get(NetDesc.TOPIC, action + "?results=1"); } }); pollInnerWrapper.addView(b); } // remove the poll element so it doesn't get put in unprocessedMessageText messageIn.getElementsByClass("board_poll").first().remove(); } Elements images = messageIn.select("a.img_container"); for (Element i : images) { i.html(i.attr("href")); } Elements videos = messageIn.select("div.vid_container"); for (Element v : videos) { String type = v.child(0).attr("class"); String data = v.child(0).attr("data-id"); String url; switch (type) { case "yt_player": url = "https://www.youtube.com/watch?v=" + data; break; default: url = "GameRaven error: Video source not recognized. Source: " + type + ", Data: " + data; } v.html(url); } unprocessedMessageText = messageIn.html() + sigHtml; if (BuildConfig.DEBUG) AllInOneV2.wtl("creating ssb"); SpannableStringBuilder ssb = new SpannableStringBuilder(processContent(false, true)); if (BuildConfig.DEBUG) AllInOneV2.wtl("adding bold spans"); addGenericSpans(ssb, "<b>", "</b>", new StyleSpan(Typeface.BOLD)); if (BuildConfig.DEBUG) AllInOneV2.wtl("adding italic spans"); addGenericSpans(ssb, "<i>", "</i>", new StyleSpan(Typeface.ITALIC)); if (BuildConfig.DEBUG) AllInOneV2.wtl("adding code spans"); addGenericSpans(ssb, "<code>", "</code>", new TypefaceSpan("monospace")); if (BuildConfig.DEBUG) AllInOneV2.wtl("adding cite spans"); addGenericSpans(ssb, "<cite>", "</cite>", new UnderlineSpan(), new StyleSpan(Typeface.ITALIC)); if (BuildConfig.DEBUG) AllInOneV2.wtl("adding quote spans"); addQuoteSpans(ssb); ssb.append('\n'); if (BuildConfig.DEBUG) AllInOneV2.wtl("linkifying"); MyLinkifier.addLinks(ssb, Linkify.WEB_URLS); if (BuildConfig.DEBUG) AllInOneV2.wtl("adding spoiler spans"); addSpoilerSpans(ssb); if (BuildConfig.DEBUG) AllInOneV2.wtl("replacing < with <"); while (ssb.toString().contains("<")) { int start = ssb.toString().indexOf("<"); ssb.replace(start, start + "<".length(), "<"); } if (BuildConfig.DEBUG) AllInOneV2.wtl("replacing > with >"); while (ssb.toString().contains(">")) { int start = ssb.toString().indexOf(">"); ssb.replace(start, start + ">".length(), ">"); } if (BuildConfig.DEBUG) AllInOneV2.wtl("setting spannedMessage"); spannedMessage = ssb; } public boolean isEdited() { return userTitles != null && userTitles.contains("(edited)"); } private static void addGenericSpans(SpannableStringBuilder ssb, String tag, String endTag, CharacterStyle... cs) { // initialize array int[] startEnd = spanStartAndEnd(ssb.toString(), tag, endTag); // while start and end points are found... while (!Arrays.equals(startEnd, noStartEndBase)) { // remove the start tag ssb.delete(startEnd[0], startEnd[0] + tag.length()); // adjust end point for removed start tag startEnd[1] -= tag.length(); // remove end tag ssb.delete(startEnd[1], startEnd[1] + endTag.length()); // apply styles for (CharacterStyle c : cs) ssb.setSpan(CharacterStyle.wrap(c), startEnd[0], startEnd[1], 0); // get new start and end points startEnd = spanStartAndEnd(ssb.toString(), tag, endTag); } } public static final String QUOTE_START = "<blockquote>"; public static final String QUOTE_END = "</blockquote>"; private static void addQuoteSpans(SpannableStringBuilder ssb) { // initialize array int[] startEnd = spanStartAndEnd(ssb.toString(), QUOTE_START, QUOTE_END); // while start and end points are found... while (!Arrays.equals(startEnd, noStartEndBase)) { // replace the start tag ssb.replace(startEnd[0], startEnd[0] + QUOTE_START.length(), "\n"); startEnd[0]++; // adjust end point for replaced start tag startEnd[1] -= QUOTE_START.length() - 1; // remove end tag ssb.replace(startEnd[1], startEnd[1] + QUOTE_END.length(), "\n"); // apply style ssb.setSpan(new GRQuoteSpan(), startEnd[0], startEnd[1], 0); // get new start and end points startEnd = spanStartAndEnd(ssb.toString(), QUOTE_START, QUOTE_END); } } public static final String SPOILER_START = "<s>"; public static final String SPOILER_END = "</s>"; private void addSpoilerSpans(SpannableStringBuilder ssb) { // initialize array int[] startEnd = spanStartAndEnd(ssb.toString(), SPOILER_START, SPOILER_END); // while start and end points are found... while (!Arrays.equals(startEnd, noStartEndBase)) { // remove the start tag ssb.delete(startEnd[0], startEnd[0] + SPOILER_START.length()); // adjust end point for removed start tag startEnd[1] -= SPOILER_START.length(); // remove end tag ssb.delete(startEnd[1], startEnd[1] + SPOILER_END.length()); // apply styles SpoilerBackgroundSpan spoiler = new SpoilerBackgroundSpan(Theming.colorHiddenSpoiler(), Theming.colorRevealedSpoiler()); SpoilerClickSpan spoilerClick = new SpoilerClickSpan(spoiler); ssb.setSpan(spoiler, startEnd[0], startEnd[1], 0); ssb.setSpan(spoilerClick, startEnd[0], startEnd[1], 0); // get new start and end points startEnd = spanStartAndEnd(ssb.toString(), SPOILER_START, SPOILER_END); } } private static int[] noStartEndBase = {-1, -1}; private static int[] spanStartAndEnd(String text, String openTag, String closeTag) { int start = -1; int end = -1; if (text.contains(openTag) && text.contains(closeTag)) { start = text.indexOf(openTag); end = text.indexOf(closeTag); int stackCount = 1; int closer; int opener; int innerStartPoint = start; do { opener = text.indexOf(openTag, innerStartPoint + 1); closer = text.indexOf(closeTag, innerStartPoint + 1); if (opener != -1 && opener < closer) { // found a nested tag stackCount++; innerStartPoint = opener; } else { // this closer is the right one stackCount--; innerStartPoint = closer; } } while (stackCount > 0); if (closer != -1) end = closer; } return new int[]{start, end}; } public String getMessageForQuoting() { return processContent(true, false); } public String getMessageForEditing() { return processContent(true, false); } private String processContent(boolean removeSig, boolean ignoreLtGt) { String finalBody = unprocessedMessageText; if (BuildConfig.DEBUG) AllInOneV2.wtl("beginning opening anchor tag removal"); while (finalBody.contains("<a ")) { int start = finalBody.indexOf("<a "); int end = finalBody.indexOf(">", start) + 1; finalBody = finalBody.replace(finalBody.substring(start, end), ""); } if (BuildConfig.DEBUG) AllInOneV2.wtl("removing vid container divs"); while (finalBody.contains("<div class=\"vid_container\">")) { int start = finalBody.indexOf("<div class=\"vid_container\">"); int end = finalBody.indexOf(">", start) + 1; finalBody = finalBody.replace(finalBody.substring(start, end), ""); start = finalBody.indexOf("</div>", end); end = start + 6; finalBody = finalBody.replace(finalBody.substring(start, end), ""); } if (BuildConfig.DEBUG) AllInOneV2.wtl("removing closing anchor tags"); finalBody = finalBody.replace("</a>", ""); if (BuildConfig.DEBUG) AllInOneV2.wtl("removing existing \\n, replacing linebreak tags with \\n"); if (finalBody.endsWith("<br />")) finalBody = finalBody.substring(0, finalBody.length() - 6); finalBody = finalBody.replace("\n", ""); finalBody = finalBody.replace("<br />", "\n"); if (removeSig) { if (BuildConfig.DEBUG) AllInOneV2.wtl("removing sig"); int sigStart = finalBody.lastIndexOf("\n---\n"); if (sigStart != -1) finalBody = finalBody.substring(0, sigStart); } if (ignoreLtGt) { if (BuildConfig.DEBUG) AllInOneV2.wtl("ignoring < / >, pre-unescape"); finalBody = finalBody.replace("<", "&gameravenlt;").replace(">", "&gameravengt;"); } if (BuildConfig.DEBUG) AllInOneV2.wtl("unescaping finalbody html"); finalBody = StringEscapeUtils.unescapeHtml4(finalBody); if (ignoreLtGt) { if (BuildConfig.DEBUG) AllInOneV2.wtl("ignoring < / >, post-unescape"); finalBody = finalBody.replace("&gameravenlt;", "<").replace("&gameravengt;", ">"); } if (BuildConfig.DEBUG) AllInOneV2.wtl("returning finalbody"); return finalBody; } }