SlideShare a Scribd company logo
Creating a Facebook Clone - Part XXXV
Next we’ll explore the comments UI
© Codename One 2017 all rights reserved
I've simplified the comments UI a bit mostly to keep the code short and understandable. I could support arbitrary depth threaded comments and various other
capabilities but this creates some usability issues eventually as the space narrows and it becomes harder to follow the discussion thread. So I limited the replies to top
level comments only and tried to simplify the UX a bit.

I also used pill shaped comments instead of round rectangles. They just looked nice and I left it at that.
public class CommentsForm extends Form {
private TextField commentField = new TextField();
private Container comments = new Container(BoxLayout.y());
private String replyCommentId;
public CommentsForm(Post p, Comment replyingTo) {
super(p.title.get(), new BorderLayout());
commentField.getAllStyles().setBorder(Border.createEmpty());
Button send = new Button("", "Label");
Container post = BorderLayout.centerEastWest(commentField, send,
null);
setMaterialIcon(send, MATERIAL_SEND);
replyCommentId = replyingTo == null ? null : replyingTo.id.get();
send.addActionListener(e -> postComment(p));
commentField.setDoneListener(e -> postComment(p));
Form previous = getCurrentForm();
getToolbar().addMaterialCommandToLeftBar("",
MATERIAL_ARROW_BACK, e -> previous.showBack());
for(Comment cmt : p.comments) {
addComment(cmt);
}
CommentsForm
Lets check out the code…

This is the text field where the user can type in his own comment to submit
public class CommentsForm extends Form {
private TextField commentField = new TextField();
private Container comments = new Container(BoxLayout.y());
private String replyCommentId;
public CommentsForm(Post p, Comment replyingTo) {
super(p.title.get(), new BorderLayout());
commentField.getAllStyles().setBorder(Border.createEmpty());
Button send = new Button("", "Label");
Container post = BorderLayout.centerEastWest(commentField, send,
null);
setMaterialIcon(send, MATERIAL_SEND);
replyCommentId = replyingTo == null ? null : replyingTo.id.get();
send.addActionListener(e -> postComment(p));
commentField.setDoneListener(e -> postComment(p));
Form previous = getCurrentForm();
getToolbar().addMaterialCommandToLeftBar("",
MATERIAL_ARROW_BACK, e -> previous.showBack());
for(Comment cmt : p.comments) {
addComment(cmt);
}
CommentsForm
Comments are placed in a scrollable box layout container in the center of the form
public class CommentsForm extends Form {
private TextField commentField = new TextField();
private Container comments = new Container(BoxLayout.y());
private String replyCommentId;
public CommentsForm(Post p, Comment replyingTo) {
super(p.title.get(), new BorderLayout());
commentField.getAllStyles().setBorder(Border.createEmpty());
Button send = new Button("", "Label");
Container post = BorderLayout.centerEastWest(commentField, send,
null);
setMaterialIcon(send, MATERIAL_SEND);
replyCommentId = replyingTo == null ? null : replyingTo.id.get();
send.addActionListener(e -> postComment(p));
commentField.setDoneListener(e -> postComment(p));
Form previous = getCurrentForm();
getToolbar().addMaterialCommandToLeftBar("",
MATERIAL_ARROW_BACK, e -> previous.showBack());
for(Comment cmt : p.comments) {
addComment(cmt);
}
CommentsForm
When we press reply to a specific comment its ID is placed here...
public class CommentsForm extends Form {
private TextField commentField = new TextField();
private Container comments = new Container(BoxLayout.y());
private String replyCommentId;
public CommentsForm(Post p, Comment replyingTo) {
super(p.title.get(), new BorderLayout());
commentField.getAllStyles().setBorder(Border.createEmpty());
Button send = new Button("", "Label");
Container post = BorderLayout.centerEastWest(commentField, send,
null);
setMaterialIcon(send, MATERIAL_SEND);
replyCommentId = replyingTo == null ? null : replyingTo.id.get();
send.addActionListener(e -> postComment(p));
commentField.setDoneListener(e -> postComment(p));
Form previous = getCurrentForm();
getToolbar().addMaterialCommandToLeftBar("",
MATERIAL_ARROW_BACK, e -> previous.showBack());
for(Comment cmt : p.comments) {
addComment(cmt);
}
CommentsForm
The BorderLayout is needed so we can place the text field input in the bottom of the form
public class CommentsForm extends Form {
private TextField commentField = new TextField();
private Container comments = new Container(BoxLayout.y());
private String replyCommentId;
public CommentsForm(Post p, Comment replyingTo) {
super(p.title.get(), new BorderLayout());
commentField.getAllStyles().setBorder(Border.createEmpty());
Button send = new Button("", "Label");
Container post = BorderLayout.centerEastWest(commentField, send,
null);
setMaterialIcon(send, MATERIAL_SEND);
replyCommentId = replyingTo == null ? null : replyingTo.id.get();
send.addActionListener(e -> postComment(p));
commentField.setDoneListener(e -> postComment(p));
Form previous = getCurrentForm();
getToolbar().addMaterialCommandToLeftBar("",
MATERIAL_ARROW_BACK, e -> previous.showBack());
for(Comment cmt : p.comments) {
addComment(cmt);
}
CommentsForm
We don't add the text field directly though, we also add a send button with it by wrapping them both into a border layout together
public class CommentsForm extends Form {
private TextField commentField = new TextField();
private Container comments = new Container(BoxLayout.y());
private String replyCommentId;
public CommentsForm(Post p, Comment replyingTo) {
super(p.title.get(), new BorderLayout());
commentField.getAllStyles().setBorder(Border.createEmpty());
Button send = new Button("", "Label");
Container post = BorderLayout.centerEastWest(commentField, send,
null);
setMaterialIcon(send, MATERIAL_SEND);
replyCommentId = replyingTo == null ? null : replyingTo.id.get();
send.addActionListener(e -> postComment(p));
commentField.setDoneListener(e -> postComment(p));
Form previous = getCurrentForm();
getToolbar().addMaterialCommandToLeftBar("",
MATERIAL_ARROW_BACK, e -> previous.showBack());
for(Comment cmt : p.comments) {
addComment(cmt);
}
CommentsForm
I used static import of `FontImage` so this line will work correctly
public class CommentsForm extends Form {
private TextField commentField = new TextField();
private Container comments = new Container(BoxLayout.y());
private String replyCommentId;
public CommentsForm(Post p, Comment replyingTo) {
super(p.title.get(), new BorderLayout());
commentField.getAllStyles().setBorder(Border.createEmpty());
Button send = new Button("", "Label");
Container post = BorderLayout.centerEastWest(commentField, send,
null);
setMaterialIcon(send, MATERIAL_SEND);
replyCommentId = replyingTo == null ? null : replyingTo.id.get();
send.addActionListener(e -> postComment(p));
commentField.setDoneListener(e -> postComment(p));
Form previous = getCurrentForm();
getToolbar().addMaterialCommandToLeftBar("",
MATERIAL_ARROW_BACK, e -> previous.showBack());
for(Comment cmt : p.comments) {
addComment(cmt);
}
CommentsForm
If the send button or the done button in the virtual keyboard is pressed we post the comment currently within the text field
setMaterialIcon(send, MATERIAL_SEND);
replyCommentId = replyingTo == null ? null : replyingTo.id.get();
send.addActionListener(e -> postComment(p));
commentField.setDoneListener(e -> postComment(p));
Form previous = getCurrentForm();
getToolbar().addMaterialCommandToLeftBar("",
MATERIAL_ARROW_BACK, e -> previous.showBack());
for(Comment cmt : p.comments) {
addComment(cmt);
}
add(SOUTH, post);
add(CENTER, comments);
}
private void postComment(Post p) {
Dialog ip = new InfiniteProgress().showInifiniteBlocking();
Comment cm = new Comment().
postId.set(p.id.get()).
userId.set(ServerAPI.me().id.get()).
parentComment.set(replyCommentId).
text.set(commentField.getText());
if(!ServerAPI.comment(cm)) {
ip.dispose();
ToastBar.showErrorMessage("Failed to post comment!");
return;
CommentsForm
This adds the existing comments to the UI
}
add(SOUTH, post);
add(CENTER, comments);
}
private void postComment(Post p) {
Dialog ip = new InfiniteProgress().showInifiniteBlocking();
Comment cm = new Comment().
postId.set(p.id.get()).
userId.set(ServerAPI.me().id.get()).
parentComment.set(replyCommentId).
text.set(commentField.getText());
if(!ServerAPI.comment(cm)) {
ip.dispose();
ToastBar.showErrorMessage("Failed to post comment!");
return;
}
ip.dispose();
addComment(cm);
commentField.setText("");
animateLayout(150);
}
private void addComment(Comment cm) {
Component c = createComment(cm);
CommentsForm
There were a few methods mentioned in this code that we need to go over specifically postComment, addComment. Lets start with the former
}
add(SOUTH, post);
add(CENTER, comments);
}
private void postComment(Post p) {
Dialog ip = new InfiniteProgress().showInifiniteBlocking();
Comment cm = new Comment().
postId.set(p.id.get()).
userId.set(ServerAPI.me().id.get()).
parentComment.set(replyCommentId).
text.set(commentField.getText());
if(!ServerAPI.comment(cm)) {
ip.dispose();
ToastBar.showErrorMessage("Failed to post comment!");
return;
}
ip.dispose();
addComment(cm);
commentField.setText("");
animateLayout(150);
}
private void addComment(Comment cm) {
Component c = createComment(cm);
CommentsForm
We connect to the server so we need to show a blocking UI dialog
}
add(SOUTH, post);
add(CENTER, comments);
}
private void postComment(Post p) {
Dialog ip = new InfiniteProgress().showInifiniteBlocking();
Comment cm = new Comment().
postId.set(p.id.get()).
userId.set(ServerAPI.me().id.get()).
parentComment.set(replyCommentId).
text.set(commentField.getText());
if(!ServerAPI.comment(cm)) {
ip.dispose();
ToastBar.showErrorMessage("Failed to post comment!");
return;
}
ip.dispose();
addComment(cm);
commentField.setText("");
animateLayout(150);
}
private void addComment(Comment cm) {
Component c = createComment(cm);
CommentsForm
We construct a comment object and set the values for it, notice we don't need the comment ID as that's generated in the server
}
add(SOUTH, post);
add(CENTER, comments);
}
private void postComment(Post p) {
Dialog ip = new InfiniteProgress().showInifiniteBlocking();
Comment cm = new Comment().
postId.set(p.id.get()).
userId.set(ServerAPI.me().id.get()).
parentComment.set(replyCommentId).
text.set(commentField.getText());
if(!ServerAPI.comment(cm)) {
ip.dispose();
ToastBar.showErrorMessage("Failed to post comment!");
return;
}
ip.dispose();
addComment(cm);
commentField.setText("");
animateLayout(150);
}
private void addComment(Comment cm) {
Component c = createComment(cm);
CommentsForm
The ServerAPI does the heavy lifting of submitting the comment
}
add(SOUTH, post);
add(CENTER, comments);
}
private void postComment(Post p) {
Dialog ip = new InfiniteProgress().showInifiniteBlocking();
Comment cm = new Comment().
postId.set(p.id.get()).
userId.set(ServerAPI.me().id.get()).
parentComment.set(replyCommentId).
text.set(commentField.getText());
if(!ServerAPI.comment(cm)) {
ip.dispose();
ToastBar.showErrorMessage("Failed to post comment!");
return;
}
ip.dispose();
addComment(cm);
commentField.setText("");
animateLayout(150);
}
private void addComment(Comment cm) {
Component c = createComment(cm);
CommentsForm
When we are done we need to explicitly clear the text field otherwise the submitted comment would still be there, we animate the UI to valid state after the addition of the
comment
commentField.setText("");
animateLayout(150);
}
private void addComment(Comment cm) {
Component c = createComment(cm);
if(cm.parentComment.get() != null) {
Component parent = findParentComment(cm.parentComment.get());
if(parent != null) {
Container chld = (Container)parent.
getClientProperty("child");
if(chld == null) {
chld = BoxLayout.encloseY(c);
chld.getAllStyles().setPaddingLeft(convertToPixels(3));
parent.putClientProperty("child", chld);
int pos = comments.getComponentIndex(parent);
comments.addComponent(pos + 1, chld);
} else {
chld.add(c);
}
} else {
comments.add(c);
}
} else {
comments.add(c);
CommentsForm
This naturally leads us to the addComment method
commentField.setText("");
animateLayout(150);
}
private void addComment(Comment cm) {
Component c = createComment(cm);
if(cm.parentComment.get() != null) {
Component parent = findParentComment(cm.parentComment.get());
if(parent != null) {
Container chld = (Container)parent.
getClientProperty("child");
if(chld == null) {
chld = BoxLayout.encloseY(c);
chld.getAllStyles().setPaddingLeft(convertToPixels(3));
parent.putClientProperty("child", chld);
int pos = comments.getComponentIndex(parent);
comments.addComponent(pos + 1, chld);
} else {
chld.add(c);
}
} else {
comments.add(c);
}
} else {
comments.add(c);
CommentsForm
createComment() creates the visual representation of a comment, we'll discuss it next
}
private void addComment(Comment cm) {
Component c = createComment(cm);
if(cm.parentComment.get() != null) {
Component parent = findParentComment(cm.parentComment.get());
if(parent != null) {
Container chld = (Container)parent.
getClientProperty("child");
if(chld == null) {
chld = BoxLayout.encloseY(c);
chld.getAllStyles().setPaddingLeft(convertToPixels(3));
parent.putClientProperty("child", chld);
int pos = comments.getComponentIndex(parent);
comments.addComponent(pos + 1, chld);
} else {
chld.add(c);
}
} else {
comments.add(c);
}
} else {
comments.add(c);
}
}
CommentsForm
If the comment doesn't have a parent
}
private void addComment(Comment cm) {
Component c = createComment(cm);
if(cm.parentComment.get() != null) {
Component parent = findParentComment(cm.parentComment.get());
if(parent != null) {
Container chld = (Container)parent.
getClientProperty("child");
if(chld == null) {
chld = BoxLayout.encloseY(c);
chld.getAllStyles().setPaddingLeft(convertToPixels(3));
parent.putClientProperty("child", chld);
int pos = comments.getComponentIndex(parent);
comments.addComponent(pos + 1, chld);
} else {
chld.add(c);
}
} else {
comments.add(c);
}
} else {
comments.add(c);
}
}
CommentsForm
we'll just add it directly to the list of comments
}
private void addComment(Comment cm) {
Component c = createComment(cm);
if(cm.parentComment.get() != null) {
Component parent = findParentComment(cm.parentComment.get());
if(parent != null) {
Container chld = (Container)parent.
getClientProperty("child");
if(chld == null) {
chld = BoxLayout.encloseY(c);
chld.getAllStyles().setPaddingLeft(convertToPixels(3));
parent.putClientProperty("child", chld);
int pos = comments.getComponentIndex(parent);
comments.addComponent(pos + 1, chld);
} else {
chld.add(c);
}
} else {
comments.add(c);
}
} else {
comments.add(c);
}
}
CommentsForm
We search through the hierarchy to find the parent comment component, assuming we find it we can nest the component
}
private void addComment(Comment cm) {
Component c = createComment(cm);
if(cm.parentComment.get() != null) {
Component parent = findParentComment(cm.parentComment.get());
if(parent != null) {
Container chld = (Container)parent.
getClientProperty("child");
if(chld == null) {
chld = BoxLayout.encloseY(c);
chld.getAllStyles().setPaddingLeft(convertToPixels(3));
parent.putClientProperty("child", chld);
int pos = comments.getComponentIndex(parent);
comments.addComponent(pos + 1, chld);
} else {
chld.add(c);
}
} else {
comments.add(c);
}
} else {
comments.add(c);
}
}
CommentsForm
Every parent comment component has a `Container` for replies (children) that's nested into it
}
private void addComment(Comment cm) {
Component c = createComment(cm);
if(cm.parentComment.get() != null) {
Component parent = findParentComment(cm.parentComment.get());
if(parent != null) {
Container chld = (Container)parent.
getClientProperty("child");
if(chld == null) {
chld = BoxLayout.encloseY(c);
chld.getAllStyles().setPaddingLeft(convertToPixels(3));
parent.putClientProperty("child", chld);
int pos = comments.getComponentIndex(parent);
comments.addComponent(pos + 1, chld);
} else {
chld.add(c);
}
} else {
comments.add(c);
}
} else {
comments.add(c);
}
}
CommentsForm
If the child Container isn't there yet we create it dynamically and add it
}
private void addComment(Comment cm) {
Component c = createComment(cm);
if(cm.parentComment.get() != null) {
Component parent = findParentComment(cm.parentComment.get());
if(parent != null) {
Container chld = (Container)parent.
getClientProperty("child");
if(chld == null) {
chld = BoxLayout.encloseY(c);
chld.getAllStyles().setPaddingLeft(convertToPixels(3));
parent.putClientProperty("child", chld);
int pos = comments.getComponentIndex(parent);
comments.addComponent(pos + 1, chld);
} else {
chld.add(c);
}
} else {
comments.add(c);
}
} else {
comments.add(c);
}
}
CommentsForm
Notice that we add to a specific offset within the Container so the comments appear below the parent
}
private void addComment(Comment cm) {
Component c = createComment(cm);
if(cm.parentComment.get() != null) {
Component parent = findParentComment(cm.parentComment.get());
if(parent != null) {
Container chld = (Container)parent.
getClientProperty("child");
if(chld == null) {
chld = BoxLayout.encloseY(c);
chld.getAllStyles().setPaddingLeft(convertToPixels(3));
parent.putClientProperty("child", chld);
int pos = comments.getComponentIndex(parent);
comments.addComponent(pos + 1, chld);
} else {
chld.add(c);
}
} else {
comments.add(c);
}
} else {
comments.add(c);
}
}
CommentsForm
If the child Container already exists we can add to it directly.

Notice that since comments are already sorted by time adding to the end will always work correctly and consistently. 

With that we effectively implemented one level of comment nesting. We can further generalize the nesting logic to allow additional or even infinite levels of nesting but as I
said earlier this might have usability issues.
}
private void addComment(Comment cm) {
Component c = createComment(cm);
if(cm.parentComment.get() != null) {
Component parent = findParentComment(cm.parentComment.get());
if(parent != null) {
Container chld = (Container)parent.
getClientProperty("child");
if(chld == null) {
chld = BoxLayout.encloseY(c);
chld.getAllStyles().setPaddingLeft(convertToPixels(3));
parent.putClientProperty("child", chld);
int pos = comments.getComponentIndex(parent);
comments.addComponent(pos + 1, chld);
} else {
chld.add(c);
}
} else {
comments.add(c);
}
} else {
comments.add(c);
}
}
CommentsForm
If the child Container already exists we can add to it directly.

Notice that since comments are already sorted by time adding to the end will always work correctly and consistently. 

With that we effectively implemented one level of comment nesting. We can further generalize the nesting logic to allow additional or even infinite levels of nesting but as I
said earlier this might have usability issues.
} else {
comments.add(c);
}
}
private Component createComment(Comment cm) {
TextArea c = new TextArea(cm.text.get());
c.setEditable(false);
c.setFocusable(false);
c.setUIID("Comment");
Label avatar = new Label(getAvatarImage(cm.userId.get()));
Container content = BorderLayout.centerEastWest(c, null, avatar);
if(cm.id.get() != null && cm.parentComment.get() == null) {
Button reply = new Button("reply", "SmallBlueLabel");
content.add(SOUTH, FlowLayout.encloseRight(reply));
reply.addActionListener(e -> replyCommentId = cm.id.get());
}
content.putClientProperty("comment", cm);
return content;
}
private Component findParentComment(String id) {
for(Component cmp : comments) {
Comment c = (Comment)cmp.getClientProperty("comment");
if(c != null && id.equals(c.id.get())) {
CommentsForm
Finally we need a few additional methods to implement the creation of the comment etc. createComment() generates the component that represents this given comment
& potentially the reply button
} else {
comments.add(c);
}
}
private Component createComment(Comment cm) {
TextArea c = new TextArea(cm.text.get());
c.setEditable(false);
c.setFocusable(false);
c.setUIID("Comment");
Label avatar = new Label(getAvatarImage(cm.userId.get()));
Container content = BorderLayout.centerEastWest(c, null, avatar);
if(cm.id.get() != null && cm.parentComment.get() == null) {
Button reply = new Button("reply", "SmallBlueLabel");
content.add(SOUTH, FlowLayout.encloseRight(reply));
reply.addActionListener(e -> replyCommentId = cm.id.get());
}
content.putClientProperty("comment", cm);
return content;
}
private Component findParentComment(String id) {
for(Component cmp : comments) {
Comment c = (Comment)cmp.getClientProperty("comment");
if(c != null && id.equals(c.id.get())) {
CommentsForm
The comment itself is just a text area with the Comment UIID, we make that text uneditable
} else {
comments.add(c);
}
}
private Component createComment(Comment cm) {
TextArea c = new TextArea(cm.text.get());
c.setEditable(false);
c.setFocusable(false);
c.setUIID("Comment");
Label avatar = new Label(getAvatarImage(cm.userId.get()));
Container content = BorderLayout.centerEastWest(c, null, avatar);
if(cm.id.get() != null && cm.parentComment.get() == null) {
Button reply = new Button("reply", "SmallBlueLabel");
content.add(SOUTH, FlowLayout.encloseRight(reply));
reply.addActionListener(e -> replyCommentId = cm.id.get());
}
content.putClientProperty("comment", cm);
return content;
}
private Component findParentComment(String id) {
for(Component cmp : comments) {
Comment c = (Comment)cmp.getClientProperty("comment");
if(c != null && id.equals(c.id.get())) {
CommentsForm
We fetch an avatar image for the chat, I could have used rounding and similar effects but I chose to keep the code simple
} else {
comments.add(c);
}
}
private Component createComment(Comment cm) {
TextArea c = new TextArea(cm.text.get());
c.setEditable(false);
c.setFocusable(false);
c.setUIID("Comment");
Label avatar = new Label(getAvatarImage(cm.userId.get()));
Container content = BorderLayout.centerEastWest(c, null, avatar);
if(cm.id.get() != null && cm.parentComment.get() == null) {
Button reply = new Button("reply", "SmallBlueLabel");
content.add(SOUTH, FlowLayout.encloseRight(reply));
reply.addActionListener(e -> replyCommentId = cm.id.get());
}
content.putClientProperty("comment", cm);
return content;
}
private Component findParentComment(String id) {
for(Component cmp : comments) {
Comment c = (Comment)cmp.getClientProperty("comment");
if(c != null && id.equals(c.id.get())) {
CommentsForm
If we reply to a specific comment we just change the id of the parent comment
} else {
comments.add(c);
}
}
private Component createComment(Comment cm) {
TextArea c = new TextArea(cm.text.get());
c.setEditable(false);
c.setFocusable(false);
c.setUIID("Comment");
Label avatar = new Label(getAvatarImage(cm.userId.get()));
Container content = BorderLayout.centerEastWest(c, null, avatar);
if(cm.id.get() != null && cm.parentComment.get() == null) {
Button reply = new Button("reply", "SmallBlueLabel");
content.add(SOUTH, FlowLayout.encloseRight(reply));
reply.addActionListener(e -> replyCommentId = cm.id.get());
}
content.putClientProperty("comment", cm);
return content;
}
private Component findParentComment(String id) {
for(Component cmp : comments) {
Comment c = (Comment)cmp.getClientProperty("comment");
if(c != null && id.equals(c.id.get())) {
CommentsForm
The Comment instance is stored in the components client properties so we can later connect them in findParentComment
content.add(SOUTH, FlowLayout.encloseRight(reply));
reply.addActionListener(e -> replyCommentId = cm.id.get());
}
content.putClientProperty("comment", cm);
return content;
}
private Component findParentComment(String id) {
for(Component cmp : comments) {
Comment c = (Comment)cmp.getClientProperty("comment");
if(c != null && id.equals(c.id.get())) {
return cmp;
}
}
return null;
}
private Image getAvatarImage(String userId) {
int size = convertToPixels(5);
return URLImage.createCachedImage(userId + "avatar",
ServerAPI.BASE_URL + "user/avatar/" + userId,
Image.createImage(size, size),
URLImage.FLAG_RESIZE_SCALE_TO_FILL);
}
}
CommentsForm
findParentComment loops through the container and finds the component matching this comment id
content.add(SOUTH, FlowLayout.encloseRight(reply));
reply.addActionListener(e -> replyCommentId = cm.id.get());
}
content.putClientProperty("comment", cm);
return content;
}
private Component findParentComment(String id) {
for(Component cmp : comments) {
Comment c = (Comment)cmp.getClientProperty("comment");
if(c != null && id.equals(c.id.get())) {
return cmp;
}
}
return null;
}
private Image getAvatarImage(String userId) {
int size = convertToPixels(5);
return URLImage.createCachedImage(userId + "avatar",
ServerAPI.BASE_URL + "user/avatar/" + userId,
Image.createImage(size, size),
URLImage.FLAG_RESIZE_SCALE_TO_FILL);
}
}
CommentsForm
If the comment we are looking at has the same id then we can return this component
content.add(SOUTH, FlowLayout.encloseRight(reply));
reply.addActionListener(e -> replyCommentId = cm.id.get());
}
content.putClientProperty("comment", cm);
return content;
}
private Component findParentComment(String id) {
for(Component cmp : comments) {
Comment c = (Comment)cmp.getClientProperty("comment");
if(c != null && id.equals(c.id.get())) {
return cmp;
}
}
return null;
}
private Image getAvatarImage(String userId) {
int size = convertToPixels(5);
return URLImage.createCachedImage(userId + "avatar",
ServerAPI.BASE_URL + "user/avatar/" + userId,
Image.createImage(size, size),
URLImage.FLAG_RESIZE_SCALE_TO_FILL);
}
}
CommentsForm
In this case the component matching this id doesn't exist or isn't there yet
content.add(SOUTH, FlowLayout.encloseRight(reply));
reply.addActionListener(e -> replyCommentId = cm.id.get());
}
content.putClientProperty("comment", cm);
return content;
}
private Component findParentComment(String id) {
for(Component cmp : comments) {
Comment c = (Comment)cmp.getClientProperty("comment");
if(c != null && id.equals(c.id.get())) {
return cmp;
}
}
return null;
}
private Image getAvatarImage(String userId) {
int size = convertToPixels(5);
return URLImage.createCachedImage(userId + "avatar",
ServerAPI.BASE_URL + "user/avatar/" + userId,
Image.createImage(size, size),
URLImage.FLAG_RESIZE_SCALE_TO_FILL);
}
}
CommentsForm
In this method we return an image matching the users avatar which is exposed via /user/avatar/userId on the server
font-family: "native:MainLight";
font-size: 2mm;
}
FloatingActionButton {
background: #587EBE;
padding: 1.5mm;
text-align: center;
}
Comment {
color: black;
font-family: "native:MainThin";
font-size: 2.6mm;
border: cn1-pill-border;
background-color: #dbdbdb;
padding: 1mm 3mm 1mm 3mm;
margin: 1mm;
}
SettingsMargin {
padding: 0px;
margin: 0px 0px 5mm 0px;
}
LabelFrame {
theme.css
With that CommentsForm is nearly done... We need a CSS style for Comment. It's black text over a light gray background in pill rounded shape. The one thing to notice
is the padding, we need a lot of it to keep the text away from the curve of the border.
body.setUIID(style);
} else {
body.setUIID("HalfPaddedContainer");
}
}
CheckBox like = CheckBox.createToggle("Like");
like.setUIID("CleanButton");
Button comment = new Button("Comment", "CleanButton");
Button share = new Button("Share", "CleanButton");
FontImage.setMaterialIcon(like, FontImage.MATERIAL_THUMB_UP);
FontImage.setMaterialIcon(comment,
FontImage.MATERIAL_COMMENT);
FontImage.setMaterialIcon(share, FontImage.MATERIAL_SHARE);
Container buttonBar = GridLayout.encloseIn(3, like, comment, share);
buttonBar.setUIID("HalfPaddedContainer");
like.setSelected(p.likes.contains(u));
like.addActionListener(e -> ServerAPI.like(p));
comment.addActionListener(e -> new CommentsForm(p, null).show());
return BoxLayout.encloseY(
titleArea, body, createPostStats(p), buttonBar);
}
NewsfeedContainer
Now CommentsForm should "just work" once we plug it in. To do that we need to go to createNewsItem in NewsfeedContainer. While we are here we might as well
implement the "like" functionality too.

