This commit is contained in:
Raven Scott
2026-02-21 23:25:27 -05:00
parent 54b5c8b2c9
commit c912ab1b4d
4 changed files with 729 additions and 102 deletions
+27 -11
View File
@@ -54,6 +54,22 @@ cd BridgeSwarm
.\scripts\install.ps1
```
### Quick Install (No Clone)
Run directly from the web — downloads BridgeSwarm, builds, and packages the extension:
```bash
curl -fsSL https://ssh.surf/bridgeswarm/install.sh -o install.sh && bash install.sh
```
Or from GitLab:
```bash
curl -fsSL https://git.ssh.surf/snxraven/BridgeSwarm/src/branch/main/scripts/web-installer.sh -o install.sh && bash install.sh
```
This installs the native host to `~/.bridgeswarm/`, saves the extension package to `~/Downloads/`, and opens the browser for you to load it.
### Load the Extension
1. Open Chrome/Edge and go to `chrome://extensions`
@@ -107,23 +123,23 @@ swarm.destroy();
```
┌─────────────────────────────────────────────────────────────┐
│ Your Web Page
│ Your Web Page │
│ │
│ window.BridgeSwarm │
│ ├── new BridgeSwarm({ appName }) │
│ ├── swarm.join(topic) │
│ ├── swarm.on('connection', ...) │
│ ├── conn.write(data) │
│ └── BridgeSwarm.request('beeGet', { key }) │
│ ├── new BridgeSwarm({ appName })
│ ├── swarm.join(topic)
│ ├── swarm.on('connection', ...)
│ ├── conn.write(data)
│ └── BridgeSwarm.request('beeGet', { key })
└─────────────────────────────────────────────────────────────┘
│ window.postMessage
┌─────────────────────────────────────────────────────────────┐
│ Chrome Extension
│ content.js ─────► background.js ─────► native host │
│ (injects API) (service worker) (P2P networking) │
│ Chrome Extension │
│ │
│ content.js ─────► background.js ─────► native host
│ (injects API) (service worker) (P2P networking)
└─────────────────────────────────────────────────────────────┘
```
+453 -48
View File
@@ -1,7 +1,42 @@
(function () {
'use strict';
const EMOJI_LIST = ['😀', '😃', '😄', '😁', '😅', '😂', '🤣', '😊', '😇', '🙂', '😉', '😌', '😍', '🥰', '😘', '😗', '😙', '😚', '😋', '😛', '😜', '🤪', '😝', '🤑', '🤗', '🤭', '🤫', '🤔', '🤐', '🤨', '😐', '😑', '😶', '😏', '😒', '🙄', '😬', '🤥', '😌', '😔', '😪', '🤤', '😴', '😷', '🤒', '🤕', '🤢', '🤮', '🤧', '🥵', '🥶', '🥴', '😵', '🤯', '🤠', '🥳', '😎', '🤓', '🧐', '😕', '😟', '🙁', '😮', '😯', '😲', '😳', '🥺', '😦', '😧', '😨', '😰', '😥', '😢', '😭', '😱', '😖', '😣', '😞', '😓', '😩', '😫', '🥱', '😤', '😡', '😠', '🤬', '😈', '👿', '💀', '💩', '🤡', '👹', '👺', '👻', '👽', '👾', '🤖', '👍', '👎', '👏', '🙌', '🤝', '🙏', '💪', '🤘', '❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '💔', '❣️', '💕', '💞', '💓', '💗', '💖', '💘', '💝', '⭐', '🌟', '✨', '💫', '🔥', '💥', '🎉', '🎊', '🎈'];
const EMOJI_LIST = ['😀', '😃', '😄', '😁', '😅', '😂', '🤣', '😊', '😇', '🙂', '😉', '😌', '😍', '🥰', '😘', '😗', '😙', '😚', '😋', '😛', '😜', '🤪', '😝', '🤑', '🤗', '🤭', '🤫', '🤔', '🤐', '🤨', '😐', '😑', '😶', '😏', '😒', '🙄', '😬', '🤥', '😌', '😔', '😪', '🤤', '😴', '😷', '🤒', '🤕', '🤢', '🤮', '🤧', '🥵', '🥶', '🥴', '😵', '🤯', '🤠', '🥳', '😎', '🤓', '🧐', '😕', '😟', '🙁', '😮', '😯', '😲', '😳', '🥺', '😦', '😧', '😨', '😰', '😥', '😢', '😭', '😱', '😖', '😣', '😞', '😓', '😩', '😫', '🥱', '😤', '😡', '😠', '🤬', '😈', '👿', '💀', '💩', '🤡', '👹', '👺', '👻', '👽', '👾', '🤖', '👍', '👎', '👏', '🙌', '🤝', '🙏', '💪', '🤘', '❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '💔', '❣️', '💕', '💞', '💓', '💗', '💖', '💘', '💝', '⭐', '🌟', '✨', '💫', '🔥', '💥', '🎉', '🎊', '🎈', '🎁', '🏆', '🥇', '🚀', '💻', '📱', '📞', '📧', '🔗', '📝', '🔒', '🌈', '☀️', '🌙', '⚡', '💡', '🎵', '🎮', '🎲'];
// Configure marked for markdown rendering
if (typeof marked !== 'undefined') {
marked.use({
gfm: true,
breaks: true,
});
}
// Try to set up markedHighlight when it becomes available
function trySetupMarkedHighlight() {
if (typeof marked !== 'undefined' && typeof markedHighlight !== 'undefined') {
marked.use(markedHighlight({
langPrefix: 'hljs language-',
highlight(code, lang) {
if (typeof hljs !== 'undefined') {
if (lang && hljs.getLanguage(lang)) {
try {
return hljs.highlight(code, { language: lang }).value;
} catch (e) {}
}
try {
return hljs.highlightAuto(code).value;
} catch (e) {}
}
return code;
}
}));
} else {
setTimeout(trySetupMarkedHighlight, 100);
}
}
trySetupMarkedHighlight();
// We'll do syntax highlighting manually in renderMessage as fallback
const state = {
swarm: null,
@@ -16,8 +51,35 @@
autoScroll: true,
notifications: false,
sounds: true,
markdown: true,
sharedFiles: new Map(),
driveKey: null,
savedNickname: '',
savedPublicKey: '',
savedPrivateKey: '',
};
const STORAGE_KEY = 'chatAdvanced_userData';
function getStoredUserData() {
try {
const data = localStorage.getItem(STORAGE_KEY);
return data ? JSON.parse(data) : null;
} catch (e) {
return null;
}
}
function saveUserData() {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify({
nickname: state.savedNickname,
publicKey: state.savedPublicKey,
privateKey: state.savedPrivateKey,
}));
} catch (e) {}
}
const elements = {};
function $(id) {
@@ -61,17 +123,22 @@
elements.btnConfirmCreate = $('btnConfirmCreate');
elements.btnSend = $('btnSend');
elements.btnToggleEmoji = $('btnToggleEmoji');
elements.btnEmoji2 = $('btnEmoji2');
elements.btnSendFile = $('btnSendFile');
elements.btnClearChat = $('btnClearChat');
elements.btnBold = $('btnBold');
elements.btnItalic = $('btnItalic');
elements.btnStrike = $('btnStrike');
elements.btnCode = $('btnCode');
elements.btnCodeBlock = $('btnCodeBlock');
elements.btnLink = $('btnLink');
elements.btnList = $('btnList');
elements.btnQuote = $('btnQuote');
elements.btnEmoji2 = $('btnEmoji2');
elements.fileInput = $('fileInput');
elements.chkNotifications = $('chkNotifications');
elements.chkSounds = $('chkSounds');
elements.chkAutoScroll = $('chkAutoScroll');
elements.chkMarkdown = $('chkMarkdown');
elements.toastContainer = $('toastContainer');
}
@@ -97,20 +164,44 @@
e.preventDefault();
sendMessage();
}
// Keyboard shortcuts
if (e.ctrlKey || e.metaKey) {
if (e.key === 'b') {
e.preventDefault();
wrapText('**');
} else if (e.key === 'i') {
e.preventDefault();
wrapText('*');
} else if (e.key === 'k') {
e.preventDefault();
wrapText('`');
}
}
});
elements.messageInput.addEventListener('input', autoResize);
elements.messageInput.addEventListener('input', handleTyping);
elements.btnToggleEmoji.addEventListener('click', toggleEmojiPicker);
elements.btnEmoji2.addEventListener('click', toggleEmojiPicker);
elements.btnToggleEmoji.addEventListener('click', (e) => {
e.stopPropagation();
toggleEmojiPicker();
});
elements.btnEmoji2.addEventListener('click', (e) => {
e.stopPropagation();
toggleEmojiPicker();
});
elements.btnSendFile.addEventListener('click', () => elements.fileInput.click());
elements.fileInput.addEventListener('change', handleFileSelect);
elements.btnClearChat.addEventListener('click', clearChat);
elements.btnBold.addEventListener('click', () => wrapText('**'));
elements.btnItalic.addEventListener('click', () => wrapText('*'));
elements.btnStrike.addEventListener('click', () => wrapText('~~'));
elements.btnCode.addEventListener('click', () => wrapText('`'));
elements.btnCodeBlock.addEventListener('click', () => wrapText('\n```\n', '\n```\n'));
elements.btnLink.addEventListener('click', insertLink);
elements.btnList.addEventListener('click', () => wrapText('\n- ', ''));
elements.btnQuote.addEventListener('click', () => wrapText('\n> ', ''));
elements.chkNotifications.addEventListener('change', (e) => {
state.notifications = e.target.checked;
@@ -127,10 +218,25 @@
saveSettings();
});
elements.chkMarkdown.addEventListener('change', (e) => {
state.markdown = e.target.checked;
saveSettings();
// Re-render messages with new setting
renderCurrentRoomMessages();
});
elements.publicKey.addEventListener('click', copyPublicKey);
elements.avatar.addEventListener('click', generateNewAvatar);
elements.nickname.addEventListener('change', updateNickname);
// Close emoji picker when clicking outside
document.addEventListener('click', (e) => {
const picker = document.querySelector('.emoji-picker');
if (picker && !picker.contains(e.target) && !elements.btnToggleEmoji.contains(e.target) && !elements.btnEmoji2.contains(e.target)) {
picker.remove();
}
});
}
function loadSettings() {
@@ -139,11 +245,21 @@
state.notifications = settings.notifications ?? false;
state.sounds = settings.sounds ?? true;
state.autoScroll = settings.autoScroll ?? true;
state.markdown = settings.markdown ?? true;
elements.chkNotifications.checked = state.notifications;
elements.chkSounds.checked = state.sounds;
elements.chkAutoScroll.checked = state.autoScroll;
elements.chkMarkdown.checked = state.markdown;
} catch (e) {}
// Load saved user data
const userData = getStoredUserData();
if (userData) {
state.savedNickname = userData.nickname || '';
state.savedPublicKey = userData.publicKey || '';
state.savedPrivateKey = userData.privateKey || '';
}
}
function saveSettings() {
@@ -151,11 +267,19 @@
notifications: state.notifications,
sounds: state.sounds,
autoScroll: state.autoScroll,
markdown: state.markdown,
}));
saveUserData();
}
function showJoinModal() {
elements.joinModal.classList.remove('hidden');
// Pre-fill saved nickname
if (state.savedNickname) {
elements.joinNickname.value = state.savedNickname;
}
elements.joinNickname.focus();
}
@@ -182,6 +306,11 @@
await state.swarm.join(topic);
const pubKey = await state.swarm.getPublicKey();
state.publicKey = pubKey;
// Save user data for next time
state.savedNickname = nickname;
state.savedPublicKey = pubKey;
saveUserData();
elements.publicKey.textContent = 'Your ID: ' + pubKey.slice(0, 16) + '...';
updateAvatar(nickname);
@@ -235,10 +364,15 @@
conn.on('end', () => handleDisconnect(connId));
conn.on('error', (err) => console.error('Connection error:', err));
sendToPeer(connId, { type: 'handshake', nickname: state.nickname, publicKey: state.publicKey });
sendToPeer(connId, {
type: 'handshake',
nickname: state.nickname,
publicKey: state.publicKey,
driveKey: state.driveKey
});
}
function handleMessage(connId, data) {
async function handleMessage(connId, data) {
try {
const msg = JSON.parse(new TextDecoder().decode(data));
@@ -247,7 +381,7 @@
handleHandshake(connId, msg);
break;
case 'chat':
handleChatMessage(msg);
await handleChatMessage(msg);
break;
case 'typing':
handleTypingIndicator(msg);
@@ -258,8 +392,17 @@
case 'room join':
handleRoomJoin(msg);
break;
case 'file':
await handleFileShare(msg);
break;
case 'file share':
handleFileShare(msg);
await handleFileShare(msg);
break;
case 'file request':
await handleFileRequest(msg);
break;
case 'file data':
await handleFileData(msg);
break;
}
} catch (e) {
@@ -272,6 +415,7 @@
if (conn) {
conn.nickname = msg.nickname || 'Anonymous';
conn.publicKey = msg.publicKey || '';
conn.driveKey = msg.driveKey || null;
}
if (msg.publicKey) {
@@ -280,6 +424,7 @@
publicKey: msg.publicKey,
status: 'online',
lastSeen: Date.now(),
driveKey: msg.driveKey || null,
});
}
@@ -287,7 +432,7 @@
updateUserList();
}
function handleChatMessage(msg) {
async function handleChatMessage(msg) {
if (!msg || !msg.content) return;
const timestamp = msg.timestamp || Date.now();
const message = {
@@ -307,7 +452,7 @@
state.messages.get(message.room).push(message);
if (message.room === state.currentRoom) {
renderMessage(message);
await renderMessage(message);
}
if (state.notifications && msg.authorKey !== state.publicKey) {
@@ -343,6 +488,7 @@
publicKey: u.publicKey,
status: u.status || 'offline',
lastSeen: u.lastSeen,
driveKey: u.driveKey || null,
});
}
});
@@ -356,8 +502,107 @@
renderRoomList();
}
function handleFileShare(msg) {
handleChatMessage(msg);
async function handleFileShare(msg) {
// Store file reference
state.sharedFiles.set(msg.file.id, msg.file);
await handleChatMessage(msg);
renderFileList();
// Request the file from the sender if we don't have it
const conn = Array.from(state.connections.values()).find(c => c.peerInfo && c.peerInfo.publicKey === msg.authorKey);
if (conn && msg.file && msg.file.path) {
// Try to get the file from Hyperdrive (might work if peer replicated their drive)
try {
const driveFile = await window.BridgeSwarm.request('driveGet', { path: msg.file.path });
if (driveFile.ok && driveFile.data) {
console.log('File already available in local drive');
} else {
// File not local, request from peer
console.log('Requesting file from peer...');
conn.conn.write(JSON.stringify({
type: 'file request',
fileId: msg.file.id,
filePath: msg.file.path
}));
}
} catch (e) {
console.log('Error checking file, requesting from peer:', e);
}
}
}
async function handleFileRequest(msg) {
const { fileId, requesterKey, filePath } = msg;
// Find the file - either from our sharedFiles or from Hyperdrive
let file = state.sharedFiles.get(fileId);
let fileData = null;
let fileName = 'file';
let fileSize = 0;
if (!file && filePath) {
// Try to get from Hyperdrive directly
file = { path: filePath };
}
if (!file) {
console.log('File not found:', fileId);
return;
}
// Get file from Hyperdrive
try {
const driveFile = await window.BridgeSwarm.request('driveGet', { path: file.path });
if (driveFile.ok && driveFile.data) {
fileData = driveFile.data;
// Extract filename from path
const pathParts = file.path.split('/');
fileName = pathParts[pathParts.length - 1] || 'file';
// Calculate size from base64
fileSize = Math.round((fileData.length * 3) / 4);
// Send file data ONLY to the requester (not broadcast)
const requesterConn = Array.from(state.connections.values()).find(
c => c.peerInfo && c.peerInfo.publicKey === requesterKey
);
if (requesterConn) {
requesterConn.conn.write(JSON.stringify({
type: 'file data',
fileId,
fileName,
fileSize,
data: fileData,
}));
}
}
} catch (e) {
console.error('Error retrieving file:', e);
}
}
async function handleFileData(msg) {
const { fileId, fileName, fileSize, data } = msg;
if (!data) return;
// Store the file in our Hyperdrive so we can access it
const path = '/received/' + Date.now() + '-' + fileName;
try {
await window.BridgeSwarm.request('drivePut', { path, base64: data });
// Update the file reference with the local path
const file = state.sharedFiles.get(fileId);
if (file) {
file.path = path;
file.localData = data;
}
// Re-render to show download button
renderFileList();
showToast(`File received: ${fileName}`, 'success');
} catch (e) {
console.error('Error saving received file:', e);
}
}
function handleDisconnect(connId) {
@@ -369,7 +614,7 @@
state.connections.delete(connId);
}
function sendMessage() {
async function sendMessage() {
const content = elements.messageInput.value.trim();
if (!content || !state.swarm) return;
@@ -384,7 +629,7 @@
};
broadcast(message);
handleChatMessage(message);
await handleChatMessage(message);
elements.messageInput.value = '';
elements.messageInput.style.height = 'auto';
@@ -416,6 +661,7 @@
publicKey: state.publicKey,
status: 'online',
lastSeen: Date.now(),
driveKey: state.driveKey,
}]),
});
}
@@ -430,7 +676,7 @@
});
}
function renderMessage(msg) {
async function renderMessage(msg) {
const isOwn = msg.authorKey === state.publicKey;
const time = new Date(msg.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
@@ -439,12 +685,33 @@
const avatarInitial = msg.author ? msg.author[0].toUpperCase() : '?';
let contentHtml = '';
if (state.markdown && typeof marked !== 'undefined') {
// Render markdown
try {
const result = marked.parse(msg.content);
contentHtml = result instanceof Promise ? await result : result;
// Manual syntax highlighting for code blocks
if (typeof hljs !== 'undefined') {
div.querySelectorAll('pre code').forEach((block) => {
hljs.highlightElement(block);
});
}
} catch (e) {
contentHtml = escapeHtml(msg.content);
}
} else {
contentHtml = formatMessageSimple(msg.content);
}
let content = `<div class="message-content">
<div class="message-header">
<span class="message-author">${escapeHtml(msg.author)}</span>
<span class="message-time">${time}</span>
</div>
<div class="message-text">${formatMessage(msg.content)}</div>
<div class="message-text">${contentHtml}</div>
`;
if (msg.file) {
@@ -453,6 +720,7 @@
<div class="file-info">
<div class="file-name">${escapeHtml(msg.file.name)}</div>
<div class="file-size">${formatFileSize(msg.file.size)}</div>
<button class="btn-download" data-file-id="${msg.file.id}">Download</button>
</div>
</div>`;
}
@@ -464,22 +732,73 @@
${content}
`;
// Add download handler
const downloadBtn = div.querySelector('.btn-download');
if (downloadBtn) {
downloadBtn.addEventListener('click', () => downloadFile(msg.file));
}
elements.messages.appendChild(div);
scrollToBottom();
}
function formatMessage(text) {
function formatMessageSimple(text) {
text = escapeHtml(text);
text = text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
text = text.replace(/\*(.+?)\*/g, '<em>$1</em>');
text = text.replace(/`(.+?)`/g, '<code>$1</code>');
text = text.replace(/\n/g, '<br>');
return text;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
async function downloadFile(file) {
if (!file) {
showToast('File not available', 'error');
return;
}
try {
let fileData = null;
// First check if we have the data directly in the file object
if (file.data) {
fileData = file.data;
} else if (file.localData) {
fileData = file.localData;
} else if (file.path) {
// Try to get from Hyperdrive
try {
const result = await window.BridgeSwarm.request('driveGet', { path: file.path });
if (result.ok && result.data) {
fileData = result.data;
}
} catch (e) {
console.log('Could not get from drive:', e.message);
}
}
if (fileData) {
const binary = atob(fileData);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
const blob = new Blob([bytes]);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = file.name;
a.click();
URL.revokeObjectURL(url);
showToast('Download started!', 'success');
} else {
showToast('File not found', 'error');
}
} catch (e) {
console.error('Download error:', e);
showToast('Download failed: ' + e.message, 'error');
}
}
function updateUserList() {
@@ -531,14 +850,26 @@
});
}
async function renderCurrentRoomMessages() {
elements.messages.innerHTML = '';
const messages = state.messages.get(state.currentRoom) || [];
for (const msg of messages) {
await renderMessage(msg);
}
}
function switchRoom(room) {
state.currentRoom = room;
elements.currentRoom.textContent = '#' + room;
elements.messages.innerHTML = '';
const messages = state.messages.get(room) || [];
messages.forEach(msg => renderMessage(msg));
const roomData = state.rooms.get(room);
if (roomData && roomData.topic) {
elements.roomTopic.textContent = roomData.topic;
} else {
elements.roomTopic.textContent = '';
}
renderCurrentRoomMessages();
renderRoomList();
updateMemberCount();
}
@@ -603,7 +934,7 @@
picker = document.createElement('div');
picker.className = 'emoji-picker';
EMOJI_LIST.slice(0, 64).forEach(emoji => {
EMOJI_LIST.forEach(emoji => {
const span = document.createElement('span');
span.className = 'emoji';
span.textContent = emoji;
@@ -611,6 +942,7 @@
elements.messageInput.value += emoji;
elements.messageInput.focus();
picker.remove();
autoResize();
});
picker.appendChild(span);
});
@@ -626,37 +958,104 @@
elements.messageInput.value = text.slice(0, start) + before + selected + after + text.slice(end);
elements.messageInput.focus();
// Set cursor position
const newPos = start + before.length + selected.length;
elements.messageInput.setSelectionRange(newPos, newPos);
autoResize();
}
function handleFileSelect(e) {
function insertLink() {
const start = elements.messageInput.selectionStart;
const end = elements.messageInput.selectionEnd;
const text = elements.messageInput.value;
const selected = text.slice(start, end);
const linkText = selected || 'link text';
elements.messageInput.value = text.slice(0, start) + '[' + linkText + '](url)' + text.slice(end);
elements.messageInput.focus();
// Select the 'url' part for easy replacement
const urlStart = start + linkText.length + 3;
elements.messageInput.setSelectionRange(urlStart, urlStart + 3);
autoResize();
}
async function handleFileSelect(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => {
const base64 = reader.result.split(',')[1];
const message = {
type: 'file',
id: 'msg_' + Date.now(),
author: state.nickname,
authorKey: state.publicKey,
content: `Shared a file: ${file.name}`,
room: state.currentRoom,
timestamp: Date.now(),
file: {
name: file.name,
size: file.size,
data: base64,
},
};
showToast('Uploading file...', 'info');
broadcast(message);
handleChatMessage(message);
};
reader.readAsDataURL(file);
try {
// Read file as base64
const reader = new FileReader();
reader.onload = async () => {
const base64 = reader.result.split(',')[1];
const fileId = 'file_' + Date.now();
const path = '/shared/' + fileId + '-' + file.name;
// Try to save to Hyperdrive, but handle lock errors
let localPath = null;
try {
await window.BridgeSwarm.request('drivePut', { path, base64 });
localPath = path;
} catch (driveErr) {
console.log('Hyperdrive unavailable, storing locally:', driveErr.message);
// Store in memory instead - file will be sent directly to peers
}
// Create file share message
const message = {
type: 'file',
id: 'msg_' + Date.now(),
author: state.nickname,
authorKey: state.publicKey,
content: `Shared a file: ${file.name}`,
room: state.currentRoom,
timestamp: Date.now(),
file: {
id: fileId,
name: file.name,
size: file.size,
path: localPath,
data: base64, // Store data directly for peer-to-peer transfer
},
};
// Store file reference
state.sharedFiles.set(fileId, message.file);
renderFileList();
broadcast(message);
await handleChatMessage(message);
showToast('File uploaded!', 'success');
};
reader.readAsDataURL(file);
} catch (e) {
console.error('File upload error:', e);
showToast('Upload failed: ' + e.message, 'error');
}
e.target.value = '';
}
function renderFileList() {
elements.fileList.innerHTML = '';
state.sharedFiles.forEach((file, id) => {
const li = document.createElement('li');
li.className = 'file-item';
li.innerHTML = `
<span class="file-icon">📄</span>
<span class="file-name">${escapeHtml(file.name)}</span>
`;
li.addEventListener('click', () => downloadFile(file));
elements.fileList.appendChild(li);
});
}
function clearChat() {
if (!confirm('Clear all messages in this room?')) return;
elements.messages.innerHTML = '';
@@ -733,6 +1132,12 @@
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function handleResize() {
scrollToBottom();
}
+23 -5
View File
@@ -2,9 +2,10 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BridgeSwarm Advanced Chat</title>
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/styles/atom-one-dark.min.css">
</head>
<body>
<div class="app-container">
@@ -64,13 +65,18 @@
<div class="compose-area">
<div class="compose-tools">
<button class="btn-tool" id="btnBold" title="Bold"><strong>B</strong></button>
<button class="btn-tool" id="btnItalic" title="Italic"><em>I</em></button>
<button class="btn-tool" id="btnCode" title="Code">&lt;/&gt;</button>
<button class="btn-tool" id="btnBold" title="Bold (Ctrl+B)"><strong>B</strong></button>
<button class="btn-tool" id="btnItalic" title="Italic (Ctrl+I)"><em>I</em></button>
<button class="btn-tool" id="btnStrike" title="Strikethrough"><s>S</s></button>
<button class="btn-tool" id="btnCode" title="Inline Code">&lt;/&gt;</button>
<button class="btn-tool" id="btnCodeBlock" title="Code Block">{ }</button>
<button class="btn-tool" id="btnLink" title="Link">🔗</button>
<button class="btn-tool" id="btnList" title="List"></button>
<button class="btn-tool" id="btnQuote" title="Quote">"</button>
<button class="btn-tool" id="btnEmoji2" title="Emoji">😀</button>
</div>
<div class="compose-input">
<textarea id="messageInput" placeholder="Type a message..." rows="1"></textarea>
<textarea id="messageInput" placeholder="Type a message... (Markdown supported)" rows="1"></textarea>
<button class="btn-send" id="btnSend">Send</button>
</div>
</div>
@@ -114,6 +120,12 @@
Auto-scroll
</label>
</div>
<div class="setting-item">
<label>
<input type="checkbox" id="chkMarkdown" checked>
Markdown
</label>
</div>
</div>
</aside>
</div>
@@ -156,6 +168,12 @@
<input type="file" id="fileInput" hidden>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/highlight.min.js"></script>
<script type="module">
import { markedHighlight } from 'https://cdn.jsdelivr.net/npm/[email protected]/+esm';
window.markedHighlight = markedHighlight;
</script>
<script src="app.js"></script>
</body>
</html>
+226 -38
View File
@@ -64,7 +64,7 @@ body {
.sidebar-header {
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--border-color));
border-bottom: 1px solid var(--border-color);
flex-shrink: 0;
}
@@ -77,7 +77,7 @@ body {
padding: 0.75rem;
display: flex;
gap: 0.5rem;
border-bottom: 1px solid var(--border-color));
border-bottom: 1px solid var(--border-color);
flex-shrink: 0;
}
@@ -92,6 +92,12 @@ body {
font-size: 1.5rem;
font-weight: 600;
flex-shrink: 0;
cursor: pointer;
transition: transform 0.15s;
}
.avatar:hover {
transform: scale(1.05);
}
.user-info {
@@ -225,6 +231,11 @@ body {
align-items: center;
gap: 0.6rem;
padding: 0.5rem 1rem;
transition: background 0.15s;
}
.user-item:hover {
background: var(--bg-hover);
}
.user-item .avatar {
@@ -235,16 +246,21 @@ body {
.user-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.user-status {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.user-status.online { background: var(--online); }
.user-status.away { background: var(--away); }
.user-status.offline { background: var(--offline); }
.chat-area {
display: flex;
@@ -344,44 +360,146 @@ body {
.message-text {
word-wrap: break-word;
line-height: 1.5;
line-height: 1.6;
}
/* Markdown styling */
.message-text p {
margin-bottom: 0.5rem;
}
.message-text p:last-child {
margin-bottom: 0;
}
.message-text code {
background: var(--bg-tertiary);
padding: 0.1rem 0.3rem;
padding: 0.1rem 0.4rem;
border-radius: 4px;
font-family: monospace;
font-family: 'SF Mono', Monaco, 'Courier New', monospace;
font-size: 0.9em;
}
.message-text strong {
font-weight: 700;
.message-text pre {
background: var(--bg-tertiary);
border-radius: 8px;
padding: 0.75rem;
overflow-x: auto;
margin: 0.5rem 0;
}
.message-text em {
font-style: italic;
.message-text pre code {
background: none;
padding: 0;
font-family: 'SF Mono', Monaco, 'Courier New', monospace;
font-size: 0.9em;
}
/* Fallback syntax colors if highlight.js fails to load */
.message-text .hljs {
background: var(--bg-tertiary);
color: #abb2bf;
}
.message-text .hljs-keyword { color: #c678dd; }
.message-text .hljs-string { color: #98c379; }
.message-text .hljs-number { color: #d19a66; }
.message-text .hljs-function { color: #61afef; }
.message-text .hljs-comment { color: #5c6370; font-style: italic; }
.message-text .hljs-class { color: #e5c07b; }
.message-text .hljs-variable { color: #e06c75; }
.message-text .hljs-built_in { color: #e5c07b; }
.message-text .hljs-literal { color: #56b6c2; }
.message-text .hljs-attr { color: #d19a66; }
.message-text .hljs-property { color: #e06c75; }
.message-text .hljs-title { color: #61afef; }
.message-text .hljs-params { color: #abb2bf; }
.message-text .hljs-bullet { color: #e5c07b; }
.message-text .hljs-code { color: #98c379; }
.message-text .hljs-addition { color: #98c379; background-color: #2d4a3e; }
.message-text .hljs-deletion { color: #e06c75; background-color: #4a2d2d; }
.message-text blockquote {
border-left: 3px solid var(--accent-primary);
padding-left: 0.75rem;
margin: 0.5rem 0;
color: var(--text-secondary);
}
.message-text ul, .message-text ol {
margin: 0.5rem 0;
padding-left: 1.5rem;
}
.message-text li {
margin: 0.25rem 0;
}
.message-text a {
color: var(--accent-secondary);
text-decoration: none;
}
.message-text a:hover {
text-decoration: underline;
}
.message-text img {
max-width: 100%;
border-radius: 8px;
margin: 0.5rem 0;
}
.message-text table {
width: 100%;
border-collapse: collapse;
margin: 0.5rem 0;
}
.message-text th, .message-text td {
border: 1px solid var(--border-color);
padding: 0.4rem 0.6rem;
text-align: left;
}
.message-text th {
background: var(--bg-tertiary);
}
.message-text hr {
border: none;
border-top: 1px solid var(--border-color);
margin: 0.75rem 0;
}
.message-text input[type="checkbox"] {
margin-right: 0.5rem;
}
.message-file {
display: flex;
align-items: center;
gap: 0.5rem;
gap: 0.75rem;
background: var(--bg-tertiary);
padding: 0.5rem;
border-radius: 6px;
margin-top: 0.3rem;
padding: 0.75rem;
border-radius: 8px;
margin-top: 0.5rem;
}
.message-file .file-icon {
font-size: 1.5rem;
font-size: 1.75rem;
}
.message-file .file-info {
flex: 1;
min-width: 0;
}
.message-file .file-name {
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.message-file .file-size {
@@ -389,12 +507,30 @@ body {
color: var(--text-muted);
}
.btn-download {
background: var(--accent-primary);
color: white;
border: none;
padding: 0.35rem 0.75rem;
border-radius: 6px;
cursor: pointer;
font-size: 0.8rem;
font-weight: 500;
transition: background 0.15s;
margin-left: auto;
}
.btn-download:hover {
background: var(--accent-primary-hover);
}
.typing-indicator {
padding: 0.5rem 1rem;
font-size: 0.8rem;
color: var(--text-muted);
font-style: italic;
display: none;
flex-shrink: 0;
}
.typing-indicator.active {
@@ -412,6 +548,7 @@ body {
display: flex;
gap: 0.25rem;
margin-bottom: 0.5rem;
flex-wrap: wrap;
}
.btn-tool {
@@ -426,6 +563,7 @@ body {
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
}
.btn-tool:hover {
@@ -433,6 +571,11 @@ body {
color: var(--text-primary);
}
.btn-tool:active {
background: var(--accent-primary);
color: white;
}
.compose-input {
display: flex;
gap: 0.5rem;
@@ -501,21 +644,37 @@ body {
.file-list {
list-style: none;
max-height: 200px;
overflow-y: auto;
}
.file-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.4rem 0;
padding: 0.5rem;
font-size: 0.85rem;
cursor: pointer;
border-radius: 6px;
transition: background 0.15s;
}
.file-item:hover {
background: var(--bg-hover);
color: var(--accent-secondary);
}
.file-item .file-icon {
font-size: 1.2rem;
}
.file-item .file-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.setting-item {
margin-bottom: 0.5rem;
}
@@ -531,6 +690,7 @@ body {
.setting-item input[type="checkbox"] {
width: 16px;
height: 16px;
accent-color: var(--accent-primary);
}
.modal {
@@ -556,6 +716,18 @@ body {
padding: 1.5rem;
width: 100%;
max-width: 400px;
animation: modalIn 0.2s ease;
}
@keyframes modalIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
.modal-content h3 {
@@ -625,8 +797,8 @@ body {
.toast-container {
position: fixed;
bottom: 2rem;
right: 2rem;
bottom: 2.5rem;
right: 1rem;
z-index: 1001;
display: flex;
flex-direction: column;
@@ -642,6 +814,7 @@ body {
align-items: center;
gap: 0.5rem;
animation: slideIn 0.3s ease;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
.toast.success { border-left: 3px solid var(--success); }
@@ -697,25 +870,22 @@ body {
padding: 0.5rem;
}
.message-input-container {
display: flex;
align-items: center;
gap: 0.5rem;
background: var(--bg-tertiary);
border-radius: 8px;
padding: 0.25rem;
}
.emoji-picker {
position: absolute;
bottom: 100%;
left: 0;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
border-radius: 12px;
padding: 0.5rem;
display: grid;
grid-template-columns: repeat(8, 1fr);
gap: 0.25rem;
max-height: 250px;
overflow-y: auto;
z-index: 1000;
box-shadow: 0 -4px 20px rgba(0,0,0,0.4);
margin-bottom: 0.5rem;
}
.emoji {
@@ -725,12 +895,14 @@ body {
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 4px;
font-size: 1.2rem;
border-radius: 6px;
font-size: 1.25rem;
transition: transform 0.1s, background 0.1s;
}
.emoji:hover {
background: var(--bg-hover);
transform: scale(1.2);
}
textarea {
@@ -739,6 +911,31 @@ textarea {
max-height: 120px;
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-primary);
}
::-webkit-scrollbar-thumb {
background: var(--bg-tertiary);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--bg-hover);
}
/* Highlight.js theme adjustments */
.hljs {
background: transparent !important;
padding: 0 !important;
}
@media (max-width: 900px) {
.app-container {
grid-template-columns: 200px 1fr;
@@ -789,12 +986,3 @@ textarea {
padding: 0.5rem;
}
}
@media (max-width: 600px) {
.app-container {
grid-template-columns: 1fr;
}
.sidebar {
display: none;
}
}