Compare commits

..

No commits in common. "0af52a3764bb555833aea9745c75c9baa04be865" and "f706e7d621534ad12a81644fdba7754ba0d5e729" have entirely different histories.

18 changed files with 551 additions and 1641 deletions

122
README.md
View File

@ -10,125 +10,3 @@ To launch the App in Dev Mode:
Lastly - run the app:
`pear dev -s /tmp/tmp_pear`
--------
# LinkUp Documentation
## Overview
LinkUp is a peer-to-peer chat application that allows users to create and join chat rooms, share files, and communicate with each other in real-time. The application uses the Hyperswarm, Hyperdrive, and Corestore libraries for decentralized networking and storage. This documentation provides a comprehensive guide to understanding, setting up, and using LinkUp.
![Screenshot](images/screenshot.png)
## Table of Contents
- [Installation](#installation)
- [Configuration](#configuration)
- [Usage](#usage)
- [Registering a User](#registering-a-user)
- [Creating a Chat Room](#creating-a-chat-room)
- [Joining a Chat Room](#joining-a-chat-room)
- [Sending Messages](#sending-messages)
- [Attaching Files](#attaching-files)
- [Recording and Sending Audio Messages](#recording-and-sending-audio-messages)
- [Commands](#commands)
- [Development](#development)
- [Project Structure](#project-structure)
- [Event Handling](#event-handling)
- [FAQs](#faqs)
## Configuration
LinkUp uses a configuration file (`config.json`) to store user information, chat room details, and registered users. This file is automatically created and managed by the application.
### Configuration File Structure
```json
{
"userName": "",
"userAvatar": "",
"rooms": [],
"registeredUsers": {}
}
```
- `userName`: The username of the registered user.
- `userAvatar`: The URL of the user's avatar image.
- `rooms`: An array of chat rooms the user has joined or created.
- `registeredUsers`: An object containing registered usernames and their avatar URLs.
## Usage
### Registering a User
1. Launch the application.
2. If the configuration file does not exist, you will be prompted to register.
3. Enter a username and optionally select an avatar image.
4. Submit the registration form to complete the process.
### Creating a Chat Room
1. Click on the "Create/Join Room" button in the sidebar.
2. Click on "Create Room".
3. A new room will be created with a random topic.
### Joining a Chat Room
1. Click on the "Create/Join Room" button in the sidebar.
2. Enter the topic of the room you wish to join in the "Topic" field.
3. Click on "Join Room".
### Sending Messages
1. Select a chat room from the sidebar.
2. Enter your message in the message input box at the bottom of the chat window.
3. Press `Enter` or click on the "Send" button to send the message.
### Attaching Files
1. Click on the "Attach File" button in the message form.
2. Select a file from your device.
3. The file will be uploaded and a download link will be shared in the chat.
### Recording and Sending Audio Messages
1. Click and hold the "Talk" button to start recording an audio message.
2. Release the button to stop recording and send the audio message.
## Commands
LinkUp supports several commands that can be entered in the chat input box:
- `~clear`: Clears the chat messages.
- `~ping`: Responds with "pong".
- `~help`: Lists available commands.
- `~join [topic]`: Joins a chat room with the specified topic.
- `~leave`: Leaves the current chat room.
- `~create [alias]`: Creates a new chat room with the specified alias.
- `~list-files`: Lists all files available in the current chat room.
## Development
### Project Structure
- `app.js`: Main application logic.
- `commands.js`: Command handling logic.
- `index.html`: HTML structure of the application.
- `style.css`: CSS for styling the application.
- `config.json`: Configuration file for storing user and room data.
### Event Handling
LinkUp uses an `EventEmitter` to handle various events such as receiving messages, joining rooms, and updating the peer count. Event listeners are set up during the initialization process.
### Key Functions
- `initialize()`: Initializes the application, sets up event listeners, and loads configuration.
- `registerUser()`: Handles user registration.
- `createChatRoom()`: Creates a new chat room.
- `joinChatRoom()`: Joins an existing chat room.
- `sendMessage()`: Sends a text message.
- `handleFileInput()`: Handles file attachments.
- `setupEventListeners()`: Sets up event listeners for UI interactions.
## FAQs
**Q: How do I change my username or avatar?**
A: Currently, changing username or avatar after registration is not supported. You would need to delete the `config.json` file and restart the application to re-register.
**Q: How can I see the list of files shared in a room?**
A: Use the `~list-files` command to see all files shared in the current chat room.
**Q: How do I leave a chat room?**
A: Click on the "Leave Room" button in the chat window.
For more information, please refer to the [Pears Documentation](https://docs.pears.com/).
Feel free to contribute to this project by submitting issues or pull requests on [Git](https://git.ssh.surf/snxraven/LinkUp-P2P-Chat).

1204
app.js

File diff suppressed because it is too large Load Diff

View File

@ -1,220 +0,0 @@
# Bot System Documentation
## Overview
This document provides a comprehensive guide to the bot system, detailing its structure, functionalities, and how to set it up. This bot system leverages the Hyperswarm network to enable decentralized communication and supports various types of messages, including text, files, and audio.
## Table of Contents
1. [Introduction](#introduction)
2. [Getting Started](#getting-started)
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Configuration](#configuration)
3. [Bot Architecture](#bot-architecture)
- [Client Class](#client-class)
- [Message Classes](#message-classes)
4. [Command Handling](#command-handling)
5. [Event Handling](#event-handling)
6. [Running the Bot](#running-the-bot)
7. [API Reference](#api-reference)
## Introduction
This bot system is designed for decentralized chat rooms using the Hyperswarm network. It supports multiple message types and dynamic command handling, making it highly customizable and extensible.
## Getting Started
### Prerequisites
- Node.js (version 14.x or later)
- npm (version 6.x or later)
### Installation
1. Clone the repository:
```bash
git clone https://git.ssh.surf/snxraven/LinkUp-P2P-Chat.git
cd LinkUp-P2P-Chat
```
2. Install the dependencies:
```bash
npm install
```
3. Change to bot directory
```bash
cd chatBot
```
4. Run the bot:
```
node bot.js
```
### Configuration
Create a `.env` file in the root directory and add the following environment variables:
```
BOT_NAME=MyBot
ON_READY_MESSAGE=Bot is ready and operational!
LINKUP_ROOM_ID=<room_id>
```
## Bot Architecture
### Client Class
The `Client` class is the core component of the bot system. It handles connections to the Hyperswarm network, manages message sending and receiving, and emits events for various actions.
#### Constructor
```javascript
constructor(botName)
```
- `botName`: The name of the bot.
#### Methods
- `initializeServeDrive()`: Initializes the ServeDrive for serving files and audio.
- `getRandomPort()`: Returns a random port number.
- `fetchAvatar(filePath)`: Fetches and sets the bot's avatar from a local file.
- `setupSwarm()`: Sets up the Hyperswarm network and connection handlers.
- `joinChatRoom(chatRoomID)`: Joins a specified chat room.
- `sendTextMessage(message)`: Sends a text message.
- `sendFileMessage(filePath, fileType)`: Sends a file message.
- `sendAudioMessage(filePath, audioType)`: Sends an audio message.
- `sendMessage(message)`: Sends a generic message.
- `destroy()`: Disconnects the bot and shuts down the Hyperswarm network.
### Message Classes
The bot system supports various message types through dedicated classes, all extending the base `Message` class.
#### Message Class
```javascript
class Message {
constructor(messageType, peerName, peerAvatar, topic, timestamp)
}
```
#### TextMessage Class
```javascript
class TextMessage extends Message {
constructor(peerName, peerAvatar, topic, timestamp, message)
static new(bot, message)
}
```
#### FileMessage Class
```javascript
class FileMessage extends Message {
constructor(peerName, peerAvatar, topic, timestamp, fileName, fileUrl, fileType, fileData)
static new(bot, fileName, fileUrl, fileType, fileBuffer)
}
```
#### AudioMessage Class
```javascript
class AudioMessage extends Message {
constructor(peerName, peerAvatar, topic, timestamp, audioUrl, audioType, audioData)
static new(bot, audioUrl, audioType, audioBuffer)
}
```
#### IconMessage Class
```javascript
class IconMessage extends Message {
constructor(peerName, peerAvatar, timestamp)
static new(bot, avatarBuffer)
}
```
## Command Handling
Commands are dynamically loaded from the `commands` directory. Each command is a JavaScript module with a default export containing a `handler` function.
### Example Command Module
```javascript
// commands/example.js
export default {
handler: (bot, args, message) => {
bot.sendTextMessage(`Example command executed with args: ${args.join(' ')}`);
}
};
```
## Event Handling
The bot uses Node.js `EventEmitter` to handle events. Key events include:
- `onMessage`: Triggered when a new message is received.
- `onFile`: Triggered when a file message is received.
- `onAudio`: Triggered when an audio message is received.
- `onIcon`: Triggered when an icon message is received.
- `onError`: Triggered when there is a connection error.
- `onBotJoinRoom`: Triggered when the bot joins a room.
### Example Event Listener
```javascript
bot.on('onMessage', (peer, message) => {
console.log(`Message from ${message.peerName}: ${message.message}`);
});
```
## Running the Bot
To start the bot, run the following command:
```bash
node bot.js
```
Ensure that the `.env` file is correctly configured and that all necessary dependencies are installed.
## API Reference
### Client
- `constructor(botName)`: Creates a new instance of the Client.
- `initializeServeDrive()`: Initializes ServeDrive.
- `getRandomPort()`: Generates a random port number.
- `fetchAvatar(filePath)`: Fetches the bot's avatar.
- `setupSwarm()`: Sets up the Hyperswarm network.
- `joinChatRoom(chatRoomID)`: Joins a specified chat room.
- `sendTextMessage(message)`: Sends a text message.
- `sendFileMessage(filePath, fileType)`: Sends a file message.
- `sendAudioMessage(filePath, audioType)`: Sends an audio message.
- `sendMessage(message)`: Sends a generic message.
- `destroy()`: Disconnects the bot.
### TextMessage
- `constructor(peerName, peerAvatar, topic, timestamp, message)`: Creates a new text message.
- `static new(bot, message)`: Creates a new text message instance.
### FileMessage
- `constructor(peerName, peerAvatar, topic, timestamp, fileName, fileUrl, fileType, fileData)`: Creates a new file message.
- `static new(bot, fileName, fileUrl, fileType, fileBuffer)`: Creates a new file message instance.
### AudioMessage
- `constructor(peerName, peerAvatar, topic, timestamp, audioUrl, audioType, audioData)`: Creates a new audio message.
- `static new(bot, audioUrl, audioType, audioBuffer)`: Creates a new audio message instance.
### IconMessage
- `constructor(peerName, peerAvatar, timestamp)`: Creates a new icon message.
- `static new(bot, avatarBuffer)`: Creates a new icon message instance.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

View File

@ -52,10 +52,7 @@ loadCommands().then(commands => {
// If the command exists, execute its handler
if (commandHandler && typeof commandHandler.handler === 'function') {
console.log(`Executing command: ${command} with arguments: ${args}`);
commandHandler.handler(bot, args, message);
} else {
console.warn(`Command not found: ${command}`);
}
}
});
@ -67,9 +64,7 @@ loadCommands().then(commands => {
bot.joinChatRoom(process.env.LINKUP_ROOM_ID);
const iconPath = path.join(new URL('./assets/icon.png', import.meta.url).pathname);
bot.fetchAvatar(iconPath); // Fetch the avatar from local file
bot.fetchAvatar(`https://avatar.iran.liara.run/username?username=${bot.botName}&background=f4d9b2&color=FF9800&size=40`); // Debugging avatar
}).catch(error => {
console.error('Error loading commands:', error);
});
});

View File

@ -1,10 +1,7 @@
export default {
handler: function(bot, args, message) {
// Specify the path to the file you want to send
const filePath = '/Users/raven/chat/chatBot/bot.js'; // Replace with the actual file path
const fileType = 'text/html'; // Specify the correct file type
// ping.js
// Send the file message using the bot instance
bot.sendFileMessage(filePath, fileType);
}
export default {
handler: function(bot, args, message) {
bot.sendTextMessage('Pong!');
}
};

View File

@ -1,10 +0,0 @@
export default {
handler: function(bot, args, message) {
// Specify the path to the audio file you want to send
const audioFilePath = '/to/file/path.mp3'; // Replace with the actual audio file path
const audioType = 'audio';
// Send the audio message using the bot instance
bot.sendAudioMessage(audioFilePath, audioType);
}
};

View File

@ -2,4 +2,4 @@ LINKUP_ROOM_ID=
BOT_NAME=myBot
ON_READY_MESSAGE="Hello, I am a bot. I am here to help you."
ON_READY_MESSAGE="Hello, I am a bot. I am here to help you. Please type 'help' to see the list of commands."

View File

@ -1,4 +1,3 @@
import path from 'path';
import Hyperswarm from 'hyperswarm';
import EventEmitter from 'node:events';
import b4a from "b4a";
@ -7,29 +6,16 @@ import FileMessage from "./message/FileMessage.js";
import AudioMessage from "./message/AudioMessage.js";
import Message from "./message/Message.js";
import IconMessage from "./message/IconMessage.js";
import Corestore from 'corestore';
import Hyperdrive from 'hyperdrive';
import fs from 'fs';
import ServeDrive from 'serve-drive';
class Client extends EventEmitter {
constructor(botName) {
super();
if (!botName) return console.error("Bot Name is not defined!");
this.botName = botName;
this.botAvatar = "";
this.swarm = new Hyperswarm();
this.joinedRooms = new Set(); // Track the rooms the bot has joined
this.currentTopic = null; // Track the current topic
// Initialize Corestore and Hyperdrive
this.storagePath = './storage/';
this.store = new Corestore(this.storagePath);
this.drive = new Hyperdrive(this.store);
// Initialize ServeDrive
this.servePort = null;
this.initializeServeDrive();
this.setupSwarm();
process.on('exit', () => {
@ -39,89 +25,55 @@ class Client extends EventEmitter {
process.on('SIGTERM', async () => {
console.log('SIGTERM signal received. Shutting down HyperSwarm...');
await this.destroy();
await this.destroy()
console.log('HyperSwarm was shut down. Exiting the process with exit code 0.');
process.exit(0);
});
process.on('SIGINT', async () => {
console.log('SIGINT signal received. Shutting down HyperSwarm...');
await this.destroy();
await this.destroy()
console.log('HyperSwarm was shut down. Exiting the process with exit code 0.');
process.exit(0);
});
}
async initializeServeDrive() {
try {
this.servePort = this.getRandomPort();
const serve = new ServeDrive({
port: this.servePort,
get: ({ key, filename, version }) => this.drive
});
await serve.ready();
console.log('ServeDrive listening on port:', this.servePort);
} catch (error) {
console.error('Error initializing ServeDrive:', error);
}
}
getRandomPort() {
return Math.floor(Math.random() * (65535 - 49152 + 1)) + 49152;
}
async fetchAvatar(filePath) {
try {
await this.drive.ready();
const iconBuffer = fs.readFileSync(filePath);
await this.drive.put(`/icons/${this.botName}.png`, iconBuffer);
this.botAvatar = `http://localhost:${this.servePort}/icons/${this.botName}.png`;
// Cache the icon message
this.iconMessage = IconMessage.new(this, iconBuffer);
} catch (error) {
console.error('Error fetching avatar:', error);
}
async fetchAvatar(url) {
this.botAvatar = url;
const web = await fetch(url);
const img = await web.body.getReader().read();
this.sendMessage(IconMessage.new(this, img.value));
}
setupSwarm() {
this.swarm.on('connection', (peer) => {
// Send the cached icon message to the new peer
if (this.iconMessage) {
peer.write(this.iconMessage.toJsonString());
}
peer.on('data', async message => {
peer.on('data', message => {
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
const msgType = messageObj.type;
const peerName = messageObj.name; // Changed from name to userName
const peerName = messageObj.name;
const peerAvatar = messageObj.avatar;
const timestamp = messageObj.timestamp;
if (msgType === "message")
this.emit('onMessage', peer, new TextMessage(peerName, peerAvatar, this.currentTopic, timestamp, messageObj.message));
if (msgType === "file") {
const fileBuffer = await this.drive.get(`/files/${messageObj.fileName}`);
this.emit('onFile', peer, new FileMessage(peerName, peerAvatar, this.currentTopic, timestamp, messageObj.fileName, `http://localhost:${this.servePort}/files/${messageObj.fileName}`, messageObj.fileType, messageObj.fileData));
}
if (msgType === "file")
this.emit('onFile', peer, new FileMessage(peerName, peerAvatar, this.currentTopic, timestamp, messageObj.fileName, messageObj.fileUrl, messageObj.fileType));
if (msgType === "icon")
this.emit('onIcon', peer, new IconMessage(peerName, peerAvatar, timestamp));
if (msgType === "audio") {
const audioBuffer = await this.drive.get(`/audio/${messageObj.audioName}`);
this.emit('onAudio', peer, new AudioMessage(peerName, peerAvatar, this.currentTopic, timestamp, `http://localhost:${this.servePort}/audio/${messageObj.audioName}`, messageObj.audioType, messageObj.audioData));
}
if (msgType === "audio")
this.emit('onAudio', peer, new AudioMessage(peerName, peerAvatar, this.currentTopic, timestamp, messageObj.audio, messageObj.audioType));
}
});
peer.on('error', e => {
this.emit('onError', e);
console.error(`Connection error: ${e}`);
console.error(`Connection error: ${e}`)
});
});
@ -145,61 +97,17 @@ class Client extends EventEmitter {
}
sendTextMessage(message) {
console.log(`Preparing to send text message: ${message}`);
this.sendMessage(TextMessage.new(this, message));
}
async sendFileMessage(filePath, fileType) {
try {
await this.drive.ready();
const fileBuffer = fs.readFileSync(filePath);
const fileName = path.basename(filePath);
await this.drive.put(`/files/${fileName}`, fileBuffer);
const fileUrl = `http://localhost:${this.servePort}/files/${fileName}`;
const fileMessage = FileMessage.new(this, fileName, fileUrl, fileType, fileBuffer); // Pass fileBuffer to the new method
this.sendMessage(fileMessage);
} catch (error) {
console.error('Error sending file message:', error);
}
}
async sendAudioMessage(filePath, audioType) {
try {
await this.drive.ready();
const audioBuffer = fs.readFileSync(filePath);
const audioName = path.basename(filePath);
await this.drive.put(`/audio/${audioName}`, audioBuffer);
const audioUrl = `http://localhost:${this.servePort}/audio/${audioName}`;
const audioMessage = AudioMessage.new(this, audioUrl, audioType, audioBuffer); // Pass audioBuffer to the new method
this.sendMessage(audioMessage);
} catch (error) {
console.error('Error sending audio message:', error);
}
}
sendMessage(message) {
if (!(message instanceof Message)) {
console.error(`message does not extend Message class (TextMessage, FileMessage, AudioMessage).`, message);
return;
}
if(!(message instanceof Message)) return console.log(`message does not extend Message class (TextMessage, FileMessage, AudioMessage).`, message);
// console.log('Bot name:', this.botName);
console.log("Sending message:", message);
const data = message.toJsonString();
const peers = [...this.swarm.connections];
if (peers.length === 0) {
console.warn("No active peer connections found.");
return;
}
console.log(`Sending message to ${peers.length} peers.`);
for (const peer of peers) {
try {
peer.write(data);
console.log(`Message sent to peer: ${peer.remoteAddress}`);
} catch (error) {
console.error(`Failed to send message to peer: ${peer.remoteAddress}`, error);
}
}
for (const peer of peers) peer.write(data);
}
async destroy() {

View File

@ -1,26 +1,22 @@
import Message from "./Message.js";
import b4a from "b4a";
class AudioMessage extends Message {
constructor(peerName, peerAvatar, topic, timestamp, audioUrl, audioType, audioData) {
constructor(peerName, peerAvatar, topic, timestamp, audio, audioType) {
super("audio", peerName, peerAvatar, topic, timestamp);
this.audioUrl = audioUrl;
this.audio = audio;
this.audioType = audioType;
this.audioData = audioData; // Add audio data property
}
toJsonString() {
return JSON.stringify({
...this.toJson(),
audioUrl: this.audioUrl,
audioType: this.audioType,
audio: this.audioData // Include audio data in JSON
audio: this.audio,
audioType: this.audioType
});
}
static new(bot, audioUrl, audioType, audioBuffer) {
const audioData = b4a.toString(audioBuffer, 'base64'); // Convert audio buffer to base64
return new AudioMessage(bot.botName, bot.botAvatar, bot.currentTopic, Date.now(), audioUrl, audioType, audioData);
static new(bot, audio, audioType) {
return new AudioMessage(bot.botName, bot.botAvatar, bot.currentTopic, Date.now(), audio, audioType);
}
}

View File

@ -1,13 +1,11 @@
import Message from "./Message.js";
import b4a from "b4a";
class FileMessage extends Message {
constructor(peerName, peerAvatar, topic, timestamp, fileName, fileUrl, fileType, fileData) {
constructor(peerName, peerAvatar, topic, timestamp, fileName, fileUrl, fileType) {
super("file", peerName, peerAvatar, topic, timestamp);
this.fileName = fileName;
this.fileUrl = fileUrl;
this.fileType = fileType;
this.fileData = fileData; // Add file data property
}
toJsonString() {
@ -15,14 +13,12 @@ class FileMessage extends Message {
...this.toJson(),
fileName: this.fileName,
fileUrl: this.fileUrl,
fileType: this.fileType,
file: this.fileData // Include file data in JSON
fileType: this.fileType
});
}
static new(bot, fileName, fileUrl, fileType, fileBuffer) {
const fileData = b4a.toString(fileBuffer, 'base64'); // Convert file buffer to base64
return new FileMessage(bot.botName, bot.botAvatar, bot.currentTopic, Date.now(), fileName, fileUrl, fileType, fileData);
static new(bot, fileName, fileUrl, fileType) {
return new FileMessage(bot.botName, bot.botAvatar, bot.currentTopic, Date.now(), fileName, fileUrl, fileType);
}
}

View File

@ -2,8 +2,8 @@ import Message from "./Message.js";
import b4a from "b4a";
class IconMessage extends Message {
constructor(peerName, peerAvatar, timestamp) {
super("icon", peerName, peerAvatar, null, timestamp);
constructor(peerName, peerAvatar, topic, timestamp) {
super("icon", peerName, peerAvatar, topic, timestamp);
}
toJsonString() {

View File

@ -1,21 +1,21 @@
import Message from "./Message.js";
class TextMessage extends Message {
constructor(peerName, peerAvatar, topic, timestamp, message) {
super("message", peerName, peerAvatar, topic, timestamp);
this.message = message;
}
constructor(peerName, peerAvatar, topic, timestamp, message) {
super("message", peerName, peerAvatar, topic, timestamp);
this.message = message;
}
toJsonString() {
return JSON.stringify({
...this.toJson(),
message: this.message,
});
}
toJsonString() {
return JSON.stringify({
...this.toJson(),
message: this.message,
});
}
static new(bot, message) {
return new TextMessage(bot.botName, bot.botAvatar, bot.currentTopic, Date.now(), message);
}
static new(bot, message) {
return new TextMessage(bot.botName, bot.botAvatar, bot.currentTopic, Date.now(), message);
}
}
export default TextMessage;

View File

@ -1,3 +1,4 @@
// commands.js
import b4a from 'b4a';
import fs from 'fs';
@ -11,94 +12,45 @@ if (fs.existsSync(agentAvatarPath)) {
}
export default async function handleCommand(command, context) {
const { eventEmitter, currentTopic, clearMessages, addMessage, joinRoom, leaveRoom, createRoom, listFiles, deleteFile, servePort } = context;
console.log("Context received in handleCommand:", context); // Add logging
const { eventEmitter, currentTopic, clearMessages, addMessage, joinRoom, leaveRoom, createRoom, listFiles } = context;
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(currentTopic);
case '~clear':
clearMessages();
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(currentTopic, restArgs);
await joinRoom(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(currentTopic, restArgs);
await createRoom(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);
// Render the file list with delete buttons
renderFileList(files, deleteFile, servePort);
addMessage('LinkUp', `Available files:\n${fileList}`, agentAvatar, currentTopic());
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);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

View File

@ -17,10 +17,10 @@
</header>
<main>
<div id="sidebar">
<ul id="guild-list" class="list-group">
<!-- Guild list will be populated dynamically -->
<ul id="room-list">
<!-- Room list will be populated dynamically -->
</ul>
<button id="toggle-setup-btn">Create/Join Guild</button>
<button id="toggle-setup-btn">Create/Join Room</button>
</div>
<div id="content">
<div id="register" class="hidden">
@ -35,12 +35,12 @@
<div id="user-info">
<!-- User info will be populated dynamically -->
</div>
<button id="create-guild-btn">Create Guild</button>
<button id="create-chat-room">Create Room</button>
<div id="or">- or -</div>
<div id="join-guild-container">
<label for="join-guild-topic">Guild Topic:</label>
<input type="text" id="join-guild-topic" placeholder="guild topic">
<button id="join-guild">Join Guild</button>
<div id="join-chat-room-container">
<label for="connection-topic">Topic:</label>
<input type="text" id="join-chat-room-topic" placeholder="connection topic">
<button id="join-chat-room">Join Room</button>
</div>
</div>
<div id="chat" class="hidden">
@ -49,10 +49,11 @@
<div style="display: inline;">
<strong><span id="chat-room-name"></span></strong> | <a href="#" id="copy-topic-link" class="mini-button">Copy Topic</a>
<span id="chat-room-topic" style="display: none;"></span>
<span id="chat-guild-topic" style="display: none;"></span>
<span id="chat-guild-name" style="display: none;"></span>
</div>
</div>
<div id="user-list">
<!-- User list will be populated here -->
</div>
</div>
<div id="messages-container">
<div id="messages"></div>
@ -61,7 +62,7 @@
<textarea id="message" rows="4"></textarea>
<input type="file" id="file-input" style="display:none;">
<button type="button" id="attach-file">Attach File</button>
<button type="button" id="talk-btn">Talk</button>
<button type="button" id="talk-btn">Talk</button> <!-- New Talk button -->
<input type="submit" value="Send" />
</form>
<button id="remove-room-btn">Leave Room</button>
@ -69,46 +70,11 @@
<div id="loading" class="hidden">Loading ...</div>
</div>
</main>
<!-- Create Guild Modal -->
<div id="create-guild-modal" class="modal hidden">
<div class="modal-content">
<span class="close-btn" id="close-create-guild-modal">&times;</span>
<h2>Create Guild</h2>
<form id="create-guild-form">
<label for="guild-name">Guild Name:</label>
<input type="text" id="guild-name" required>
<button type="submit">Create</button>
</form>
</div>
</div>
<!-- Manage Guild Modal -->
<div id="manage-guild-modal" class="modal hidden">
<div class="modal-content">
<span class="close-btn" id="close-manage-guild-modal">&times;</span>
<h2>Manage Guild</h2>
<div id="guild-info">
<!-- Guild info will be populated dynamically -->
</div>
<button id="copy-guild-id" class="mini-button">Copy Guild ID</button>
<form id="add-room-form">
<label for="room-name">Room Name:</label>
<input type="text" id="room-name" required>
<button type="submit">Add Room</button>
</form>
<ul id="room-list">
<!-- Room list will be populated dynamically -->
</ul>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const messageInput = document.getElementById('message');
const copyTopicLink = document.getElementById('copy-topic-link');
const chatRoomTopic = document.getElementById('chat-room-topic');
const copyGuildIdButton = document.getElementById('copy-guild-id');
if (messageInput) {
messageInput.addEventListener('keydown', function(event) {
@ -135,123 +101,32 @@
});
}
if (copyGuildIdButton) {
copyGuildIdButton.addEventListener('click', function(event) {
event.preventDefault();
const guildTopic = document.getElementById('manage-guild-modal').dataset.guildTopic;
navigator.clipboard.writeText(guildTopic).then(() => {
console.log('Guild ID copied to clipboard:', guildTopic);
}).catch(err => {
console.error('Failed to copy guild ID:', err);
});
});
}
const switchRoom = (topic) => {
console.log('Switching to room:', topic); // Debugging log
const chatRoomTopic = document.querySelector('#chat-room-topic');
const chatRoomName = document.querySelector('#chat-room-name');
const guildList = document.querySelector('#guild-list');
if (guildList) {
guildList.addEventListener('click', function(event) {
const roomItem = event.target.closest('.room-item');
if (chatRoomTopic) {
chatRoomTopic.innerText = topic; // Set full topic here
} else {
console.error('Element #chat-room-topic not found');
}
// Show chat UI elements
document.querySelector('#chat').classList.remove('hidden');
document.querySelector('#setup').classList.add('hidden');
};
const roomList = document.querySelector('#room-list');
if (roomList) {
roomList.addEventListener('click', function(event) {
const roomItem = event.target.closest('li');
if (roomItem) {
const guildTopic = roomItem.dataset.guildTopic;
const roomTopic = roomItem.dataset.topic;
switchRoom(guildTopic, roomTopic);
switchRoom(roomItem.dataset.topic);
}
});
}
const createGuildBtn = document.getElementById('create-guild-btn');
const createGuildModal = document.getElementById('create-guild-modal');
const closeCreateGuildModal = document.getElementById('close-create-guild-modal');
const createGuildForm = document.getElementById('create-guild-form');
createGuildBtn.addEventListener('click', () => {
createGuildModal.classList.remove('hidden');
});
closeCreateGuildModal.addEventListener('click', () => {
createGuildModal.classList.add('hidden');
});
createGuildForm.addEventListener('submit', (event) => {
event.preventDefault();
const guildName = document.getElementById('guild-name').value.trim();
if (guildName) {
createGuild(guildName);
createGuildModal.classList.add('hidden');
}
});
const joinGuildBtn = document.getElementById('join-guild');
joinGuildBtn.addEventListener('click', async (event) => {
const guildTopic = document.getElementById('join-guild-topic').value.trim();
if (guildTopic) {
await joinGuildRequest(guildTopic);
}
});
const manageGuildModal = document.getElementById('manage-guild-modal');
const closeManageGuildModal = document.getElementById('close-manage-guild-modal');
const addRoomForm = document.getElementById('add-room-form');
const roomNameInput = document.getElementById('room-name');
closeManageGuildModal.addEventListener('click', () => {
manageGuildModal.classList.add('hidden');
});
addRoomForm.addEventListener('submit', (event) => {
event.preventDefault();
const roomName = roomNameInput.value.trim();
const guildTopic = manageGuildModal.dataset.guildTopic;
if (roomName && guildTopic) {
addRoomToGuild(guildTopic, roomName);
roomNameInput.value = '';
}
});
});
function createGuild(guildName) {
const event = new CustomEvent('createGuild', { detail: { guildName } });
document.dispatchEvent(event);
}
function addRoomToGuild(guildTopic, roomName) {
const event = new CustomEvent('addRoomToGuild', { detail: { guildTopic, roomName } });
document.dispatchEvent(event);
}
function manageGuild(guildTopic) {
const event = new CustomEvent('manageGuild', { detail: { guildTopic } });
document.dispatchEvent(event);
}
function switchRoom(guildTopic, roomTopic) {
const event = new CustomEvent('switchRoom', { detail: { guildTopic, roomTopic } });
document.dispatchEvent(event);
}
function openManageGuildModal(guildTopic) {
const manageGuildModal = document.getElementById('manage-guild-modal');
const guildInfo = document.getElementById('guild-info');
const roomList = document.getElementById('room-list');
manageGuildModal.dataset.guildTopic = guildTopic;
guildInfo.innerHTML = `<h3>${config.guilds[guildTopic].alias}</h3>`;
roomList.innerHTML = '';
config.guilds[guildTopic].rooms.forEach(room => {
const roomItem = document.createElement('li');
roomItem.textContent = room.alias;
roomItem.dataset.guildTopic = guildTopic;
roomItem.dataset.topic = room.topic;
roomItem.addEventListener('dblclick', () => enterEditMode(roomItem, guildTopic));
roomItem.addEventListener('click', () => switchRoom(guildTopic, room.topic));
roomList.appendChild(roomItem);
});
manageGuildModal.classList.remove('hidden');
}
</script>
</body>
</html>

View File

@ -24,15 +24,12 @@
"dependencies": {
"b4a": "^1.6.6",
"corestore": "^6.18.2",
"dompurify": "^3.1.6",
"dotenv": "^16.4.5",
"electron": "^30.0.8",
"highlight.js": "^11.10.0",
"hypercore-crypto": "^3.4.1",
"hyperdrive": "^11.8.1",
"hyperswarm": "^4.7.14",
"localdrive": "^1.11.4",
"markdown-it": "^14.1.0",
"marked": "^12.0.2",
"serve-drive": "^5.0.8"
}

148
style.css
View File

@ -281,6 +281,7 @@ textarea::placeholder {
background-color: #f04747;
}
/* Main container styles */
main {
display: flex;
@ -443,7 +444,6 @@ main {
border-radius: 50%;
margin-right: 0.5rem;
border: 2px solid #464343;
user-select: none;
}
.message-content {
@ -473,146 +473,78 @@ main {
}
/* Updated Room List Styles */
#guild-list {
#room-list {
list-style: none;
padding: 0;
margin: 0;
}
.guild-item {
#room-list li {
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;
}
.guild-item .room-list li:hover {
background-color: #5a5f64;
#room-list li:hover {
background-color: #4a5258;
}
.guild-item .room-list li span {
#room-list li span {
flex: 1;
}
.guild-item .room-list li .edit-icon,
.guild-item .room-list li .delete-icon {
#room-list li .edit-icon,
#room-list li .delete-icon {
margin-left: 10px;
color: #e0e0e0;
cursor: pointer;
transition: color 0.3s ease;
}
.guild-item .room-list li .edit-icon:hover,
.guild-item .room-list li .delete-icon:hover {
#room-list li .edit-icon:hover,
#room-list li .delete-icon:hover {
color: #a0a0a0;
}
/* 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 {
/* Style for Edit Mode Input Box */
#room-list li input[type="text"] {
background-color: #2e2e2e;
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;
float: right;
font-size: 1.5rem;
cursor: pointer;
}
.close-btn:hover {
color: #f04747;
}
.modal h2 {
margin-bottom: 1rem;
color: #fff;
}
.modal form {
display: flex;
flex-direction: column;
align-items: center;
}
.modal form input,
.modal form button {
margin-bottom: 1rem;
width: 100%;
}
.modal form button {
width: auto;
background-color: #3e3e3e;
color: #fff;
border: none;
padding: 0.75rem 1.5rem;
border: 1px solid #464343;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.3s ease;
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;
}
.modal form button:hover {
background-color: #191919;
transform: scale(1.05);
#room-list li input[type="text"]:focus {
outline: none;
background-color: #3a3a3a;
}
/* 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;
}
a:hover {
color: #b0b0b0; /* Lighter color on hover */
text-decoration: underline; /* Underline on hover */
}
a:active {
color: #a0a0a0; /* Slightly darker color on active */
}
a:visited {
color: #b0b0b0; /* Different color for visited links */
}