We set the like toggle buttons selected state if the user already liked this post. We invoke like() on a click. This is already implemented in the ServerAPI so this is just one
call.

We add the action listener that invokes the comments form and that’s it. Like & comments should work

More Related Content

PDF
Creating a Facebook Clone - Part XXXV.pdf
PDF
Creating a Facebook Clone - Part VI - Transcript.pdf
PDF
Creating a Facebook Clone - Part XLI - Transcript.pdf
PDF
Creating a Facebook Clone - Part XXVIII - Transcript.pdf
PDF
Creating a Facebook Clone - Part XVI.pdf
PDF
Creating a Facebook Clone - Part XII.pdf
PDF
Creating a Facebook Clone - Part XXIX - Transcript.pdf
PDF
Creating a Facebook Clone - Part XVI - Transcript.pdf
Creating a Facebook Clone - Part XXXV.pdf
Creating a Facebook Clone - Part VI - Transcript.pdf
Creating a Facebook Clone - Part XLI - Transcript.pdf
Creating a Facebook Clone - Part XXVIII - Transcript.pdf
Creating a Facebook Clone - Part XVI.pdf
Creating a Facebook Clone - Part XII.pdf
Creating a Facebook Clone - Part XXIX - Transcript.pdf
Creating a Facebook Clone - Part XVI - Transcript.pdf

