forked from snxraven/LinkUp-P2P-Chat
Compare commits
20 Commits
edf61f0462
...
4aa205cd73
Author | SHA1 | Date | |
---|---|---|---|
|
4aa205cd73 | ||
|
5bb5e63105 | ||
|
4415d196de | ||
|
cd4b333954 | ||
|
a92b22ef17 | ||
|
7a74d3fc4d | ||
|
6eab069cc5 | ||
7ec2901165 | |||
|
9aedd9fda4 | ||
|
e57db27a13 | ||
|
157c8af4f4 | ||
|
6a0b05df86 | ||
|
d0d408230a | ||
|
3dcabac0b4 | ||
|
8d1ecc2c19 | ||
|
01fee56692 | ||
|
e1153cb5df | ||
|
25e421e982 | ||
|
3a0af4ace2 | ||
4fbbc73527 |
3
.idea/.gitignore
vendored
Normal file
3
.idea/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
12
.idea/LinkUp-P2P-Chat.iml
Normal file
12
.idea/LinkUp-P2P-Chat.iml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
6
.idea/misc.xml
Normal file
6
.idea/misc.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="JavaScriptSettings">
|
||||||
|
<option name="languageLevel" value="FLOW" />
|
||||||
|
</component>
|
||||||
|
</project>
|
8
.idea/modules.xml
Normal file
8
.idea/modules.xml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/LinkUp-P2P-Chat.iml" filepath="$PROJECT_DIR$/.idea/LinkUp-P2P-Chat.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
6
.idea/vcs.xml
Normal file
6
.idea/vcs.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
340
app.js
340
app.js
@ -16,7 +16,7 @@ await drive.ready();
|
|||||||
let swarm;
|
let swarm;
|
||||||
let registeredUsers = JSON.parse(localStorage.getItem('registeredUsers')) || {};
|
let registeredUsers = JSON.parse(localStorage.getItem('registeredUsers')) || {};
|
||||||
let peerCount = 0;
|
let peerCount = 0;
|
||||||
let currentRoom = null;
|
let activeRooms = [];
|
||||||
const eventEmitter = new EventEmitter();
|
const eventEmitter = new EventEmitter();
|
||||||
|
|
||||||
// Define servePort at the top level
|
// Define servePort at the top level
|
||||||
@ -29,7 +29,10 @@ let config = {
|
|||||||
rooms: []
|
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() {
|
function getRandomPort() {
|
||||||
return Math.floor(Math.random() * (65535 - 49152 + 1)) + 49152;
|
return Math.floor(Math.random() * (65535 - 49152 + 1)) + 49152;
|
||||||
}
|
}
|
||||||
@ -73,40 +76,38 @@ async function initialize() {
|
|||||||
toggleSetupBtn.addEventListener('click', toggleSetupView);
|
toggleSetupBtn.addEventListener('click', toggleSetupView);
|
||||||
}
|
}
|
||||||
if (removeRoomBtn) {
|
if (removeRoomBtn) {
|
||||||
removeRoomBtn.addEventListener('click', leaveRoom);
|
removeRoomBtn.addEventListener('click', () => {
|
||||||
|
const topic = document.querySelector('#chat-room-topic').innerText;
|
||||||
|
leaveRoom(topic);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (attachFileButton) {
|
if (attachFileButton) {
|
||||||
attachFileButton.addEventListener('click', () => fileInput.click());
|
attachFileButton.addEventListener('click', () => fileInput.click());
|
||||||
}
|
}
|
||||||
if (fileInput) {
|
if (fileInput) {
|
||||||
fileInput.addEventListener('change', async (event) => {
|
fileInput.addEventListener('change', handleFileInput);
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const configExists = fs.existsSync("./config.json");
|
const configExists = fs.existsSync("./config.json");
|
||||||
if (configExists) {
|
if (configExists) {
|
||||||
config = JSON.parse(fs.readFileSync("./config.json", 'utf8'));
|
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
|
// Update port in URLs
|
||||||
config.userAvatar = updatePortInUrl(config.userAvatar);
|
config.userAvatar = updatePortInUrl(config.userAvatar);
|
||||||
config.rooms.forEach(room => {
|
config.rooms.forEach(room => {
|
||||||
addRoomToListWithoutWritingToConfig(room);
|
room.alias = room.alias || truncateHash(room.topic);
|
||||||
});
|
});
|
||||||
for (let user in registeredUsers) {
|
for (let user in registeredUsers) {
|
||||||
registeredUsers[user] = updatePortInUrl(registeredUsers[user]);
|
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');
|
const registerDiv = document.querySelector('#register');
|
||||||
@ -115,15 +116,30 @@ async function initialize() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
eventEmitter.on('onMessage', async (messageObj) => {
|
eventEmitter.on('onMessage', async (messageObj) => {
|
||||||
|
console.log('Received message:', messageObj); // Debugging log
|
||||||
|
|
||||||
if (messageObj.type === 'icon') {
|
if (messageObj.type === 'icon') {
|
||||||
const username = messageObj.username;
|
const username = messageObj.username;
|
||||||
const avatarBuffer = Buffer.from(messageObj.avatar, 'base64');
|
if (messageObj.avatar) {
|
||||||
|
const avatarBuffer = b4a.from(messageObj.avatar, 'base64');
|
||||||
await drive.put(`/icons/${username}.png`, avatarBuffer);
|
await drive.put(`/icons/${username}.png`, avatarBuffer);
|
||||||
updateIcon(username, avatarBuffer);
|
updateIcon(username, avatarBuffer);
|
||||||
} else if (messageObj.type === 'file') {
|
|
||||||
addFileMessage(messageObj.name, messageObj.fileName, messageObj.fileUrl, messageObj.fileType, messageObj.avatar);
|
|
||||||
} else {
|
} else {
|
||||||
onMessageAdded(messageObj.name, messageObj.message, messageObj.avatar);
|
console.error('Received icon message with missing avatar data:', messageObj);
|
||||||
|
}
|
||||||
|
} else if (messageObj.type === 'file') {
|
||||||
|
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 {
|
||||||
|
console.error('Received unknown message type:', messageObj);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -138,7 +154,7 @@ async function initialize() {
|
|||||||
const iconMessage = JSON.stringify({
|
const iconMessage = JSON.stringify({
|
||||||
type: 'icon',
|
type: 'icon',
|
||||||
username: config.userName,
|
username: config.userName,
|
||||||
avatar: iconBuffer.toString('base64'),
|
avatar: b4a.toString(iconBuffer, 'base64'),
|
||||||
});
|
});
|
||||||
connection.write(iconMessage);
|
connection.write(iconMessage);
|
||||||
}
|
}
|
||||||
@ -162,6 +178,11 @@ async function initialize() {
|
|||||||
swarm.on('close', () => {
|
swarm.on('close', () => {
|
||||||
console.log('Swarm closed');
|
console.log('Swarm closed');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Initialize highlight.js once the DOM is fully loaded
|
||||||
|
document.addEventListener("DOMContentLoaded", (event) => {
|
||||||
|
hljs.highlightAll();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerUser(e) {
|
function registerUser(e) {
|
||||||
@ -214,7 +235,7 @@ async function createChatRoom() {
|
|||||||
const topicBuffer = crypto.randomBytes(32);
|
const topicBuffer = crypto.randomBytes(32);
|
||||||
const topic = b4a.toString(topicBuffer, 'hex');
|
const topic = b4a.toString(topicBuffer, 'hex');
|
||||||
addRoomToList(topic);
|
addRoomToList(topic);
|
||||||
joinSwarm(topicBuffer);
|
await joinSwarm(topicBuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function joinChatRoom(e) {
|
async function joinChatRoom(e) {
|
||||||
@ -222,27 +243,21 @@ async function joinChatRoom(e) {
|
|||||||
const topicStr = document.querySelector('#join-chat-room-topic').value;
|
const topicStr = document.querySelector('#join-chat-room-topic').value;
|
||||||
const topicBuffer = b4a.from(topicStr, 'hex');
|
const topicBuffer = b4a.from(topicStr, 'hex');
|
||||||
addRoomToList(topicStr);
|
addRoomToList(topicStr);
|
||||||
joinSwarm(topicBuffer);
|
await joinSwarm(topicBuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function joinSwarm(topicBuffer) {
|
async function joinSwarm(topicBuffer) {
|
||||||
if (currentRoom) {
|
const topic = b4a.toString(topicBuffer, 'hex');
|
||||||
currentRoom.destroy();
|
if (!activeRooms.some(room => room.topic === topic)) {
|
||||||
}
|
|
||||||
|
|
||||||
document.querySelector('#setup').classList.add('hidden');
|
|
||||||
document.querySelector('#loading').classList.remove('hidden');
|
|
||||||
|
|
||||||
const discovery = swarm.join(topicBuffer, { client: true, server: true });
|
const discovery = swarm.join(topicBuffer, { client: true, server: true });
|
||||||
await discovery.flushed();
|
await discovery.flushed();
|
||||||
|
|
||||||
const topic = b4a.toString(topicBuffer, 'hex');
|
activeRooms.push({ topic, discovery });
|
||||||
document.querySelector('#chat-room-topic').innerText = topic; // Set full topic here
|
|
||||||
document.querySelector('#loading').classList.add('hidden');
|
|
||||||
document.querySelector('#chat').classList.remove('hidden');
|
|
||||||
|
|
||||||
currentRoom = discovery;
|
console.log('Joined room:', topic); // Debugging log
|
||||||
clearMessages();
|
|
||||||
|
renderMessagesForRoom(topic);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function addRoomToList(topic) {
|
function addRoomToList(topic) {
|
||||||
@ -250,51 +265,130 @@ function addRoomToList(topic) {
|
|||||||
const roomItem = document.createElement('li');
|
const roomItem = document.createElement('li');
|
||||||
roomItem.textContent = truncateHash(topic);
|
roomItem.textContent = truncateHash(topic);
|
||||||
roomItem.dataset.topic = topic;
|
roomItem.dataset.topic = topic;
|
||||||
|
|
||||||
|
roomItem.addEventListener('dblclick', () => enterEditMode(roomItem));
|
||||||
roomItem.addEventListener('click', () => switchRoom(topic));
|
roomItem.addEventListener('click', () => switchRoom(topic));
|
||||||
roomList.appendChild(roomItem);
|
roomList.appendChild(roomItem);
|
||||||
|
|
||||||
config.rooms.push(topic);
|
config.rooms.push({ topic, alias: truncateHash(topic) });
|
||||||
writeConfigToFile("./config.json");
|
writeConfigToFile("./config.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
function addRoomToListWithoutWritingToConfig(topic) {
|
function enterEditMode(roomItem) {
|
||||||
const roomList = document.querySelector('#room-list');
|
const originalText = roomItem.textContent;
|
||||||
const roomItem = document.createElement('li');
|
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);
|
roomItem.textContent = truncateHash(topic);
|
||||||
roomItem.dataset.topic = topic;
|
}
|
||||||
roomItem.addEventListener('click', () => switchRoom(topic));
|
|
||||||
roomList.appendChild(roomItem);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchRoom(topic) {
|
function switchRoom(topic) {
|
||||||
const topicBuffer = b4a.from(topic, 'hex');
|
console.log('Switching to room:', topic); // Debugging log
|
||||||
joinSwarm(topicBuffer);
|
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(topic) {
|
||||||
|
const roomIndex = activeRooms.findIndex(room => room.topic === topic);
|
||||||
|
if (roomIndex !== -1) {
|
||||||
|
const room = activeRooms[roomIndex];
|
||||||
|
room.discovery.destroy();
|
||||||
|
activeRooms.splice(roomIndex, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function leaveRoom() {
|
|
||||||
if (currentRoom) {
|
|
||||||
const topic = b4a.toString(currentRoom.topic, 'hex');
|
|
||||||
const roomItem = document.querySelector(`li[data-topic="${topic}"]`);
|
const roomItem = document.querySelector(`li[data-topic="${topic}"]`);
|
||||||
if (roomItem) {
|
if (roomItem) {
|
||||||
roomItem.remove();
|
roomItem.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
config.rooms = config.rooms.filter(e => e !== topic);
|
config.rooms = config.rooms.filter(e => e.topic !== topic);
|
||||||
writeConfigToFile("./config.json");
|
writeConfigToFile("./config.json");
|
||||||
|
|
||||||
currentRoom.destroy();
|
if (activeRooms.length > 0) {
|
||||||
currentRoom = null;
|
switchRoom(activeRooms[0].topic);
|
||||||
}
|
} else {
|
||||||
document.querySelector('#chat').classList.add('hidden');
|
document.querySelector('#chat').classList.add('hidden');
|
||||||
document.querySelector('#setup').classList.remove('hidden');
|
document.querySelector('#setup').classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function sendMessage(e) {
|
function sendMessage(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const message = document.querySelector('#message').value;
|
const message = document.querySelector('#message').value;
|
||||||
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 = [];
|
let peersPublicKeys = [];
|
||||||
peersPublicKeys.push([...swarm.connections].map(peer => peer.remotePublicKey.toString('hex')));
|
peersPublicKeys.push([...swarm.connections].map(peer => peer.remotePublicKey.toString('hex')));
|
||||||
@ -306,7 +400,7 @@ function sendMessage(e) {
|
|||||||
name: config.userName,
|
name: config.userName,
|
||||||
message,
|
message,
|
||||||
avatar: config.userAvatar,
|
avatar: config.userAvatar,
|
||||||
topic: b4a.toString(currentRoom.topic, 'hex'),
|
topic: topic,
|
||||||
peers: peersPublicKeys, // Deprecated. To be deleted in future updates
|
peers: peersPublicKeys, // Deprecated. To be deleted in future updates
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
readableTimestamp: new Date().toLocaleString(), // Added human-readable timestamp
|
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) {
|
function sendFileMessage(name, fileUrl, fileType, avatar) {
|
||||||
const fileName = fileUrl.split('/').pop();
|
const fileName = fileUrl.split('/').pop();
|
||||||
const messageObj = JSON.stringify({
|
const messageObj = JSON.stringify({
|
||||||
@ -327,7 +456,7 @@ function sendFileMessage(name, fileUrl, fileType, avatar) {
|
|||||||
fileUrl,
|
fileUrl,
|
||||||
fileType,
|
fileType,
|
||||||
avatar,
|
avatar,
|
||||||
topic: b4a.toString(currentRoom.topic, 'hex'),
|
topic: document.querySelector('#chat-room-topic').innerText,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -336,10 +465,11 @@ function sendFileMessage(name, fileUrl, fileType, avatar) {
|
|||||||
peer.write(messageObj);
|
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');
|
const $div = document.createElement('div');
|
||||||
$div.classList.add('message');
|
$div.classList.add('message');
|
||||||
|
|
||||||
@ -363,16 +493,27 @@ function addFileMessage(from, fileName, fileUrl, fileType, avatar) {
|
|||||||
$image.classList.add('attached-image');
|
$image.classList.add('attached-image');
|
||||||
$content.appendChild($image);
|
$content.appendChild($image);
|
||||||
} else {
|
} else {
|
||||||
|
const $fileButton = document.createElement('button');
|
||||||
|
$fileButton.textContent = `Download File: ${fileName}`;
|
||||||
|
$fileButton.onclick = function() {
|
||||||
const $fileLink = document.createElement('a');
|
const $fileLink = document.createElement('a');
|
||||||
$fileLink.href = fileUrl;
|
$fileLink.href = fileUrl;
|
||||||
$fileLink.textContent = `File: ${fileName}`;
|
|
||||||
$fileLink.download = fileName;
|
$fileLink.download = fileName;
|
||||||
$content.appendChild($fileLink);
|
$fileLink.click();
|
||||||
|
};
|
||||||
|
$content.appendChild($fileButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
$div.appendChild($content);
|
$div.appendChild($content);
|
||||||
|
|
||||||
|
// 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);
|
document.querySelector('#messages').appendChild($div);
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
|
} else {
|
||||||
|
console.log(`Message for topic ${topic} not rendered because current topic is ${currentTopic}`); // Debugging log
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updatePeerCount() {
|
function updatePeerCount() {
|
||||||
@ -387,14 +528,25 @@ function scrollToBottom() {
|
|||||||
container.scrollTop = container.scrollHeight;
|
container.scrollTop = container.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMessageAdded(from, message, avatar) {
|
function onMessageAdded(from, message, avatar, topic) {
|
||||||
|
console.log('Adding message:', { from, message, avatar, topic }); // Debugging log
|
||||||
|
const messageObj = {
|
||||||
|
from,
|
||||||
|
message,
|
||||||
|
avatar
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add the message to the store
|
||||||
|
addMessageToStore(topic, messageObj);
|
||||||
|
|
||||||
|
// Only render messages for the current room
|
||||||
|
const currentTopic = document.querySelector('#chat-room-topic').innerText;
|
||||||
|
if (currentTopic === topic) {
|
||||||
const $div = document.createElement('div');
|
const $div = document.createElement('div');
|
||||||
$div.classList.add('message');
|
$div.classList.add('message');
|
||||||
|
|
||||||
const $img = document.createElement('img');
|
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.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');
|
$img.classList.add('avatar');
|
||||||
$div.appendChild($img);
|
$div.appendChild($img);
|
||||||
|
|
||||||
@ -408,7 +560,17 @@ function onMessageAdded(from, message, avatar) {
|
|||||||
const $text = document.createElement('div');
|
const $text = document.createElement('div');
|
||||||
$text.classList.add('message-text');
|
$text.classList.add('message-text');
|
||||||
|
|
||||||
const md = window.markdownit();
|
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);
|
const markdownContent = md.render(message);
|
||||||
$text.innerHTML = markdownContent;
|
$text.innerHTML = markdownContent;
|
||||||
|
|
||||||
@ -418,6 +580,9 @@ function onMessageAdded(from, message, avatar) {
|
|||||||
|
|
||||||
document.querySelector('#messages').appendChild($div);
|
document.querySelector('#messages').appendChild($div);
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
|
} else {
|
||||||
|
console.log(`Message for topic ${topic} not rendered because current topic is ${currentTopic}`); // Debugging log
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function truncateHash(hash) {
|
function truncateHash(hash) {
|
||||||
@ -429,7 +594,7 @@ async function updateIcon(username, avatarBuffer) {
|
|||||||
if (userIcon) {
|
if (userIcon) {
|
||||||
const avatarBlob = new Blob([avatarBuffer], { type: 'image/png' });
|
const avatarBlob = new Blob([avatarBuffer], { type: 'image/png' });
|
||||||
const avatarUrl = URL.createObjectURL(avatarBlob);
|
const avatarUrl = URL.createObjectURL(avatarBlob);
|
||||||
userIcon.src = avatarUrl;
|
userIcon.src = updatePortInUrl(avatarUrl);
|
||||||
|
|
||||||
config.userAvatar = avatarUrl;
|
config.userAvatar = avatarUrl;
|
||||||
writeConfigToFile("./config.json");
|
writeConfigToFile("./config.json");
|
||||||
@ -462,4 +627,45 @@ function updatePortInUrl(url) {
|
|||||||
return urlObject.toString();
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
initialize();
|
@ -1,8 +0,0 @@
|
|||||||
class ChatRoom {
|
|
||||||
public ChatRoom(topic, peers) {
|
|
||||||
this.topic = topic;
|
|
||||||
this.peers = peers;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ChatRoom;
|
|
@ -1,5 +1,5 @@
|
|||||||
import Hyperswarm from 'hyperswarm';
|
import Hyperswarm from 'hyperswarm';
|
||||||
import EventEmitter from 'node:events'
|
import EventEmitter from 'node:events';
|
||||||
import b4a from "b4a";
|
import b4a from "b4a";
|
||||||
|
|
||||||
class Client extends EventEmitter {
|
class Client extends EventEmitter {
|
||||||
@ -8,15 +8,21 @@ class Client extends EventEmitter {
|
|||||||
if(!botName) return console.error("Bot Name is not defined!");
|
if(!botName) return console.error("Bot Name is not defined!");
|
||||||
this.botName = botName;
|
this.botName = botName;
|
||||||
this.swarm = new Hyperswarm();
|
this.swarm = new Hyperswarm();
|
||||||
|
this.joinedRooms = new Set(); // Track the rooms the bot has joined
|
||||||
|
this.currentTopic = null; // Track the current topic
|
||||||
this.setupSwarm();
|
this.setupSwarm();
|
||||||
}
|
}
|
||||||
|
|
||||||
setupSwarm() {
|
setupSwarm() {
|
||||||
this.swarm.on('connection', (peer) => {
|
this.swarm.on('connection', (peer) => {
|
||||||
peer.on('data', message => {
|
peer.on('data', message => {
|
||||||
if(message.type === "message") this.emit('onMessage', peer, JSON.parse(message.toString()));
|
const messageObj = JSON.parse(message.toString());
|
||||||
if(message.type === "icon") this.emit('onIcon', peer, JSON.parse(message.toString()));
|
if (this.joinedRooms.has(messageObj.topic)) { // Process message only if it is from a joined room
|
||||||
if(message.type === "file") this.emit('onFile', peer, JSON.parse(message.toString()));
|
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 => {
|
peer.on('error', e => {
|
||||||
@ -27,19 +33,12 @@ class Client extends EventEmitter {
|
|||||||
|
|
||||||
this.swarm.on('update', () => {
|
this.swarm.on('update', () => {
|
||||||
console.log(`Connections count: ${this.swarm.connections.size} / Peers count: ${this.swarm.peers.size}`);
|
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) {
|
joinChatRoom(chatRoomID) {
|
||||||
|
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 = this.swarm.join(Buffer.from(chatRoomID, 'hex'), { client: true, server: true });
|
||||||
this.discovery.flushed().then(() => {
|
this.discovery.flushed().then(() => {
|
||||||
console.log(`Bot ${this.botName} joined the chat room.`);
|
console.log(`Bot ${this.botName} joined the chat room.`);
|
||||||
@ -47,19 +46,19 @@ class Client extends EventEmitter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMessage(roomPeers, message) {
|
// TODO: Make topic here actually work.
|
||||||
|
sendMessage(topic, message) {
|
||||||
console.log('Bot name:', this.botName);
|
console.log('Bot name:', this.botName);
|
||||||
const timestamp = Date.now(); // Generate timestamp
|
const timestamp = Date.now();
|
||||||
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 messageObj = {
|
||||||
const data = JSON.stringify({name: this.botName, message, timestamp}); // Include timestamp
|
type: 'message',
|
||||||
for (const peer of peers) peer.write(data);
|
name: this.botName,
|
||||||
}
|
message,
|
||||||
|
timestamp,
|
||||||
sendMessageToAll(message) {
|
topic: this.currentTopic // Include the current topic
|
||||||
console.log('Bot name:', this.botName);
|
};
|
||||||
const timestamp = Date.now(); // Generate timestamp
|
const data = JSON.stringify(messageObj);
|
||||||
const peers = [...this.swarm.connections]
|
const peers = [...this.swarm.connections];
|
||||||
const data = JSON.stringify({name: this.botName, message, timestamp}); // Include timestamp
|
|
||||||
for (const peer of peers) peer.write(data);
|
for (const peer of peers) peer.write(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
class UserPeer {
|
|
||||||
public UserPeer(peer, topics, username, avatar) {
|
|
||||||
this.peer = peer;
|
|
||||||
this.topics = topics;
|
|
||||||
this.username = username;
|
|
||||||
this.avatar = avatar;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default UserPeer;
|
|
59
index.html
59
index.html
@ -7,11 +7,13 @@
|
|||||||
<link rel="stylesheet" type="text/css" href="style.css">
|
<link rel="stylesheet" type="text/css" href="style.css">
|
||||||
<script type="module" src="./app.js"></script>
|
<script type="module" src="./app.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/markdown-it/dist/markdown-it.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/markdown-it/dist/markdown-it.min.js"></script>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/styles/atom-one-dark.min.css">
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/highlight.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<pear-ctrl></pear-ctrl>
|
<pear-ctrl></pear-ctrl>
|
||||||
<div id="linkup-text">LinkUp</div>
|
<div id="linkup-text">LinkUp | Peers: <span id="peers-count">0</span></div>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<div id="sidebar">
|
<div id="sidebar">
|
||||||
@ -44,11 +46,9 @@
|
|||||||
<div id="chat" class="hidden">
|
<div id="chat" class="hidden">
|
||||||
<div id="header">
|
<div id="header">
|
||||||
<div id="details">
|
<div id="details">
|
||||||
<div>
|
<div style="display: inline;">
|
||||||
Topic: <span id="chat-room-topic"></span>
|
<strong><span id="chat-room-name"></span></strong> | <a href="#" id="copy-topic-link" class="mini-button">Copy Topic</a>
|
||||||
</div>
|
<span id="chat-room-topic" style="display: none;"></span>
|
||||||
<div>
|
|
||||||
Peers: <span id="peers-count">0</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="user-list">
|
<div id="user-list">
|
||||||
@ -72,12 +72,59 @@
|
|||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const messageInput = document.getElementById('message');
|
const messageInput = document.getElementById('message');
|
||||||
|
const copyTopicLink = document.getElementById('copy-topic-link');
|
||||||
|
const chatRoomTopic = document.getElementById('chat-room-topic');
|
||||||
|
|
||||||
|
if (messageInput) {
|
||||||
messageInput.addEventListener('keydown', function(event) {
|
messageInput.addEventListener('keydown', function(event) {
|
||||||
if (event.key === 'Enter' && !event.shiftKey) {
|
if (event.key === 'Enter' && !event.shiftKey) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
document.getElementById('message-form').dispatchEvent(new Event('submit'));
|
document.getElementById('message-form').dispatchEvent(new Event('submit'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (copyTopicLink) {
|
||||||
|
copyTopicLink.addEventListener('click', function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (chatRoomTopic) {
|
||||||
|
const topic = chatRoomTopic.innerText;
|
||||||
|
navigator.clipboard.writeText(topic).then(() => {
|
||||||
|
alert('Topic copied to clipboard!');
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Failed to copy topic:', err);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error('Element #chat-room-topic not found');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
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) {
|
||||||
|
switchRoom(roomItem.dataset.topic);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
21
style.css
21
style.css
@ -12,6 +12,27 @@ body {
|
|||||||
transition: background-color 0.3s ease, color 0.3s ease;
|
transition: background-color 0.3s ease, color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mini-button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 3px 7px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #fff;
|
||||||
|
background-color: #7289da;
|
||||||
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-button:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bold {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
pear-ctrl[data-platform="darwin"] { float: right; margin-top: 4px; }
|
pear-ctrl[data-platform="darwin"] { float: right; margin-top: 4px; }
|
||||||
|
|
||||||
pear-ctrl {
|
pear-ctrl {
|
||||||
|
Loading…
Reference in New Issue
Block a user