Wed, 25 Dec 2024 21:49:48 +0100
add initial code with minimal working contact list and conversations
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.hgignore Wed Dec 25 21:49:48 2024 +0100 @@ -0,0 +1,5 @@ +^nbactions.xml$ +^nb-configuration.xml$ +^target/.*$ +^.idea/ +^dist/.*$
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pom.xml Wed Dec 25 21:49:48 2024 +0100 @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <groupId>de.unixwork</groupId> + <artifactId>IM5</artifactId> + <version>1.0-SNAPSHOT</version> + + <properties> + <maven.compiler.source>11</maven.compiler.source> + <maven.compiler.target>11</maven.compiler.target> + </properties> + + <dependencies> + <dependency> + <groupId>org.igniterealtime.smack</groupId> + <artifactId>smack-java11-full</artifactId> + <version>4.5.0-beta5</version> + </dependency> + + <dependency> + <groupId>org.jitsi</groupId> + <artifactId>org.otr4j</artifactId> + <version>0.23</version> + </dependency> + </dependencies> +</project> \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/java/de/unixwork/im/App.java Wed Dec 25 21:49:48 2024 +0100 @@ -0,0 +1,84 @@ +package de.unixwork.im; + +import javax.swing.*; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.jivesoftware.smack.roster.RosterEntry; +import org.jxmpp.jid.Jid; + +public class App { + + private static App instance; + + private final ContactListFrame contactListFrame; + private final Map<String, ConversationFrame> conversations; + + private final Xmpp xmpp; + + public App(Xmpp xmpp) throws Exception { + if(instance != null) { + throw new Exception("App already initilized"); + } + App.instance = this; + conversations = new HashMap<>(); + this.xmpp = xmpp; + + // Create the contact list window + contactListFrame = new ContactListFrame(); + contactListFrame.setContactClickListener(contact -> { + openConversation(contact.getJid().asUnescapedString()); + }); + contactListFrame.setVisible(true); + } + + public static App getInstance() { + return instance; + } + + public Xmpp getXmpp() { + return xmpp; + } + + // Method to open a conversation window + public void openConversation(String xid) { + SwingUtilities.invokeLater(() -> { + if (!conversations.containsKey(xid)) { + ConversationFrame conversationFrame = new ConversationFrame(xid); + conversations.put(xid, conversationFrame); + conversationFrame.setVisible(true); + } else { + conversations.get(xid).toFront(); + } + }); + } + + public void dispatchMessage(Jid from, String msg, boolean secure) { + SwingUtilities.invokeLater(() -> { + // add message to the correct conversation + // if no conversation exists yet, create a new window + String xid = from.asBareJid().toString(); + //String resource = from.getResourceOrNull(); + ConversationFrame conversation = conversations.get(xid); + if(conversation == null) { + conversation = new ConversationFrame(xid); + conversations.put(xid, conversation); + } + + conversation.addToLog(msg, true, false); + + conversation.setVisible(true); + }); + } + + public void setContacts(List<RosterEntry> contacts) { + SwingUtilities.invokeLater(() -> { + contactListFrame.setContacts(contacts); + }); + } + + // Method to perform actions in the GUI thread from other threads + public void runOnUiThread(Runnable action) { + SwingUtilities.invokeLater(action); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/java/de/unixwork/im/ContactListFrame.java Wed Dec 25 21:49:48 2024 +0100 @@ -0,0 +1,73 @@ +package de.unixwork.im; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.List; +import org.jivesoftware.smack.roster.RosterEntry; + +// Main class for the XMPP contact list window +public class ContactListFrame extends JFrame { + + private DefaultListModel<RosterEntry> contactListModel; + private JList<RosterEntry> contactList; + private ContactClickListener contactClickListener; + + public ContactListFrame() { + setTitle("Contact List"); + setSize(200, 300); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + setLayout(new BorderLayout()); + + // Create the list model and list view + contactListModel = new DefaultListModel<>(); + contactList = new JList<>(contactListModel); + contactList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + contactList.setCellRenderer(new DefaultListCellRenderer() { + @Override + public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) { + Component c = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + if (value instanceof RosterEntry) { + setText(((RosterEntry) value).toString()); + } + return c; + } + }); + + // Add mouse listener for click events + contactList.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (e.getClickCount() == 2) { // Double-click detected + int index = contactList.locationToIndex(e.getPoint()); + if (index >= 0 && contactClickListener != null) { + contactClickListener.onContactClicked(contactListModel.getElementAt(index)); + } + } + } + }); + + // Add the list to a scroll pane + JScrollPane scrollPane = new JScrollPane(contactList); + add(scrollPane, BorderLayout.CENTER); + } + + // Method to set the contact list data + public void setContacts(List<RosterEntry> contacts) { + contactListModel.clear(); + for (RosterEntry contact : contacts) { + contactListModel.addElement(contact); + } + } + + // Interface for click callback + public interface ContactClickListener { + void onContactClicked(RosterEntry contact); + } + + // Method to set the click listener + public void setContactClickListener(ContactClickListener listener) { + this.contactClickListener = listener; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/java/de/unixwork/im/ConversationFrame.java Wed Dec 25 21:49:48 2024 +0100 @@ -0,0 +1,143 @@ +package de.unixwork.im; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public class ConversationFrame extends JFrame implements MessageSendListener { + + private String xid; + private JTextArea messageHistory; + private JTextArea messageInput; + private JButton sendButton; + private JButton topRightButton; + private MessageSendListener messageSendListener; + private TopRightButtonListener topRightButtonListener; + + public ConversationFrame(String xid) { + this.xid = xid; + + setTitle(xid); + setSize(500, 400); + setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + setLayout(new BorderLayout(5, 5)); + + // Top panel with top-right button + JPanel topPanel = new JPanel(new BorderLayout()); + topRightButton = new JButton("Insecure"); + topPanel.add(topRightButton, BorderLayout.EAST); + add(topPanel, BorderLayout.NORTH); + + // Split pane + JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT); + splitPane.setResizeWeight(0.8); + + // Message history area (top part) + messageHistory = new JTextArea(); + messageHistory.setEditable(false); + JScrollPane messageHistoryScrollPane = new JScrollPane(messageHistory); + splitPane.setTopComponent(messageHistoryScrollPane); + + // Message input area (bottom part) + JPanel inputPanel = new JPanel(new BorderLayout(5, 5)); + messageInput = new JTextArea(3, 20); + JScrollPane messageInputScrollPane = new JScrollPane(messageInput); + sendButton = new JButton("Send"); + inputPanel.add(messageInputScrollPane, BorderLayout.CENTER); + inputPanel.add(sendButton, BorderLayout.EAST); + splitPane.setBottomComponent(inputPanel); + + add(splitPane, BorderLayout.CENTER); + + // Configure input behavior + messageInput.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_ENTER) { + if (e.isControlDown()) { + messageInput.append("\n"); + } else { + e.consume(); + triggerMessageSend(); + } + } + } + }); + + // Button actions + sendButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + triggerMessageSend(); + } + }); + + topRightButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + if (topRightButtonListener != null) { + topRightButtonListener.onTopRightButtonClicked(); + } + } + }); + + // message handler + setMessageSendListener(this); + } + + public void addToLog(String message, boolean incoming, boolean secure) { + String prefix = incoming ? "< " : "> "; + // Get the current date and time + LocalDateTime now = LocalDateTime.now(); + + // Define the desired format + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + // Format the current date and time + String formattedDateTime = now.format(formatter); + appendToMessageHistory(prefix + formattedDateTime + ": " + message); + } + + @Override + public void onMessageSend(String message) { + addToLog(message, false, false); + App.getInstance().getXmpp().sendMessage(xid, message, false); + } + + // Method to append text to the message history + public void appendToMessageHistory(String text) { + messageHistory.append(text + "\n"); + } + + // Method to set the message send listener + public void setMessageSendListener(MessageSendListener listener) { + this.messageSendListener = listener; + } + + // Method to set the top-right button listener + public void setTopRightButtonListener(TopRightButtonListener listener) { + this.topRightButtonListener = listener; + } + + // Trigger the message send callback + private void triggerMessageSend() { + if (messageSendListener != null) { + String message = messageInput.getText().trim(); + if (!message.isEmpty()) { + messageSendListener.onMessageSend(message); + messageInput.setText(""); + } + } + } + + // Interface for top-right button callback + public interface TopRightButtonListener { + void onTopRightButtonClicked(); + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/java/de/unixwork/im/Main.java Wed Dec 25 21:49:48 2024 +0100 @@ -0,0 +1,123 @@ +package de.unixwork.im; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.jivesoftware.smack.ConnectionConfiguration; +import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration; +import org.jxmpp.stringprep.XmppStringprepException; + + +public class Main { + + public static void main(String[] args) { + // Create the dialog + JDialog loginDialog = new JDialog((Frame) null, "Login", true); + loginDialog.setSize(300, 200); + loginDialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); + loginDialog.setLayout(new BorderLayout(5, 5)); + + // Create components + JPanel inputPanel = new JPanel(new GridBagLayout()); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.insets = new Insets(5, 5, 5, 5); + + JLabel usernameLabel = new JLabel("Username:"); + JTextField usernameField = new JTextField(15); + JLabel domainLabel = new JLabel("Domain:"); + JTextField domainField = new JTextField(15); + JLabel passwordLabel = new JLabel("Password:"); + JPasswordField passwordField = new JPasswordField(15); + + gbc.gridx = 0; + gbc.gridy = 0; + gbc.weightx = 0; + inputPanel.add(usernameLabel, gbc); + + gbc.gridx = 1; + gbc.gridy = 0; + gbc.weightx = 1; + inputPanel.add(usernameField, gbc); + + gbc.gridx = 0; + gbc.gridy = 1; + gbc.weightx = 0; + inputPanel.add(domainLabel, gbc); + + gbc.gridx = 1; + gbc.gridy = 1; + gbc.weightx = 1; + inputPanel.add(domainField, gbc); + + gbc.gridx = 0; + gbc.gridy = 2; + gbc.weightx = 0; + inputPanel.add(passwordLabel, gbc); + + gbc.gridx = 1; + gbc.gridy = 2; + gbc.weightx = 1; + inputPanel.add(passwordField, gbc); + + JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); + JButton loginButton = new JButton("Login"); + JButton cancelButton = new JButton("Cancel"); + buttonPanel.add(loginButton); + buttonPanel.add(cancelButton); + + // Add panels to the dialog + loginDialog.add(inputPanel, BorderLayout.CENTER); + loginDialog.add(buttonPanel, BorderLayout.SOUTH); + + // Action listeners + loginButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + String username = usernameField.getText(); + String domain = domainField.getText(); + String password = new String(passwordField.getPassword()); + + loginDialog.dispose(); + + try { + XMPPTCPConnectionConfiguration config = XMPPTCPConnectionConfiguration.builder() + .setUsernameAndPassword(username, password) + .setXmppDomain(domain) + .setResource("IM5") + .setHost(domain) + .setPort(5222) + //.addEnabledSaslMechanism("PLAIN") + //.setSecurityMode(ConnectionConfiguration.SecurityMode.disabled) + .build(); + + Xmpp xmpp = new Xmpp(config); + + App app = new App(xmpp); + + xmpp.start(); + } catch (XmppStringprepException ex) { + ex.printStackTrace(); + System.exit(-1); + } catch (Exception ex) { + ex.printStackTrace(); + System.exit(-1); + } + } + }); + + cancelButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + loginDialog.dispose(); + } + }); + + // Center the dialog and make it visible + loginDialog.setLocationRelativeTo(null); + loginDialog.setVisible(true); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/java/de/unixwork/im/MessageSendListener.java Wed Dec 25 21:49:48 2024 +0100 @@ -0,0 +1,6 @@ +package de.unixwork.im; + +// Interface for message send callback +public interface MessageSendListener { + void onMessageSend(String message); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/java/de/unixwork/im/Xmpp.java Wed Dec 25 21:49:48 2024 +0100 @@ -0,0 +1,144 @@ + +package de.unixwork.im; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.jivesoftware.smack.ConnectionConfiguration; +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.filter.MessageWithBodiesFilter; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.MessageBuilder; +import org.jivesoftware.smack.roster.Roster; +import org.jivesoftware.smack.roster.RosterEntry; +import org.jivesoftware.smack.tcp.XMPPTCPConnection; +import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration; +import org.jxmpp.stringprep.XmppStringprepException; + +public class Xmpp extends Thread { + private final XMPPTCPConnectionConfiguration config; + + private XMPPTCPConnection connection = null; + + // BlockingQueue for event-driven communication + private final BlockingQueue<XmppEvent> eventQueue = new LinkedBlockingQueue<>(); + + public Xmpp(XMPPTCPConnectionConfiguration xmppConfig) { + config = xmppConfig; + } + + // Method to send a message (this will be called from another thread) + public void sendMessage(String to, String message, boolean encrypted) { + try { + XmppMessage event = new XmppMessage(to, message, encrypted); + eventQueue.put(event); // Block if the queue is full + } catch (InterruptedException e) { + Logger.getLogger(Xmpp.class.getName()).log(Level.SEVERE, "Error adding event to queue", e); + } + } + + private void connect() throws SmackException, IOException, XMPPException, InterruptedException { + connection = new XMPPTCPConnection(config); + connection.setUseStreamManagement(false); + connection.connect(); + connection.login(); + + } + + public List<RosterEntry> getRosterItems() throws SmackException.NotLoggedInException, SmackException.NotConnectedException, InterruptedException { + try { + // Ensure we are connected + if (connection != null && connection.isConnected()) { + // Get the roster instance + Roster roster = Roster.getInstanceFor(connection); + + // Fetch all roster entries (contacts) + roster.reload(); + + // Add all roster entries to the list + ArrayList<RosterEntry> rosterList = new ArrayList<>(16); + roster.getEntries().forEach(entry -> rosterList.add(entry)); + + // Optionally, print the list to verify + System.out.println("Roster List: "); + for (RosterEntry entry : rosterList) { + System.out.println("Contact: " + entry.getUser()); + } + + return rosterList; + } else { + System.out.println("Not connected to XMPP server."); + } + } catch (SmackException e) { + Logger.getLogger(Xmpp.class.getName()).log(Level.SEVERE, "Error getting roster items", e); + } + + return null; + } + + @Override + public void run() { + try { + connect(); + connection.addAsyncStanzaListener((stanza -> { + var jid = stanza.getFrom(); + if(jid != null) { + String body = ((Message)stanza).getBody(); + App.getInstance().dispatchMessage(jid, body, true); + } + }), MessageWithBodiesFilter.INSTANCE); + List<RosterEntry> roster = getRosterItems(); + if(roster != null) { + App.getInstance().setContacts(roster); + } + + while (true) { + // Wait for an event (message to send) + XmppEvent event = eventQueue.take(); // This will block until an event is available + event.exec(this, connection); + } + } catch (SmackException ex) { + Logger.getLogger(Xmpp.class.getName()).log(Level.SEVERE, null, ex); + } catch (IOException ex) { + Logger.getLogger(Xmpp.class.getName()).log(Level.SEVERE, null, ex); + } catch (XMPPException ex) { + Logger.getLogger(Xmpp.class.getName()).log(Level.SEVERE, null, ex); + } catch (InterruptedException ex) { + Logger.getLogger(Xmpp.class.getName()).log(Level.SEVERE, null, ex); + } + } +} + +class XmppMessage implements XmppEvent { + String to; + String message; + boolean encrypted; + + XmppMessage(String to, String message, boolean encrypted) { + this.to = to; + this.message = message; + this.encrypted = encrypted; + } + + public void exec(Xmpp xmpp, XMPPTCPConnection conn) { + final Message msg; + try { + msg = MessageBuilder.buildMessage() + .to(to) + .setBody(message) + .build(); + conn.sendStanza(msg); + } catch (XmppStringprepException ex) { + Logger.getLogger(XmppMessage.class.getName()).log(Level.SEVERE, null, ex); + } catch (SmackException.NotConnectedException ex) { + Logger.getLogger(XmppMessage.class.getName()).log(Level.SEVERE, null, ex); + } catch (InterruptedException ex) { + Logger.getLogger(XmppMessage.class.getName()).log(Level.SEVERE, null, ex); + } + } +} \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/java/de/unixwork/im/XmppEvent.java Wed Dec 25 21:49:48 2024 +0100 @@ -0,0 +1,7 @@ +package de.unixwork.im; + +import org.jivesoftware.smack.tcp.XMPPTCPConnection; + +public interface XmppEvent { + public void exec(Xmpp xmpp, XMPPTCPConnection conn); +}