/* * Copyright 2010 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.facebook.stream; import java.text.ParsePosition; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashSet; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import android.util.Log; /** * Contains logic for converting a JSONObject obtained from * querying /me/home to a HTML string that can be rendered * in WebKit. * * @author yariv */ class StreamRenderer { private StringBuilder sb; /** * The main function for rendering the stream JSONObject. * * @param data * @return */ public static String render(JSONObject data) { StreamRenderer renderer = new StreamRenderer(); return renderer.doRender(data); } /** * Renders the HTML for a single post. * * @param post * @return * @throws JSONException */ public static String renderSinglePost(JSONObject post) throws JSONException { StreamRenderer renderer = new StreamRenderer(); renderer.renderPost(post); return renderer.getResult(); } /** * Renders the HTML for a single comment. * * @param comment * @return */ public static String renderSingleComment(JSONObject comment) { StreamRenderer renderer = new StreamRenderer(); renderer.renderComment(comment); return renderer.getResult(); } private StreamRenderer() { this.sb = new StringBuilder(); } /** * Returns a SimpleDateFormat object we use for * parsing and rendering timestamps. * * @return */ public static SimpleDateFormat getDateFormat() { return new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ssZ"); } /** * Returns the result html. * * @return */ private String getResult() { return sb.toString(); } private String doRender(JSONObject data) { try { JSONArray posts = data.getJSONArray("data"); String[] chunks = { "<html><head>", "<link rel=\"stylesheet\" " + "href=\"file:///android_asset/stream.css\" type=\"text/css\">", "<script src=\"file:///android_asset/stream.js\"></script>", "</head>", "<body>", "<div id=\"header\">" }; append(chunks); renderLink("app://logout", "logout"); renderStatusBox(); append("<div id=\"posts\">"); for (int i = 0; i < posts.length(); i++) { renderPost(posts.getJSONObject(i)); } append("</div></body></html>"); return getResult(); } catch (JSONException e) { // TODO Auto-generated catch block e.printStackTrace(); return ""; } } /** * Renders the "what's on your mind?" box and the Share button. */ private void renderStatusBox() { String[] chunks = new String[] { "</div><div class=\"clear\"></div>", "<div id=\"status_box\">", "<input id=\"status_input\" value=\" What's on your mind?\"", " onfocus=\"onStatusBoxFocus(this);\"/>", "<button id=\"status_submit\" class=\"hidden\" " + "onclick=\"updateStatus();\">Share</button>", "<div class=\"clear\"></div>", "</div>" }; append(chunks); } /** * Renders a single post * * @param post * @throws JSONException */ private void renderPost(JSONObject post) throws JSONException { append("<div class=\"post\">"); renderFrom(post); renderTo(post); renderMessage(post); renderAttachment(post); renderActionLinks(post); renderLikes(post); renderComments(post); renderCommentBox(post); append("</div>"); } /** * Renders the author's name * * @param post * @throws JSONException */ private void renderFrom(JSONObject post) throws JSONException { JSONObject from = post.getJSONObject("from"); String fromName = from.getString("name"); String fromId = from.getString("id"); renderAuthor(fromId, fromName); } /** * If it's a wall post on a friend's fall, renders * the recipient's name preceded by a '>'. * * @param post * @throws JSONException */ private void renderTo(JSONObject post) throws JSONException { JSONObject to = post.optJSONObject("to"); if (to != null) { JSONObject toData = to.getJSONArray("data").getJSONObject(0); String toName = toData.getString("name"); String toId = toData.getString("id"); append(" > "); renderProfileLink(toId, toName); } } /** * Renders a link to a user. * * @param id * @param name */ private void renderProfileLink(String id, String name) { renderLink(getProfileUrl(id), name); } private String getProfileUrl(String id) { return "http://touch.facebook.com/#/profile.php?id=" + id; } /** * Renders the author pic and name. * * @param id * @param name */ private void renderAuthor(String id, String name) { String[] chunks = { "<div class=\"profile_pic_container\">", "<a href=\"", getProfileUrl(id), "\"><img class=\"profile_pic\" src=\"http://graph.facebook.com/", id, "/picture\"/></a>", "</div>" }; append(chunks); renderProfileLink(id, name); } /** * Renders the post message. * * @param post */ private void renderMessage(JSONObject post) { String message = post.optString("message"); String[] chunks = { " <span class=\"msg\">", message, "</span>", "<div class=\"clear\"></div>" }; append(chunks); } /** * Renders the attachment. * * @param post */ private void renderAttachment(JSONObject post) { String name = post.optString("name"); String link = post.optString("link"); String picture = post.optString("picture"); String source = post.optString("source"); // for videos String caption = post.optString("caption"); String description = post.optString("description"); String[] fields = new String[] { name, link, picture, source, caption, description }; boolean hasAttachment = false; for (String field : fields) { if (field.length() != 0) { hasAttachment = true; break; } } if (!hasAttachment) { return; } append("<div class=\"attachment\">"); if (name != "") { append("<div class=\"title\">"); if (link != null) { renderLink(link, name); } else { append(name); } append("</div>"); } if (caption != "") { append("<div class=\"caption\">" + caption + "</div>"); } if (picture != "") { append("<div class=\"picture\">"); String img = "<img src=\"" + picture + "\"/>"; if (link != "") { renderLink(link, img); } else { append(img); } append("</div>"); } if (description != "") { append("<div class=\"description\">" + description + "</div>"); } append("<div class=\"clear\"></div></div>"); } /** * Renders an anchor tag * * @param href * @param text */ private void renderLink(String href, String text) { append(new String[] { "<a href=\"", href, "\">", text, "</a>" }); } /** * Renders the posts' action links. * * @param post */ private void renderActionLinks(JSONObject post) { HashSet<String> actions = getActions(post); append("<div class=\"action_links\">"); append("<div class=\"action_link\">"); renderTimeStamp(post); append("</div>"); String post_id = post.optString("id"); if (actions.contains("Comment")) { renderActionLink(post_id, "Comment", "comment"); } boolean canLike = actions.contains("Like"); renderActionLink(post_id, "Like", "like", canLike); renderActionLink(post_id, "Unlike", "unlike", !canLike); append("<div class=\"clear\"></div></div>"); } /** * Renders a single visible action link. * * @param post_id * @param title * @param func */ private void renderActionLink(String post_id, String title, String func) { renderActionLink(post_id, title, func, true); } /** * Renders an action link with optional visibility. * * @param post_id * @param title * @param func * @param visible */ private void renderActionLink(String post_id, String title, String func, boolean visible) { String extraClass = visible ? "" : "hidden"; String[] chunks = new String[] { "<div id=\"", func, post_id, "\" class=\"action_link ", extraClass, "\">", "<a href=\"#\" onclick=\"",func, "('", post_id, "'); return false;\">", title, "</a></div>" }; append(chunks); } /** * Renders the post's timestamp. * * @param post */ private void renderTimeStamp(JSONObject post) { String dateStr = post.optString("created_time"); SimpleDateFormat formatter = getDateFormat(); ParsePosition pos = new ParsePosition(0); long then = formatter.parse(dateStr, pos).getTime(); long now = new Date().getTime(); long seconds = (now - then)/1000; long minutes = seconds/60; long hours = minutes/60; long days = hours/24; String friendly = null; long num = 0; if (days > 0) { num = days; friendly = days + " day"; } else if (hours > 0) { num = hours; friendly = hours + " hour"; } else if (minutes > 0) { num = minutes; friendly = minutes + " minute"; } else { num = seconds; friendly = seconds + " second"; } if (num > 1) { friendly += "s"; } String[] chunks = new String[] { "<div class=\"timestamp\">", friendly, " ago", "</div>" }; append(chunks); } /** * Returns the available actions for the post. * * @param post * @return */ private HashSet<String> getActions(JSONObject post) { HashSet<String> actionsSet = new HashSet<String>(); JSONArray actions = post.optJSONArray("actions"); if (actions != null) { for (int j = 0; j < actions.length(); j++) { JSONObject action = actions.optJSONObject(j); String actionName = action.optString("name"); actionsSet.add(actionName); } } return actionsSet; } /** * Renders the 'x people like this' text, * * @param post */ private void renderLikes(JSONObject post) { int numLikes = post.optInt("likes", 0); if (numLikes > 0) { String desc = numLikes == 1 ? "person likes this" : "people like this"; String[] chunks = new String[] { "<div class=\"like_icon\">", "<img src=\"file:///android_asset/like_icon.png\"/>", "</div>", "<div class=\"num_likes\">", new Integer(numLikes).toString(), " ", desc, "</div>" }; append(chunks); } } /** * Renders the post's comments. * * @param post * @throws JSONException */ private void renderComments(JSONObject post) throws JSONException { append("<div class=\"comments\" id=\"comments" + post.optString("id") + "\">"); JSONObject comments = post.optJSONObject("comments"); if (comments != null) { JSONArray data = comments.optJSONArray("data"); if (data != null) { for (int j = 0; j < data.length(); j++) { JSONObject comment = data.getJSONObject(j); renderComment(comment); } } } append("</div>"); } /** * Renders an individual comment. * * @param comment */ private void renderComment(JSONObject comment) { JSONObject from = comment.optJSONObject("from"); if (from == null) { Log.w("StreamRenderer", "Comment missing from field: " + comment.toString()); } else { String authorId = from.optString("id"); String authorName = from.optString("name"); renderAuthor(authorId, authorName); } String message = comment.optString("message"); append("<div class=\"comment\">"); String[] chunks = { " ", message, "</div>" }; append(chunks); } /** * Renders the new comment input box. * * @param post */ private void renderCommentBox(JSONObject post) { String id = post.optString("id"); String[] chunks = new String[] { "<div class=\"comment_box\" id=\"comment_box", id, "\">", "<input id=\"comment_box_input", id, "\"/>", "<button onclick=\"postComment('", id , "');\">Post</button>", "<div class=\"clear\"></div>", "</div>" }; append(chunks); } private void append(String str) { sb.append(str); } private void append(String[] chunks) { for (String chunk : chunks) { sb.append(chunk); } } }