Moved most JSON parsing code to windows.

Made windows use JSON as data store.
This commit is contained in:
Snowyfox 2022-04-23 10:19:43 -04:00
parent a033a23ab9
commit 117503c5f2
3 changed files with 219 additions and 298 deletions

View File

@ -8,6 +8,7 @@ import java.awt.Cursor;
import java.awt.Image; import java.awt.Image;
import java.util.List; import java.util.List;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import cafe.biskuteri.hinoki.Tree;
class class
@ -153,53 +154,12 @@ TimelinePage {
public String public String
accountNumId, listId; accountNumId, listId;
public List<Post> public Tree<String>
posts; posts;
} }
class
Post {
public String
text,
contentWarning,
html;
public String
authorId, authorName;
public Image
authorAvatar;
public String
authorNumId;
public String
boosterName;
public ZonedDateTime
date;
public PostVisibility
visibility;
public String
postId;
public boolean
boosted, favourited;
public Attachment[]
attachments;
public String[][]
emojiUrls;
}
class class
Notification { Notification {

View File

@ -30,12 +30,14 @@ import java.util.List;
import java.util.ArrayList; import java.util.ArrayList;
import java.net.URL; import java.net.URL;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.io.IOException; import java.io.IOException;
import cafe.biskuteri.hinoki.Tree; import cafe.biskuteri.hinoki.Tree;
import java.text.BreakIterator; import java.text.BreakIterator;
import java.util.Locale; import java.util.Locale;
import java.time.ZonedDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
class class
@ -48,8 +50,8 @@ implements ActionListener {
private MastodonApi private MastodonApi
api; api;
private Post private Tree<String>
post; post;
// - -%- - // - -%- -
@ -67,53 +69,58 @@ implements ActionListener {
// ---%-@-%--- // ---%-@-%---
public void public void
showPost(Post post) displayEntity(Tree<String> post)
{ {
assert post != null;
this.post = post; this.post = post;
List<RepliesComponent.Reply> replies = null; Tree<String> boosted = post.get("reblog");
{ if (boosted.size() > 0) post = boosted;
List<Post> posts = null;
// We should make a request to JKomasto here.
}
if (replies == null)
{
RepliesComponent.Reply reply1, reply2, reply3;
reply1 = new RepliesComponent.Reply();
reply1.author = "Black tea";
reply1.text = "Rich..";
reply2 = new RepliesComponent.Reply();
reply2.author = "Green tea";
reply2.text = "Clean!";
reply3 = new RepliesComponent.Reply();
reply3.author = "Coffee";
reply3.text = "sleepy..";
replies = new ArrayList<>(); Tree<String> author = post.get("account");
replies.add(reply1); Tree<String> emojis = post.get("emojis");
replies.add(reply2); Tree<String> media = post.get("media_attachments");
replies.add(reply3);
}
postDisplay.setAuthorName(post.authorName); String an = author.get("display_name").value;
postDisplay.setAuthorId(post.authorId); if (an.isEmpty()) an = author.get("username").value;
postDisplay.setAuthorAvatar(post.authorAvatar); postDisplay.setAuthorName(an);
postDisplay.setDate(DATE_FORMAT.format(post.date)); postDisplay.setAuthorId(author.get("acct").value);
postDisplay.setTime(TIME_FORMAT.format(post.date));
postDisplay.setEmojiUrls(post.emojiUrls); String avurl = author.get("avatar").value;
postDisplay.setText(post.text); postDisplay.setAuthorAvatar(ImageApi.remote(avurl));
postDisplay.setHtml(post.html);
postDisplay.setFavourited(post.favourited); String sdate = post.get("created_at").value;
postDisplay.setBoosted(post.boosted); ZonedDateTime date = ZonedDateTime.parse(sdate);
postDisplay.setMediaPreview( date = date.withZoneSameInstant(ZoneId.systemDefault());
post.attachments.length == 0 postDisplay.setDate(DATE_FORMAT.format(date));
? null postDisplay.setTime(TIME_FORMAT.format(date));
: post.attachments[0].image
); String[][] emojiUrls = new String[emojis.size()][];
for (int o = 0; o < emojiUrls.length; ++o) {
Tree<String> emoji = emojis.get(o);
emojiUrls[o] = new String[2];
emojiUrls[o][0] = emoji.get("shortcode").value;
emojiUrls[o][1] = emoji.get("url").value;
}
postDisplay.setEmojiUrls(emojiUrls);
postDisplay.setHtml(post.get("content").value);
boolean f = post.get("favourited").value.equals("true");
boolean b = post.get("reblogged").value.equals("true");
postDisplay.setFavourited(f);
postDisplay.setBoosted(b);
if (media.size() > 0)
{
Tree<String> first = media.get(0);
String u1 = first.get("remote_url").value;
String u2 = first.get("text_url").value;
String u3 = first.get("url").value;
String purl = u1 != null ? u1 : u2 != null ? u2 : u3;
postDisplay.setMediaPreview(ImageApi.remote(purl));
}
else postDisplay.setMediaPreview(null);
repliesDisplay.setReplies(replies);
postDisplay.resetFocus(); postDisplay.resetFocus();
repaint(); repaint();
} }
@ -121,8 +128,12 @@ implements ActionListener {
public void public void
openAuthorProfile() openAuthorProfile()
{ {
TimelineWindow w = new TimelineWindow(primaire); Tree<String> post = this.post;
w.showAuthorPosts(post.authorNumId); Tree<String> boosted = post.get("reblog");
if (boosted.size() > 0) post = boosted;
TimelineWindow w = new TimelineWindow(primaire);
w.showAuthorPosts(post.get("account").get("id").value);
w.showLatestPage(); w.showLatestPage();
w.setLocationRelativeTo(this); w.setLocationRelativeTo(this);
w.setVisible(true); w.setVisible(true);
@ -131,6 +142,10 @@ implements ActionListener {
public void public void
favourite(boolean favourited) favourite(boolean favourited)
{ {
Tree<String> post = this.post;
Tree<String> boosted = post.get("reblog");
if (boosted.size() > 0) post = boosted;
postDisplay.setCursor(new Cursor(Cursor.WAIT_CURSOR)); postDisplay.setCursor(new Cursor(Cursor.WAIT_CURSOR));
postDisplay.setFavouriteBoostEnabled(false); postDisplay.setFavouriteBoostEnabled(false);
postDisplay.paintImmediately(postDisplay.getBounds()); postDisplay.paintImmediately(postDisplay.getBounds());
@ -160,11 +175,13 @@ implements ActionListener {
public void public void
requestSucceeded(Tree<String> json) requestSucceeded(Tree<String> json)
{ {
post.favourited = favourited; String n = Boolean.toString(favourited);
PostWindow.this.post.get("favourited").value = n;
} }
}; };
api.setPostFavourited(post.postId, favourited, handler); String postId = post.get("id").value;
api.setPostFavourited(postId, favourited, handler);
postDisplay.setCursor(null); postDisplay.setCursor(null);
postDisplay.setFavouriteBoostEnabled(true); postDisplay.setFavouriteBoostEnabled(true);
postDisplay.repaint(); postDisplay.repaint();
@ -173,6 +190,10 @@ implements ActionListener {
public void public void
boost(boolean boosted) boost(boolean boosted)
{ {
Tree<String> post = this.post;
Tree<String> boosted2 = post.get("reblog");
if (boosted2.size() > 0) post = boosted2;
postDisplay.setCursor(new Cursor(Cursor.WAIT_CURSOR)); postDisplay.setCursor(new Cursor(Cursor.WAIT_CURSOR));
postDisplay.setFavouriteBoostEnabled(false); postDisplay.setFavouriteBoostEnabled(false);
postDisplay.paintImmediately(postDisplay.getBounds()); postDisplay.paintImmediately(postDisplay.getBounds());
@ -202,11 +223,13 @@ implements ActionListener {
public void public void
requestSucceeded(Tree<String> json) requestSucceeded(Tree<String> json)
{ {
post.boosted = boosted; String n = Boolean.toString(boosted);
PostWindow.this.post.get("reblogged").value = n;
} }
}; };
api.setPostBoosted(post.postId, boosted, handler); String postId = post.get("id").value;
api.setPostBoosted(postId, boosted, handler);
postDisplay.setCursor(null); postDisplay.setCursor(null);
postDisplay.setFavouriteBoostEnabled(true); postDisplay.setFavouriteBoostEnabled(true);
postDisplay.repaint(); postDisplay.repaint();
@ -215,23 +238,57 @@ implements ActionListener {
public void public void
reply() reply()
{ {
Tree<String> post = this.post;
Tree<String> boosted = post.get("reblog");
if (boosted.size() > 0) post = boosted;
String authorId = post.get("account").get("acct").value;
String postId = post.get("id").value;
String cw = post.get("spoiler_text").value;
String ourId = api.getAccountDetails().get("acct").value;
boolean replying = authorId != ourId;
String vs = post.get("visibility").value;
PostVisibility v = null;
if (vs.equals("public")) v = PostVisibility.PUBLIC;
if (vs.equals("unlisted")) v = PostVisibility.UNLISTED;
if (vs.equals("private")) v = PostVisibility.FOLLOWERS;
if (vs.equals("direct")) v = PostVisibility.MENTIONED;
Composition c = new Composition();
c.contentWarning = cw;
c.text = replying ? "@" + authorId + " " : "";
c.visibility = v;
c.replyToPostId = postId;
ComposeWindow w = primaire.getComposeWindow(); ComposeWindow w = primaire.getComposeWindow();
w.setLocation(getX(), getY() + 100); w.setLocation(getX(), getY() + 100);
w.setVisible(true); w.setVisible(true);
Composition c = new Composition(); w.setComposition(c);
c.text = "@" + post.authorId + " ";
c.visibility = PostVisibility.PUBLIC;
c.replyToPostId = post.postId;
w.setComposition(c);
} }
public void public void
openMedia() openMedia()
{ {
Tree<String> media = post.get("media_attachments");
Attachment[] as = new Attachment[media.size()];
for (int o = 0; o < as.length; ++o)
{
Tree<String> medium = media.get(o);
String u1 = medium.get("remote_url").value;
String u2 = medium.get("text_url").value;
String u3 = medium.get("url").value;
Attachment a = as[o] = new Attachment();
a.url = u1 != null ? u1 : u2 != null ? u2 : u3;
a.type = medium.get("type").value;
a.description = medium.get("description").value;
a.image = ImageApi.remote(a.url);
}
ImageWindow w = primaire.getMediaWindow(); ImageWindow w = primaire.getMediaWindow();
w.showAttachments(post.attachments); w.setTitle(post.get("id").value);
int l = Math.min(40, post.text.length()); w.showAttachments(as);
w.setTitle(post.text.substring(0, l));
if (!w.isVisible()) { if (!w.isVisible()) {
w.setLocationRelativeTo(null); w.setLocationRelativeTo(null);
w.setVisible(true); w.setVisible(true);
@ -278,23 +335,8 @@ implements ActionListener {
setLocationByPlatform(true); setLocationByPlatform(true);
postDisplay = new PostComponent(this); postDisplay = new PostComponent(this);
repliesDisplay = new RepliesComponent(); repliesDisplay = new RepliesComponent();
Post samplePost = new Post();
samplePost.text = "This is a sample post.";
samplePost.html = "";
samplePost.authorId = "snowyfox@biskuteri.cafe";
samplePost.authorName = "snowyfox";
samplePost.date = ZonedDateTime.now();
samplePost.visibility = PostVisibility.MENTIONED;
samplePost.postId = "000000000";
samplePost.boosted = false;
samplePost.favourited = true;
samplePost.attachments = new Attachment[0];
samplePost.emojiUrls = new String[0][];
showPost(samplePost);
setContentPane(postDisplay); setContentPane(postDisplay);
} }
@ -352,9 +394,6 @@ implements ActionListener {
public void public void
setTime(String n) { time.setText(n); } setTime(String n) { time.setText(n); }
public void
setText(String n) { }
public void public void
setEmojiUrls(String[][] n) { emojiUrls = n; } setEmojiUrls(String[][] n) { emojiUrls = n; }

View File

@ -143,10 +143,9 @@ implements ActionListener {
public void public void
requestSucceeded(Tree<String> json) requestSucceeded(Tree<String> json)
{ {
List<Post> posts = toPosts(json); page.posts = json;
page.posts = posts; display.displayEntities(page.posts);
display.setPosts(page.posts); boolean full = json.size() >= PREVIEW_COUNT;
boolean full = posts.size() >= PREVIEW_COUNT;
display.setNextPageAvailable(full); display.setNextPageAvailable(full);
display.setPreviousPageAvailable(true); display.setPreviousPageAvailable(true);
display.resetFocus(); display.resetFocus();
@ -162,12 +161,13 @@ implements ActionListener {
{ {
assert page.posts != null; assert page.posts != null;
assert page.posts.size() != 0; assert page.posts.size() != 0;
Post last = page.posts.get(page.posts.size() - 1); Tree<String> last = page.posts.get(page.posts.size() - 1);
String lastId = last.get("id").value;
display.setCursor(new Cursor(Cursor.WAIT_CURSOR)); display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
api.getTimelinePage( api.getTimelinePage(
page.type, page.type,
PREVIEW_COUNT, last.postId, null, PREVIEW_COUNT, lastId, null,
page.accountNumId, page.listId, page.accountNumId, page.listId,
new RequestListener() { new RequestListener() {
@ -187,16 +187,15 @@ implements ActionListener {
public void public void
requestSucceeded(Tree<String> json) requestSucceeded(Tree<String> json)
{ {
List<Post> posts = toPosts(json); if (json.size() == 0) {
if (posts.size() == 0) {
// We should probably say something // We should probably say something
// to the user here? For now, we // to the user here? For now, we
// quietly cancel. // quietly cancel.
return; return;
} }
page.posts = posts; page.posts = json;
display.setPosts(page.posts); display.displayEntities(page.posts);
boolean full = posts.size() >= PREVIEW_COUNT; boolean full = json.size() >= PREVIEW_COUNT;
display.setNextPageAvailable(full); display.setNextPageAvailable(full);
display.setPreviousPageAvailable(true); display.setPreviousPageAvailable(true);
display.resetFocus(); display.resetFocus();
@ -212,12 +211,13 @@ implements ActionListener {
{ {
assert page.posts != null; assert page.posts != null;
assert page.posts.size() != 0; assert page.posts.size() != 0;
Post first = page.posts.get(0); Tree<String> first = page.posts.get(0);
String firstId = first.get("id").value;
display.setCursor(new Cursor(Cursor.WAIT_CURSOR)); display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
api.getTimelinePage( api.getTimelinePage(
page.type, page.type,
PREVIEW_COUNT, null, first.postId, PREVIEW_COUNT, null, firstId,
page.accountNumId, page.listId, page.accountNumId, page.listId,
new RequestListener() { new RequestListener() {
@ -237,13 +237,12 @@ implements ActionListener {
public void public void
requestSucceeded(Tree<String> json) requestSucceeded(Tree<String> json)
{ {
List<Post> posts = toPosts(json); if (json.size() < PREVIEW_COUNT) {
if (posts.size() < PREVIEW_COUNT) {
showLatestPage(); showLatestPage();
return; return;
} }
page.posts = posts; page.posts = json;
display.setPosts(page.posts); display.displayEntities(page.posts);
display.setNextPageAvailable(true); display.setNextPageAvailable(true);
display.setPreviousPageAvailable(true); display.setPreviousPageAvailable(true);
display.resetFocus(); display.resetFocus();
@ -376,16 +375,16 @@ implements ActionListener {
// - -%- - // - -%- -
public void public void
postSelected(Post post) postSelected(Tree<String> post)
{ {
primaire.getAutoViewWindow().showPost(post); primaire.getAutoViewWindow().displayEntity(post);
} }
public void public void
postOpened(Post post) postOpened(Tree<String> post)
{ {
PostWindow w = new PostWindow(primaire); PostWindow w = new PostWindow(primaire);
w.showPost(post); w.displayEntity(post);
w.setLocationRelativeTo(this); w.setLocationRelativeTo(this);
w.setVisible(true); w.setVisible(true);
} }
@ -453,112 +452,6 @@ implements ActionListener {
// - -%- - // - -%- -
private static List<Post>
toPosts(Tree<String> json)
{
List<Post> posts = new ArrayList<>();
for (Tree<String> post: json.children)
{
Post addee = new Post();
addee.postId = post.get("id").value;
// This fixes timeline navigation, but note that
// we will be pranked here if we try to reply
// to the boosted post.
if (post.get("reblog").size() != 0)
{
Tree<String> a = post.get("account");
addee.boosterName = a.get("display_name").value;
post = post.get("reblog");
}
try {
String s = post.get("created_at").value;
addee.date = ZonedDateTime.parse(s);
ZoneId z = ZoneId.systemDefault();
addee.date = addee.date.withZoneSameInstant(z);
}
catch (DateTimeParseException eDt) {
assert false;
addee.date = ZonedDateTime.now();
}
String s2 = addee.html = post.get("content").value;
StringBuilder b = new StringBuilder();
Tree<String> nodes = RudimentaryHTMLParser.depthlessRead(s2);
for (Tree<String> node: nodes)
{
if (node.key.equals("tag"))
{
String tagName = node.get(0).key;
if (tagName.equals("br")) b.append(" \n ");
if (tagName.equals("/p")) b.append(" \n \n ");
}
if (node.key.equals("text"))
b.append(node.value);
if (node.key.equals("emoji"))
b.append(":" + node.value + ":");
}
addee.text = b.toString();
String s3 = post.get("spoiler_text").value;
if (!s3.isEmpty()) addee.contentWarning = s3;
else addee.contentWarning = null;
Tree<String> account = post.get("account");
addee.authorId = account.get("acct").value;
addee.authorName = account.get("username").value;
addee.authorNumId = account.get("id").value;
String s4 = account.get("display_name").value;
if (!s4.isEmpty()) addee.authorName = s4;
String s5 = account.get("avatar").value;
addee.authorAvatar = ImageApi.remote(s5);
if (addee.authorAvatar == null) {
s5 = "defaultAvatar";
addee.authorAvatar = ImageApi.local(s5);
}
String s6 = post.get("favourited").value;
String s7 = post.get("reblogged").value;
addee.favourited = s6.equals("true");
addee.boosted = s7.equals("true");
Tree<String> as1 = post.get("media_attachments");
Attachment[] as2 = new Attachment[as1.size()];
for (int o = 0; o < as2.length; ++o)
{
Tree<String> a1 = as1.get(o);
Attachment a2 = as2[o] = new Attachment();
a2.type = a1.get("type").value;
String u1 = a1.get("remote_url").value;
String u2 = a1.get("text_url").value;
String u3 = a1.get("url").value;
a2.url = u1 != null ? u1 : u2 != null ? u2 : u3;
a2.description = a1.get("description").value;
a2.image = null;
if (a2.type.equals("image"))
a2.image = ImageApi.remote(a2.url);
}
addee.attachments = as2;
Tree<String> es1 = post.get("emojis");
String[][] es2 = new String[es1.size()][];
for (int o = 0; o < es2.length; ++o)
{
Tree<String> e1 = es1.get(o);
String[] e2 = es2[o] = new String[2];
e2[0] = e1.get("shortcode").value;
e2[1] = e1.get("url").value;
}
addee.emojiUrls = es2;
posts.add(addee);
}
return posts;
}
private static String private static String
plainify(String html) plainify(String html)
{ {
@ -629,7 +522,7 @@ implements ActionListener {
setJMenuBar(menuBar); setJMenuBar(menuBar);
page = new TimelinePage(); page = new TimelinePage();
page.posts = new ArrayList<>(); page.posts = new Tree<String>();
display = new TimelineComponent(this); display = new TimelineComponent(this);
display.setNextPageAvailable(false); display.setNextPageAvailable(false);
@ -652,8 +545,8 @@ implements
private TimelineWindow private TimelineWindow
primaire; primaire;
private final List<Post> private Tree<String>
posts = new ArrayList<>(); posts;
// - -%- - // - -%- -
@ -679,51 +572,82 @@ implements
// ---%-@-%--- // ---%-@-%---
public void public void
setPosts(List<Post> posts) displayEntities(Tree<String> postArray)
{ {
this.posts.clear(); assert postArray.size() <= postPreviews.size();
this.posts.addAll(posts); this.posts = postArray;
assert posts.size() <= postPreviews.size(); for (int o = 0; o < postArray.size(); ++o)
for (int o = 0; o < posts.size(); ++o) {
{ PostPreviewComponent c = postPreviews.get(o);
PostPreviewComponent c = postPreviews.get(o); Tree<String> p = postArray.get(o);
Post p = posts.get(o); Tree<String> a = p.get("account");
{
c.setTopLeft(p.authorName); String an = a.get("display_name").value;
if (p.boosterName != null) if (an.isEmpty()) an = a.get("username").value;
c.setTopLeft("boosted by " + p.boosterName); c.setTopLeft(an);
Tree<String> boosted = p.get("reblog");
if (boosted.size() > 0) {
c.setTopLeft("boosted by " + an);
p = boosted;
a = p.get("account");
} }
{
String f = "";
if (p.attachments.length > 0)
f += "a";
String t; String f = "";
ZonedDateTime now = ZonedDateTime.now(); if (p.get("media_attachments").size() > 0) f += "a";
long d = ChronoUnit.SECONDS.between(p.date, now); String t = "";
try {
String jv = p.get("created_at").value;
ZonedDateTime pv = ZonedDateTime.parse(jv);
ZoneId z = ZoneId.systemDefault();
pv = pv.withZoneSameInstant(z);
ZonedDateTime now = ZonedDateTime.now();
long d = ChronoUnit.SECONDS.between(pv, now);
long s = Math.abs(d); long s = Math.abs(d);
if (s < 30) t = "now"; if (s < 30) t = "now";
else if (s < 60) t = d + "s"; else if (s < 60) t = d + "s";
else if (s < 3600) t = (d / 60) + "m"; else if (s < 3600) t = (d / 60) + "m";
else if (s < 86400) t = (d / 3600) + "h"; else if (s < 86400) t = (d / 3600) + "h";
else t = (d / 86400) + "d"; else t = (d / 86400) + "d";
c.setTopRight(f + " " + t);
}
{
if (p.contentWarning != null)
c.setBottom("(" + p.contentWarning + ")");
else
c.setBottom(p.text + " ");
} }
} catch (DateTimeParseException eDt) {
assert false;
}
c.setTopRight(f + " " + t);
String html = p.get("content").value;
String cw = p.get("spoiler_text").value;
if (!cw.isEmpty()) {
c.setBottom("(" + cw + ")");
}
else {
StringBuilder bu = new StringBuilder();
Tree<String> nodes;
nodes = RudimentaryHTMLParser.depthlessRead(html);
for (Tree<String> node: nodes)
{
if (node.key.equals("tag"))
{
String tagName = node.get(0).key;
if (tagName.equals("br")) bu.append(" \n ");
if (tagName.equals("/p")) bu.append(" \n \n ");
}
if (node.key.equals("text"))
bu.append(node.value);
if (node.key.equals("emoji"))
bu.append(":" + node.value + ":");
}
c.setBottom(bu.toString() + " ");
}
}
for (int o = posts.size(); o < postPreviews.size(); ++o) for (int o = posts.size(); o < postPreviews.size(); ++o)
{ {
postPreviews.get(o).reset(); postPreviews.get(o).reset();
} }
} }
public void public void
setPageLabel(String label) setPageLabel(String label)
@ -768,18 +692,17 @@ implements
select(Object c) select(Object c)
{ {
assert c instanceof PostPreviewComponent; assert c instanceof PostPreviewComponent;
PostPreviewComponent p = (PostPreviewComponent)c;
int offset = postPreviews.indexOf(p); for (int o = 0; o < postPreviews.size(); ++o)
assert offset != -1; {
if (offset < posts.size()) { PostPreviewComponent p = postPreviews.get(o);
primaire.postSelected(posts.get(offset)); if (c == p) {
p.setSelected(true); primaire.postSelected(posts.get(o));
} p.setSelected(true);
else { p.repaint();
p.setSelected(false); }
} else deselect(p);
p.repaint(); }
} }
private void private void
@ -798,11 +721,10 @@ implements
assert c instanceof PostPreviewComponent; assert c instanceof PostPreviewComponent;
PostPreviewComponent p = (PostPreviewComponent)c; PostPreviewComponent p = (PostPreviewComponent)c;
int offset = postPreviews.indexOf(p); int o = postPreviews.indexOf(p);
assert offset != -1; assert o != -1;
if (offset < posts.size()) { if (o >= posts.size()) return;
primaire.postOpened(posts.get(offset)); primaire.postOpened(posts.get(o));
}
} }
public void public void