Narrow the desktop contact list

Each contact shows just the date it was last seen, without the client
dot and at half the usual nesting; the full time and server move under
the phonetic nickname as "Last seen … on …".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-26 11:39:15 +00:00
parent fedb7b4385
commit 75df46a03a

View File

@@ -30,6 +30,8 @@ import javax.swing.JTree;
import javax.swing.KeyStroke; import javax.swing.KeyStroke;
import javax.swing.SwingUtilities; import javax.swing.SwingUtilities;
import javax.swing.TransferHandler; import javax.swing.TransferHandler;
import javax.swing.border.Border;
import javax.swing.plaf.basic.BasicTreeUI;
import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellRenderer; import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.DefaultTreeModel;
@@ -70,6 +72,7 @@ import java.util.regex.Pattern;
final class ContactsDialog extends JDialog { final class ContactsDialog extends JDialog {
private static final DateTimeFormatter SEEN = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); private static final DateTimeFormatter SEEN = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
private static final DateTimeFormatter SEEN_DAY = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static final Pattern CLIENT_LINK = Pattern.compile("client://[^\\]\\s]+"); private static final Pattern CLIENT_LINK = Pattern.compile("client://[^\\]\\s]+");
private final ContactStore contacts; private final ContactStore contacts;
@@ -89,6 +92,7 @@ final class ContactsDialog extends JDialog {
private final JRadioButton blockedRadio = new JRadioButton("Blocked"); private final JRadioButton blockedRadio = new JRadioButton("Blocked");
private final JRadioButton friendRadio = new JRadioButton("Friend"); private final JRadioButton friendRadio = new JRadioButton("Friend");
private final JTextField phoneticField = new JTextField(18); private final JTextField phoneticField = new JTextField(18);
private final JLabel lastSeenLabel = new JLabel(" ");
private final JRadioButton bothRadio = new JRadioButton(NicknameDisplay.BOTH.label()); private final JRadioButton bothRadio = new JRadioButton(NicknameDisplay.BOTH.label());
private final JRadioButton onlyCustomRadio = new JRadioButton(NicknameDisplay.ONLY_CUSTOM.label()); private final JRadioButton onlyCustomRadio = new JRadioButton(NicknameDisplay.ONLY_CUSTOM.label());
private final JRadioButton onlyNicknameRadio = new JRadioButton(NicknameDisplay.ONLY_NICKNAME.label()); private final JRadioButton onlyNicknameRadio = new JRadioButton(NicknameDisplay.ONLY_NICKNAME.label());
@@ -173,6 +177,12 @@ final class ContactsDialog extends JDialog {
root.add(neutral); root.add(neutral);
tree.setRootVisible(false); tree.setRootVisible(false);
tree.setShowsRootHandles(true); tree.setShowsRootHandles(true);
// Half the usual nesting, so long names and their dates fit in the narrow list.
if (tree.getUI() instanceof BasicTreeUI) {
BasicTreeUI ui = (BasicTreeUI) tree.getUI();
ui.setLeftChildIndent(UIScale.scale(4));
ui.setRightChildIndent(UIScale.scale(5));
}
tree.setRowHeight(UIScale.scale(20)); tree.setRowHeight(UIScale.scale(20));
tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
tree.setCellRenderer(new ContactRenderer()); tree.setCellRenderer(new ContactRenderer());
@@ -332,6 +342,8 @@ final class ContactsDialog extends JDialog {
c.weightx = 1; c.weightx = 1;
c.fill = GridBagConstraints.HORIZONTAL; c.fill = GridBagConstraints.HORIZONTAL;
row.add(phoneticField, c); row.add(phoneticField, c);
c.gridy = 4;
row.add(lastSeenLabel, c);
return row; return row;
} }
@@ -426,10 +438,12 @@ final class ContactsDialog extends JDialog {
if (c == null) { if (c == null) {
nicknameField.setText(""); nicknameField.setText("");
phoneticField.setText(""); phoneticField.setText("");
lastSeenLabel.setText(" ");
return; return;
} }
nicknameField.setText(c.nickname); nicknameField.setText(c.nickname);
phoneticField.setText(c.phoneticNickname); phoneticField.setText(c.phoneticNickname);
lastSeenLabel.setText(lastSeen(c));
(c.type == ContactType.FRIEND ? friendRadio : c.type == ContactType.BLOCKED ? blockedRadio : neutralRadio) (c.type == ContactType.FRIEND ? friendRadio : c.type == ContactType.BLOCKED ? blockedRadio : neutralRadio)
.setSelected(true); .setSelected(true);
(c.nicknameDisplay == NicknameDisplay.ONLY_CUSTOM ? onlyCustomRadio (c.nicknameDisplay == NicknameDisplay.ONLY_CUSTOM ? onlyCustomRadio
@@ -447,6 +461,12 @@ final class ContactsDialog extends JDialog {
} }
} }
private static String lastSeen(Contact c) {
if (c.lastSeen == null) return "Never seen";
String where = c.lastSeenServerName.isEmpty() ? c.lastSeenServerAddress : c.lastSeenServerName;
return "Last seen " + SEEN.format(c.lastSeen) + (where.isEmpty() ? "" : " on " + where);
}
private static void setDetailsEnabled(Component component, boolean enabled) { private static void setDetailsEnabled(Component component, boolean enabled) {
component.setEnabled(enabled); component.setEnabled(enabled);
if (component instanceof java.awt.Container) { if (component instanceof java.awt.Container) {
@@ -514,6 +534,10 @@ final class ContactsDialog extends JDialog {
// ---- rendering ---- // ---- rendering ----
private static final class ContactRenderer extends DefaultTreeCellRenderer { private static final class ContactRenderer extends DefaultTreeCellRenderer {
private final Border margin = getBorder();
/** Keeps a group's name clear of its expand arrow, which the tight nesting brings close. */
private final Border groupMargin = BorderFactory.createCompoundBorder(margin, new ScaledEmptyBorder(0, 4, 0, 0));
@Override @Override
public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded,
boolean leaf, int row, boolean hasFocus) { boolean leaf, int row, boolean hasFocus) {
@@ -522,23 +546,18 @@ final class ContactsDialog extends JDialog {
if (obj instanceof Contact) { if (obj instanceof Contact) {
Contact c = (Contact) obj; Contact c = (Contact) obj;
setText("<html>" + esc(c.nickname) + " <span style='color:" + hex(sel ? getForeground() : Theme.chatSystem()) setText("<html>" + esc(c.nickname) + " <span style='color:" + hex(sel ? getForeground() : Theme.chatSystem())
+ "'>&nbsp;" + esc(lastSeen(c)) + "</span></html>"); + "'>&nbsp;" + (c.lastSeen == null ? "Never" : SEEN_DAY.format(c.lastSeen)) + "</span></html>");
setIcon(Icons.clientIdle());
setFont(Theme.uiFont()); setFont(Theme.uiFont());
setBorder(margin);
} else { } else {
setText(String.valueOf(obj)); setText(String.valueOf(obj));
setIcon(null);
setFont(Theme.uiBold()); setFont(Theme.uiBold());
setBorder(groupMargin);
} }
setIcon(null);
return this; return this;
} }
private static String lastSeen(Contact c) {
if (c.lastSeen == null) return "Never";
String where = c.lastSeenServerName.isEmpty() ? c.lastSeenServerAddress : c.lastSeenServerName;
return where.isEmpty() ? SEEN.format(c.lastSeen) : SEEN.format(c.lastSeen) + " on " + where;
}
private static String esc(String s) { private static String esc(String s) {
return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;"); return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;");
} }