/*
* 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);
}
}
}