diff --git a/app.js b/app.js index b1df6c3..df2a563 100644 --- a/app.js +++ b/app.js @@ -5,7 +5,7 @@ import ServeDrive from 'serve-drive'; import Hyperdrive from 'hyperdrive'; import Corestore from 'corestore'; import { EventEmitter } from 'events'; -import fs from "fs"; +import fs from 'fs'; import handleCommand from './commands.js'; const agentAvatarPath = './assets/agent.png'; @@ -32,7 +32,7 @@ let servePort; let config = { userName: '', userAvatar: '', - rooms: [], + guilds: {}, registeredUsers: {} }; @@ -44,8 +44,8 @@ function getRandomPort() { return Math.floor(Math.random() * (65535 - 49152 + 1)) + 49152; } -function currentTopic() { - return document.querySelector('#chat-room-topic').innerText; +function currentGuildTopic() { + return document.querySelector('#chat-guild-topic').innerText; } function getCurrentPeerCount() { @@ -61,17 +61,57 @@ function updatePeerCount() { } } -async function joinRoom(topicStr) { - const topicBuffer = b4a.from(topicStr, 'hex'); - addRoomToList(topicStr); +async function processGuild(guildData) { + const parsedData = JSON.parse(guildData); + config.guilds[parsedData.guildTopic] = { + alias: parsedData.guildAlias, + rooms: parsedData.rooms, + owner: parsedData.owner + }; + writeConfigToFile("./config.json"); + renderGuildList(); + await joinGuild(parsedData.guildTopic); +} + +async function joinGuild(guildTopic) { + const guild = config.guilds[guildTopic]; + if (guild) { + for (const room of guild.rooms) { + await joinRoom(guildTopic, room.topic, room.alias); + } + } +} + +async function joinRoom(guildTopic, roomTopic, alias) { + const topicBuffer = b4a.from(roomTopic, 'hex'); + addRoomToList(guildTopic, roomTopic, alias); await joinSwarm(topicBuffer); } -async function createRoom(alias) { - const topicBuffer = crypto.randomBytes(32); - const topic = b4a.toString(topicBuffer, 'hex'); - addRoomToList(topic, alias); - await joinSwarm(topicBuffer); +async function createRoom(guildTopic, alias) { + const roomTopicBuffer = crypto.randomBytes(32); + const roomTopic = b4a.toString(roomTopicBuffer, 'hex'); + + config.guilds[guildTopic].rooms.push({ topic: roomTopic, alias: alias || truncateHash(roomTopic) }); + writeConfigToFile("./config.json"); + + addRoomToList(guildTopic, roomTopic, alias); + await joinSwarm(roomTopicBuffer); + + // Synchronize the new room with other peers + const roomMessage = JSON.stringify({ + type: 'room', + guildTopic, + room: { topic: roomTopic, alias: alias || truncateHash(roomTopic) } + }); + + const guildSwarm = activeRooms.find(room => room.topic === guildTopic); + if (guildSwarm) { + const peers = [...guildSwarm.swarm.connections]; + for (const peer of peers) { + peer.write(roomMessage); + } + } } async function listFiles() { @@ -81,6 +121,7 @@ async function listFiles() { } return files; } + async function deleteFile(filename) { await drive.del(`/files/${filename}`); } @@ -98,8 +139,9 @@ async function initialize() { const configExists = fs.existsSync("./config.json"); if (configExists) { loadConfigFromFile(); - renderRoomList(); + renderGuildList(); await connectToAllRooms(); + await joinAllGuilds(); // Ensure the app joins all guilds on startup } if (!configExists) { @@ -113,11 +155,47 @@ async function initialize() { document.addEventListener("DOMContentLoaded", (event) => { hljs.highlightAll(); }); + + document.addEventListener('createGuild', (event) => { + const { guildName } = event.detail; + createGuild(guildName); + }); + + document.addEventListener('addRoomToGuild', (event) => { + const { guildTopic, roomName } = event.detail; + createRoom(guildTopic, roomName); + }); + + document.addEventListener('manageGuild', (event) => { + const { guildTopic } = event.detail; + openManageGuildModal(guildTopic); + }); + + document.addEventListener('switchRoom', (event) => { + const { guildTopic, roomTopic } = event.detail; + if (!roomTopic) { + console.error('Invalid room topic:', roomTopic); + return; + } + console.log('Event switchRoom with roomTopic:', roomTopic); + switchRoom(guildTopic, roomTopic); + }); + + document.addEventListener('joinGuildRequest', (event) => { + const { guildTopic } = event.detail; + joinGuildRequest(guildTopic); + }); } catch (error) { console.error('Error during initialization:', error); } } +async function joinAllGuilds() { + for (const guildTopic in config.guilds) { + await joinGuildRequest(guildTopic); + } +} + function setupEventListeners() { const registerForm = document.querySelector('#register-form'); const selectAvatarButton = document.querySelector('#select-avatar'); @@ -129,6 +207,7 @@ function setupEventListeners() { const attachFileButton = document.getElementById('attach-file'); const fileInput = document.getElementById('file-input'); const talkButton = document.getElementById('talk-btn'); + const joinGuildBtn = document.getElementById('join-guild'); if (registerForm) { registerForm.addEventListener('submit', registerUser); @@ -139,7 +218,10 @@ function setupEventListeners() { }); } if (createChatRoomButton) { - createChatRoomButton.addEventListener('click', createChatRoom); + createChatRoomButton.addEventListener('click', () => { + const guildTopic = currentGuildTopic(); + createRoom(guildTopic); + }); } if (joinChatRoomButton) { joinChatRoomButton.addEventListener('click', joinChatRoom); @@ -165,8 +247,30 @@ function setupEventListeners() { if (talkButton) { setupTalkButton(); } + if (joinGuildBtn) { + joinGuildBtn.addEventListener('click', () => { + const guildTopic = document.getElementById('join-guild-topic').value.trim(); + if (guildTopic) { + joinGuildRequest(guildTopic); + } + }); + } + + // Add event listeners only for room items + document.querySelectorAll('.room-item').forEach(item => { + item.addEventListener('click', () => { + const guildTopic = item.dataset.guildTopic; + const roomTopic = item.dataset.topic; + if (!roomTopic) { + console.error('Invalid room topic for item:', item); + return; + } + switchRoom(guildTopic, roomTopic); + }); + }); } -function handleIncomingMessage(messageObj) { + +function handleIncomingMessage(messageObj, connection) { console.log('Received message:', messageObj); // Debugging log if (messageObj.type === 'icon') { @@ -209,10 +313,84 @@ function handleIncomingMessage(messageObj) { }).catch(error => { console.error(`Failed to store audio message:`, error); }); + } else if (messageObj.type === 'guild') { + const guildData = messageObj.guildData; + processGuild(guildData); + } else if (messageObj.type === 'room') { + const { guildTopic, room } = messageObj; + if (config.guilds[guildTopic]) { + config.guilds[guildTopic].rooms.push(room); + writeConfigToFile("./config.json"); + renderGuildList(); + joinRoom(guildTopic, room.topic, room.alias); + } + } else if (messageObj.type === 'remove-room') { + const { guildTopic, roomTopic } = messageObj; + if (config.guilds[guildTopic]) { + const roomIndex = config.guilds[guildTopic].rooms.findIndex(room => room.topic === roomTopic); + if (roomIndex !== -1) { + config.guilds[guildTopic].rooms.splice(roomIndex, 1); + writeConfigToFile("./config.json"); + renderGuildList(); + leaveRoom(roomTopic); + } + } + } else if (messageObj.type === 'rename-room') { + const { guildTopic, roomTopic, newAlias } = messageObj; + if (config.guilds[guildTopic]) { + const room = config.guilds[guildTopic].rooms.find(room => room.topic === roomTopic); + if (room) { + room.alias = newAlias; + writeConfigToFile("./config.json"); + + // Synchronize the room rename with other peers + const renameMessage = JSON.stringify({ + type: 'rename-room', + guildTopic, + roomTopic: topic, + newAlias + }); + + const guildSwarm = activeRooms.find(room => room.topic === guildTopic); + if (guildSwarm) { + const peers = [...guildSwarm.swarm.connections]; + for (const peer of peers) { + peer.write(renameMessage); + } + } + } + } + } else if (messageObj.type === 'guildJoin') { + const { guildTopic, guildData } = messageObj; + if (!config.guilds[guildTopic]) { + config.guilds[guildTopic] = guildData; + writeConfigToFile("./config.json"); + renderGuildList(); + joinGuild(guildTopic); + } + } else if (messageObj.type === 'guildRequest') { + const guildTopic = messageObj.guildTopic; + const guild = config.guilds[guildTopic]; + if (guild) { + const guildResponseMessage = JSON.stringify({ + type: 'guildResponse', + guildData: JSON.stringify({ + guildTopic, + guildAlias: guild.alias, + rooms: guild.rooms, + owner: guild.owner + }) + }); + connection.write(guildResponseMessage); + } + } else if (messageObj.type === 'guildResponse') { + const guildData = messageObj.guildData; + processGuild(guildData); } else { console.error('Received unknown message type:', messageObj); } } + async function handleConnection(connection, info) { console.log('New connection', info); @@ -221,7 +399,7 @@ async function handleConnection(connection, info) { if (iconBuffer) { const iconMessage = JSON.stringify({ type: 'icon', - username: config.userName, + name: config.userName, avatar: b4a.toString(iconBuffer, 'base64'), timestamp: Date.now() }); @@ -231,9 +409,37 @@ async function handleConnection(connection, info) { console.error('Icon not found for user:', config.userName); } + // Sending the guilds and rooms information + for (const guildTopic in config.guilds) { + const guild = config.guilds[guildTopic]; + const guildMessage = JSON.stringify({ + type: 'guild', + guildData: JSON.stringify({ + guildTopic, + guildAlias: guild.alias, + rooms: guild.rooms, + owner: guild.owner + }) + }); + console.log('Sending guild information to new peer:', guildMessage); + connection.write(guildMessage); + } + connection.on('data', (data) => { const messageObj = JSON.parse(data.toString()); - eventEmitter.emit('onMessage', messageObj); + if (messageObj.type === 'guildJoin') { + const guildData = config.guilds[messageObj.guildTopic]; + if (guildData) { + const guildJoinMessage = JSON.stringify({ + type: 'guildJoin', + guildTopic: messageObj.guildTopic, + guildData: guildData + }); + connection.write(guildJoinMessage); + } + } else { + eventEmitter.emit('onMessage', messageObj, connection); + } }); connection.on('close', () => { @@ -377,27 +583,51 @@ async function continueRegistration(regUsername) { writeConfigToFile("./config.json"); } -async function createChatRoom() { +async function createGuild(guildName) { const topicBuffer = crypto.randomBytes(32); const topic = b4a.toString(topicBuffer, 'hex'); - addRoomToList(topic); - await joinSwarm(topicBuffer); + + config.guilds[topic] = { + alias: guildName, + rooms: [], + owner: config.userName + }; + + addGuildToList(topic, guildName); + writeConfigToFile("./config.json"); } async function joinChatRoom(e) { e.preventDefault(); - const topicStr = document.querySelector('#join-chat-room-topic').value.trim(); + const guildTopic = document.querySelector('#join-guild-topic').value.trim(); + const roomTopic = document.querySelector('#join-chat-room-topic').value.trim(); // Validate the topic string - const isValidTopic = /^[0-9a-fA-F]{64}$/.test(topicStr); - if (!isValidTopic) { - alert('Invalid topic format. Please enter a 64-character hexadecimal string.'); + const isValidGuildTopic = /^[0-9a-fA-F]{64}$/.test(guildTopic); + const isValidRoomTopic = /^[0-9a-fA-F]{64}$/.test(roomTopic); + if (!isValidGuildTopic || !isValidRoomTopic) { + alert('Invalid topic format. Please enter 64-character hexadecimal strings.'); return; } - const topicBuffer = b4a.from(topicStr, 'hex'); - addRoomToList(topicStr); - await joinSwarm(topicBuffer); + const guildTopicBuffer = b4a.from(guildTopic, 'hex'); + const roomTopicBuffer = b4a.from(roomTopic, 'hex'); + addRoomToList(guildTopic, roomTopic); + await joinSwarm(roomTopicBuffer); + + // Fetch guild data from a peer + const guildJoinMessage = JSON.stringify({ + type: 'guildJoin', + guildTopic: guildTopic + }); + + const guildSwarm = activeRooms.find(room => room.topic === guildTopic); + if (guildSwarm) { + const peers = [...guildSwarm.swarm.connections]; + for (const peer of peers) { + peer.write(guildJoinMessage); + } + } } async function joinSwarm(topicBuffer) { @@ -424,18 +654,100 @@ async function joinSwarm(topicBuffer) { } } -function addRoomToList(topic, alias) { - const roomList = document.querySelector('#room-list'); - const roomItem = document.createElement('li'); - roomItem.textContent = alias || truncateHash(topic); - roomItem.dataset.topic = topic; +function addGuildToList(guildTopic, alias) { + const guildList = document.querySelector('#guild-list'); + const guildItem = document.createElement('li'); + guildItem.textContent = alias || truncateHash(guildTopic); + guildItem.dataset.guildTopic = guildTopic; + guildItem.classList.add('guild-item'); - roomItem.addEventListener('dblclick', () => enterEditMode(roomItem)); - roomItem.addEventListener('click', () => switchRoom(topic)); - roomList.appendChild(roomItem); + if (config.guilds[guildTopic].owner === config.userName) { + const manageButton = document.createElement('button'); + manageButton.textContent = 'Manage'; + manageButton.classList.add('mini-button', 'manage-guild-btn'); + manageButton.addEventListener('click', (e) => { + e.stopPropagation(); + openManageGuildModal(guildTopic); + }); + guildItem.appendChild(manageButton); + } - config.rooms.push({ topic, alias: alias || truncateHash(topic) }); - writeConfigToFile("./config.json"); + const roomList = document.createElement('ul'); + roomList.classList.add('room-list'); + guildItem.appendChild(roomList); + + guildList.appendChild(guildItem); + + // Add the rooms for this guild + config.guilds[guildTopic].rooms.forEach(room => { + addRoomToList(guildTopic, room.topic, room.alias); + }); +} + +function addRoomToList(guildTopic, roomTopic, alias) { + const guildItem = document.querySelector(`li[data-guild-topic="${guildTopic}"]`); + if (guildItem) { + let roomList = guildItem.querySelector('.room-list'); + if (!roomList) { + roomList = document.createElement('ul'); + roomList.classList.add('room-list'); + guildItem.appendChild(roomList); + } + + if (!roomList.querySelector(`li[data-topic="${roomTopic}"]`)) { + const roomItem = document.createElement('li'); + roomItem.textContent = alias || truncateHash(roomTopic); + roomItem.dataset.topic = roomTopic; + roomItem.dataset.guildTopic = guildTopic; + roomItem.classList.add('room-item'); + + roomItem.addEventListener('dblclick', () => enterEditMode(roomItem)); + roomItem.addEventListener('click', () => switchRoom(guildTopic, roomTopic)); + roomList.appendChild(roomItem); + } + } +} + +function openManageGuildModal(guildTopic) { + const guild = config.guilds[guildTopic]; + if (!guild) return; + + const manageGuildModal = document.getElementById('manage-guild-modal'); + manageGuildModal.dataset.guildTopic = guildTopic; + + const guildInfo = manageGuildModal.querySelector('#guild-info'); + guildInfo.innerHTML = `

${guild.alias}

`; + + const roomList = manageGuildModal.querySelector('#room-list'); + roomList.innerHTML = ''; + + guild.rooms.forEach(room => { + const roomItem = document.createElement('li'); + roomItem.textContent = room.alias; + roomItem.dataset.topic = room.topic; + + const editIcon = document.createElement('span'); + editIcon.textContent = '✏️'; + editIcon.classList.add('edit-icon'); + editIcon.addEventListener('click', (e) => { + e.stopPropagation(); + enterEditMode(roomItem); + }); + + const deleteIcon = document.createElement('span'); + deleteIcon.textContent = '❌'; + deleteIcon.classList.add('delete-icon'); + deleteIcon.addEventListener('click', (e) => { + e.stopPropagation(); + removeRoom(guildTopic, room.topic); + }); + + roomItem.appendChild(editIcon); + roomItem.appendChild(deleteIcon); + roomList.appendChild(roomItem); + }); + + manageGuildModal.classList.remove('hidden'); } function enterEditMode(roomItem) { @@ -472,10 +784,31 @@ function exitEditMode(roomItem, input, topic) { 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"); + for (const guildTopic in config.guilds) { + const guild = config.guilds[guildTopic]; + const room = guild.rooms.find(room => room.topic === topic); + if (room) { + room.alias = newAlias; + writeConfigToFile("./config.json"); + + // Synchronize the room rename with other peers + const renameMessage = JSON.stringify({ + type: 'rename-room', + guildTopic, + roomTopic: topic, + newAlias + }); + + const guildSwarm = activeRooms.find(room => room.topic === guildTopic); + if (guildSwarm) { + const peers = [...guildSwarm.swarm.connections]; + for (const peer of peers) { + peer.write(renameMessage); + } + } + + break; + } } // Check if the edited room is the current room in view @@ -490,40 +823,83 @@ function exitEditMode(roomItem, input, topic) { } } -function switchRoom(topic) { - console.log('Switching to room:', topic); // Debugging log +function removeRoom(guildTopic, roomTopic) { + const guild = config.guilds[guildTopic]; + if (guild) { + const roomIndex = guild.rooms.findIndex(room => room.topic === roomTopic); + if (roomIndex !== -1) { + guild.rooms.splice(roomIndex, 1); + writeConfigToFile("./config.json"); + renderGuildList(); + leaveRoom(roomTopic); + + // Synchronize the room removal with other peers + const removeMessage = JSON.stringify({ + type: 'remove-room', + guildTopic, + roomTopic + }); + + const guildSwarm = activeRooms.find(room => room.topic === guildTopic); + if (guildSwarm) { + const peers = [...guildSwarm.swarm.connections]; + for (const peer of peers) { + peer.write(removeMessage); + } + } + } + } +} + +function switchRoom(guildTopic, roomTopic) { + if (!roomTopic) { + console.error('Invalid room topic:', roomTopic); + return; + } + + console.log('Switching to room:', roomTopic); const chatRoomTopic = document.querySelector('#chat-room-topic'); const chatRoomName = document.querySelector('#chat-room-name'); + const guild = config.guilds[guildTopic]; + + if (!guild) { + console.error('Guild not found:', guildTopic); + return; + } if (chatRoomTopic) { - chatRoomTopic.innerText = topic; // Set full topic here + chatRoomTopic.innerText = roomTopic; // 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; + // Find the room in the current guild + const room = guild.rooms.find(room => room.topic === roomTopic); + if (room) { + // Update the room name in the header + chatRoomName.innerText = room.alias; + } else { + console.error('Room not found in the current guild:', roomTopic); + } } else { console.error('Element #chat-room-name not found'); } - clearMessages(); - renderMessagesForRoom(topic); - updatePeerCount(); - - // Show chat UI elements + // Show the chat view document.querySelector('#chat').classList.remove('hidden'); document.querySelector('#setup').classList.add('hidden'); + + // Render the messages for the room + renderMessagesForRoom(roomTopic); } -function leaveRoom(topic) { +async function leaveRoom(topic) { const roomIndex = activeRooms.findIndex(room => room.topic === topic); if (roomIndex !== -1) { - const room = activeRooms[roomIndex]; - room.swarm.destroy(); + const { swarm, discovery } = activeRooms[roomIndex]; + await discovery.destroy(); + swarm.destroy(); activeRooms.splice(roomIndex, 1); } @@ -532,286 +908,230 @@ function leaveRoom(topic) { roomItem.remove(); } - config.rooms = config.rooms.filter(e => e.topic !== topic); - writeConfigToFile("./config.json"); + const messagesContainer = document.querySelector('#messages'); + if (messagesContainer) { + messagesContainer.innerHTML = ''; + } - if (activeRooms.length > 0) { - switchRoom(activeRooms[0].topic); - } else { - document.querySelector('#chat').classList.add('hidden'); - document.querySelector('#setup').classList.remove('hidden'); + const chatRoomName = document.querySelector('#chat-room-name'); + if (chatRoomName) { + chatRoomName.innerText = ''; + } + + const chatRoomTopic = document.querySelector('#chat-room-topic'); + if (chatRoomTopic) { + chatRoomTopic.innerText = ''; + } + + const chatDiv = document.querySelector('#chat'); + const setupDiv = document.querySelector('#setup'); + if (chatDiv && setupDiv) { + chatDiv.classList.add('hidden'); + setupDiv.classList.remove('hidden'); } } +function writeConfigToFile(path) { + fs.writeFileSync(path, JSON.stringify(config, null, 2)); +} -async function sendMessage(e) { - e.preventDefault(); - const message = document.querySelector('#message').value; - document.querySelector('#message').value = ''; +function loadConfigFromFile() { + const configFile = fs.readFileSync("./config.json", 'utf8'); + config = JSON.parse(configFile); +} - const topic = currentTopic(); - const timestamp = Date.now(); +function renderGuildList() { + const guildList = document.querySelector('#guild-list'); + guildList.innerHTML = ''; - if (message.startsWith('~')) { - // Handle command - await handleCommand(message, { - eventEmitter, - currentTopic, - clearMessages, - addMessage: (from, message, avatar, topic) => onMessageAdded(from, message, avatar, topic, timestamp), - joinRoom, - leaveRoom, - createRoom, - listFiles, - deleteFile - }); - return; - } - - console.log('Sending message:', message); // Debugging log - - onMessageAdded(config.userName, message, config.userAvatar, topic, timestamp); - - const messageObj = JSON.stringify({ - type: 'message', - name: config.userName, - avatar: config.userAvatar, - topic: topic, - timestamp: timestamp, - message - }); - - const peers = [...activeRooms.find(room => room.topic === topic).swarm.connections]; - for (const peer of peers) { - peer.write(messageObj); + for (const guildTopic in config.guilds) { + const guild = config.guilds[guildTopic]; + addGuildToList(guildTopic, guild.alias); } } -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 = currentTopic(); - - const fileMessage = { - type: 'file', - name: config.userName, - avatar: updatePortInUrl(config.userAvatar), - topic: topic, - timestamp: Date.now(), - fileName: file.name, - file: b4a.toString(buffer, 'base64'), - fileType: file.type - }; - - console.log('Sending file message:', fileMessage); // Debugging log - - const peers = [...activeRooms.find(room => room.topic === topic).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); +async function connectToAllRooms() { + for (const guildTopic in config.guilds) { + const guild = config.guilds[guildTopic]; + for (const room of guild.rooms) { + await joinRoom(guildTopic, room.topic, room.alias); + } } } -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'); - - const $img = document.createElement('img'); - $img.src = updatePortInUrl(avatar) || 'https://via.placeholder.com/40'; - $img.classList.add('avatar'); - $img.draggable = false; - $div.appendChild($img); - - const $content = document.createElement('div'); - $content.classList.add('message-content'); - - const $header = document.createElement('div'); - $header.classList.add('message-header'); - $header.textContent = from; - $content.appendChild($header); - - if (fileType.startsWith('image/')) { - const $image = document.createElement('img'); - $image.src = updatePortInUrl(fileUrl); - $image.alt = fileName; - $image.classList.add('attached-image'); - $content.appendChild($image); - } else { - 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); - - // Only render the message if it's for the current room - 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 scrollToBottom() { - var container = document.getElementById("messages-container"); - container.scrollTop = container.scrollHeight; -} - -function onMessageAdded(from, message, avatar, topic, timestamp) { - console.log('Adding message:', { from, message, avatar, topic }); // Debugging log - const messageObj = { - from, - message, - avatar, - timestamp: timestamp || Date.now() - }; - - // Add the message to the store - addMessageToStore(topic, messageObj); - - // Only render messages for the current room - if (currentTopic() === topic) { - const $div = document.createElement('div'); - $div.classList.add('message'); - - 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'); - $img.draggable = false; - $div.appendChild($img); - - const $content = document.createElement('div'); - $content.classList.add('message-content'); - - const $header = document.createElement('div'); - $header.classList.add('message-header'); - $header.textContent = from; - - const $text = document.createElement('div'); - $text.classList.add('message-text'); - - if (message.includes('Available files:')) { - const files = message.split('\n').slice(1); // Skip the "Available files:" line - const fileList = document.createElement('ul'); - - files.forEach(file => { - file = file.replace("- ", "") - const listItem = document.createElement('li'); - const fileButton = document.createElement('button'); - fileButton.textContent = file.trim(); - fileButton.onclick = () => downloadFile(file.trim()); - - const deleteButton = document.createElement('button'); - deleteButton.textContent = 'Delete'; - deleteButton.onclick = () => { - console.log("file to delete: ", file); - deleteFile(file); - listItem.remove(); - }; - - - listItem.appendChild(fileButton); - listItem.appendChild(deleteButton); - fileList.appendChild(listItem); - }); - - $text.appendChild(fileList); - } else { - - 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 downloadFile(filename) { - const fileUrl = `http://localhost:${servePort}/files/${filename}`; - const a = document.createElement('a'); - a.href = fileUrl; - a.download = filename; - a.click(); -} - - -function addAudioMessage(from, audioUrl, avatar, topic) { - console.log('Adding audio message:', { from, audioUrl, avatar, topic }); // Debugging log - const $div = document.createElement('div'); - $div.classList.add('message'); - - const $img = document.createElement('img'); - $img.src = updatePortInUrl(avatar) || 'https://via.placeholder.com/40'; - $img.classList.add('avatar'); - $img.draggable = false; - $div.appendChild($img); - - const $content = document.createElement('div'); - $content.classList.add('message-content'); - - const $header = document.createElement('div'); - $header.classList.add('message-header'); - $header.textContent = from; - $content.appendChild($header); - - const $audio = document.createElement('audio'); - $audio.controls = true; - if (from !== config.userName) { - $audio.autoplay = true; // Add autoplay attribute for peers only - } - $audio.src = updatePortInUrl(audioUrl); - $audio.classList.add('attached-audio'); - $content.appendChild($audio); - - $div.appendChild($content); - - // Only render the message if it's for the current room - 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 toggleSetupView() { + const setupDiv = document.querySelector('#setup'); + const chatDiv = document.querySelector('#chat'); + setupDiv.classList.toggle('hidden'); + chatDiv.classList.toggle('hidden'); } function truncateHash(hash) { - return `${hash.slice(0, 6)}...${hash.slice(-6)}`; + return `${hash.substring(0, 4)}...${hash.substring(hash.length - 4)}`; +} + +function updatePortInUrl(url) { + if (typeof url !== 'string') { + console.error('Invalid URL format:', url); + return ''; + } + const urlObj = new URL(url); + urlObj.port = servePort; + return urlObj.toString(); +} + +function addFileMessage(name, fileName, fileUrl, fileType, avatar, topic) { + const container = document.querySelector('#messages'); + if (!container) { + console.error('Element #messages not found'); + return; + } + + const messageDiv = document.createElement('div'); + messageDiv.classList.add('message'); + if (topic !== currentTopic()) { + messageDiv.classList.add('hidden'); // Hide messages not belonging to the current room + } + + const avatarImg = document.createElement('img'); + avatarImg.src = updatePortInUrl(avatar); + avatarImg.alt = `${name}'s avatar`; + avatarImg.classList.add('avatar'); + + const messageContent = document.createElement('div'); + messageContent.classList.add('message-content'); + + const senderName = document.createElement('div'); + senderName.classList.add('message-sender'); + senderName.textContent = name; + + const fileLink = document.createElement('a'); + fileLink.href = fileUrl; + fileLink.textContent = `File: ${fileName}`; + fileLink.classList.add('message-file'); + + if (fileType.startsWith('image/')) { + const filePreview = document.createElement('img'); + filePreview.src = fileUrl; + filePreview.alt = fileName; + filePreview.classList.add('file-preview'); + messageContent.appendChild(filePreview); + } else if (fileType.startsWith('video/')) { + const filePreview = document.createElement('video'); + filePreview.src = fileUrl; + filePreview.alt = fileName; + filePreview.classList.add('file-preview'); + filePreview.controls = true; + messageContent.appendChild(filePreview); + } else if (fileType.startsWith('audio/')) { + const filePreview = document.createElement('audio'); + filePreview.src = fileUrl; + filePreview.alt = fileName; + filePreview.classList.add('file-preview'); + filePreview.controls = true; + messageContent.appendChild(filePreview); + } else { + const filePreview = document.createElement('div'); + filePreview.textContent = 'No preview available'; + filePreview.classList.add('file-preview'); + messageContent.appendChild(filePreview); + } + + messageContent.appendChild(senderName); + messageContent.appendChild(fileLink); + messageDiv.appendChild(avatarImg); + messageDiv.appendChild(messageContent); + container.appendChild(messageDiv); + + if (topic === currentTopic()) { + container.scrollTop = container.scrollHeight; + } +} +function addAudioMessage(name, audioUrl, avatar, topic) { + const container = document.querySelector('#messages'); + if (!container) { + console.error('Element #messages not found'); + return; + } + + const messageDiv = document.createElement('div'); + messageDiv.classList.add('message'); + if (topic !== currentTopic()) { + messageDiv.classList.add('hidden'); // Hide messages not belonging to the current room + } + + const avatarImg = document.createElement('img'); + avatarImg.src = updatePortInUrl(avatar); + avatarImg.alt = `${name}'s avatar`; + avatarImg.classList.add('avatar'); + + const messageContent = document.createElement('div'); + messageContent.classList.add('message-content'); + + const senderName = document.createElement('div'); + senderName.classList.add('message-sender'); + senderName.textContent = name; + + const audioElement = document.createElement('audio'); + audioElement.src = audioUrl; + audioElement.controls = true; + // Autoplay only if the message is from a peer + if (name !== config.userName) { + audioElement.autoplay = true; + } + audioElement.classList.add('message-audio'); + + messageContent.appendChild(senderName); + messageContent.appendChild(audioElement); + messageDiv.appendChild(avatarImg); + messageDiv.appendChild(messageContent); + container.appendChild(messageDiv); + + if (topic === currentTopic()) { + container.scrollTop = container.scrollHeight; + } +} + + +function addMessage(name, message, avatar, topic) { + const container = document.querySelector('#messages'); + if (!container) { + console.error('Element #messages not found'); + return; + } + + const messageDiv = document.createElement('div'); + messageDiv.classList.add('message'); + if (topic !== currentTopic()) { + messageDiv.classList.add('hidden'); // Hide messages not belonging to the current room + } + + const avatarImg = document.createElement('img'); + avatarImg.src = updatePortInUrl(avatar); + avatarImg.alt = `${name}'s avatar`; + avatarImg.classList.add('avatar'); + + const messageContent = document.createElement('div'); + messageContent.classList.add('message-content'); + + const senderName = document.createElement('div'); + senderName.classList.add('message-sender'); + senderName.textContent = name; + + const messageText = document.createElement('div'); + messageText.classList.add('message-text'); + + messageText.innerHTML = message; + + messageContent.appendChild(senderName); + messageContent.appendChild(messageText); + messageDiv.appendChild(avatarImg); + messageDiv.appendChild(messageContent); + container.appendChild(messageDiv); + + if (topic === currentTopic()) { + container.scrollTop = container.scrollHeight; + } } async function updateIcon(username, avatarBuffer) { @@ -826,120 +1146,153 @@ async function updateIcon(username, avatarBuffer) { } } -function clearMessagesCMD() { - const messagesContainer = document.querySelector('#messages'); - while (messagesContainer.firstChild) { - messagesContainer.removeChild(messagesContainer.firstChild); - } - - // Clear the messages from the store for the current room - const topic = currentTopic(); - messagesStore[topic] = []; -} function clearMessages() { const messagesContainer = document.querySelector('#messages'); while (messagesContainer.firstChild) { messagesContainer.removeChild(messagesContainer.firstChild); } - - // Clear the messages from the store for the current room - // const topic = currentTopic(); - // messagesStore[topic] = []; } -function toggleSetupView() { - const setupDiv = document.querySelector('#setup'); - setupDiv.classList.toggle('hidden'); +function currentTopic() { + return document.querySelector('#chat-room-topic').innerText; } -function writeConfigToFile(filePath) { - fs.writeFile(filePath, JSON.stringify(config), (err) => { - if (err) return console.error(err); - console.log("File has been created"); +async function sendMessage(e) { + e.preventDefault(); + const message = document.querySelector('#message').value; + document.querySelector('#message').value = ''; + + const topic = currentTopic(); + const timestamp = Date.now(); + + console.log("Sending message to current topic:", topic); // Add logging + if (message.startsWith('>')) { + await handleCommand(message, { + eventEmitter, + currentTopic: topic, // Pass the current topic as a string + clearMessages, + addMessage, + joinRoom, + leaveRoom, + createRoom, + listFiles, + deleteFile, + servePort + }); + return; + } + + console.log('Sending message:', message); // Debugging log + + onMessageAdded(config.userName, message, config.userAvatar, topic, timestamp); + + const messageObj = JSON.stringify({ + type: 'message', + name: config.userName, + avatar: updatePortInUrl(config.userAvatar), + topic: topic, + timestamp: timestamp, + message }); + + const peers = [...activeRooms.find(room => room.topic === topic).swarm.connections]; + for (const peer of peers) { + peer.write(messageObj); + } } -function updatePortInUrl(url) { - if (!url) return url; - const urlObject = new URL(url); - if(!urlObject.host.startsWith("localhost")) return urlObject.toString(); - urlObject.port = servePort; - return urlObject.toString(); -} - -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, message.timestamp); - }); -} - -function getMessagesForRoom(topic) { - return messagesStore[topic] || []; -} - -function addMessageToStore(topic, messageObj) { +function onMessageAdded(name, message, avatar, topic, timestamp) { if (!messagesStore[topic]) { messagesStore[topic] = []; } - - // Check for duplicates using a combination of message content and timestamp - const isDuplicate = messagesStore[topic].some(msg => - msg.from === messageObj.from && - msg.message === messageObj.message && - msg.timestamp === messageObj.timestamp - ); + messagesStore[topic].push({ name, message, avatar, timestamp }); - if (!isDuplicate) { - messagesStore[topic].push(messageObj); - } else { - console.log('Duplicate message detected:', messageObj); // Debugging log + const chatRoomTopic = currentTopic(); + if (topic === chatRoomTopic) { + addMessage(name, message, avatar, topic); } } -function loadConfigFromFile() { - config = JSON.parse(fs.readFileSync("./config.json", 'utf8')); - console.log("Read config from file:", config); - // Update port in URLs - config.userAvatar = updatePortInUrl(config.userAvatar); - config.rooms.forEach(room => { - room.alias = room.alias || truncateHash(room.topic); +function renderMessagesForRoom(topic) { + const container = document.querySelector('#messages'); + if (!container) { + console.error('Element #messages not found'); + return; + } + + container.innerHTML = ''; + + if (!messagesStore[topic]) return; + + messagesStore[topic].forEach(({ name, message, avatar }) => { + addMessage(name, message, avatar, topic); }); - for (let user in config.registeredUsers) { - config.registeredUsers[user] = updatePortInUrl(config.registeredUsers[user]); +} + +function handleFileInput(event) { + const file = event.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = async function(event) { + const buffer = new Uint8Array(event.target.result); + const filePath = `/files/${file.name}`; + await drive.put(filePath, buffer); + + const fileUrl = `http://localhost:${servePort}/files/${file.name}`; + const topic = currentTopic(); + + const fileMessage = { + type: 'file', + name: config.userName, + fileName: file.name, + file: b4a.toString(buffer, 'base64'), + fileType: file.type, + avatar: updatePortInUrl(config.userAvatar), + topic: topic, + timestamp: Date.now() + }; + + const peers = [...activeRooms.find(room => room.topic === topic).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); } } -async function connectToAllRooms() { - // Connect to all rooms on startup - for (const room of config.rooms) { - const topicBuffer = b4a.from(room.topic, 'hex'); - await joinSwarm(topicBuffer); +async function joinGuildRequest(guildTopic) { + const guildTopicBuffer = b4a.from(guildTopic, 'hex'); + if (!activeRooms.some(room => room.topic === guildTopic)) { + try { + const swarm = new Hyperswarm(); + const discovery = swarm.join(guildTopicBuffer, { client: true, server: true }); + await discovery.flushed(); + + swarm.on('connection', (connection, info) => { + handleConnection(connection, info); + // Request guild information from peers + const guildRequestMessage = JSON.stringify({ + type: 'guildRequest', + guildTopic + }); + connection.write(guildRequestMessage); + }); + + activeRooms.push({ topic: guildTopic, swarm, discovery }); + + console.log('Joined guild topic:', guildTopic); // Debugging log + + updatePeerCount(); + } catch (error) { + console.error('Error joining swarm for guild topic:', guildTopic, error); + } } } -// Call this function when loading the rooms initially -renderRoomList(); +window.joinGuildRequest = joinGuildRequest; initialize(); diff --git a/chatBot/commands/ping.js b/chatBot/commands/ping.js index 8ba0a84..d7566cd 100644 --- a/chatBot/commands/ping.js +++ b/chatBot/commands/ping.js @@ -1,7 +1,7 @@ export default { handler: function(bot, args, message) { // Specify the path to the file you want to send - const filePath = '/to/file/path.js'; // Replace with the actual file path + const filePath = '/Users/raven/chat/chatBot/bot.js'; // Replace with the actual file path const fileType = 'text/html'; // Specify the correct file type // Send the file message using the bot instance diff --git a/commands.js b/commands.js index 2d6b26f..2131078 100644 --- a/commands.js +++ b/commands.js @@ -1,4 +1,3 @@ -// commands.js import b4a from 'b4a'; import fs from 'fs'; @@ -12,45 +11,94 @@ if (fs.existsSync(agentAvatarPath)) { } export default async function handleCommand(command, context) { - const { eventEmitter, currentTopic, clearMessages, addMessage, joinRoom, leaveRoom, createRoom, listFiles } = context; - + const { eventEmitter, currentTopic, clearMessages, addMessage, joinRoom, leaveRoom, createRoom, listFiles, deleteFile, servePort } = context; + + console.log("Context received in handleCommand:", context); // Add logging + const args = command.trim().split(' '); const cmd = args[0].toLowerCase(); const restArgs = args.slice(1).join(' '); + console.log("Command received:", cmd); // Add logging + console.log("Current topic:", currentTopic); // Add logging to check the current topic + switch (cmd) { - case '~clear': - clearMessages(); + case '>clear': + clearMessages(currentTopic); break; - case '~ping': - addMessage('LinkUp', 'pong', agentAvatar, currentTopic()); + case '>ping': + addMessage('LinkUp', 'pong', agentAvatar, currentTopic); break; - case '~help': - addMessage('LinkUp', 'Available commands:\n- ~clear\n- ~ping\n- ~help\n- ~join [topic]\n- ~leave\n- ~create [alias]\n- ~list-files', agentAvatar, currentTopic()); + case '>help': + addMessage('LinkUp', 'Available commands:\n- >clear\n- >ping\n- >help\n- >join [topic]\n- >leave\n- >create [alias]\n- >list-files', agentAvatar, currentTopic); break; - case '~join': + case '>join': if (restArgs) { - await joinRoom(restArgs); + await joinRoom(currentTopic, restArgs); } else { - addMessage('LinkUp', 'Usage: ~join [topic]', agentAvatar, currentTopic()); + addMessage('LinkUp', 'Usage: >join [topic]', agentAvatar, currentTopic); } break; - case '~leave': - leaveRoom(currentTopic()); + case '>leave': + leaveRoom(currentTopic); break; - case '~create': + case '>create': if (restArgs) { - await createRoom(restArgs); + await createRoom(currentTopic, restArgs); } else { - addMessage('LinkUp', 'Usage: ~create [alias]', agentAvatar, currentTopic()); + addMessage('LinkUp', 'Usage: >create [alias]', agentAvatar, currentTopic); } break; - case '~list-files': + case '>list-files': const files = await listFiles(); const fileList = files.length > 0 ? files.map(file => `- ${file}`).join('\n') : 'No files available'; - addMessage('LinkUp', `Available files:\n${fileList}`, agentAvatar, currentTopic()); + addMessage('LinkUp', `Available files:\n${fileList}`, agentAvatar, currentTopic); + + // Render the file list with delete buttons + renderFileList(files, deleteFile, servePort); break; default: + addMessage('LinkUp', `Unknown command: ${cmd}`, agentAvatar, currentTopic); console.log('Unknown command:', command); } } + +function renderFileList(files, deleteFile, servePort) { + const container = document.querySelector('#messages'); + if (!container) { + console.error('Element #messages not found'); + return; + } + + const fileList = document.createElement('ul'); + + files.forEach(file => { + const listItem = document.createElement('li'); + const fileButton = document.createElement('button'); + fileButton.textContent = file.trim(); + fileButton.onclick = () => downloadFile(file.trim(), servePort); + + const deleteButton = document.createElement('button'); + deleteButton.textContent = 'Delete'; + deleteButton.onclick = async () => { + await deleteFile(file); + listItem.remove(); + }; + + listItem.appendChild(fileButton); + listItem.appendChild(deleteButton); + fileList.appendChild(listItem); + }); + + container.appendChild(fileList); +} + +function downloadFile(filename, servePort) { + const url = `http://localhost:${servePort}/files/${filename}`; + const link = document.createElement('a'); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); +} diff --git a/index.html b/index.html index d918404..6d89123 100644 --- a/index.html +++ b/index.html @@ -17,10 +17,10 @@
@@ -62,7 +61,7 @@ - + @@ -70,11 +69,46 @@
+ + + + + + + diff --git a/style.css b/style.css index a48d1e1..9e76c3c 100644 --- a/style.css +++ b/style.css @@ -281,7 +281,6 @@ textarea::placeholder { background-color: #f04747; } - /* Main container styles */ main { display: flex; @@ -474,78 +473,146 @@ main { } /* Updated Room List Styles */ -#room-list { +#guild-list { list-style: none; padding: 0; margin: 0; } -#room-list li { +.guild-item { padding: 10px; margin-bottom: 10px; background-color: #3a3f44; border-radius: 5px; cursor: pointer; display: flex; + flex-direction: column; + transition: background-color 0.3s ease; +} + +.guild-item:hover { + background-color: #4a5258; +} + +.guild-item h3 { + margin: 0; + font-size: 16px; + color: #ffffff; +} + +.guild-item .manage-guild-btn { + align-self: flex-end; + margin-top: 10px; + font-size: 12px; + padding: 5px 10px; +} + +.guild-item .room-list { + list-style: none; + padding: 0; + margin: 10px 0 0 0; + display: flex; + flex-direction: column; +} + +.guild-item .room-list li { + padding: 5px; + margin-bottom: 5px; + background-color: #464343; + border-radius: 3px; + cursor: pointer; + display: flex; justify-content: space-between; align-items: center; transition: background-color 0.3s ease; } -#room-list li:hover { - background-color: #4a5258; +.guild-item .room-list li:hover { + background-color: #5a5f64; } -#room-list li span { +.guild-item .room-list li span { flex: 1; } -#room-list li .edit-icon, -#room-list li .delete-icon { +.guild-item .room-list li .edit-icon, +.guild-item .room-list li .delete-icon { margin-left: 10px; color: #e0e0e0; cursor: pointer; transition: color 0.3s ease; } -#room-list li .edit-icon:hover, -#room-list li .delete-icon:hover { +.guild-item .room-list li .edit-icon:hover, +.guild-item .room-list li .delete-icon:hover { color: #a0a0a0; } -/* Style for Edit Mode Input Box */ -#room-list li input[type="text"] { +/* Modal styles */ +.modal { + display: flex; + justify-content: center; + align-items: center; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.8); + z-index: 1000; +} + +.modal-content { background-color: #2e2e2e; - border: 1px solid #464343; - border-radius: 5px; + padding: 1.5rem; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5); + max-width: 500px; + width: 100%; + text-align: center; +} + +.close-btn { color: #e0e0e0; - padding: 5px; - width: calc(100% - 40px); - margin-right: 10px; - transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease; + float: right; + font-size: 1.5rem; + cursor: pointer; } -#room-list li input[type="text"]:focus { - outline: none; - background-color: #3a3a3a; +.close-btn:hover { + color: #f04747; } -/* Link styles */ -a { - color: #e0e0e0; /* Base color for links matching the text color */ - text-decoration: none; /* Remove underline */ - transition: color 0.3s ease, text-decoration 0.3s ease; +.modal h2 { + margin-bottom: 1rem; + color: #fff; } -a:hover { - color: #b0b0b0; /* Lighter color on hover */ - text-decoration: underline; /* Underline on hover */ +.modal form { + display: flex; + flex-direction: column; + align-items: center; } -a:active { - color: #a0a0a0; /* Slightly darker color on active */ +.modal form input, +.modal form button { + margin-bottom: 1rem; + width: 100%; } -a:visited { - color: #b0b0b0; /* Different color for visited links */ +.modal form button { + width: auto; + background-color: #3e3e3e; + color: #fff; + border: none; + padding: 0.75rem 1.5rem; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s ease, transform 0.3s ease; } + +.modal form button:hover { + background-color: #191919; + transform: scale(1.05); +} +