Added reactive updating to TimelineWindow and NotificationsWindow

Thread unsafe, will probably explode eventually
This commit is contained in:
Snowyfox 2022-05-01 23:05:36 -04:00
parent db304ad2df
commit 4ed5b3536c
6 changed files with 387 additions and 280 deletions

View File

@ -32,8 +32,8 @@ JKomasto {
private NotificationsWindow private NotificationsWindow
notificationsWindow; notificationsWindow;
private TimelineWindowUpdater private WindowUpdater
timelineWindowUpdater; windowUpdater;
private MastodonApi private MastodonApi
api; api;
@ -46,22 +46,14 @@ JKomasto {
public void public void
finishedLogin() finishedLogin()
{ {
autoViewWindow.setCursor(new Cursor(Cursor.WAIT_CURSOR));
timelineWindow.setCursor(new Cursor(Cursor.WAIT_CURSOR)); timelineWindow.setCursor(new Cursor(Cursor.WAIT_CURSOR));
notificationsWindow.showLatestPage();
timelineWindow.showLatestPage(); timelineWindow.showLatestPage();
timelineWindow.setLocationByPlatform(true); notificationsWindow.showLatestPage();
timelineWindow.setVisible(true); timelineWindow.setVisible(true);
loginWindow.dispose();
autoViewWindow.setTitle("Auto view - JKomasto");
//autoViewWindow.setVisible(true);
loginWindow.dispose();
autoViewWindow.setCursor(null);
timelineWindow.setCursor(null); timelineWindow.setCursor(null);
timelineWindowUpdater.addWindow(timelineWindow);
} }
public PostWindow public PostWindow
@ -76,10 +68,16 @@ JKomasto {
public NotificationsWindow public NotificationsWindow
getNotificationsWindow() { return notificationsWindow; } getNotificationsWindow() { return notificationsWindow; }
public WindowUpdater
getWindowUpdater() { return windowUpdater; }
// ---%-@-%--- // ---%-@-%---
public static void public static void
main(String... args) { new JKomasto(); } main(String... args)
{
new JKomasto().loginWindow.setVisible(true);
}
// ---%-@-%--- // ---%-@-%---
@ -87,6 +85,7 @@ JKomasto {
JKomasto() JKomasto()
{ {
api = new MastodonApi(); api = new MastodonApi();
windowUpdater = new WindowUpdater(this);
timelineWindow = new TimelineWindow(this); timelineWindow = new TimelineWindow(this);
composeWindow = new ComposeWindow(this); composeWindow = new ComposeWindow(this);
@ -95,15 +94,16 @@ JKomasto {
mediaWindow = new ImageWindow(); mediaWindow = new ImageWindow();
notificationsWindow = new NotificationsWindow(this); notificationsWindow = new NotificationsWindow(this);
autoViewWindow.setTitle("Auto view - JKomasto");
composeWindow.dispose(); composeWindow.dispose();
autoViewWindow.dispose(); autoViewWindow.dispose();
timelineWindow.dispose(); timelineWindow.dispose();
mediaWindow.dispose(); mediaWindow.dispose();
notificationsWindow.dispose(); notificationsWindow.dispose();
loginWindow.setLocationByPlatform(true);
loginWindow.setVisible(true);
timelineWindowUpdater = new TimelineWindowUpdater(this); timelineWindow.setLocationByPlatform(true);
loginWindow.setLocationByPlatform(true);
} }
} }

View File

@ -14,6 +14,7 @@ import java.io.FileWriter;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.nio.channels.ClosedByInterruptException;
class class
MastodonApi { MastodonApi {
@ -440,6 +441,7 @@ MastodonApi {
public void public void
monitorTimeline( monitorTimeline(
TimelineType type, ServerSideEventsListener handler) TimelineType type, ServerSideEventsListener handler)
throws InterruptedException
{ {
String token = accessToken.get("access_token").value; String token = accessToken.get("access_token").value;
@ -474,11 +476,16 @@ MastodonApi {
input = new InputStreamReader(conn.getInputStream()); input = new InputStreamReader(conn.getInputStream());
BufferedReader br = new BufferedReader(input); BufferedReader br = new BufferedReader(input);
while (true) { while (true)
{
String line = br.readLine(); String line = br.readLine();
if (line != null) handler.lineReceived(line); if (line != null) handler.lineReceived(line);
} }
} }
catch (ClosedByInterruptException eIt)
{
throw new InterruptedException();
}
catch (IOException eIo) { handler.connectionFailed(eIo); } catch (IOException eIo) { handler.connectionFailed(eIo); }
} }

View File

@ -40,6 +40,9 @@ NotificationsWindow extends JFrame {
private NotificationsComponent private NotificationsComponent
display; display;
private boolean
showingLatest;
// - -%- - // - -%- -
private static int private static int
@ -48,7 +51,7 @@ NotificationsWindow extends JFrame {
// ---%-@-%--- // ---%-@-%---
public void public void
displayEntity(Tree<String> entity) readEntity(Tree<String> entity)
{ {
notifications = new ArrayList<>(); notifications = new ArrayList<>();
for (Tree<String> t: entity) for (Tree<String> t: entity)
@ -97,20 +100,45 @@ NotificationsWindow extends JFrame {
} }
} }
public void
refresh()
{
String firstId = null;
if (!showingLatest)
{
assert !notifications.isEmpty();
firstId = notifications.get(0).id;
}
if (fetchPage(firstId, null))
{
if (notifications.size() < ROW_COUNT) showLatestPage();
display.showNotifications(notifications);
}
}
public void public void
showLatestPage() showLatestPage()
{ {
fetchPage(null, null); if (fetchPage(null, null))
display.showNotifications(notifications); {
display.showNotifications(notifications);
showingLatest = true;
primaire.getWindowUpdater().add(this);
}
} }
public void public void
showPrevPage() showPrevPage()
{ {
assert !notifications.isEmpty(); assert !notifications.isEmpty();
fetchPage(null, notifications.get(0).id); if (fetchPage(null, notifications.get(0).id))
if (notifications.size() < ROW_COUNT) showLatestPage(); {
display.showNotifications(notifications); if (notifications.size() < ROW_COUNT) showLatestPage();
display.showNotifications(notifications);
showingLatest = false;
primaire.getWindowUpdater().remove(this);
}
} }
public void public void
@ -118,41 +146,51 @@ NotificationsWindow extends JFrame {
{ {
assert !notifications.isEmpty(); assert !notifications.isEmpty();
int last = notifications.size() - 1; int last = notifications.size() - 1;
fetchPage(notifications.get(last).id, null); if (fetchPage(notifications.get(last).id, null))
display.showNotifications(notifications); {
display.showNotifications(notifications);
showingLatest = false;
primaire.getWindowUpdater().remove(this);
}
} }
// - -%- - // - -%- -
private void private boolean
fetchPage(String maxId, String minId) fetchPage(String maxId, String minId)
{ {
display.setCursor(new Cursor(Cursor.WAIT_CURSOR)); display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
api.getNotifications( class Handler implements RequestListener {
ROW_COUNT, maxId, minId,
new RequestListener() {
public void boolean
connectionFailed(IOException eIo) succeeded = false;
{
eIo.printStackTrace();
}
public void // -=%=-
requestFailed(int httpCode, Tree<String> json)
{
System.err.println(httpCode + json.get("error").value);
}
public void public void
requestSucceeded(Tree<String> json) connectionFailed(IOException eIo)
{ {
displayEntity(json); eIo.printStackTrace();
} }
}
); public void
requestFailed(int httpCode, Tree<String> json)
{
System.err.println(httpCode + json.get("error").value);
}
public void
requestSucceeded(Tree<String> json)
{
readEntity(json);
succeeded = true;
}
}
Handler handler = new Handler();
api.getNotifications(ROW_COUNT, maxId, minId, handler);
display.setCursor(null); display.setCursor(null);
repaint(); repaint();
return handler.succeeded;
} }
// ---%-@-%--- // ---%-@-%---

View File

@ -55,6 +55,9 @@ implements ActionListener {
private MastodonApi private MastodonApi
api; api;
private WindowUpdater
windowUpdater;
private TimelinePage private TimelinePage
page; page;
@ -78,6 +81,9 @@ implements ActionListener {
private JMenuItem private JMenuItem
flipToNewestPost; flipToNewestPost;
private boolean
showingLatest;
// - -%- - // - -%- -
private static final int private static final int
@ -158,10 +164,14 @@ implements ActionListener {
public void public void
refresh() refresh()
{ {
assert page.posts != null; String firstId = null;
assert page.posts.size() != 0; if (!showingLatest)
Tree<String> first = page.posts.get(0); {
String firstId = first.get("id").value; assert page.posts != null;
assert page.posts.size() != 0;
Tree<String> first = page.posts.get(0);
firstId = first.get("id").value;
}
display.setCursor(new Cursor(Cursor.WAIT_CURSOR)); display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
api.getTimelinePage( api.getTimelinePage(
@ -241,6 +251,8 @@ implements ActionListener {
requestSucceeded(Tree<String> json) requestSucceeded(Tree<String> json)
{ {
readEntity(json); readEntity(json);
showingLatest = true;
windowUpdater.add(TimelineWindow.this);
} }
} }
@ -294,6 +306,8 @@ implements ActionListener {
return; return;
} }
readEntity(json); readEntity(json);
showingLatest = false;
windowUpdater.remove(TimelineWindow.this);
} }
} }
@ -345,6 +359,8 @@ implements ActionListener {
return; return;
} }
readEntity(json); readEntity(json);
showingLatest = false;
windowUpdater.remove(TimelineWindow.this);
} }
} }
@ -639,6 +655,7 @@ implements ActionListener {
{ {
this.primaire = primaire; this.primaire = primaire;
this.api = primaire.getMastodonApi(); this.api = primaire.getMastodonApi();
this.windowUpdater = primaire.getWindowUpdater();
getContentPane().setPreferredSize(new Dimension(320, 460)); getContentPane().setPreferredSize(new Dimension(320, 460));
pack(); pack();

View File

@ -1,230 +0,0 @@
import java.util.List;
import java.util.ArrayList;
import java.io.IOException;
import cafe.biskuteri.hinoki.Tree;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.UnsupportedAudioFileException;
import javax.sound.sampled.LineUnavailableException;
import java.net.URL;
class
TimelineWindowUpdater {
private JKomasto
primaire;
private MastodonApi
api;
// - -%- -
private List<TimelineWindow>
timelineUpdatees;
private List<NotificationsWindow>
notificationUpdatees;
private StringBuilder
event, data;
private Clip
notificationSound;
// - -%- -
private Thread
spublic,
user;
// ---%-@-%---
public void
addWindow(TimelineWindow updatee)
{
timelineUpdatees.add(updatee);
Connection c = new Connection();
c.type = updatee.getTimelineType();
switch (c.type)
{
case FEDERATED:
case LOCAL:
if (spublic != null) return;
spublic = new Thread(c);
spublic.start();
break;
case HOME:
if (user != null) return;
user = new Thread(c);
user.start();
break;
}
}
public void
addWindow(NotificationsWindow updatee)
{
notificationUpdatees.add(updatee);
Connection c = new Connection();
c.type = TimelineType.HOME;
if (user != null) return;
user = new Thread(c);
user.start();
}
public void
removeWindow(TimelineWindow updatee)
{
timelineUpdatees.remove(updatee);
}
public void
removeWindow(NotificationsWindow updatee)
{
notificationUpdatees.remove(updatee);
}
// - -%- -
private void
handle(TimelineType type, String event, String data)
{
assert !data.isEmpty();
if (event.isEmpty()) return;
boolean newPost = event.equals("update");
boolean newNotif = event.equals("notification");
if (!(newPost || newNotif)) return;
for (TimelineWindow updatee: filter(type)) {
updatee.showLatestPage();
}
for (NotificationsWindow updatee: notificationUpdatees) {
updatee.showLatestPage();
}
if (newNotif)
{
notificationSound.setFramePosition(0);
notificationSound.start();
}
}
private List<TimelineWindow>
filter(TimelineType type)
{
List<TimelineWindow> returnee = new ArrayList<>();
for (TimelineWindow updatee: timelineUpdatees)
if (updatee.getTimelineType() == type)
returnee.add(updatee);
return returnee;
}
// ---%-@-%---
private class
Connection
implements Runnable, ServerSideEventsListener {
private TimelineType
type;
// -=-
private StringBuilder
event, data;
// -=%=-
public void
run()
{
event = new StringBuilder();
data = new StringBuilder();
api.monitorTimeline(type, this);
// monitorTimeline should not return
// until the connection is closed.
}
public void
lineReceived(String line)
{
if (line.startsWith(":")) return;
if (line.isEmpty())
{
handle(type, event.toString(), data.toString());
event.delete(0, event.length());
data.delete(0, event.length());
}
if (line.startsWith("data: "))
data.append(line.substring("data: ".length()));
if (line.startsWith("event: "))
event.append(line.substring("event: ".length()));
/*
* Note that I utterly ignore https://html.spec.whatwg.org
* /multipage/server-sent-events.html#dispatchMessage.
* That is because I am not a browser.
*/
}
public void
connectionFailed(IOException eIo)
{
// sais pas dois-je faire..
eIo.printStackTrace();
}
public void
requestFailed(int httpCode, Tree<String> json)
{
// mo shiranu
System.err.println(httpCode + ", " + json.get("error").value);
}
}
// ---%-@-%---
TimelineWindowUpdater(JKomasto primaire)
{
this.primaire = primaire;
this.api = primaire.getMastodonApi();
this.timelineUpdatees = new ArrayList<>();
this.notificationUpdatees = new ArrayList<>();
loadNotificationSound();
}
void
loadNotificationSound()
{
URL url = getClass().getResource("KDE_Dialog_Appear.wav");
try {
Clip clip = AudioSystem.getClip();
clip.open(AudioSystem.getAudioInputStream(url));
notificationSound = clip;
}
catch (LineUnavailableException eLu) {
assert false;
}
catch (UnsupportedAudioFileException eUa) {
assert false;
}
catch (IOException eIo) {
assert false;
}
catch (IllegalArgumentException eIa) {
assert false;
}
}
}

275
WindowUpdater.java Normal file
View File

@ -0,0 +1,275 @@
import java.util.List;
import java.util.ArrayList;
import java.io.IOException;
import cafe.biskuteri.hinoki.Tree;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.UnsupportedAudioFileException;
import javax.sound.sampled.LineUnavailableException;
import java.net.URL;
class
WindowUpdater {
private JKomasto
primaire;
private MastodonApi
api;
// - -%- -
private List<TimelineWindow>
timelineWindows;
private List<NotificationsWindow>
notificationWindows;
private Clip
notificationSound;
private Connection
publicConn,
userConn;
// ---%-@-%---
public void
add(TimelineWindow updatee)
{
if (timelineWindows.contains(updatee)) return;
timelineWindows.add(updatee);
publicConn.reevaluate();
userConn.reevaluate();
}
public void
add(NotificationsWindow updatee)
{
if (notificationWindows.contains(updatee)) return;
notificationWindows.add(updatee);
userConn.reevaluate();
}
public void
remove(TimelineWindow updatee)
{
timelineWindows.remove(updatee);
publicConn.reevaluate();
userConn.reevaluate();
}
public void
remove(NotificationsWindow updatee)
{
notificationWindows.remove(updatee);
userConn.reevaluate();
}
// ---%-@-%---
private class
Connection
implements Runnable, ServerSideEventsListener {
private TimelineType
type;
// -=-
private Thread
thread;
private StringBuilder
event, data;
// -=%=-
public void
restart()
{
if (thread != null) stop();
thread = new Thread(this);
thread.start();
}
public void
stop()
{
try {
thread.interrupt();
thread.join();
thread = null;
}
catch (InterruptedException eIt) {
assert false;
// Who would do that to us..
}
}
public void
reevaluate()
{
boolean hasUpdatee = false;
for (NotificationsWindow updatee: notificationWindows)
if (responsibleFor(updatee)) hasUpdatee = true;
for (TimelineWindow updatee: timelineWindows)
if (responsibleFor(updatee)) hasUpdatee = true;
if (!hasUpdatee && thread != null) stop();
if (hasUpdatee && thread == null) restart();
}
// -=-
private boolean
responsibleFor(TimelineWindow updatee)
{
return type == updatee.getTimelineType();
}
private boolean
responsibleFor(NotificationsWindow updatee)
{
return type == TimelineType.HOME;
}
public void
run()
{
try {
event = new StringBuilder();
data = new StringBuilder();
api.monitorTimeline(type, this);
// monitorTimeline should not return
// until the connection is closed.
}
catch (InterruptedException eIt) { }
}
public void
lineReceived(String line)
{
if (line.startsWith(":")) return;
if (line.isEmpty())
{
handle(event.toString(), data.toString());
event.delete(0, event.length());
data.delete(0, event.length());
}
if (line.startsWith("data: "))
data.append(line.substring("data: ".length()));
if (line.startsWith("event: "))
event.append(line.substring("event: ".length()));
/*
* Note that I ignore https://html.spec.whatwg.org
* /multipage/server-sent-events.html#dispatchMessage.
* That is because I am not a browser.
*/
}
private void
handle(String event, String data)
{
assert !data.isEmpty();
if (event.isEmpty()) return;
boolean newPost = event.equals("update");
boolean newNotif = event.equals("notification");
if (!(newPost || newNotif)) return;
if (newNotif)
{
notificationSound.setFramePosition(0);
notificationSound.start();
}
for (TimelineWindow updatee: timelineWindows)
{
if (!responsibleFor(updatee)) continue;
updatee.refresh();
/*
* () Note that we're in a separate thread,
* and our windows aren't thread-safe. We could
* probably make them a bit bananas asking
* for a refresh while they're in the middle
* of one. Could we add mutexes?
*/
}
for (NotificationsWindow updatee: notificationWindows)
{
if (!responsibleFor(updatee)) continue;
updatee.refresh();
}
}
public void
connectionFailed(IOException eIo)
{
// sais pas dois-je faire..
eIo.printStackTrace();
}
public void
requestFailed(int httpCode, Tree<String> json)
{
// mo shiranu
System.err.println(httpCode + ", " + json.get("error").value);
}
}
// ---%-@-%---
WindowUpdater(JKomasto primaire)
{
this.primaire = primaire;
this.api = primaire.getMastodonApi();
this.timelineWindows = new ArrayList<>();
this.notificationWindows = new ArrayList<>();
publicConn = new Connection();
publicConn.type = TimelineType.FEDERATED;
userConn = new Connection();
userConn.type = TimelineType.HOME;
loadNotificationSound();
}
void
loadNotificationSound()
{
URL url = getClass().getResource("KDE_Dialog_Appear.wav");
try {
Clip clip = AudioSystem.getClip();
clip.open(AudioSystem.getAudioInputStream(url));
notificationSound = clip;
}
catch (LineUnavailableException eLu) {
assert false;
}
catch (UnsupportedAudioFileException eUa) {
assert false;
}
catch (IOException eIo) {
assert false;
}
catch (IllegalArgumentException eIa) {
assert false;
}
}
}