biskuteri-cafe-JKomasto2/RepliesWindow.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

314 lines
7.4 KiB
Java

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTree;
import javax.swing.JOptionPane;
import javax.swing.BorderFactory;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeNode;
import javax.swing.tree.MutableTreeNode;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.TreeSelectionModel;
import javax.swing.event.TreeSelectionListener;
import javax.swing.event.TreeSelectionEvent;
import java.awt.Dimension;
import java.awt.Cursor;
import java.awt.BorderLayout;
import java.util.Enumeration;
import cafe.biskuteri.hinoki.Tree;
import java.io.IOException;
class
RepliesWindow extends JFrame {
private JKomasto
primaire;
private MastodonApi
api;
private PostWindow
postWindow;
// - -%- -
private RepliesComponent
display;
// ---%-@-%---
public synchronized void
showFor(String postId)
{
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
Tree<String> thread = getThread(postId);
if (thread != null) display.showThread(thread);
display.setCursor(null);
if (thread == null) dispose();
}
// - -%- -
public synchronized void
postSelected(Tree<String> post)
{
postWindow.readEntity(post);
postWindow.setVisible(true);
}
private Tree<String>
getThread(String postId)
{
abstract class Handler implements RequestListener {
boolean
failed = false;
// -=%=-
public void
connectionFailed(IOException eIo)
{
JOptionPane.showMessageDialog(
RepliesWindow.this,
"Failed to fetch post context...."
+ "\n" + eIo.getMessage()
);
failed = true;
}
public void
requestFailed(int httpCode, Tree<String> json)
{
JOptionPane.showMessageDialog(
RepliesWindow.this,
"Failed to fetch post context...."
+ "\n" + json.get("error").value
+ "\n(HTTP code: " + httpCode + ")"
);
failed = true;
}
}
class TopPostIdGetter extends Handler {
String
topPostId;
// -=%=-
public void
requestSucceeded(Tree<String> json)
{
Tree<String> ancestors = json.get("ancestors");
if (ancestors.size() == 0) topPostId = postId;
else topPostId = ancestors.get(0).get("id").value;
}
};
class DescendantsGetter extends Handler {
Tree<String>
descendants;
// -=%=-
public void
requestSucceeded(Tree<String> json)
{
descendants = json.get("descendants");
}
};
class PostGetter extends Handler {
Tree<String>
post;
// -=%=-
public void
requestSucceeded(Tree<String> json)
{
post = json;
}
}
TopPostIdGetter phase1 = new TopPostIdGetter();
api.getPostContext(postId, phase1);
if (phase1.failed) return null;
DescendantsGetter phase2 = new DescendantsGetter();
api.getPostContext(phase1.topPostId, phase2);
if (phase2.failed) return null;
PostGetter phase3 = new PostGetter();
api.getSpecificPost(phase1.topPostId, phase3);
if (phase3.failed) return null;
Tree<String> thread = new Tree<String>();
phase3.post.key = "top";
thread.add(phase3.post);
thread.add(phase2.descendants);
return thread;
}
// ---%-@-%---
RepliesWindow(JKomasto primaire, PostWindow postWindow)
{
super("Thread");
this.primaire = primaire;
this.api = primaire.getMastodonApi();
this.postWindow = postWindow;
display = new RepliesComponent(this);
setContentPane(display);
setSize(384, 224);
setIconImage(primaire.getProgramIcon());
}
}
class
RepliesComponent extends JPanel
implements TreeSelectionListener {
private RepliesWindow
primaire;
private Tree<String>
thread;
// - -%- -
private JTree
tree;
// ---%-@-%---
public void
showThread(Tree<String> thread)
{
Enumeration<TreeNode> e;
DefaultMutableTreeNode root;
TreeItem item;
item = new TreeItem(thread.get("top"));
root = new DefaultMutableTreeNode(item);
for (Tree<String> desc: thread.get("descendants"))
{
String target = desc.get("in_reply_to_id").value;
assert target != null;
DefaultMutableTreeNode p = null;
e = root.breadthFirstEnumeration();
while (e.hasMoreElements())
{
DefaultMutableTreeNode node;
node = (DefaultMutableTreeNode)e.nextElement();
item = (TreeItem)node.getUserObject();
String postId = item.post.get("id").value;
if (postId.equals(target))
{
p = node;
break;
}
}
if (p == null)
{
assert false;
/*
* Besides descendants possibly not being in order,
* the top of the thread might be deleted and so
* thread.top gets set to the given post. Which
* sibling replies aren't replying to, resulting
* in assertion failure.
*/
continue;
}
item = new TreeItem(desc);
p.add(new DefaultMutableTreeNode(item));
}
tree.setModel(new DefaultTreeModel(root));
for (int o = 0; o < tree.getRowCount(); ++o)
tree.expandRow(o);
}
// - -%- -
public void
valueChanged(TreeSelectionEvent eT)
{
Object selected = eT.getPath().getLastPathComponent();
assert selected instanceof DefaultMutableTreeNode;
TreeItem item = (TreeItem)
((DefaultMutableTreeNode)selected)
.getUserObject();
primaire.postSelected(item.post);
}
// ---%-@-%---
private static class
TreeItem {
public Tree<String>
post;
// -=%=-
public String
toString()
{
Post post = new Post(this.post);
post.resolveApproximateText();
return post.approximateText;
}
// -=%=-
TreeItem(Tree<String> post)
{
this.post = post;
}
}
// ---%-@-%---
RepliesComponent(RepliesWindow primaire)
{
this.primaire = primaire;
tree = new JTree();
tree.setBackground(null);
DefaultTreeCellRenderer renderer;
renderer = new DefaultTreeCellRenderer();
renderer.setBackgroundNonSelectionColor(null);
renderer.setOpenIcon(null);
renderer.setClosedIcon(null);
renderer.setLeafIcon(null);
tree.setCellRenderer(renderer);
int mode = TreeSelectionModel.SINGLE_TREE_SELECTION;
tree.getSelectionModel().setSelectionMode(mode);
tree.addTreeSelectionListener(this);
tree.setFont(tree.getFont().deriveFont(16f));
setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
setLayout(new BorderLayout());
add(tree);
}
}