diff --git a/JKomasto.java b/JKomasto.java index b8571e6..4110601 100755 --- a/JKomasto.java +++ b/JKomasto.java @@ -28,6 +28,9 @@ JKomasto { private ImageWindow mediaWindow; + private NotificationsWindow + notificationsWindow; + private TimelineWindowUpdater timelineWindowUpdater; @@ -45,6 +48,7 @@ JKomasto { autoViewWindow.setCursor(new Cursor(Cursor.WAIT_CURSOR)); timelineWindow.setCursor(new Cursor(Cursor.WAIT_CURSOR)); + notificationsWindow.showLatestPage(); timelineWindow.showLatestPage(); timelineWindow.setLocationByPlatform(true); timelineWindow.setVisible(true); @@ -68,6 +72,9 @@ JKomasto { public ImageWindow getMediaWindow() { return mediaWindow; } + public NotificationsWindow + getNotificationsWindow() { return notificationsWindow; } + // ---%-@-%--- public static void @@ -85,11 +92,13 @@ JKomasto { autoViewWindow = new PostWindow(this); loginWindow = new LoginWindow(this); mediaWindow = new ImageWindow(); + notificationsWindow = new NotificationsWindow(this); composeWindow.dispose(); autoViewWindow.dispose(); timelineWindow.dispose(); mediaWindow.dispose(); + notificationsWindow.dispose(); loginWindow.setLocationByPlatform(true); loginWindow.setVisible(true); @@ -116,12 +125,23 @@ TimelineType { FEDERATED, LOCAL, HOME, - NOTIFICATIONS, - CONVERSATIONS, LIST } +enum +NotificationType { + + MENTION, + BOOST, + FAVOURITE, + FOLLOW, + FOLLOWREQ, + POLL, + ALERT + +} + class @@ -131,7 +151,7 @@ TimelinePage { type; public String - accountNumId; + accountNumId, listId; public List posts; @@ -180,6 +200,20 @@ Post { } +class +Notification { + + public NotificationType + type; + + public String + id; + + public String + postId, postText, actorNumId, actorName; + +} + class Attachment { diff --git a/MastodonApi.java b/MastodonApi.java index c83004f..483e05d 100644 --- a/MastodonApi.java +++ b/MastodonApi.java @@ -178,29 +178,29 @@ MastodonApi { public void getTimelinePage( - TimelineType type, String accountId, + TimelineType type, int count, String maxId, String minId, + String accountId, String listId, RequestListener handler) { String token = accessToken.get("access_token").value; + assert !(accountId != null && listId != null); + String url = instanceUrl + "/api/v1"; if (accountId != null) { url += "/accounts/" + accountId + "/statuses"; } + else if (listId != null) + { + url += "/lists/" + listId; + } else switch (type) { case FEDERATED: case LOCAL: url += "/timelines/public"; break; case HOME: url += "/timelines/home"; break; - case NOTIFICATIONS: - url += "/notifications"; - // Note that this endpoint returns Notifications, - // not Statuses. But we uniformly return Tree, - // we expect the caller can handle it. - break; - case CONVERSATIONS: url += "/timelines/public"; break; default: assert false; } url += "?limit=" + count; @@ -325,6 +325,32 @@ MastodonApi { catch (IOException eIo) { handler.connectionFailed(eIo); } } + public void + getNotifications( + int count, String maxId, String minId, + RequestListener handler) + { + String token = accessToken.get("access_token").value; + + String url = instanceUrl + "/api/v1/notifications"; + url += "?limit=" + count; + if (maxId != null) url += "&max_id=" + maxId; + if (minId != null) url += "&min_id=" + minId; + + try + { + URL endpoint = new URL(url); + HttpURLConnection conn; + conn = (HttpURLConnection)endpoint.openConnection(); + String s1 = "Bearer " + token; + conn.setRequestProperty("Authorization", s1); + conn.connect(); + + doStandardJsonReturn(conn, handler); + } + catch (IOException eIo) { handler.connectionFailed(eIo); } + } + public void monitorTimeline( TimelineType type, ServerSideEventsListener handler) @@ -336,8 +362,7 @@ MastodonApi { { case FEDERATED: url += "/public"; break; case LOCAL: url += "/public/local"; break; - case HOME: - case NOTIFICATIONS: url += "/user"; break; + case HOME: url += "/user"; break; default: assert false; } diff --git a/NotificationsWindow.java b/NotificationsWindow.java new file mode 100644 index 0000000..635fc6d --- /dev/null +++ b/NotificationsWindow.java @@ -0,0 +1,330 @@ + +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.BorderFactory; +import javax.swing.border.Border; +import java.awt.Font; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.GridLayout; +import java.awt.BorderLayout; +import java.util.List; +import java.util.ArrayList; +import java.awt.event.ActionListener; +import java.awt.event.ActionEvent; +import java.awt.Cursor; +import cafe.biskuteri.hinoki.Tree; +import java.io.IOException; +import java.awt.event.ComponentListener; +import java.awt.event.ComponentEvent; + +class +NotificationsWindow extends JFrame { + + private JKomasto + primaire; + + private List + notifications; + + private MastodonApi + api; + +// - -%- - + + private NotificationsComponent + display; + +// - -%- - + + private static int + ROW_COUNT = NotificationsComponent.ROW_COUNT; + +// ---%-@-%--- + + public void + showLatestPage() + { + fetchPage(null, null); + display.showNotifications(notifications); + } + + public void + showPrevPage() + { + assert !notifications.isEmpty(); + fetchPage(null, notifications.get(0).id); + if (notifications.size() < ROW_COUNT) showLatestPage(); + display.showNotifications(notifications); + } + + public void + showNextPage() + { + assert !notifications.isEmpty(); + int last = notifications.size() - 1; + fetchPage(notifications.get(last).id, null); + display.showNotifications(notifications); + } + +// - -%- - + + private void + fetchPage(String maxId, String minId) + { + display.setCursor(new Cursor(Cursor.WAIT_CURSOR)); + api.getNotifications( + ROW_COUNT, maxId, minId, + new RequestListener() { + + public void + connectionFailed(IOException eIo) + { + eIo.printStackTrace(); + } + + public void + requestFailed(int httpCode, Tree json) + { + System.err.println(httpCode + json.get("error").value); + } + + public void + requestSucceeded(Tree json) + { + notifications = new ArrayList<>(); + for (Tree t: json) + { + Notification n = new Notification(); + + n.id = t.get("id").value; + + String type = t.get("type").value; + if (type.equals("favourite")) + n.type = NotificationType.FAVOURITE; + else if (type.equals("reblog")) + n.type = NotificationType.BOOST; + else if (type.equals("mention")) + n.type = NotificationType.MENTION; + else if (type.equals("follow")) + n.type = NotificationType.FOLLOW; + else if (type.equals("follow_request")) + n.type = NotificationType.FOLLOWREQ; + else if (type.equals("poll")) + n.type = NotificationType.POLL; + else if (type.equals("status")) + n.type = NotificationType.ALERT; + + Tree actor = t.get("account"); + String aid, aname, adisp; + aid = actor.get("id").value; + aname = actor.get("username").value; + adisp = actor.get("display_name").value; + if (!adisp.isEmpty()) n.actorName = adisp; + else n.actorName = aname; + n.actorNumId = aid; + + if (n.type != NotificationType.FOLLOW) + { + Tree post = t.get("status"); + String pid = post.get("id").value; + String ptext = post.get("content").value; + n.postId = pid; + n.postText = ptext; + // Should we ask TimelineWindow for help here? + // Or should we break our text parsers into + // a separate class? + } + + notifications.add(n); + } + } + + } + ); + display.setCursor(null); + repaint(); + } + +// ---%-@-%--- + + NotificationsWindow(JKomasto primaire) + { + super("Notifications"); + this.primaire = primaire; + this.api = primaire.getMastodonApi(); + + notifications = new ArrayList<>(); + + display = new NotificationsComponent(this); + display.setPreferredSize(new Dimension(256, 400)); + setContentPane(display); + pack(); + + setDefaultCloseOperation(DISPOSE_ON_CLOSE); + setVisible(true); + } + +} + +class +NotificationsComponent extends JPanel +implements ActionListener { + + private NotificationsWindow + primaire; + + private JButton + prev, next; + +// - -%- - + + private List + rows; + +// - -%- - + + static final int + ROW_COUNT = 16; + +// ---%-@-%--- + + public void + showNotifications(List notifications) + { + assert notifications.size() == rows.size(); + for (int o = 0; o < rows.size(); ++o) + { + Notification n = notifications.get(o); + NotificationComponent c = rows.get(o); + c.setName(n.actorName); + c.setType(n.type.toString()); + c.setText(n.postText); + } + } + +// - -%- - + + public void + actionPerformed(ActionEvent eA) + { + if (eA.getSource() == prev) primaire.showPrevPage(); + if (eA.getSource() == next) primaire.showNextPage(); + } + +// ---%-@-%--- + + NotificationsComponent(NotificationsWindow primaire) + { + this.primaire = primaire; + + Border b = BorderFactory.createEmptyBorder(8, 8, 8, 8); + + rows = new ArrayList<>(); + for (int n = ROW_COUNT; n > 0; --n) + rows.add(new NotificationComponent()); + + prev = new JButton("<"); + next = new JButton(">"); + prev.addActionListener(this); + next.addActionListener(this); + + JPanel centre = new JPanel(); + centre.setLayout(new GridLayout(ROW_COUNT, 1)); + for (NotificationComponent c: rows) centre.add(c); + + Box bottom = Box.createHorizontalBox(); + bottom.add(Box.createGlue()); + bottom.add(prev); + bottom.add(Box.createHorizontalStrut(8)); + bottom.add(next); + bottom.setBorder(b); + + setLayout(new BorderLayout()); + add(centre); + add(bottom, BorderLayout.SOUTH); + } + +} + +class +NotificationComponent extends JComponent +implements ComponentListener { + + private JLabel + type; + + private JLabel + name, text; + +// ---%-@-%--- + + public void + setType(String n) { type.setText(n); } + + public void + setName(String n) { name.setText(n); } + + public void + setText(String n) { text.setText(n); } + +// - -%- - + + public void + componentResized(ComponentEvent eC) + { + int w = getWidth(), h = getHeight(); + name.setPreferredSize(new Dimension(w * 4 / 10, h)); + type.setPreferredSize(new Dimension(w * 3 / 10, h)); + text.setPreferredSize(new Dimension(w * 2 / 10, h)); + + name.setMaximumSize(new Dimension(w * 4 / 10, h)); + type.setMaximumSize(new Dimension(w * 3 / 10, h)); + text.setMaximumSize(new Dimension(w * 2 / 10, h)); + } + + public void + componentShown(ComponentEvent eC) { } + + public void + componentHidden(ComponentEvent eC) { } + + public void + componentMoved(ComponentEvent eC) { } + +// ---%-@-%--- + + NotificationComponent() + { + Font f1 = new Font("Dialog", Font.PLAIN, 12); + Font f2 = new Font("Dialog", Font.PLAIN, 10); + Font f3 = new Font("Dialog", Font.ITALIC, 12); + + name = new JLabel(); + type = new JLabel(); + text = new JLabel(); + name.setFont(f1); + type.setFont(f2); + text.setFont(f3); + type.setHorizontalAlignment(JLabel.RIGHT); + + setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); + add(name); + add(Box.createHorizontalStrut(4)); + add(type); + add(Box.createHorizontalStrut(4)); + add(text); + + this.addComponentListener(this); + setBorder( + BorderFactory.createMatteBorder + (1, 0, 0, 0, new Color(0, 0, 0, 25)) + ); + } + +} \ No newline at end of file diff --git a/TimelineWindow.java b/TimelineWindow.java index 6c24e99..d9ab2e8 100755 --- a/TimelineWindow.java +++ b/TimelineWindow.java @@ -65,6 +65,7 @@ implements ActionListener { openMessages, openLocal, openFederated, + openNotifications, createPost, openAutoPostView, quit; @@ -113,8 +114,9 @@ implements ActionListener { { display.setCursor(new Cursor(Cursor.WAIT_CURSOR)); api.getTimelinePage( - page.type, page.accountNumId, - PREVIEW_COUNT, null, null, + page.type, + PREVIEW_COUNT, null, null, + page.accountNumId, page.listId, new RequestListener() { public void @@ -159,8 +161,9 @@ implements ActionListener { display.setCursor(new Cursor(Cursor.WAIT_CURSOR)); api.getTimelinePage( - page.type, page.accountNumId, - PREVIEW_COUNT, last.postId, null, + page.type, + PREVIEW_COUNT, last.postId, null, + page.accountNumId, page.listId, new RequestListener() { public void @@ -208,8 +211,9 @@ implements ActionListener { display.setCursor(new Cursor(Cursor.WAIT_CURSOR)); api.getTimelinePage( - page.type, page.accountNumId, - PREVIEW_COUNT, null, first.postId, + page.type, + PREVIEW_COUNT, null, first.postId, + page.accountNumId, page.listId, new RequestListener() { public void @@ -291,6 +295,12 @@ implements ActionListener { PostWindow w = primaire.getAutoViewWindow(); w.setLocation(getX() + 10 + getWidth(), getY()); w.setVisible(true); + } + if (src == openNotifications) + { + NotificationsWindow w = primaire.getNotificationsWindow(); + w.setLocationByPlatform(true); + w.setVisible(true); } if (src == flipToNewestPost) { @@ -447,11 +457,13 @@ implements ActionListener { openHome = new JMenuItem("Open home timeline"); openFederated = new JMenuItem("Open federated timeline"); + openNotifications = new JMenuItem("Open notifications"); createPost = new JMenuItem("Create a post"); openAutoPostView = new JMenuItem("Open auto post view"); quit = new JMenuItem("Quit"); openHome.addActionListener(this); openFederated.addActionListener(this); + openNotifications.addActionListener(this); createPost.addActionListener(this); openAutoPostView.addActionListener(this); quit.addActionListener(this); @@ -463,6 +475,7 @@ implements ActionListener { programMenu.setMnemonic(KeyEvent.VK_P); programMenu.add(openHome); programMenu.add(openFederated); + programMenu.add(openNotifications); programMenu.add(new JSeparator()); programMenu.add(createPost); programMenu.add(openAutoPostView); diff --git a/TimelineWindowUpdater.java b/TimelineWindowUpdater.java index fa9a2aa..200d574 100755 --- a/TimelineWindowUpdater.java +++ b/TimelineWindowUpdater.java @@ -26,8 +26,7 @@ TimelineWindowUpdater { private Thread federated, local, - home, - notifications; + home; // ---%-@-%--- @@ -49,9 +48,6 @@ TimelineWindowUpdater { case HOME: if (home != null) return; home = t; break; - case NOTIFICATIONS: - if (notifications != null) return; - notifications = t; break; default: return; } t.start();