AI+A11Y 11MAY2024 HYDERBAD GAAD 2024 - HelloA11Y (11 May 2024)
Creating a Facebook Clone - Part XXIX - Transcript.pdf
1. Creating a Facebook Clone - Part XXIX
Mapping the newsfeed is pretty simple now that we have a functional ServerAPI.
2. return l;
}
public static String formatTimeAgo(long time) {
long current = System.currentTimeMillis() - time;
if(current < HOUR) {
long minutes = current / 60000;
if(minutes < 2) {
return "Just now";
}
return minutes + " minutes ago";
}
if(current < HOUR * 10) {
return (current / HOUR) + " hours ago";
}
return L10NManager.getInstance().
formatDateTimeShort(new Date(time));
}
public static Component[] toArray(List<Component> components) {
Component[] cmps = new Component[components.size()];
components.toArray(cmps);
return cmps;
}
}
UIUtils
First we need a new utility method for the UIUtils class.
This is mostly boilerplate code… Notice that a lot of Java developers would just do something like components.toArray() but that's problematic as it requires reflection to
detect the generic type of the list and allocate an array of that specific type. Codename One doesn't support that as it would include runtime overhead.
3. public class NewsfeedContainer extends InfiniteContainer {
@Override
public Component[] fetchComponents(int index, int amount) {
ArrayList<Component> components = new ArrayList<>();
if(index == 0) {
components.add(createPostBar());
components.add(UIUtils.createSpace());
}
int page = index / amount;
if(index % amount > 0) {
page++;
}
List<Post> response = ServerAPI.newsfeed(page, amount);
if(response == null) {
if(index == 0) {
return UIUtils.toArray(components);
}
return null;
}
for(Post p : response) {
components.add(createNewsItem(p.user.get(), p));
NewsfeedContainer
Once this is out of the way the generic code in the NewsfeedContainer class is simpler
4. public class NewsfeedContainer extends InfiniteContainer {
@Override
public Component[] fetchComponents(int index, int amount) {
ArrayList<Component> components = new ArrayList<>();
if(index == 0) {
components.add(createPostBar());
components.add(UIUtils.createSpace());
}
int page = index / amount;
if(index % amount > 0) {
page++;
}
List<Post> response = ServerAPI.newsfeed(page, amount);
if(response == null) {
if(index == 0) {
return UIUtils.toArray(components);
}
return null;
}
for(Post p : response) {
components.add(createNewsItem(p.user.get(), p));
NewsfeedContainer
We no longer need the lastTime variable as the code works with standard paging now
5. public class NewsfeedContainer extends InfiniteContainer {
@Override
public Component[] fetchComponents(int index, int amount) {
ArrayList<Component> components = new ArrayList<>();
if(index == 0) {
components.add(createPostBar());
components.add(UIUtils.createSpace());
}
int page = index / amount;
if(index % amount > 0) {
page++;
}
List<Post> response = ServerAPI.newsfeed(page, amount);
if(response == null) {
if(index == 0) {
return UIUtils.toArray(components);
}
return null;
}
for(Post p : response) {
components.add(createNewsItem(p.user.get(), p));
NewsfeedContainer
This is a special case for the edge of the data where the number of entries is smaller than a full page size
6. public class NewsfeedContainer extends InfiniteContainer {
@Override
public Component[] fetchComponents(int index, int amount) {
ArrayList<Component> components = new ArrayList<>();
if(index == 0) {
components.add(createPostBar());
components.add(UIUtils.createSpace());
}
int page = index / amount;
if(index % amount > 0) {
page++;
}
List<Post> response = ServerAPI.newsfeed(page, amount);
if(response == null) {
if(index == 0) {
return UIUtils.toArray(components);
}
return null;
}
for(Post p : response) {
components.add(createNewsItem(p.user.get(), p));
NewsfeedContainer
We request the page data using a synchronous API call this specific API call uses invokeAndBlock internally and is thus safe on the EDT
7. if(index == 0) {
components.add(createPostBar());
components.add(UIUtils.createSpace());
}
int page = index / amount;
if(index % amount > 0) {
page++;
}
List<Post> response = ServerAPI.newsfeed(page, amount);
if(response == null) {
if(index == 0) {
return UIUtils.toArray(components);
}
return null;
}
for(Post p : response) {
components.add(createNewsItem(p.user.get(), p));
components.add(UIUtils.createHalfSpace());
}
return UIUtils.toArray(components);
}
private static Container createNewsTitle(User u, Post p) {
Button avatar = new Button("", u.getAvatar(6.5f), "CleanButton");
Button name = new Button(u.fullName(), "PostTitle");
NewsfeedContainer
We build components for the responses and then convert them to an array as required by the API. That's pretty simple and will automatically fetch additional data as we
scroll down in the UI.
8. public class NewPostForm extends Form {
private static final String[] POST_STYLES = {
"Label", "PostStyleHearts", "PostStyleHands", "PostStyleBlack",
"PostStyleRed", "PostStylePurple" };
private TextArea post = new TextArea(3, 80);
private String postStyleValue;
public NewPostForm() {
super("Create Post", new BorderLayout());
Form current = getCurrentForm();
getToolbar().setBackCommand("Cancel",
Toolbar.BackCommandPolicy.
WHEN_USES_TITLE_OTHERWISE_ARROW,
e -> current.showBack());
getToolbar().addMaterialCommandToRightBar("",
FontImage.MATERIAL_DONE, e -> post(current));
User me = ServerAPI.me();
Container userSettings = BorderLayout.west(
new Label(me.getAvatar(6.5f), "HalfPaddedContainer"));
Button friends = new Button("Friends", "FriendCombo");
FontImage.setMaterialIcon(friends, FontImage.MATERIAL_PEOPLE);
userSettings.add(CENTER,
BoxLayout.encloseY(
NewPostForm
So the big missing piece is actual content that fits into that feed. To do that we need to make some changes to the NewPostForm. First I extracted two of the local fields
into class level variables that I can use later
9. public class NewPostForm extends Form {
private static final String[] POST_STYLES = {
"Label", "PostStyleHearts", "PostStyleHands", "PostStyleBlack",
"PostStyleRed", "PostStylePurple" };
private TextArea post = new TextArea(3, 80);
private String postStyleValue;
public NewPostForm() {
super("Create Post", new BorderLayout());
Form current = getCurrentForm();
getToolbar().setBackCommand("Cancel",
Toolbar.BackCommandPolicy.
WHEN_USES_TITLE_OTHERWISE_ARROW,
e -> current.showBack());
getToolbar().addMaterialCommandToRightBar("",
FontImage.MATERIAL_DONE, e -> post(current));
User me = ServerAPI.me();
Container userSettings = BorderLayout.west(
new Label(me.getAvatar(6.5f), "HalfPaddedContainer"));
Button friends = new Button("Friends", "FriendCombo");
FontImage.setMaterialIcon(friends, FontImage.MATERIAL_PEOPLE);
userSettings.add(CENTER,
BoxLayout.encloseY(
NewPostForm
We can then change the event handling code to invoke the post method instead of the blank block we had before
10. BoxLayout.encloseY(
new Label(me.fullName(), "MultiLine1"),
FlowLayout.encloseIn(friends)));
add(NORTH, userSettings);
post.setUIID("Label");
post.setGrowByContent(false);
Container postStyles = createPostStyles(post);
add(CENTER, LayeredLayout.encloseIn(
BorderLayout.north(post), BorderLayout.south(postStyles)));
setEditOnShow(post);
}
private void post(Form previousForm) {
Dialog dlg = new InfiniteProgress().showInifiniteBlocking();
if(!ServerAPI.post(new Post().
content.set(post.getText()).
visibility.set("public").
styling.set(postStyleValue))) {
dlg.dispose();
ToastBar.showErrorMessage("Error posting to server");
return;
}
previousForm.showBack();
}
private Container createPostStyles(TextArea post) {
NewPostForm
After that all the logic is in the post method itself…
11. BoxLayout.encloseY(
new Label(me.fullName(), "MultiLine1"),
FlowLayout.encloseIn(friends)));
add(NORTH, userSettings);
post.setUIID("Label");
post.setGrowByContent(false);
Container postStyles = createPostStyles(post);
add(CENTER, LayeredLayout.encloseIn(
BorderLayout.north(post), BorderLayout.south(postStyles)));
setEditOnShow(post);
}
private void post(Form previousForm) {
Dialog dlg = new InfiniteProgress().showInifiniteBlocking();
if(!ServerAPI.post(new Post().
content.set(post.getText()).
visibility.set("public").
styling.set(postStyleValue))) {
dlg.dispose();
ToastBar.showErrorMessage("Error posting to server");
return;
}
previousForm.showBack();
}
private Container createPostStyles(TextArea post) {
NewPostForm
We create a Post object on the fly, right now all posts are public as I don't want to get into "friends only" UI etc.
12. BoxLayout.encloseY(
new Label(me.fullName(), "MultiLine1"),
FlowLayout.encloseIn(friends)));
add(NORTH, userSettings);
post.setUIID("Label");
post.setGrowByContent(false);
Container postStyles = createPostStyles(post);
add(CENTER, LayeredLayout.encloseIn(
BorderLayout.north(post), BorderLayout.south(postStyles)));
setEditOnShow(post);
}
private void post(Form previousForm) {
Dialog dlg = new InfiniteProgress().showInifiniteBlocking();
if(!ServerAPI.post(new Post().
content.set(post.getText()).
visibility.set("public").
styling.set(postStyleValue))) {
dlg.dispose();
ToastBar.showErrorMessage("Error posting to server");
return;
}
previousForm.showBack();
}
private Container createPostStyles(TextArea post) {
NewPostForm
If the post succeeded we can go back to the previousForm
Notice that you will need to refresh after adding a post right now, this is something we'll resolve when integrating push later
With that we can post and see the entries appear in the newsfeed...
13. public class FriendsContainer extends Container {
public FriendsContainer() {
super(BoxLayout.y());
setScrollableY(true);
init();
addPullToRefresh(() -> {
ServerAPI.refreshMe();
removeAll();
init();
revalidate();
});
}
private void init() {
int friendCount = ServerAPI.me().friendRequests.size();
EncodedImage placeholder = FontImage.createMaterial(
MATERIAL_PERSON, "Label", 18).toEncodedImage();
add(createTitle("FRIEND REQUESTS", friendCount));
if(friendCount == 0) {
Container padded = new Container(new BorderLayout(),
FriendsContainer
Because of the way login works our User object stored in ServerAPI.me() will include the friend requests and people we should know. This should work out of the box and
suggest friend requests whenever we login. One missing piece is refresh. Since we cache the user object we might have additional friends waiting.
We can implement that in the addPullToRefresh call within the FriendsContainer constructor.
That updates the User object and after that the UI is rebuilt with the new friends.