/**
* Copyright (c) 2012-2013 Nokia Corporation. All rights reserved.
* Nokia and Nokia Connecting People are registered trademarks of Nokia Corporation.
* Oracle and Java are trademarks or registered trademarks of Oracle and/or its
* affiliates. Other product and company names mentioned herein may be trademarks
* or trade names of their respective owners.
* See LICENSE.TXT for license information.
*/
package com.nokia.example.rlinks.view;
import com.nokia.example.rlinks.view.item.CommentItem;
import com.nokia.example.rlinks.Main;
import com.nokia.example.rlinks.model.CommentThing;
import com.nokia.example.rlinks.model.LinkThing;
import com.nokia.example.rlinks.model.Voteable;
import com.nokia.example.rlinks.network.operation.CommentsLoadOperation;
import com.nokia.example.rlinks.network.operation.CommentsLoadOperation.LoadCommentsListener;
import com.nokia.example.rlinks.network.HttpOperation;
import com.nokia.example.rlinks.network.operation.MoreCommentsLoadOperation;
import com.nokia.example.rlinks.view.CommentDetailsView.CommentDetailsBackListener;
import com.nokia.example.rlinks.view.item.LoaderItem;
import com.nokia.example.rlinks.view.item.CommentItem.CommentSelectionListener;
import com.nokia.example.rlinks.view.item.LinkItem;
import com.nokia.example.rlinks.view.item.LinkItem.LinkSelectionListener;
import com.nokia.example.rlinks.view.item.LinkMetadataItem;
import com.nokia.example.rlinks.view.item.MoreCommentsItem;
import com.nokia.example.rlinks.view.item.VoteItem;
import java.util.Vector;
import javax.microedition.io.ConnectionNotFoundException;
import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.Item;
/**
* View displaying comments for a Link.
*/
public class CommentsView
extends BaseFormView
implements LoadCommentsListener, CommentSelectionListener {
private static final int COMMENT_CHUNK_SIZE = 20;
private final Command commentCommand = new Command("Comment", "Comment/Vote", Command.SCREEN, 0);
private final LinkThing link;
private final BackCommandListener backListener;
private LinkItem linkItem;
private HttpOperation operation;
private int loadingItemIndex = -1;
/**
* Create a CommentsView.
*
* @param link Link item to show comments for
* @param backListener Listener signaling back commands to
*/
public CommentsView(LinkThing link, BackCommandListener backListener) {
super("Comments", new Item[]{});
this.link = link;
this.backListener = backListener;
setupCommands();
}
protected final void setupCommands() {
addCommand(backCommand);
addCommand(commentCommand);
addCommand(refreshCommand);
addCommand(aboutCommand);
}
public void show() {
// Update Login/Logout commands depending on current login status
setupLoginCommands();
if (size() > 0) {
setItem(get(0));
return;
}
addLoginStatusItem();
addMetaItems();
loadComments();
}
private void refresh() {
if (operation != null && !operation.isFinished()) {
return;
}
operation = null;
deleteAll();
show();
}
/**
* Adds the meta items:
*
* - related link item as the topmost item in the list view.
* - item for voting (when logged in)
* - item showing metadata (author, date)
*/
private void addMetaItems() {
append(createLinkItem());
append(new VoteItem(link, getWidth(), new VoteItem.VoteListener() {
public void voteSubmitted(int vote) {
link.setVote(vote);
linkItem.refresh();
}
}, this, this));
append(new LinkMetadataItem(link, getWidth(), this));
}
/**
* Create a Link item to be shown as the topmost item in the view.
*
* Tapping on the link will prompt to open the link URL in the
* platform browser.
*
* @return Link item
*/
private LinkItem createLinkItem() {
// A listener to invoke whenever the link gets selected; either by
// direct touch selection or by the selection command
final LinkSelectionListener selectionListener = new LinkSelectionListener() {
public void linkSelected(LinkThing link) {
try {
Main.getInstance().platformRequest(link.getUrl());
}
catch (ConnectionNotFoundException ex) {
System.out.println("Connection not found: " + ex.getMessage());
}
}
};
linkItem = new LinkItem(link, getWidth(), true, selectionListener, null, this);
return linkItem;
}
public void commandAction(Command command, Displayable displayable) {
if (command == commentCommand) {
openDetailsView(null);
}
else if (command == backCommand) {
abortLoadingComments();
backListener.backCommanded();
}
else if (command == refreshCommand) {
refresh();
}
else if (command == aboutCommand) {
showAboutView();
}
else if (command == loginCommand) {
showLoginView();
}
else if (command == logoutCommand) {
session.setLoggedOut();
setupCommands();
}
}
/**
* Load comments to be shown in the view.
*/
private void loadComments() {
if (link.getNumComments() == 0) {
return;
}
// Do not reload if already loaded, or if in process
if (HttpOperation.reloadNeeded(operation)) {
// Add 'Loading' item
loadingItemIndex = append(new LoaderItem());
operation = new CommentsLoadOperation(link.getId(), COMMENT_CHUNK_SIZE, this);
operation.start();
}
}
/**
* Abort any unfinished comment loading operations.
*/
private void abortLoadingComments() {
if (operation != null && !operation.isFinished()) {
operation.abort();
deleteAll();
}
}
/**
* Load more comments for the given comment.
*
* This solution aims to be scalable in that it:
* - only gets one new child item (and its children) at a time,
* - updates the list of child IDs for the original selection, and
* - creates a new 'load more replies' item with the indexes taken care of.
*
* When there are no more children to be loaded, a new 'Load more' item
* will not be added.
*
* @param comment Comment to get children for
* @param itemIndex Index of the 'Load more' item that was selected
*/
private void loadMoreComments(final CommentThing comment, final int itemIndex) {
final String[] childIds = comment.getChildIds();
final MoreCommentsItem moreItem = ((MoreCommentsItem) get(itemIndex));
moreItem.setLoading();
LoadCommentsListener listener = new LoadCommentsListener() {
public synchronized void commentsReceived(final Vector comments) {
// First, create custom list items from the comment items
final CommentItem[] commentItems = createCommentItems(comments);
// Update the view in the main UI thread
Runnable updateView = new Runnable() {
public void run() {
// Remove the original "Load more comments" item
if (get(itemIndex) == moreItem) {
delete(itemIndex);
}
// Insert items
int lastIndex = insertCommentItems(commentItems, itemIndex);
// If there's more content to be loaded, create a new
// 'Load More Comments' item
if (childIds.length > 1) {
createMoreItem(comment, lastIndex + 1);
}
// Update indexes for items after the inserted items
for (int i = lastIndex, len = size(); i < len; i++) {
((CommentItem) get(i)).setItemIndex(i);
}
}
};
Main.executeInUIThread(updateView);
}
};
// Load comments for the first child only, add the rest as "more" items
operation = new MoreCommentsLoadOperation(
link.getId(), childIds[0], comment.getLevel(), COMMENT_CHUNK_SIZE, listener);
operation.start();
}
/**
* Create an array custom list items from a Vector of CommentThings.
*
* @param comments Vector of comment items
* @return Array of RedditCommentItem items
*/
private CommentItem[] createCommentItems(final Vector comments) {
int numComments = comments.size();
final CommentItem[] commentItems = new CommentItem[numComments];
final int width = getWidth();
CommentThing comment;
for (int i = 0; i < numComments; i++) {
comment = (CommentThing) comments.elementAt(i);
if (comment.getHiddenChildCount() > 0) {
commentItems[i] = new MoreCommentsItem(comment, this, width, this);
} else {
commentItems[i] = new CommentItem(comment, this, width, this);
}
}
return commentItems;
}
/**
* Insert given comment items at a specified index, maintaining the
* 'itemIndex' property of the items.
*
* @param commentItems Array of comment items to add
* @param startIndex Index to start adding at
* @return The index of the last item that was added
*/
private synchronized int insertCommentItems(final CommentItem[] commentItems, int startIndex) {
int idx = startIndex;
for (int i = 0, len = commentItems.length; i < len; i++) {
idx = startIndex + i;
commentItems[i].setItemIndex(idx);
insert(idx, commentItems[i]);
}
return idx;
}
/**
* Create a new 'Load more' item with the first child item ID removed.
*
* @param comment Comment to be updated
* @param index Index of the item
*/
private void createMoreItem(CommentThing comment, int index) {
String[] childIds = comment.getChildIds();
String[] newChildIds = new String[childIds.length - 1];
try {
System.arraycopy(childIds, 1, newChildIds, 0, childIds.length - 1);
}
catch (Exception e) {
System.out.println("Arraycopy failed: " + e.getMessage());
}
comment.setChildIds(newChildIds);
// Add a new More item
CommentItem more = new MoreCommentsItem(comment, this, getWidth(), this);
more.setItemIndex(index);
insert(index, more);
}
/**
* Process comments received.
*
* @param comments Vector of comments loaded
*/
public synchronized void commentsReceived(Vector comments) {
if (comments == null) {
Main.executeInUIThread(new Runnable() {
public void run() {
delete(loadingItemIndex);
loadingItemIndex = -1;
showNetworkError();
}
});
return;
}
// First, create custom list items from the comment items
final CommentItem[] commentItems = createCommentItems(comments);
comments = null;
// Then add the comment items to the view in the UI thread
Main.executeInUIThread(new Runnable() {
public void run() {
// Remove 'Loading' item
delete(loadingItemIndex);
insertCommentItems(commentItems, size());
}
});
}
/**
* Act on a comment (or "load more replies" item) being selected.
*
* @param comment Comment selected
* @param itemIndex Its index in the list
*/
public void commentSelected(final CommentThing comment, final int itemIndex) {
final boolean operationPending =
operation != null && !operation.isFinished();
// Only allow one operation at a time
if (operationPending) {
return;
}
else if (comment.getHiddenChildCount() > 0) {
loadMoreComments(comment, itemIndex);
}
// In case an ordinary Comment is selected, open up comment details view
else {
openDetailsView(comment);
}
}
/**
* Show detalis view for a given comment.
*
* @param comment Comment to show details for
*/
private void openDetailsView(Voteable comment) {
// When writing a top-level comment, the Thing replied to is the link.
// When replying to comment, the Thing is the parent comment itself.
final Voteable item = comment != null ? comment : link;
CommentDetailsView cv = new CommentDetailsView(
item,
new CommentDetailsBackListener() {
public void backCommanded(boolean commentAdded) {
// New comment added, refreshing
if (commentAdded) {
link.setNumComments(link.getNumComments() + 1);
refresh();
}
setDisplay(self);
}
},
new VoteItem.VoteListener() {
public void voteSubmitted(int vote) {
item.setVote(vote);
}
}
);
setDisplay(cv);
cv.show();
}
}