diff --git a/JKomasto.java b/JKomasto.java index 4110601..d585f62 100755 --- a/JKomasto.java +++ b/JKomasto.java @@ -8,6 +8,7 @@ import java.awt.Cursor; import java.awt.Image; import java.util.List; import java.time.ZonedDateTime; +import cafe.biskuteri.hinoki.Tree; class @@ -153,53 +154,12 @@ TimelinePage { public String accountNumId, listId; - public List - posts; + public Tree + 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 Notification { diff --git a/PostWindow.java b/PostWindow.java index b39d39c..83c0bc9 100755 --- a/PostWindow.java +++ b/PostWindow.java @@ -30,12 +30,14 @@ import java.util.List; import java.util.ArrayList; import java.net.URL; import java.net.MalformedURLException; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; import java.io.IOException; import cafe.biskuteri.hinoki.Tree; import java.text.BreakIterator; import java.util.Locale; +import java.time.ZonedDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; + class @@ -48,8 +50,8 @@ implements ActionListener { private MastodonApi api; - private Post - post; + private Tree + post; // - -%- - @@ -67,53 +69,58 @@ implements ActionListener { // ---%-@-%--- - public void - showPost(Post post) - { - assert post != null; + public void + displayEntity(Tree post) + { this.post = post; - List replies = null; - { - List 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.."; + Tree boosted = post.get("reblog"); + if (boosted.size() > 0) post = boosted; - replies = new ArrayList<>(); - replies.add(reply1); - replies.add(reply2); - replies.add(reply3); - } + Tree author = post.get("account"); + Tree emojis = post.get("emojis"); + Tree media = post.get("media_attachments"); - postDisplay.setAuthorName(post.authorName); - postDisplay.setAuthorId(post.authorId); - postDisplay.setAuthorAvatar(post.authorAvatar); - postDisplay.setDate(DATE_FORMAT.format(post.date)); - postDisplay.setTime(TIME_FORMAT.format(post.date)); - postDisplay.setEmojiUrls(post.emojiUrls); - postDisplay.setText(post.text); - postDisplay.setHtml(post.html); - postDisplay.setFavourited(post.favourited); - postDisplay.setBoosted(post.boosted); - postDisplay.setMediaPreview( - post.attachments.length == 0 - ? null - : post.attachments[0].image - ); + String an = author.get("display_name").value; + if (an.isEmpty()) an = author.get("username").value; + postDisplay.setAuthorName(an); + postDisplay.setAuthorId(author.get("acct").value); + + String avurl = author.get("avatar").value; + postDisplay.setAuthorAvatar(ImageApi.remote(avurl)); + + String sdate = post.get("created_at").value; + ZonedDateTime date = ZonedDateTime.parse(sdate); + date = date.withZoneSameInstant(ZoneId.systemDefault()); + postDisplay.setDate(DATE_FORMAT.format(date)); + postDisplay.setTime(TIME_FORMAT.format(date)); + + String[][] emojiUrls = new String[emojis.size()][]; + for (int o = 0; o < emojiUrls.length; ++o) { + Tree 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 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(); repaint(); } @@ -121,8 +128,12 @@ implements ActionListener { public void openAuthorProfile() { - TimelineWindow w = new TimelineWindow(primaire); - w.showAuthorPosts(post.authorNumId); + Tree post = this.post; + Tree 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.setLocationRelativeTo(this); w.setVisible(true); @@ -131,6 +142,10 @@ implements ActionListener { public void favourite(boolean favourited) { + Tree post = this.post; + Tree boosted = post.get("reblog"); + if (boosted.size() > 0) post = boosted; + postDisplay.setCursor(new Cursor(Cursor.WAIT_CURSOR)); postDisplay.setFavouriteBoostEnabled(false); postDisplay.paintImmediately(postDisplay.getBounds()); @@ -160,11 +175,13 @@ implements ActionListener { public void requestSucceeded(Tree 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.setFavouriteBoostEnabled(true); postDisplay.repaint(); @@ -173,6 +190,10 @@ implements ActionListener { public void boost(boolean boosted) { + Tree post = this.post; + Tree boosted2 = post.get("reblog"); + if (boosted2.size() > 0) post = boosted2; + postDisplay.setCursor(new Cursor(Cursor.WAIT_CURSOR)); postDisplay.setFavouriteBoostEnabled(false); postDisplay.paintImmediately(postDisplay.getBounds()); @@ -202,11 +223,13 @@ implements ActionListener { public void requestSucceeded(Tree 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.setFavouriteBoostEnabled(true); postDisplay.repaint(); @@ -215,23 +238,57 @@ implements ActionListener { public void reply() { + Tree post = this.post; + Tree 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(); w.setLocation(getX(), getY() + 100); w.setVisible(true); - Composition c = new Composition(); - c.text = "@" + post.authorId + " "; - c.visibility = PostVisibility.PUBLIC; - c.replyToPostId = post.postId; - w.setComposition(c); + w.setComposition(c); } public void openMedia() { + Tree media = post.get("media_attachments"); + Attachment[] as = new Attachment[media.size()]; + for (int o = 0; o < as.length; ++o) + { + Tree 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(); - w.showAttachments(post.attachments); - int l = Math.min(40, post.text.length()); - w.setTitle(post.text.substring(0, l)); + w.setTitle(post.get("id").value); + w.showAttachments(as); if (!w.isVisible()) { w.setLocationRelativeTo(null); w.setVisible(true); @@ -278,23 +335,8 @@ implements ActionListener { setLocationByPlatform(true); postDisplay = new PostComponent(this); - 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); } @@ -352,9 +394,6 @@ implements ActionListener { public void setTime(String n) { time.setText(n); } - public void - setText(String n) { } - public void setEmojiUrls(String[][] n) { emojiUrls = n; } diff --git a/TimelineWindow.java b/TimelineWindow.java index 40d9913..68f252b 100755 --- a/TimelineWindow.java +++ b/TimelineWindow.java @@ -143,10 +143,9 @@ implements ActionListener { public void requestSucceeded(Tree json) { - List posts = toPosts(json); - page.posts = posts; - display.setPosts(page.posts); - boolean full = posts.size() >= PREVIEW_COUNT; + page.posts = json; + display.displayEntities(page.posts); + boolean full = json.size() >= PREVIEW_COUNT; display.setNextPageAvailable(full); display.setPreviousPageAvailable(true); display.resetFocus(); @@ -162,12 +161,13 @@ implements ActionListener { { assert page.posts != null; assert page.posts.size() != 0; - Post last = page.posts.get(page.posts.size() - 1); + Tree last = page.posts.get(page.posts.size() - 1); + String lastId = last.get("id").value; display.setCursor(new Cursor(Cursor.WAIT_CURSOR)); api.getTimelinePage( page.type, - PREVIEW_COUNT, last.postId, null, + PREVIEW_COUNT, lastId, null, page.accountNumId, page.listId, new RequestListener() { @@ -187,16 +187,15 @@ implements ActionListener { public void requestSucceeded(Tree json) { - List posts = toPosts(json); - if (posts.size() == 0) { + if (json.size() == 0) { // We should probably say something // to the user here? For now, we // quietly cancel. return; } - page.posts = posts; - display.setPosts(page.posts); - boolean full = posts.size() >= PREVIEW_COUNT; + page.posts = json; + display.displayEntities(page.posts); + boolean full = json.size() >= PREVIEW_COUNT; display.setNextPageAvailable(full); display.setPreviousPageAvailable(true); display.resetFocus(); @@ -212,12 +211,13 @@ implements ActionListener { { assert page.posts != null; assert page.posts.size() != 0; - Post first = page.posts.get(0); + Tree first = page.posts.get(0); + String firstId = first.get("id").value; display.setCursor(new Cursor(Cursor.WAIT_CURSOR)); api.getTimelinePage( page.type, - PREVIEW_COUNT, null, first.postId, + PREVIEW_COUNT, null, firstId, page.accountNumId, page.listId, new RequestListener() { @@ -237,13 +237,12 @@ implements ActionListener { public void requestSucceeded(Tree json) { - List posts = toPosts(json); - if (posts.size() < PREVIEW_COUNT) { + if (json.size() < PREVIEW_COUNT) { showLatestPage(); return; } - page.posts = posts; - display.setPosts(page.posts); + page.posts = json; + display.displayEntities(page.posts); display.setNextPageAvailable(true); display.setPreviousPageAvailable(true); display.resetFocus(); @@ -376,16 +375,16 @@ implements ActionListener { // - -%- - public void - postSelected(Post post) + postSelected(Tree post) { - primaire.getAutoViewWindow().showPost(post); + primaire.getAutoViewWindow().displayEntity(post); } public void - postOpened(Post post) + postOpened(Tree post) { PostWindow w = new PostWindow(primaire); - w.showPost(post); + w.displayEntity(post); w.setLocationRelativeTo(this); w.setVisible(true); } @@ -453,112 +452,6 @@ implements ActionListener { // - -%- - - private static List - toPosts(Tree json) - { - List posts = new ArrayList<>(); - for (Tree 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 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 nodes = RudimentaryHTMLParser.depthlessRead(s2); - for (Tree 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 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 as1 = post.get("media_attachments"); - Attachment[] as2 = new Attachment[as1.size()]; - for (int o = 0; o < as2.length; ++o) - { - Tree 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 es1 = post.get("emojis"); - String[][] es2 = new String[es1.size()][]; - for (int o = 0; o < es2.length; ++o) - { - Tree 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 plainify(String html) { @@ -629,7 +522,7 @@ implements ActionListener { setJMenuBar(menuBar); page = new TimelinePage(); - page.posts = new ArrayList<>(); + page.posts = new Tree(); display = new TimelineComponent(this); display.setNextPageAvailable(false); @@ -652,8 +545,8 @@ implements private TimelineWindow primaire; - private final List - posts = new ArrayList<>(); + private Tree + posts; // - -%- - @@ -679,51 +572,82 @@ implements // ---%-@-%--- - public void - setPosts(List posts) - { - this.posts.clear(); - this.posts.addAll(posts); + public void + displayEntities(Tree postArray) + { + assert postArray.size() <= postPreviews.size(); + this.posts = postArray; - assert posts.size() <= postPreviews.size(); - for (int o = 0; o < posts.size(); ++o) - { - PostPreviewComponent c = postPreviews.get(o); - Post p = posts.get(o); - { - c.setTopLeft(p.authorName); - if (p.boosterName != null) - c.setTopLeft("boosted by " + p.boosterName); + for (int o = 0; o < postArray.size(); ++o) + { + PostPreviewComponent c = postPreviews.get(o); + Tree p = postArray.get(o); + Tree a = p.get("account"); + + String an = a.get("display_name").value; + if (an.isEmpty()) an = a.get("username").value; + c.setTopLeft(an); + + Tree 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; - ZonedDateTime now = ZonedDateTime.now(); - long d = ChronoUnit.SECONDS.between(p.date, now); + String f = ""; + if (p.get("media_attachments").size() > 0) f += "a"; + 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); if (s < 30) t = "now"; else if (s < 60) t = d + "s"; else if (s < 3600) t = (d / 60) + "m"; else if (s < 86400) t = (d / 3600) + "h"; 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 nodes; + nodes = RudimentaryHTMLParser.depthlessRead(html); + for (Tree 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) { postPreviews.get(o).reset(); } - } + } public void setPageLabel(String label) @@ -768,18 +692,17 @@ implements select(Object c) { assert c instanceof PostPreviewComponent; - PostPreviewComponent p = (PostPreviewComponent)c; - int offset = postPreviews.indexOf(p); - assert offset != -1; - if (offset < posts.size()) { - primaire.postSelected(posts.get(offset)); - p.setSelected(true); - } - else { - p.setSelected(false); - } - p.repaint(); + for (int o = 0; o < postPreviews.size(); ++o) + { + PostPreviewComponent p = postPreviews.get(o); + if (c == p) { + primaire.postSelected(posts.get(o)); + p.setSelected(true); + p.repaint(); + } + else deselect(p); + } } private void @@ -798,11 +721,10 @@ implements assert c instanceof PostPreviewComponent; PostPreviewComponent p = (PostPreviewComponent)c; - int offset = postPreviews.indexOf(p); - assert offset != -1; - if (offset < posts.size()) { - primaire.postOpened(posts.get(offset)); - } + int o = postPreviews.indexOf(p); + assert o != -1; + if (o >= posts.size()) return; + primaire.postOpened(posts.get(o)); } public void