diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/LinkUp-P2P-Chat.iml b/.idea/LinkUp-P2P-Chat.iml new file mode 100644 index 0000000..24643cc --- /dev/null +++ b/.idea/LinkUp-P2P-Chat.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..3668dc8 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..8ab2f2f --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app.js b/app.js index 6c3dbf7..fa93d67 100644 --- a/app.js +++ b/app.js @@ -16,7 +16,7 @@ await drive.ready(); let swarm; let registeredUsers = JSON.parse(localStorage.getItem('registeredUsers')) || {}; let peerCount = 0; -let currentRoom = null; +let activeRooms = []; const eventEmitter = new EventEmitter(); // Define servePort at the top level @@ -29,7 +29,10 @@ let config = { rooms: [] }; -// Function to get a random port between 1337 and 2223 +// Store messages for each room +let messagesStore = {}; + +// Function to get a random port between 49152 and 65535 function getRandomPort() { return Math.floor(Math.random() * (65535 - 49152 + 1)) + 49152; } @@ -73,40 +76,38 @@ async function initialize() { toggleSetupBtn.addEventListener('click', toggleSetupView); } if (removeRoomBtn) { - removeRoomBtn.addEventListener('click', leaveRoom); + removeRoomBtn.addEventListener('click', () => { + const topic = document.querySelector('#chat-room-topic').innerText; + leaveRoom(topic); + }); } if (attachFileButton) { attachFileButton.addEventListener('click', () => fileInput.click()); } if (fileInput) { - fileInput.addEventListener('change', async (event) => { - const file = event.target.files[0]; - if (file) { - const reader = new FileReader(); - reader.onload = async (event) => { - const buffer = new Uint8Array(event.target.result); - const filePath = `/files/${file.name}`; - await drive.put(filePath, buffer); - const fileUrl = `http://localhost:${servePort}${filePath}`; - sendFileMessage(config.userName, fileUrl, file.type, config.userAvatar); - }; - reader.readAsArrayBuffer(file); - } - }); + fileInput.addEventListener('change', handleFileInput); } const configExists = fs.existsSync("./config.json"); if (configExists) { config = JSON.parse(fs.readFileSync("./config.json", 'utf8')); - console.log("Read config from file:", config) + console.log("Read config from file:", config); // Update port in URLs config.userAvatar = updatePortInUrl(config.userAvatar); config.rooms.forEach(room => { - addRoomToListWithoutWritingToConfig(room); + room.alias = room.alias || truncateHash(room.topic); }); for (let user in registeredUsers) { registeredUsers[user] = updatePortInUrl(registeredUsers[user]); } + + renderRoomList(); // Render the room list with aliases + + // Connect to all rooms on startup + for (const room of config.rooms) { + const topicBuffer = b4a.from(room.topic, 'hex'); + await joinSwarm(topicBuffer); + } } const registerDiv = document.querySelector('#register'); @@ -115,15 +116,30 @@ async function initialize() { } eventEmitter.on('onMessage', async (messageObj) => { + console.log('Received message:', messageObj); // Debugging log + if (messageObj.type === 'icon') { const username = messageObj.username; - const avatarBuffer = Buffer.from(messageObj.avatar, 'base64'); - await drive.put(`/icons/${username}.png`, avatarBuffer); - updateIcon(username, avatarBuffer); + if (messageObj.avatar) { + const avatarBuffer = b4a.from(messageObj.avatar, 'base64'); + await drive.put(`/icons/${username}.png`, avatarBuffer); + updateIcon(username, avatarBuffer); + } else { + console.error('Received icon message with missing avatar data:', messageObj); + } } else if (messageObj.type === 'file') { - addFileMessage(messageObj.name, messageObj.fileName, messageObj.fileUrl, messageObj.fileType, messageObj.avatar); + if (messageObj.file && messageObj.fileName) { + const fileBuffer = b4a.from(messageObj.file, 'base64'); + await drive.put(`/files/${messageObj.fileName}`, fileBuffer); + const fileUrl = `http://localhost:${servePort}/files/${messageObj.fileName}`; + addFileMessage(messageObj.name, messageObj.fileName, updatePortInUrl(fileUrl), messageObj.fileType, updatePortInUrl(messageObj.avatar), messageObj.topic); + } else { + console.error('Received file message with missing file data or fileName:', messageObj); + } + } else if (messageObj.type === 'message') { + onMessageAdded(messageObj.name, messageObj.message, messageObj.avatar, messageObj.topic); } else { - onMessageAdded(messageObj.name, messageObj.message, messageObj.avatar); + console.error('Received unknown message type:', messageObj); } }); @@ -138,7 +154,7 @@ async function initialize() { const iconMessage = JSON.stringify({ type: 'icon', username: config.userName, - avatar: iconBuffer.toString('base64'), + avatar: b4a.toString(iconBuffer, 'base64'), }); connection.write(iconMessage); } @@ -162,6 +178,11 @@ async function initialize() { swarm.on('close', () => { console.log('Swarm closed'); }); + + // Initialize highlight.js once the DOM is fully loaded + document.addEventListener("DOMContentLoaded", (event) => { + hljs.highlightAll(); + }); } function registerUser(e) { @@ -214,7 +235,7 @@ async function createChatRoom() { const topicBuffer = crypto.randomBytes(32); const topic = b4a.toString(topicBuffer, 'hex'); addRoomToList(topic); - joinSwarm(topicBuffer); + await joinSwarm(topicBuffer); } async function joinChatRoom(e) { @@ -222,27 +243,21 @@ async function joinChatRoom(e) { const topicStr = document.querySelector('#join-chat-room-topic').value; const topicBuffer = b4a.from(topicStr, 'hex'); addRoomToList(topicStr); - joinSwarm(topicBuffer); + await joinSwarm(topicBuffer); } async function joinSwarm(topicBuffer) { - if (currentRoom) { - currentRoom.destroy(); - } - - document.querySelector('#setup').classList.add('hidden'); - document.querySelector('#loading').classList.remove('hidden'); - - const discovery = swarm.join(topicBuffer, { client: true, server: true }); - await discovery.flushed(); - const topic = b4a.toString(topicBuffer, 'hex'); - document.querySelector('#chat-room-topic').innerText = topic; // Set full topic here - document.querySelector('#loading').classList.add('hidden'); - document.querySelector('#chat').classList.remove('hidden'); + if (!activeRooms.some(room => room.topic === topic)) { + const discovery = swarm.join(topicBuffer, { client: true, server: true }); + await discovery.flushed(); - currentRoom = discovery; - clearMessages(); + activeRooms.push({ topic, discovery }); + + console.log('Joined room:', topic); // Debugging log + + renderMessagesForRoom(topic); + } } function addRoomToList(topic) { @@ -250,43 +265,118 @@ function addRoomToList(topic) { const roomItem = document.createElement('li'); roomItem.textContent = truncateHash(topic); roomItem.dataset.topic = topic; + + roomItem.addEventListener('dblclick', () => enterEditMode(roomItem)); roomItem.addEventListener('click', () => switchRoom(topic)); roomList.appendChild(roomItem); - config.rooms.push(topic); + config.rooms.push({ topic, alias: truncateHash(topic) }); writeConfigToFile("./config.json"); } -function addRoomToListWithoutWritingToConfig(topic) { - const roomList = document.querySelector('#room-list'); - const roomItem = document.createElement('li'); - roomItem.textContent = truncateHash(topic); - roomItem.dataset.topic = topic; - roomItem.addEventListener('click', () => switchRoom(topic)); - roomList.appendChild(roomItem); +function enterEditMode(roomItem) { + const originalText = roomItem.textContent; + const topic = roomItem.dataset.topic; + roomItem.innerHTML = ''; + + const input = document.createElement('input'); + input.type = 'text'; + input.value = originalText; + input.style.maxWidth = '100%'; // Add this line to set the max width + input.style.boxSizing = 'border-box'; // Add this line to ensure padding and border are included in the width + + roomItem.appendChild(input); + + input.focus(); + + input.addEventListener('blur', () => { + exitEditMode(roomItem, input, topic); + }); + + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + exitEditMode(roomItem, input, topic); + } else if (e.key === 'Escape') { + roomItem.textContent = originalText; + } + }); +} + +function exitEditMode(roomItem, input, topic) { + const newAlias = input.value.trim(); + if (newAlias) { + roomItem.textContent = newAlias; + + // Update the config with the new alias + const roomConfig = config.rooms.find(room => room.topic === topic); + if (roomConfig) { + roomConfig.alias = newAlias; + writeConfigToFile("./config.json"); + } + + // Check if the edited room is the current room in view + const currentTopic = document.querySelector('#chat-room-topic').innerText; + if (currentTopic === topic) { + const chatRoomName = document.querySelector('#chat-room-name'); + if (chatRoomName) { + chatRoomName.innerText = newAlias; + } + } + } else { + roomItem.textContent = truncateHash(topic); + } } function switchRoom(topic) { - const topicBuffer = b4a.from(topic, 'hex'); - joinSwarm(topicBuffer); + console.log('Switching to room:', topic); // Debugging log + const chatRoomTopic = document.querySelector('#chat-room-topic'); + const chatRoomName = document.querySelector('#chat-room-name'); + + if (chatRoomTopic) { + chatRoomTopic.innerText = topic; // Set full topic here + } else { + console.error('Element #chat-room-topic not found'); + } + + if (chatRoomName) { + // Update the room name in the header + const room = config.rooms.find(r => r.topic === topic); + const roomName = room ? room.alias : truncateHash(topic); + chatRoomName.innerText = roomName; + } else { + console.error('Element #chat-room-name not found'); + } + + clearMessages(); + renderMessagesForRoom(topic); + + // Show chat UI elements + document.querySelector('#chat').classList.remove('hidden'); + document.querySelector('#setup').classList.add('hidden'); } -function leaveRoom() { - if (currentRoom) { - const topic = b4a.toString(currentRoom.topic, 'hex'); - const roomItem = document.querySelector(`li[data-topic="${topic}"]`); - if (roomItem) { - roomItem.remove(); - } - - config.rooms = config.rooms.filter(e => e !== topic); - writeConfigToFile("./config.json"); - - currentRoom.destroy(); - currentRoom = null; +function leaveRoom(topic) { + const roomIndex = activeRooms.findIndex(room => room.topic === topic); + if (roomIndex !== -1) { + const room = activeRooms[roomIndex]; + room.discovery.destroy(); + activeRooms.splice(roomIndex, 1); + } + + const roomItem = document.querySelector(`li[data-topic="${topic}"]`); + if (roomItem) { + roomItem.remove(); + } + + config.rooms = config.rooms.filter(e => e.topic !== topic); + writeConfigToFile("./config.json"); + + if (activeRooms.length > 0) { + switchRoom(activeRooms[0].topic); + } else { + document.querySelector('#chat').classList.add('hidden'); + document.querySelector('#setup').classList.remove('hidden'); } - document.querySelector('#chat').classList.add('hidden'); - document.querySelector('#setup').classList.remove('hidden'); } function sendMessage(e) { @@ -294,7 +384,11 @@ function sendMessage(e) { const message = document.querySelector('#message').value; document.querySelector('#message').value = ''; - onMessageAdded(config.userName, message, config.userAvatar); + const topic = document.querySelector('#chat-room-topic').innerText; + + console.log('Sending message:', message); // Debugging log + + onMessageAdded(config.userName, message, config.userAvatar, topic); let peersPublicKeys = []; peersPublicKeys.push([...swarm.connections].map(peer => peer.remotePublicKey.toString('hex'))); @@ -306,7 +400,7 @@ function sendMessage(e) { name: config.userName, message, avatar: config.userAvatar, - topic: b4a.toString(currentRoom.topic, 'hex'), + topic: topic, peers: peersPublicKeys, // Deprecated. To be deleted in future updates timestamp: Date.now(), readableTimestamp: new Date().toLocaleString(), // Added human-readable timestamp @@ -318,6 +412,41 @@ function sendMessage(e) { } } +async function handleFileInput(event) { + const file = event.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = async (event) => { + const buffer = new Uint8Array(event.target.result); + const filePath = `/files/${file.name}`; + await drive.put(filePath, buffer); + const fileUrl = `http://localhost:${servePort}${filePath}`; + + const topic = document.querySelector('#chat-room-topic').innerText; + + const fileMessage = { + type: 'file', + name: config.userName, + fileName: file.name, + file: b4a.toString(buffer, 'base64'), + fileType: file.type, + avatar: updatePortInUrl(config.userAvatar), + topic: topic + }; + + console.log('Sending file message:', fileMessage); // Debugging log + + const peers = [...swarm.connections]; + for (const peer of peers) { + peer.write(JSON.stringify(fileMessage)); + } + + addFileMessage(config.userName, file.name, fileUrl, file.type, config.userAvatar, topic); + }; + reader.readAsArrayBuffer(file); + } +} + function sendFileMessage(name, fileUrl, fileType, avatar) { const fileName = fileUrl.split('/').pop(); const messageObj = JSON.stringify({ @@ -327,7 +456,7 @@ function sendFileMessage(name, fileUrl, fileType, avatar) { fileUrl, fileType, avatar, - topic: b4a.toString(currentRoom.topic, 'hex'), + topic: document.querySelector('#chat-room-topic').innerText, timestamp: Date.now(), }); @@ -336,10 +465,11 @@ function sendFileMessage(name, fileUrl, fileType, avatar) { peer.write(messageObj); } - addFileMessage(name, fileName, fileUrl, fileType, avatar); + addFileMessage(name, fileName, fileUrl, fileType, avatar, document.querySelector('#chat-room-topic').innerText); } -function addFileMessage(from, fileName, fileUrl, fileType, avatar) { +function addFileMessage(from, fileName, fileUrl, fileType, avatar, topic) { + console.log('Adding file message:', { from, fileName, fileUrl, fileType, avatar, topic }); // Debugging log const $div = document.createElement('div'); $div.classList.add('message'); @@ -363,16 +493,27 @@ function addFileMessage(from, fileName, fileUrl, fileType, avatar) { $image.classList.add('attached-image'); $content.appendChild($image); } else { - const $fileLink = document.createElement('a'); - $fileLink.href = fileUrl; - $fileLink.textContent = `File: ${fileName}`; - $fileLink.download = fileName; - $content.appendChild($fileLink); + const $fileButton = document.createElement('button'); + $fileButton.textContent = `Download File: ${fileName}`; + $fileButton.onclick = function() { + const $fileLink = document.createElement('a'); + $fileLink.href = fileUrl; + $fileLink.download = fileName; + $fileLink.click(); + }; + $content.appendChild($fileButton); } $div.appendChild($content); - document.querySelector('#messages').appendChild($div); - scrollToBottom(); + + // Only render the message if it's for the current room + const currentTopic = document.querySelector('#chat-room-topic').innerText; + if (currentTopic === topic) { + document.querySelector('#messages').appendChild($div); + scrollToBottom(); + } else { + console.log(`Message for topic ${topic} not rendered because current topic is ${currentTopic}`); // Debugging log + } } function updatePeerCount() { @@ -387,37 +528,61 @@ function scrollToBottom() { container.scrollTop = container.scrollHeight; } -function onMessageAdded(from, message, avatar) { - const $div = document.createElement('div'); - $div.classList.add('message'); - - const $img = document.createElement('img'); +function onMessageAdded(from, message, avatar, topic) { + console.log('Adding message:', { from, message, avatar, topic }); // Debugging log + const messageObj = { + from, + message, + avatar + }; - $img.src = updatePortInUrl(avatar) || 'https://via.placeholder.com/40'; // Default to a placeholder image if avatar URL is not provided - console.log(updatePortInUrl(avatar)) - $img.classList.add('avatar'); - $div.appendChild($img); + // Add the message to the store + addMessageToStore(topic, messageObj); - const $content = document.createElement('div'); - $content.classList.add('message-content'); + // Only render messages for the current room + const currentTopic = document.querySelector('#chat-room-topic').innerText; + if (currentTopic === topic) { + const $div = document.createElement('div'); + $div.classList.add('message'); - const $header = document.createElement('div'); - $header.classList.add('message-header'); - $header.textContent = from; + const $img = document.createElement('img'); + $img.src = updatePortInUrl(avatar) || 'https://via.placeholder.com/40'; // Default to a placeholder image if avatar URL is not provided + $img.classList.add('avatar'); + $div.appendChild($img); - const $text = document.createElement('div'); - $text.classList.add('message-text'); + const $content = document.createElement('div'); + $content.classList.add('message-content'); - const md = window.markdownit(); - const markdownContent = md.render(message); - $text.innerHTML = markdownContent; + const $header = document.createElement('div'); + $header.classList.add('message-header'); + $header.textContent = from; - $content.appendChild($header); - $content.appendChild($text); - $div.appendChild($content); + const $text = document.createElement('div'); + $text.classList.add('message-text'); - document.querySelector('#messages').appendChild($div); - scrollToBottom(); + const md = window.markdownit({ + highlight: function (str, lang) { + if (lang && hljs.getLanguage(lang)) { + try { + return hljs.highlight(str, { language: lang }).value; + } catch (__) {} + } + return ''; // use external default escaping + } + }); + + const markdownContent = md.render(message); + $text.innerHTML = markdownContent; + + $content.appendChild($header); + $content.appendChild($text); + $div.appendChild($content); + + document.querySelector('#messages').appendChild($div); + scrollToBottom(); + } else { + console.log(`Message for topic ${topic} not rendered because current topic is ${currentTopic}`); // Debugging log + } } function truncateHash(hash) { @@ -429,7 +594,7 @@ async function updateIcon(username, avatarBuffer) { if (userIcon) { const avatarBlob = new Blob([avatarBuffer], { type: 'image/png' }); const avatarUrl = URL.createObjectURL(avatarBlob); - userIcon.src = avatarUrl; + userIcon.src = updatePortInUrl(avatarUrl); config.userAvatar = avatarUrl; writeConfigToFile("./config.json"); @@ -462,4 +627,45 @@ function updatePortInUrl(url) { return urlObject.toString(); } -initialize(); \ No newline at end of file +function renderRoomList() { + const roomList = document.querySelector('#room-list'); + roomList.innerHTML = ''; + + config.rooms.forEach(room => { + const roomItem = document.createElement('li'); + roomItem.textContent = room.alias || truncateHash(room.topic); + roomItem.dataset.topic = room.topic; + + roomItem.addEventListener('dblclick', () => enterEditMode(roomItem)); + roomItem.addEventListener('click', () => switchRoom(room.topic)); + roomList.appendChild(roomItem); + }); +} + +function renderMessagesForRoom(topic) { + console.log('Rendering messages for room:', topic); // Debugging log + // Clear the message area + clearMessages(); + + // Fetch and render messages for the selected room + const messages = getMessagesForRoom(topic); + messages.forEach(message => { + onMessageAdded(message.from, message.message, message.avatar, topic); + }); +} + +function getMessagesForRoom(topic) { + return messagesStore[topic] || []; +} + +function addMessageToStore(topic, messageObj) { + if (!messagesStore[topic]) { + messagesStore[topic] = []; + } + messagesStore[topic].push(messageObj); +} + +// Call this function when loading the rooms initially +renderRoomList(); + +initialize(); diff --git a/chatBot/includes/Client.js b/chatBot/includes/Client.js index 78d364f..f249b23 100644 --- a/chatBot/includes/Client.js +++ b/chatBot/includes/Client.js @@ -1,5 +1,5 @@ import Hyperswarm from 'hyperswarm'; -import EventEmitter from 'node:events' +import EventEmitter from 'node:events'; import b4a from "b4a"; class Client extends EventEmitter { @@ -8,15 +8,21 @@ class Client extends EventEmitter { if(!botName) return console.error("Bot Name is not defined!"); this.botName = botName; this.swarm = new Hyperswarm(); + this.joinedRooms = new Set(); // Track the rooms the bot has joined + this.currentTopic = null; // Track the current topic this.setupSwarm(); } setupSwarm() { this.swarm.on('connection', (peer) => { peer.on('data', message => { - if(message.type === "message") this.emit('onMessage', peer, JSON.parse(message.toString())); - if(message.type === "icon") this.emit('onIcon', peer, JSON.parse(message.toString())); - if(message.type === "file") this.emit('onFile', peer, JSON.parse(message.toString())); + const messageObj = JSON.parse(message.toString()); + if (this.joinedRooms.has(messageObj.topic)) { // Process message only if it is from a joined room + this.currentTopic = messageObj.topic; // Set the current topic from the incoming message + if (messageObj.type === "message") this.emit('onMessage', peer, messageObj); + if (messageObj.type === "icon") this.emit('onIcon', peer, messageObj); + if (messageObj.type === "file") this.emit('onFile', peer, messageObj); + } }); peer.on('error', e => { @@ -27,20 +33,13 @@ class Client extends EventEmitter { this.swarm.on('update', () => { console.log(`Connections count: ${this.swarm.connections.size} / Peers count: ${this.swarm.peers.size}`); - - this.swarm.peers.forEach((peerInfo, peerId) => { - // Please do not try to understand what is going on here. I have no idea anyway. But it surprisingly works - - const peer = [peerId]; - const peerTopics = [peerInfo.topics] - .filter(topics => topics) - .map(topics => topics.map(topic => b4a.toString(topic, 'hex'))); - }); }); } joinChatRoom(chatRoomID) { - this.discovery = this.swarm.join(Buffer.from(chatRoomID, 'hex'), {client: true, server: true}); + this.joinedRooms.add(chatRoomID); // Add the room to the list of joined rooms + this.currentTopic = chatRoomID; // Store the current topic + this.discovery = this.swarm.join(Buffer.from(chatRoomID, 'hex'), { client: true, server: true }); this.discovery.flushed().then(() => { console.log(`Bot ${this.botName} joined the chat room.`); this.emit('onBotJoinRoom'); @@ -49,17 +48,31 @@ class Client extends EventEmitter { sendMessage(roomPeers, message) { console.log('Bot name:', this.botName); - const timestamp = Date.now(); // Generate timestamp - const peers = [...this.swarm.connections].filter(peer => roomPeers.includes(peer.remotePublicKey.toString('hex'))); // We remove all the peers that arent included in a room - const data = JSON.stringify({name: this.botName, message, timestamp}); // Include timestamp + const timestamp = Date.now(); + const messageObj = { + type: 'message', + name: this.botName, + message, + timestamp, + topic: this.currentTopic // Include the current topic + }; + const data = JSON.stringify(messageObj); + const peers = [...this.swarm.connections].filter(peer => roomPeers.includes(peer.remotePublicKey.toString('hex'))); for (const peer of peers) peer.write(data); } sendMessageToAll(message) { console.log('Bot name:', this.botName); - const timestamp = Date.now(); // Generate timestamp - const peers = [...this.swarm.connections] - const data = JSON.stringify({name: this.botName, message, timestamp}); // Include timestamp + const timestamp = Date.now(); + const messageObj = { + type: 'message', + name: this.botName, + message, + timestamp, + topic: this.currentTopic // Include the current topic + }; + const data = JSON.stringify(messageObj); + const peers = [...this.swarm.connections]; for (const peer of peers) peer.write(data); } diff --git a/index.html b/index.html index ffe7718..0f31c1b 100644 --- a/index.html +++ b/index.html @@ -7,11 +7,13 @@ + +
-
LinkUp
+
LinkUp | Peers: 0