Similar to Creating a Facebook Clone - Part XXXV - Transcript.pdf (20)

PDF
Creating a Facebook Clone - Part VI.pdf
PDF
Creating a Facebook Clone - Part XLI.pdf
PDF
Creating a Facebook Clone - Part VII.pdf
PDF
Creating a Facebook Clone - Part VIII.pdf
PDF
Creating an Uber - Part VI - Transcript.pdf
PDF
Creating an Uber Clone - Part IX.pdf
PDF
Creating a Facebook Clone - Part IV.pdf
PDF
Creating an Uber Clone - Part XXXX.pdf
PDF
Creating a Facebook Clone - Part VIII - Transcript.pdf
PDF
Creating a Facebook Clone - Part XLII.pdf
PDF
Creating an Uber Clone - Part XXXX - Transcript.pdf
PDF
Extracting ui Design - part 6 - transcript.pdf
PDF
Creating an Uber Clone - Part V - Transcript.pdf
PPTX
Windows Form - Lec12 (Workshop on C# Programming: Learn to Build)
PDF
Creating a Facebook Clone - Part XII - Transcript.pdf
PDF
Creating an Uber Clone - Part XX - Transcript.pdf
PDF
Creating a Whatsapp Clone - Part IX.pdf
PDF
Creating a Facebook Clone - Part V.pdf
PDF
Creating a Facebook Clone - Part IV - Transcript.pdf
PDF
2. Section 2. Implementing functionality in the PersonEntry. (1.5 ma.pdf
Creating a Facebook Clone - Part VI.pdf
Creating a Facebook Clone - Part XLI.pdf
Creating a Facebook Clone - Part VII.pdf
Creating a Facebook Clone - Part VIII.pdf
Creating an Uber - Part VI - Transcript.pdf
Creating an Uber Clone - Part IX.pdf
Creating a Facebook Clone - Part IV.pdf
Creating an Uber Clone - Part XXXX.pdf
Creating a Facebook Clone - Part VIII - Transcript.pdf
Creating a Facebook Clone - Part XLII.pdf
Creating an Uber Clone - Part XXXX - Transcript.pdf
Extracting ui Design - part 6 - transcript.pdf
Creating an Uber Clone - Part V - Transcript.pdf
Windows Form - Lec12 (Workshop on C# Programming: Learn to Build)
Creating a Facebook Clone - Part XII - Transcript.pdf
Creating an Uber Clone - Part XX - Transcript.pdf
Creating a Whatsapp Clone - Part IX.pdf
Creating a Facebook Clone - Part V.pdf
Creating a Facebook Clone - Part IV - Transcript.pdf
2. Section 2. Implementing functionality in the PersonEntry. (1.5 ma.pdf

More from ShaiAlmog1 (20)

PDF
The Duck Teaches Learn to debug from the masters. Local to production- kill ...
PDF
create-netflix-clone-06-client-ui.pdf
PDF
create-netflix-clone-01-introduction_transcript.pdf
PDF
create-netflix-clone-02-server_transcript.pdf
PDF
create-netflix-clone-04-server-continued_transcript.pdf
PDF
create-netflix-clone-01-introduction.pdf
PDF
create-netflix-clone-06-client-ui_transcript.pdf
PDF
create-netflix-clone-03-server.pdf
PDF
create-netflix-clone-04-server-continued.pdf
PDF
create-netflix-clone-05-client-model_transcript.pdf
PDF
create-netflix-clone-03-server_transcript.pdf
PDF
create-netflix-clone-02-server.pdf
PDF
create-netflix-clone-05-client-model.pdf
PDF
Creating a Whatsapp Clone - Part II.pdf
PDF
Creating a Whatsapp Clone - Part IX - Transcript.pdf
PDF
Creating a Whatsapp Clone - Part II - Transcript.pdf
PDF
Creating a Whatsapp Clone - Part V - Transcript.pdf
PDF
Creating a Whatsapp Clone - Part IV - Transcript.pdf
PDF
Creating a Whatsapp Clone - Part IV.pdf
PDF
Creating a Whatsapp Clone - Part I - Transcript.pdf
The Duck Teaches Learn to debug from the masters. Local to production- kill ...
create-netflix-clone-06-client-ui.pdf
create-netflix-clone-01-introduction_transcript.pdf
create-netflix-clone-02-server_transcript.pdf
create-netflix-clone-04-server-continued_transcript.pdf
create-netflix-clone-01-introduction.pdf
create-netflix-clone-06-client-ui_transcript.pdf
create-netflix-clone-03-server.pdf
create-netflix-clone-04-server-continued.pdf
create-netflix-clone-05-client-model_transcript.pdf
create-netflix-clone-03-server_transcript.pdf
create-netflix-clone-02-server.pdf
create-netflix-clone-05-client-model.pdf
Creating a Whatsapp Clone - Part II.pdf
Creating a Whatsapp Clone - Part IX - Transcript.pdf
Creating a Whatsapp Clone - Part II - Transcript.pdf
Creating a Whatsapp Clone - Part V - Transcript.pdf
Creating a Whatsapp Clone - Part IV - Transcript.pdf
Creating a Whatsapp Clone - Part IV.pdf
Creating a Whatsapp Clone - Part I - Transcript.pdf

Recently uploaded (20)

PDF
Review of recent advances in non-invasive hemoglobin estimation
PPTX
ACSFv1EN-58255 AWS Academy Cloud Security Foundations.pptx
PPTX
20250228 LYD VKU AI Blended-Learning.pptx
PDF
Reach Out and Touch Someone: Haptics and Empathic Computing
PDF
Approach and Philosophy of On baking technology
PPTX
KOM of Painting work and Equipment Insulation REV00 update 25-dec.pptx
PDF
cuic standard and advanced reporting.pdf
PPTX
Digital-Transformation-Roadmap-for-Companies.pptx
PPTX
sap open course for s4hana steps from ECC to s4
PDF
Empathic Computing: Creating Shared Understanding
PDF
Build a system with the filesystem maintained by OSTree @ COSCUP 2025
PDF
Building Integrated photovoltaic BIPV_UPV.pdf
PDF
Electronic commerce courselecture one. Pdf
PPTX
Programs and apps: productivity, graphics, security and other tools
PPTX
Cloud computing and distributed systems.
PPT
“AI and Expert System Decision Support & Business Intelligence Systems”
PDF
Unlocking AI with Model Context Protocol (MCP)
PDF
Agricultural_Statistics_at_a_Glance_2022_0.pdf
PDF
Mobile App Security Testing_ A Comprehensive Guide.pdf
PDF
MIND Revenue Release Quarter 2 2025 Press Release
Review of recent advances in non-invasive hemoglobin estimation
ACSFv1EN-58255 AWS Academy Cloud Security Foundations.pptx
20250228 LYD VKU AI Blended-Learning.pptx
Reach Out and Touch Someone: Haptics and Empathic Computing
Approach and Philosophy of On baking technology
KOM of Painting work and Equipment Insulation REV00 update 25-dec.pptx
cuic standard and advanced reporting.pdf
Digital-Transformation-Roadmap-for-Companies.pptx
sap open course for s4hana steps from ECC to s4
Empathic Computing: Creating Shared Understanding
Build a system with the filesystem maintained by OSTree @ COSCUP 2025
Building Integrated photovoltaic BIPV_UPV.pdf
Electronic commerce courselecture one. Pdf
Programs and apps: productivity, graphics, security and other tools
Cloud computing and distributed systems.
“AI and Expert System Decision Support & Business Intelligence Systems”
Unlocking AI with Model Context Protocol (MCP)
Agricultural_Statistics_at_a_Glance_2022_0.pdf
Mobile App Security Testing_ A Comprehensive Guide.pdf
MIND Revenue Release Quarter 2 2025 Press Release

Creating a Facebook Clone - Part XXXV - Transcript.pdf

  • 1. Creating a Facebook Clone - Part XXXV Next we’ll explore the comments UI
  • 2. © Codename One 2017 all rights reserved I've simplified the comments UI a bit mostly to keep the code short and understandable. I could support arbitrary depth threaded comments and various other capabilities but this creates some usability issues eventually as the space narrows and it becomes harder to follow the discussion thread. So I limited the replies to top level comments only and tried to simplify the UX a bit. I also used pill shaped comments instead of round rectangles. They just looked nice and I left it at that.
  • 3. public class CommentsForm extends Form { private TextField commentField = new TextField(); private Container comments = new Container(BoxLayout.y()); private String replyCommentId; public CommentsForm(Post p, Comment replyingTo) { super(p.title.get(), new BorderLayout()); commentField.getAllStyles().setBorder(Border.createEmpty()); Button send = new Button("", "Label"); Container post = BorderLayout.centerEastWest(commentField, send, null); setMaterialIcon(send, MATERIAL_SEND); replyCommentId = replyingTo == null ? null : replyingTo.id.get(); send.addActionListener(e -> postComment(p)); commentField.setDoneListener(e -> postComment(p)); Form previous = getCurrentForm(); getToolbar().addMaterialCommandToLeftBar("", MATERIAL_ARROW_BACK, e -> previous.showBack()); for(Comment cmt : p.comments) { addComment(cmt); } CommentsForm Lets check out the code… This is the text field where the user can type in his own comment to submit
  • 4. public class CommentsForm extends Form { private TextField commentField = new TextField(); private Container comments = new Container(BoxLayout.y()); private String replyCommentId; public CommentsForm(Post p, Comment replyingTo) { super(p.title.get(), new BorderLayout()); commentField.getAllStyles().setBorder(Border.createEmpty()); Button send = new Button("", "Label"); Container post = BorderLayout.centerEastWest(commentField, send, null); setMaterialIcon(send, MATERIAL_SEND); replyCommentId = replyingTo == null ? null : replyingTo.id.get(); send.addActionListener(e -> postComment(p)); commentField.setDoneListener(e -> postComment(p)); Form previous = getCurrentForm(); getToolbar().addMaterialCommandToLeftBar("", MATERIAL_ARROW_BACK, e -> previous.showBack()); for(Comment cmt : p.comments) { addComment(cmt); } CommentsForm Comments are placed in a scrollable box layout container in the center of the form
  • 5. public class CommentsForm extends Form { private TextField commentField = new TextField(); private Container comments = new Container(BoxLayout.y()); private String replyCommentId; public CommentsForm(Post p, Comment replyingTo) { super(p.title.get(), new BorderLayout()); commentField.getAllStyles().setBorder(Border.createEmpty()); Button send = new Button("", "Label"); Container post = BorderLayout.centerEastWest(commentField, send, null); setMaterialIcon(send, MATERIAL_SEND); replyCommentId = replyingTo == null ? null : replyingTo.id.get(); send.addActionListener(e -> postComment(p)); commentField.setDoneListener(e -> postComment(p)); Form previous = getCurrentForm(); getToolbar().addMaterialCommandToLeftBar("", MATERIAL_ARROW_BACK, e -> previous.showBack()); for(Comment cmt : p.comments) { addComment(cmt); } CommentsForm When we press reply to a specific comment its ID is placed here...
  • 6. public class CommentsForm extends Form { private TextField commentField = new TextField(); private Container comments = new Container(BoxLayout.y()); private String replyCommentId; public CommentsForm(Post p, Comment replyingTo) { super(p.title.get(), new BorderLayout()); commentField.getAllStyles().setBorder(Border.createEmpty()); Button send = new Button("", "Label"); Container post = BorderLayout.centerEastWest(commentField, send, null); setMaterialIcon(send, MATERIAL_SEND); replyCommentId = replyingTo == null ? null : replyingTo.id.get(); send.addActionListener(e -> postComment(p)); commentField.setDoneListener(e -> postComment(p)); Form previous = getCurrentForm(); getToolbar().addMaterialCommandToLeftBar("", MATERIAL_ARROW_BACK, e -> previous.showBack()); for(Comment cmt : p.comments) { addComment(cmt); } CommentsForm The BorderLayout is needed so we can place the text field input in the bottom of the form
  • 7. public class CommentsForm extends Form { private TextField commentField = new TextField(); private Container comments = new Container(BoxLayout.y()); private String replyCommentId; public CommentsForm(Post p, Comment replyingTo) { super(p.title.get(), new BorderLayout()); commentField.getAllStyles().setBorder(Border.createEmpty()); Button send = new Button("", "Label"); Container post = BorderLayout.centerEastWest(commentField, send, null); setMaterialIcon(send, MATERIAL_SEND); replyCommentId = replyingTo == null ? null : replyingTo.id.get(); send.addActionListener(e -> postComment(p)); commentField.setDoneListener(e -> postComment(p)); Form previous = getCurrentForm(); getToolbar().addMaterialCommandToLeftBar("", MATERIAL_ARROW_BACK, e -> previous.showBack()); for(Comment cmt : p.comments) { addComment(cmt); } CommentsForm We don't add the text field directly though, we also add a send button with it by wrapping them both into a border layout together
  • 8. public class CommentsForm extends Form { private TextField commentField = new TextField(); private Container comments = new Container(BoxLayout.y()); private String replyCommentId; public CommentsForm(Post p, Comment replyingTo) { super(p.title.get(), new BorderLayout()); commentField.getAllStyles().setBorder(Border.createEmpty()); Button send = new Button("", "Label"); Container post = BorderLayout.centerEastWest(commentField, send, null); setMaterialIcon(send, MATERIAL_SEND); replyCommentId = replyingTo == null ? null : replyingTo.id.get(); send.addActionListener(e -> postComment(p)); commentField.setDoneListener(e -> postComment(p)); Form previous = getCurrentForm(); getToolbar().addMaterialCommandToLeftBar("", MATERIAL_ARROW_BACK, e -> previous.showBack()); for(Comment cmt : p.comments) { addComment(cmt); } CommentsForm I used static import of `FontImage` so this line will work correctly
  • 9. public class CommentsForm extends Form { private TextField commentField = new TextField(); private Container comments = new Container(BoxLayout.y()); private String replyCommentId; public CommentsForm(Post p, Comment replyingTo) { super(p.title.get(), new BorderLayout()); commentField.getAllStyles().setBorder(Border.createEmpty()); Button send = new Button("", "Label"); Container post = BorderLayout.centerEastWest(commentField, send, null); setMaterialIcon(send, MATERIAL_SEND); replyCommentId = replyingTo == null ? null : replyingTo.id.get(); send.addActionListener(e -> postComment(p)); commentField.setDoneListener(e -> postComment(p)); Form previous = getCurrentForm(); getToolbar().addMaterialCommandToLeftBar("", MATERIAL_ARROW_BACK, e -> previous.showBack()); for(Comment cmt : p.comments) { addComment(cmt); } CommentsForm If the send button or the done button in the virtual keyboard is pressed we post the comment currently within the text field
  • 10. setMaterialIcon(send, MATERIAL_SEND); replyCommentId = replyingTo == null ? null : replyingTo.id.get(); send.addActionListener(e -> postComment(p)); commentField.setDoneListener(e -> postComment(p)); Form previous = getCurrentForm(); getToolbar().addMaterialCommandToLeftBar("", MATERIAL_ARROW_BACK, e -> previous.showBack()); for(Comment cmt : p.comments) { addComment(cmt); } add(SOUTH, post); add(CENTER, comments); } private void postComment(Post p) { Dialog ip = new InfiniteProgress().showInifiniteBlocking(); Comment cm = new Comment(). postId.set(p.id.get()). userId.set(ServerAPI.me().id.get()). parentComment.set(replyCommentId). text.set(commentField.getText()); if(!ServerAPI.comment(cm)) { ip.dispose(); ToastBar.showErrorMessage("Failed to post comment!"); return; CommentsForm This adds the existing comments to the UI
  • 11. } add(SOUTH, post); add(CENTER, comments); } private void postComment(Post p) { Dialog ip = new InfiniteProgress().showInifiniteBlocking(); Comment cm = new Comment(). postId.set(p.id.get()). userId.set(ServerAPI.me().id.get()). parentComment.set(replyCommentId). text.set(commentField.getText()); if(!ServerAPI.comment(cm)) { ip.dispose(); ToastBar.showErrorMessage("Failed to post comment!"); return; } ip.dispose(); addComment(cm); commentField.setText(""); animateLayout(150); } private void addComment(Comment cm) { Component c = createComment(cm); CommentsForm There were a few methods mentioned in this code that we need to go over specifically postComment, addComment. Lets start with the former
  • 12. } add(SOUTH, post); add(CENTER, comments); } private void postComment(Post p) { Dialog ip = new InfiniteProgress().showInifiniteBlocking(); Comment cm = new Comment(). postId.set(p.id.get()). userId.set(ServerAPI.me().id.get()). parentComment.set(replyCommentId). text.set(commentField.getText()); if(!ServerAPI.comment(cm)) { ip.dispose(); ToastBar.showErrorMessage("Failed to post comment!"); return; } ip.dispose(); addComment(cm); commentField.setText(""); animateLayout(150); } private void addComment(Comment cm) { Component c = createComment(cm); CommentsForm We connect to the server so we need to show a blocking UI dialog
  • 13. } add(SOUTH, post); add(CENTER, comments); } private void postComment(Post p) { Dialog ip = new InfiniteProgress().showInifiniteBlocking(); Comment cm = new Comment(). postId.set(p.id.get()). userId.set(ServerAPI.me().id.get()). parentComment.set(replyCommentId). text.set(commentField.getText()); if(!ServerAPI.comment(cm)) { ip.dispose(); ToastBar.showErrorMessage("Failed to post comment!"); return; } ip.dispose(); addComment(cm); commentField.setText(""); animateLayout(150); } private void addComment(Comment cm) { Component c = createComment(cm); CommentsForm We construct a comment object and set the values for it, notice we don't need the comment ID as that's generated in the server
  • 14. } add(SOUTH, post); add(CENTER, comments); } private void postComment(Post p) { Dialog ip = new InfiniteProgress().showInifiniteBlocking(); Comment cm = new Comment(). postId.set(p.id.get()). userId.set(ServerAPI.me().id.get()). parentComment.set(replyCommentId). text.set(commentField.getText()); if(!ServerAPI.comment(cm)) { ip.dispose(); ToastBar.showErrorMessage("Failed to post comment!"); return; } ip.dispose(); addComment(cm); commentField.setText(""); animateLayout(150); } private void addComment(Comment cm) { Component c = createComment(cm); CommentsForm The ServerAPI does the heavy lifting of submitting the comment
  • 15. } add(SOUTH, post); add(CENTER, comments); } private void postComment(Post p) { Dialog ip = new InfiniteProgress().showInifiniteBlocking(); Comment cm = new Comment(). postId.set(p.id.get()). userId.set(ServerAPI.me().id.get()). parentComment.set(replyCommentId). text.set(commentField.getText()); if(!ServerAPI.comment(cm)) { ip.dispose(); ToastBar.showErrorMessage("Failed to post comment!"); return; } ip.dispose(); addComment(cm); commentField.setText(""); animateLayout(150); } private void addComment(Comment cm) { Component c = createComment(cm); CommentsForm When we are done we need to explicitly clear the text field otherwise the submitted comment would still be there, we animate the UI to valid state after the addition of the comment
  • 16. commentField.setText(""); animateLayout(150); } private void addComment(Comment cm) { Component c = createComment(cm); if(cm.parentComment.get() != null) { Component parent = findParentComment(cm.parentComment.get()); if(parent != null) { Container chld = (Container)parent. getClientProperty("child"); if(chld == null) { chld = BoxLayout.encloseY(c); chld.getAllStyles().setPaddingLeft(convertToPixels(3)); parent.putClientProperty("child", chld); int pos = comments.getComponentIndex(parent); comments.addComponent(pos + 1, chld); } else { chld.add(c); } } else { comments.add(c); } } else { comments.add(c); CommentsForm This naturally leads us to the addComment method
  • 17. commentField.setText(""); animateLayout(150); } private void addComment(Comment cm) { Component c = createComment(cm); if(cm.parentComment.get() != null) { Component parent = findParentComment(cm.parentComment.get()); if(parent != null) { Container chld = (Container)parent. getClientProperty("child"); if(chld == null) { chld = BoxLayout.encloseY(c); chld.getAllStyles().setPaddingLeft(convertToPixels(3)); parent.putClientProperty("child", chld); int pos = comments.getComponentIndex(parent); comments.addComponent(pos + 1, chld); } else { chld.add(c); } } else { comments.add(c); } } else { comments.add(c); CommentsForm createComment() creates the visual representation of a comment, we'll discuss it next
  • 18. } private void addComment(Comment cm) { Component c = createComment(cm); if(cm.parentComment.get() != null) { Component parent = findParentComment(cm.parentComment.get()); if(parent != null) { Container chld = (Container)parent. getClientProperty("child"); if(chld == null) { chld = BoxLayout.encloseY(c); chld.getAllStyles().setPaddingLeft(convertToPixels(3)); parent.putClientProperty("child", chld); int pos = comments.getComponentIndex(parent); comments.addComponent(pos + 1, chld); } else { chld.add(c); } } else { comments.add(c); } } else { comments.add(c); } } CommentsForm If the comment doesn't have a parent
  • 19. } private void addComment(Comment cm) { Component c = createComment(cm); if(cm.parentComment.get() != null) { Component parent = findParentComment(cm.parentComment.get()); if(parent != null) { Container chld = (Container)parent. getClientProperty("child"); if(chld == null) { chld = BoxLayout.encloseY(c); chld.getAllStyles().setPaddingLeft(convertToPixels(3)); parent.putClientProperty("child", chld); int pos = comments.getComponentIndex(parent); comments.addComponent(pos + 1, chld); } else { chld.add(c); } } else { comments.add(c); } } else { comments.add(c); } } CommentsForm we'll just add it directly to the list of comments
  • 20. } private void addComment(Comment cm) { Component c = createComment(cm); if(cm.parentComment.get() != null) { Component parent = findParentComment(cm.parentComment.get()); if(parent != null) { Container chld = (Container)parent. getClientProperty("child"); if(chld == null) { chld = BoxLayout.encloseY(c); chld.getAllStyles().setPaddingLeft(convertToPixels(3)); parent.putClientProperty("child", chld); int pos = comments.getComponentIndex(parent); comments.addComponent(pos + 1, chld); } else { chld.add(c); } } else { comments.add(c); } } else { comments.add(c); } } CommentsForm We search through the hierarchy to find the parent comment component, assuming we find it we can nest the component
  • 21. } private void addComment(Comment cm) { Component c = createComment(cm); if(cm.parentComment.get() != null) { Component parent = findParentComment(cm.parentComment.get()); if(parent != null) { Container chld = (Container)parent. getClientProperty("child"); if(chld == null) { chld = BoxLayout.encloseY(c); chld.getAllStyles().setPaddingLeft(convertToPixels(3)); parent.putClientProperty("child", chld); int pos = comments.getComponentIndex(parent); comments.addComponent(pos + 1, chld); } else { chld.add(c); } } else { comments.add(c); } } else { comments.add(c); } } CommentsForm Every parent comment component has a `Container` for replies (children) that's nested into it
  • 22. } private void addComment(Comment cm) { Component c = createComment(cm); if(cm.parentComment.get() != null) { Component parent = findParentComment(cm.parentComment.get()); if(parent != null) { Container chld = (Container)parent. getClientProperty("child"); if(chld == null) { chld = BoxLayout.encloseY(c); chld.getAllStyles().setPaddingLeft(convertToPixels(3)); parent.putClientProperty("child", chld); int pos = comments.getComponentIndex(parent); comments.addComponent(pos + 1, chld); } else { chld.add(c); } } else { comments.add(c); } } else { comments.add(c); } } CommentsForm If the child Container isn't there yet we create it dynamically and add it
  • 23. } private void addComment(Comment cm) { Component c = createComment(cm); if(cm.parentComment.get() != null) { Component parent = findParentComment(cm.parentComment.get()); if(parent != null) { Container chld = (Container)parent. getClientProperty("child"); if(chld == null) { chld = BoxLayout.encloseY(c); chld.getAllStyles().setPaddingLeft(convertToPixels(3)); parent.putClientProperty("child", chld); int pos = comments.getComponentIndex(parent); comments.addComponent(pos + 1, chld); } else { chld.add(c); } } else { comments.add(c); } } else { comments.add(c); } } CommentsForm Notice that we add to a specific offset within the Container so the comments appear below the parent
  • 24. } private void addComment(Comment cm) { Component c = createComment(cm); if(cm.parentComment.get() != null) { Component parent = findParentComment(cm.parentComment.get()); if(parent != null) { Container chld = (Container)parent. getClientProperty("child"); if(chld == null) { chld = BoxLayout.encloseY(c); chld.getAllStyles().setPaddingLeft(convertToPixels(3)); parent.putClientProperty("child", chld); int pos = comments.getComponentIndex(parent); comments.addComponent(pos + 1, chld); } else { chld.add(c); } } else { comments.add(c); } } else { comments.add(c); } } CommentsForm If the child Container already exists we can add to it directly. Notice that since comments are already sorted by time adding to the end will always work correctly and consistently. With that we effectively implemented one level of comment nesting. We can further generalize the nesting logic to allow additional or even infinite levels of nesting but as I said earlier this might have usability issues.
  • 25. } private void addComment(Comment cm) { Component c = createComment(cm); if(cm.parentComment.get() != null) { Component parent = findParentComment(cm.parentComment.get()); if(parent != null) { Container chld = (Container)parent. getClientProperty("child"); if(chld == null) { chld = BoxLayout.encloseY(c); chld.getAllStyles().setPaddingLeft(convertToPixels(3)); parent.putClientProperty("child", chld); int pos = comments.getComponentIndex(parent); comments.addComponent(pos + 1, chld); } else { chld.add(c); } } else { comments.add(c); } } else { comments.add(c); } } CommentsForm If the child Container already exists we can add to it directly. Notice that since comments are already sorted by time adding to the end will always work correctly and consistently. With that we effectively implemented one level of comment nesting. We can further generalize the nesting logic to allow additional or even infinite levels of nesting but as I said earlier this might have usability issues.
  • 26. } else { comments.add(c); } } private Component createComment(Comment cm) { TextArea c = new TextArea(cm.text.get()); c.setEditable(false); c.setFocusable(false); c.setUIID("Comment"); Label avatar = new Label(getAvatarImage(cm.userId.get())); Container content = BorderLayout.centerEastWest(c, null, avatar); if(cm.id.get() != null && cm.parentComment.get() == null) { Button reply = new Button("reply", "SmallBlueLabel"); content.add(SOUTH, FlowLayout.encloseRight(reply)); reply.addActionListener(e -> replyCommentId = cm.id.get()); } content.putClientProperty("comment", cm); return content; } private Component findParentComment(String id) { for(Component cmp : comments) { Comment c = (Comment)cmp.getClientProperty("comment"); if(c != null && id.equals(c.id.get())) { CommentsForm Finally we need a few additional methods to implement the creation of the comment etc. createComment() generates the component that represents this given comment & potentially the reply button
  • 27. } else { comments.add(c); } } private Component createComment(Comment cm) { TextArea c = new TextArea(cm.text.get()); c.setEditable(false); c.setFocusable(false); c.setUIID("Comment"); Label avatar = new Label(getAvatarImage(cm.userId.get())); Container content = BorderLayout.centerEastWest(c, null, avatar); if(cm.id.get() != null && cm.parentComment.get() == null) { Button reply = new Button("reply", "SmallBlueLabel"); content.add(SOUTH, FlowLayout.encloseRight(reply)); reply.addActionListener(e -> replyCommentId = cm.id.get()); } content.putClientProperty("comment", cm); return content; } private Component findParentComment(String id) { for(Component cmp : comments) { Comment c = (Comment)cmp.getClientProperty("comment"); if(c != null && id.equals(c.id.get())) { CommentsForm The comment itself is just a text area with the Comment UIID, we make that text uneditable
  • 28. } else { comments.add(c); } } private Component createComment(Comment cm) { TextArea c = new TextArea(cm.text.get()); c.setEditable(false); c.setFocusable(false); c.setUIID("Comment"); Label avatar = new Label(getAvatarImage(cm.userId.get())); Container content = BorderLayout.centerEastWest(c, null, avatar); if(cm.id.get() != null && cm.parentComment.get() == null) { Button reply = new Button("reply", "SmallBlueLabel"); content.add(SOUTH, FlowLayout.encloseRight(reply)); reply.addActionListener(e -> replyCommentId = cm.id.get()); } content.putClientProperty("comment", cm); return content; } private Component findParentComment(String id) { for(Component cmp : comments) { Comment c = (Comment)cmp.getClientProperty("comment"); if(c != null && id.equals(c.id.get())) { CommentsForm We fetch an avatar image for the chat, I could have used rounding and similar effects but I chose to keep the code simple
  • 29. } else { comments.add(c); } } private Component createComment(Comment cm) { TextArea c = new TextArea(cm.text.get()); c.setEditable(false); c.setFocusable(false); c.setUIID("Comment"); Label avatar = new Label(getAvatarImage(cm.userId.get())); Container content = BorderLayout.centerEastWest(c, null, avatar); if(cm.id.get() != null && cm.parentComment.get() == null) { Button reply = new Button("reply", "SmallBlueLabel"); content.add(SOUTH, FlowLayout.encloseRight(reply)); reply.addActionListener(e -> replyCommentId = cm.id.get()); } content.putClientProperty("comment", cm); return content; } private Component findParentComment(String id) { for(Component cmp : comments) { Comment c = (Comment)cmp.getClientProperty("comment"); if(c != null && id.equals(c.id.get())) { CommentsForm If we reply to a specific comment we just change the id of the parent comment
  • 30. } else { comments.add(c); } } private Component createComment(Comment cm) { TextArea c = new TextArea(cm.text.get()); c.setEditable(false); c.setFocusable(false); c.setUIID("Comment"); Label avatar = new Label(getAvatarImage(cm.userId.get())); Container content = BorderLayout.centerEastWest(c, null, avatar); if(cm.id.get() != null && cm.parentComment.get() == null) { Button reply = new Button("reply", "SmallBlueLabel"); content.add(SOUTH, FlowLayout.encloseRight(reply)); reply.addActionListener(e -> replyCommentId = cm.id.get()); } content.putClientProperty("comment", cm); return content; } private Component findParentComment(String id) { for(Component cmp : comments) { Comment c = (Comment)cmp.getClientProperty("comment"); if(c != null && id.equals(c.id.get())) { CommentsForm The Comment instance is stored in the components client properties so we can later connect them in findParentComment
  • 31. content.add(SOUTH, FlowLayout.encloseRight(reply)); reply.addActionListener(e -> replyCommentId = cm.id.get()); } content.putClientProperty("comment", cm); return content; } private Component findParentComment(String id) { for(Component cmp : comments) { Comment c = (Comment)cmp.getClientProperty("comment"); if(c != null && id.equals(c.id.get())) { return cmp; } } return null; } private Image getAvatarImage(String userId) { int size = convertToPixels(5); return URLImage.createCachedImage(userId + "avatar", ServerAPI.BASE_URL + "user/avatar/" + userId, Image.createImage(size, size), URLImage.FLAG_RESIZE_SCALE_TO_FILL); } } CommentsForm findParentComment loops through the container and finds the component matching this comment id
  • 32. content.add(SOUTH, FlowLayout.encloseRight(reply)); reply.addActionListener(e -> replyCommentId = cm.id.get()); } content.putClientProperty("comment", cm); return content; } private Component findParentComment(String id) { for(Component cmp : comments) { Comment c = (Comment)cmp.getClientProperty("comment"); if(c != null && id.equals(c.id.get())) { return cmp; } } return null; } private Image getAvatarImage(String userId) { int size = convertToPixels(5); return URLImage.createCachedImage(userId + "avatar", ServerAPI.BASE_URL + "user/avatar/" + userId, Image.createImage(size, size), URLImage.FLAG_RESIZE_SCALE_TO_FILL); } } CommentsForm If the comment we are looking at has the same id then we can return this component
  • 33. content.add(SOUTH, FlowLayout.encloseRight(reply)); reply.addActionListener(e -> replyCommentId = cm.id.get()); } content.putClientProperty("comment", cm); return content; } private Component findParentComment(String id) { for(Component cmp : comments) { Comment c = (Comment)cmp.getClientProperty("comment"); if(c != null && id.equals(c.id.get())) { return cmp; } } return null; } private Image getAvatarImage(String userId) { int size = convertToPixels(5); return URLImage.createCachedImage(userId + "avatar", ServerAPI.BASE_URL + "user/avatar/" + userId, Image.createImage(size, size), URLImage.FLAG_RESIZE_SCALE_TO_FILL); } } CommentsForm In this case the component matching this id doesn't exist or isn't there yet
  • 34. content.add(SOUTH, FlowLayout.encloseRight(reply)); reply.addActionListener(e -> replyCommentId = cm.id.get()); } content.putClientProperty("comment", cm); return content; } private Component findParentComment(String id) { for(Component cmp : comments) { Comment c = (Comment)cmp.getClientProperty("comment"); if(c != null && id.equals(c.id.get())) { return cmp; } } return null; } private Image getAvatarImage(String userId) { int size = convertToPixels(5); return URLImage.createCachedImage(userId + "avatar", ServerAPI.BASE_URL + "user/avatar/" + userId, Image.createImage(size, size), URLImage.FLAG_RESIZE_SCALE_TO_FILL); } } CommentsForm In this method we return an image matching the users avatar which is exposed via /user/avatar/userId on the server
  • 35. font-family: "native:MainLight"; font-size: 2mm; } FloatingActionButton { background: #587EBE; padding: 1.5mm; text-align: center; } Comment { color: black; font-family: "native:MainThin"; font-size: 2.6mm; border: cn1-pill-border; background-color: #dbdbdb; padding: 1mm 3mm 1mm 3mm; margin: 1mm; } SettingsMargin { padding: 0px; margin: 0px 0px 5mm 0px; } LabelFrame { theme.css With that CommentsForm is nearly done... We need a CSS style for Comment. It's black text over a light gray background in pill rounded shape. The one thing to notice is the padding, we need a lot of it to keep the text away from the curve of the border.
  • 36. body.setUIID(style); } else { body.setUIID("HalfPaddedContainer"); } } CheckBox like = CheckBox.createToggle("Like"); like.setUIID("CleanButton"); Button comment = new Button("Comment", "CleanButton"); Button share = new Button("Share", "CleanButton"); FontImage.setMaterialIcon(like, FontImage.MATERIAL_THUMB_UP); FontImage.setMaterialIcon(comment, FontImage.MATERIAL_COMMENT); FontImage.setMaterialIcon(share, FontImage.MATERIAL_SHARE); Container buttonBar = GridLayout.encloseIn(3, like, comment, share); buttonBar.setUIID("HalfPaddedContainer"); like.setSelected(p.likes.contains(u)); like.addActionListener(e -> ServerAPI.like(p)); comment.addActionListener(e -> new CommentsForm(p, null).show()); return BoxLayout.encloseY( titleArea, body, createPostStats(p), buttonBar); } NewsfeedContainer Now CommentsForm should "just work" once we plug it in. To do that we need to go to createNewsItem in NewsfeedContainer. While we are here we might as well implement the "like" functionality too. We set the like toggle buttons selected state if the user already liked this post. We invoke like() on a click. This is already implemented in the ServerAPI so this is just one call. We add the action listener that invokes the comments form and that’s it. Like & comments should work