update
This commit is contained in:
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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"></></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"></></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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user