biskuteri-cafe-JKomasto2/WindowUpdater.java
Snowyfox e6fea4c061 Fixed bug when redraft makes no changes
(Before this, JKomasto and sometimes the Mastodon web client would get '411 Record Not Found' when submitting the same text after deleting and redrafting. Presumably the Mastodon server caches both whether an idempotency key was fulfilled and which post it leads to, and for some reason it looks up the second and fails.)
2022-05-31 03:39:56 -04:00

318 lines
7.1 KiB
Java

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 synchronized void
add(TimelineWindow updatee)
{
if (!timelineWindows.contains(updatee))
timelineWindows.add(updatee);
publicConn.reevaluate();
userConn.reevaluate();
}
public synchronized void
add(NotificationsWindow updatee)
{
if (!notificationWindows.contains(updatee))
notificationWindows.add(updatee);
userConn.reevaluate();
}
public synchronized void
remove(TimelineWindow updatee)
{
timelineWindows.remove(updatee);
publicConn.reevaluate();
userConn.reevaluate();
}
public synchronized void
remove(NotificationsWindow updatee)
{
notificationWindows.remove(updatee);
userConn.reevaluate();
}
// - -%- -
public static void
printStackTrace(Thread thread)
{
for (StackTraceElement e: thread.getStackTrace())
System.err.println(e);
}
// ---%-@-%---
private class
Connection
implements Runnable, ServerSideEventsListener {
private TimelineType
type;
// -=-
private Thread
thread;
private boolean
stopping;
private StringBuilder
event, data;
// -=%=-
public void
start()
{
stopping = false;
thread = new Thread(this);
thread.setDaemon(true);
try
{
synchronized (thread)
{
thread.start();
thread.wait();
}
}
catch (InterruptedException eIt)
{
assert false;
}
}
public void
stop()
{
stopping = true;
thread.interrupt();
try
{
thread.join(3000);
/*
* That thread should notice it is
* interrupted ppromptly, and close.
*/
if (thread.isAlive()) printStackTrace(thread);
}
catch (InterruptedException eIt)
{
assert false;
}
thread = null;
}
public void
reevaluate()
{
boolean hasUpdatee = false;
for (NotificationsWindow w: notificationWindows)
if (responsibleFor(w)) hasUpdatee = true;
for (TimelineWindow w: timelineWindows)
if (responsibleFor(w)) hasUpdatee = true;
if (!hasUpdatee && thread != null) stop();
if (hasUpdatee && thread == null) start();
}
// -=-
public void
run()
{
synchronized (thread)
{
thread.notifyAll();
}
event = new StringBuilder();
data = new StringBuilder();
api.monitorTimeline(type, this);
// monitorTimeline should not return until
// the connection is closed, or this thread
// is interrupted.
System.err.println(
"Stopped monitoring."
+ thread + " " + Thread.currentThread()
);
if (thread == Thread.currentThread()) thread = null;
/*
* This isn't thread safe. But I'd like the
* restart after sleep mode, so.
*/
}
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();
}
synchronized (WindowUpdater.this)
{
for (TimelineWindow w: timelineWindows)
{
if (!responsibleFor(w)) continue;
w.refresh();
}
}
synchronized (WindowUpdater.this)
{
for (NotificationsWindow w: notificationWindows)
{
if (!responsibleFor(w)) continue;
w.refresh();
}
}
}
private boolean
responsibleFor(TimelineWindow updatee)
{
return type == updatee.getTimelineType();
}
private boolean
responsibleFor(NotificationsWindow updatee)
{
return type == TimelineType.HOME;
}
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;
}
}
}