add initial code with minimal working contact list and conversations

Wed, 25 Dec 2024 21:49:48 +0100

author
Olaf Wintermann <olaf.wintermann@gmail.com>
date
Wed, 25 Dec 2024 21:49:48 +0100
changeset 0
f3095cda599e
child 1
42d0d099492b

add initial code with minimal working contact list and conversations

.hgignore file | annotate | diff | comparison | revisions
pom.xml file | annotate | diff | comparison | revisions
src/main/java/de/unixwork/im/App.java file | annotate | diff | comparison | revisions
src/main/java/de/unixwork/im/ContactListFrame.java file | annotate | diff | comparison | revisions
src/main/java/de/unixwork/im/ConversationFrame.java file | annotate | diff | comparison | revisions
src/main/java/de/unixwork/im/Main.java file | annotate | diff | comparison | revisions
src/main/java/de/unixwork/im/MessageSendListener.java file | annotate | diff | comparison | revisions
src/main/java/de/unixwork/im/Xmpp.java file | annotate | diff | comparison | revisions
src/main/java/de/unixwork/im/XmppEvent.java file | annotate | diff | comparison | revisions
--- /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);
+}

mercurial