diff --git a/core/src/main/java/org/jivesoftware/spark/ui/ContactInfoWindow.java b/core/src/main/java/org/jivesoftware/spark/ui/ContactInfoWindow.java index 140cb4bfa..1675103e9 100644 --- a/core/src/main/java/org/jivesoftware/spark/ui/ContactInfoWindow.java +++ b/core/src/main/java/org/jivesoftware/spark/ui/ContactInfoWindow.java @@ -80,7 +80,6 @@ public class ContactInfoWindow extends JPanel { private final JLabel iconLabel = new JLabel(); private final JLabel titleLabel = new JLabel(); private final JLabel phoneLabel = new JLabel(); - private final JTextArea clientResourcesLabel = new JTextArea(); private ContactItem contactItem; @@ -109,7 +108,6 @@ public class ContactInfoWindow extends JPanel { add(titleLabel, new GridBagConstraints(2, 4, 1, 1, 1, 0, NORTHWEST, HORIZONTAL, new Insets(0, 0, 2, 2), 0, 0)); add(phoneLabel, new GridBagConstraints(2, 5, 1, 1, 1, 0, NORTHWEST, HORIZONTAL, new Insets(0, 0, 2, 2), 0, 0)); add(fullJIDLabel, new GridBagConstraints(0, 6, 4, 1, 1, 1, SOUTHWEST, HORIZONTAL, new Insets(0, 2, 2, 2), 0, 0)); - add(clientResourcesLabel, new GridBagConstraints(0, 7, 4, 1, 1, 1, SOUTHWEST, HORIZONTAL, new Insets(0, 2, 2, 2), 0, 0)); Font dialogFont = new Font("Dialog", Font.PLAIN, 12); nicknameLabel.setFont(new Font("Dialog", Font.BOLD, 14)); @@ -128,9 +126,6 @@ public class ContactInfoWindow extends JPanel { fullJIDLabel.setFont(dialogFont); fullJIDLabel.setForeground(COLOR_TEXT); fullJIDLabel.setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, COLOR_TEXT)); - clientResourcesLabel.setLineWrap(true); - clientResourcesLabel.setWrapStyleWord(true); - clientResourcesLabel.setEditable(false); phoneLabel.setBorder(null); setBorder(BorderFactory.createLineBorder(COLOR_TEXT, 1)); @@ -213,7 +208,6 @@ public class ContactInfoWindow extends JPanel { idleLabel.setText(""); titleLabel.setText(""); phoneLabel.setText(""); - showClientResources(isOnLeave, contactItem.getJid()); // Reserve avatar space immediately so the layout does not change later. final ImageIcon placeholder = GraphicUtils.scaleImageIcon(SparkRes.getImageIcon(SparkRes.Icon.DEFAULT_AVATAR_64x64_IMAGE), Sizes.Avatar.PROFILE, Sizes.Avatar.PROFILE); @@ -301,21 +295,6 @@ public class ContactInfoWindow extends JPanel { } } - private void showClientResources(boolean isOnLeave, BareJid contactJid) { - if (isOnLeave) { - clientResourcesLabel.setText(""); - return; - } - String clientResources = ""; - List allPresences = SparkManager.getRoster().getAvailablePresences(contactJid); - for (Presence p : allPresences) { - clientResources += p.getFrom().getResourceOrEmpty() + " " + - (p.getMode() != available ? p.getMode() : "") + " " + - (p.getStatus() != null ? p.getStatus() : "") + "\n"; - } - clientResourcesLabel.setText(clientResources); - } - private String retrieveIdleTimeText(ContactItem contactItem, boolean isOnLeave) { try { //If user is away (not offline), last activity request is sent to client diff --git a/core/src/main/java/org/jivesoftware/sparkimpl/plugin/jabber/JabberVersion.java b/core/src/main/java/org/jivesoftware/sparkimpl/plugin/jabber/JabberVersion.java index 4171534d8..cb516b505 100644 --- a/core/src/main/java/org/jivesoftware/sparkimpl/plugin/jabber/JabberVersion.java +++ b/core/src/main/java/org/jivesoftware/sparkimpl/plugin/jabber/JabberVersion.java @@ -1,5 +1,5 @@ -/* - * Copyright (C) 2004-2011 Jive Software. All rights reserved. +/** + * Copyright (C) 2004-2011 Jive Software, 2026 Ignite Realtime Foundation. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,15 +31,22 @@ import org.jivesoftware.spark.util.SwingWorker; import org.jivesoftware.spark.util.log.Log; import org.jivesoftware.sparkimpl.settings.JiveInfo; import org.jivesoftware.resource.Res; -import org.jxmpp.jid.Jid; +import org.jxmpp.jid.FullJid; -import javax.swing.*; +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.JComponent; +import javax.swing.JOptionPane; +import javax.swing.JPopupMenu; +import javax.swing.JTextField; +import javax.swing.KeyStroke; import java.awt.event.ActionEvent; import java.awt.event.MouseEvent; import java.time.ZonedDateTime; -import java.util.Collection; -import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; /** * Jabber Version. @@ -122,7 +129,7 @@ public class JabberVersion implements Plugin { private void viewClient() { final JTextField field = new JTextField(); final ContactList contactList = SparkManager.getWorkspace().getContactList(); - java.util.List selectedUsers = contactList.getSelectedUsers(); + List selectedUsers = contactList.getSelectedUsers(); if (selectedUsers.size() == 1) { ContactItem item = selectedUsers.get(0); final Presence presence = item.getPresence(); @@ -133,7 +140,14 @@ public class JabberVersion implements Plugin { JOptionPane.INFORMATION_MESSAGE); return; } - final Jid jid = presence.getFrom(); + + // Collect all available full JIDs for the selected user. + List presences = SparkManager.getRoster().getAvailablePresences(item.getJid()); + List allFullJids = presences.stream() + .map(p -> p.getFrom().asFullJidIfPossible()) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + SwingWorker worker = new SwingWorker() { @Override public Object construct() { @@ -143,12 +157,12 @@ public class JabberVersion implements Plugin { catch (InterruptedException e1) { // Nothing to do } - return jid; + return allFullJids; } @Override public void finished() { - VersionViewer.viewVersion(jid); + VersionViewer.viewVersion(allFullJids); } }; worker.start(); diff --git a/core/src/main/java/org/jivesoftware/sparkimpl/plugin/jabber/VersionViewer.java b/core/src/main/java/org/jivesoftware/sparkimpl/plugin/jabber/VersionViewer.java index 3e8cbbb59..e664df79f 100644 --- a/core/src/main/java/org/jivesoftware/sparkimpl/plugin/jabber/VersionViewer.java +++ b/core/src/main/java/org/jivesoftware/sparkimpl/plugin/jabber/VersionViewer.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2004-2011 Jive Software. All rights reserved. + * Copyright (C) 2004-2011 Jive Software, 2026 Ignite Realtime Foundation. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,21 +26,63 @@ import org.jivesoftware.spark.UserManager; import org.jivesoftware.spark.component.MessageDialog; import org.jivesoftware.spark.util.ResourceUtils; import org.jivesoftware.spark.util.log.Log; +import org.jxmpp.jid.FullJid; import org.jxmpp.jid.Jid; +import org.jxmpp.jid.parts.Resourcepart; import javax.swing.*; - import java.awt.*; import java.time.format.DateTimeFormatter; import java.time.format.FormatStyle; +import java.util.Collection; public class VersionViewer { private VersionViewer() { - } - public static void viewVersion(Jid jid) { + public static void viewVersion(Collection fullJids) { + final XMPPConnection connection = SparkManager.getConnection(); + + final JTabbedPane tabbedPane = new JTabbedPane(); + for (FullJid fullJid : fullJids) { + final JPanel card = createResourceCard(connection, fullJid); + final Resourcepart resource = fullJid.getResourceOrNull(); + final String title = (resource != null) + ? resource.toString() + : fullJid.toString(); + tabbedPane.addTab(title, card); + } + + // Explanatory text above the tabs. + final JTextArea explanation = new JTextArea(Res.getString("message.client.information.multiple.resources")); + explanation.setEditable(false); + explanation.setLineWrap(true); + explanation.setWrapStyleWord(true); + explanation.setOpaque(false); + explanation.setBorder(BorderFactory.createEmptyBorder(5, 5, 8, 5)); + explanation.setFont(UIManager.getFont("Label.font")); + + final JPanel content = new JPanel(new BorderLayout()); + content.add(explanation, BorderLayout.NORTH); + content.add(tabbedPane, BorderLayout.CENTER); + + // Use the bare JID of the first entry for the dialog's header text. + final Jid first = fullJids.iterator().next(); + MessageDialog.showComponent( + Res.getString("title.version.and.time"), + Res.getString("message.client.information", UserManager.unescapeJID(first.asBareJid())), + SparkRes.getImageIcon(SparkRes.Icon.PROFILE_IMAGE_24x24), + content, + SparkManager.getMainWindow(), + 450, 420, false); + } + + /** + * Builds a loading/data card pair (CardLayout) for a single resource and fires the Version + Time requests to + * populate it asynchronously. + */ + private static JPanel createResourceCard(final XMPPConnection connection, final Jid jid) { final JPanel loadingCard = new JPanel(); final ImageIcon icon = new ImageIcon( VersionViewer.class.getClassLoader().getResource( "images/ajax-loader.gif")); loadingCard.add(new JLabel("loading... ", icon, JLabel.CENTER)); @@ -90,8 +132,7 @@ public class VersionViewer { cards.add(loadingCard); cards.add(dataCard); - final XMPPConnection connection = SparkManager.getConnection(); - // Load Version + // Load version final Version versionRequest = Version.builder(connection) .ofType(IQ.Type.get) .to(jid) @@ -132,7 +173,9 @@ public class VersionViewer { cardLayout.last(cards); }); - MessageDialog.showComponent(Res.getString("title.version.and.time"), Res.getString("message.client.information", UserManager.unescapeJID(jid)), SparkRes.getImageIcon(SparkRes.Icon.PROFILE_IMAGE_24x24), cards, SparkManager.getMainWindow(), 400, 300, false); + // Wrap so the card pair sits nicely inside a tab. + final JPanel wrapper = new JPanel(new BorderLayout()); + wrapper.add(cards, BorderLayout.CENTER); + return wrapper; } - } diff --git a/core/src/main/resources/i18n/spark_i18n.properties b/core/src/main/resources/i18n/spark_i18n.properties index ac87bd828..fc6990d54 100644 --- a/core/src/main/resources/i18n/spark_i18n.properties +++ b/core/src/main/resources/i18n/spark_i18n.properties @@ -479,6 +479,7 @@ message.cert.verification.failed=Unable to verify certificate message.chat.session.ended=Chat session has ended on {0} message.click.to.open=Click to open message.client.information=Client information for {0} +message.client.information.multiple.resources=A contact can be connected with more than one client at the same time. A tab is shown for each client that the contact is currently using. Each tab is named after the client's "resource-part": a unique identifier that distinguishes concurrent clients from one another. message.close.other.chats=Close all other chats message.close.stale.chats=Close stale chats message.close.this.chat=Close this chat