From 35c2bad3b7a9d872b7e231b93d32c9511f2e4a32 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Tue, 17 Feb 2026 01:03:52 +0000 Subject: [PATCH] final v0.8 --- assets/css/discord.css | 77 ++++++++++++++++++++++ assets/js/main.js | 145 +++++++++++++++++++++++++++++++++++++---- index.php | 126 ++++++++++++++++++++++++++++------- requests.log | 17 +++++ 4 files changed, 328 insertions(+), 37 deletions(-) diff --git a/assets/css/discord.css b/assets/css/discord.css index f82ec99..767fa65 100644 --- a/assets/css/discord.css +++ b/assets/css/discord.css @@ -623,6 +623,10 @@ body { width: 100%; outline: none; font-size: 1em; + resize: none; + height: 24px; + line-height: 24px; + overflow-y: hidden; } /* Members Sidebar */ @@ -1257,3 +1261,76 @@ body { } + +/* Markdown Styles */ +.message-text code { + background-color: #1e1f22; + padding: 0.2rem 0.4rem; + border-radius: 4px; + font-family: 'Consolas', 'Monaco', 'Andale Mono', 'Ubuntu Mono', monospace; + font-size: 0.85em; + color: #dbdee1; +} + +.message-text pre.code-block { + background-color: #1e1f22; + padding: 1rem; + border-radius: 4px; + margin: 0.5rem 0; + font-family: 'Consolas', 'Monaco', 'Andale Mono', 'Ubuntu Mono', monospace; + font-size: 0.85em; + overflow-x: auto; +} + +.message-text blockquote { + border-left: 4px solid #4e5058; + padding-left: 1rem; + margin: 0.5rem 0; + color: var(--text-muted); +} + +.message-text .spoiler { + background-color: #1e1f22; + color: transparent !important; + border-radius: 3px; + cursor: pointer; + transition: background-color 0.2s, color 0.2s; +} + +.message-text .spoiler:hover { + background-color: #2b2d31; +} + +.message-text .spoiler.revealed { + background-color: rgba(255, 255, 255, 0.1); + color: inherit !important; + cursor: default; +} + +.message-text h1 { font-size: 1.5em; font-weight: 700; margin: 4px 0 2px 0; color: #fff; } +.message-text h2 { font-size: 1.25em; font-weight: 600; margin: 3px 0 1px 0; color: #fff; } +.message-text h3 { font-size: 1.1em; font-weight: 600; margin: 2px 0 0 0; color: #fff; } + +[data-theme="light"] .message-text code { + background-color: #ebedef; + color: #313338; +} + +[data-theme="light"] .message-text pre.code-block { + background-color: #ebedef; + color: #313338; +} + +[data-theme="light"] .message-text .spoiler { + background-color: #dbdee1; +} + +[data-theme="light"] .message-text .spoiler.revealed { + background-color: #f2f3f5; +} + +[data-theme="light"] .message-text h1, +[data-theme="light"] .message-text h2, +[data-theme="light"] .message-text h3 { + color: #313338; +} diff --git a/assets/js/main.js b/assets/js/main.js index 5020da2..04e2943 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -402,6 +402,7 @@ document.addEventListener('DOMContentLoaded', () => { if (chatInput) { chatInput.value += emoji; chatInput.focus(); + chatInput.dispatchEvent(new Event('input')); } }, { keepOpen: true, width: "900px", height: "500px" }); return; @@ -495,8 +496,12 @@ document.addEventListener('DOMContentLoaded', () => { } else if (msg.type === 'reaction') { updateReactionUI(msg.message_id, msg.reactions); } else if (msg.type === 'message_edit') { - const el = document.querySelector(`.message-item[data-id="${msg.message_id}"] .message-text`); - if (el) el.innerHTML = msg.content.replace(/\n/g, '
'); + const item = document.querySelector(`.message-item[data-id="${msg.message_id}"]`); + if (item) { + item.dataset.rawContent = msg.content; + const el = item.querySelector('.message-text'); + if (el) el.innerHTML = parseCustomEmotes(msg.content); + } } else if (msg.type === 'message_delete') { document.querySelector(`.message-item[data-id="${msg.message_id}"]`)?.remove(); } else if (msg.type === 'presence') { @@ -549,7 +554,22 @@ document.addEventListener('DOMContentLoaded', () => { }, 3000); } + chatInput?.addEventListener('keydown', (e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + chatForm?.dispatchEvent(new Event('submit', { cancelable: true })); + } + }); + chatInput?.addEventListener('input', () => { + chatInput.style.height = 'auto'; + chatInput.style.height = Math.min(chatInput.scrollHeight, 200) + 'px'; + if (chatInput.scrollHeight > 200) { + chatInput.style.overflowY = 'auto'; + } else { + chatInput.style.overflowY = 'hidden'; + } + if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'typing', @@ -567,6 +587,8 @@ document.addEventListener('DOMContentLoaded', () => { if (!content && !file) return; chatInput.value = ''; + chatInput.style.height = '24px'; + chatInput.style.overflowY = 'hidden'; const formData = new FormData(); formData.append('content', content); formData.append('channel_id', currentChannel); @@ -732,19 +754,32 @@ document.addEventListener('DOMContentLoaded', () => { const msgId = editBtn.dataset.id; const msgItem = editBtn.closest('.message-item'); const textEl = msgItem.querySelector('.message-text'); - const originalContent = textEl.innerText; + const originalContent = msgItem.dataset.rawContent || textEl.innerText; - const input = document.createElement('input'); - input.type = 'text'; + const input = document.createElement('textarea'); input.className = 'form-control bg-dark text-white'; + input.style.resize = 'none'; + input.style.overflowY = 'hidden'; + input.rows = 1; input.value = originalContent; textEl.innerHTML = ''; textEl.appendChild(input); + + const resizeInput = () => { + input.style.height = 'auto'; + input.style.height = Math.min(input.scrollHeight, 200) + 'px'; + input.style.overflowY = input.scrollHeight > 200 ? 'auto' : 'hidden'; + }; + + input.addEventListener('input', resizeInput); + setTimeout(resizeInput, 0); input.focus(); + input.setSelectionRange(input.value.length, input.value.length); input.onkeydown = async (ev) => { - if (ev.key === 'Enter') { + if (ev.key === 'Enter' && !ev.shiftKey) { + ev.preventDefault(); const newContent = input.value.trim(); if (newContent && newContent !== originalContent) { const resp = await fetch('api_v1_messages.php', { @@ -753,14 +788,15 @@ document.addEventListener('DOMContentLoaded', () => { body: JSON.stringify({ id: msgId, content: newContent }) }); if ((await resp.json()).success) { - textEl.innerHTML = newContent.replace(/\n/g, '
'); + textEl.innerHTML = parseCustomEmotes(newContent); + msgItem.dataset.rawContent = newContent; ws?.send(JSON.stringify({ type: 'message_edit', message_id: msgId, content: newContent })); } } else { - textEl.innerHTML = originalContent.replace(/\n/g, '
'); + textEl.innerHTML = parseCustomEmotes(originalContent); } } else if (ev.key === 'Escape') { - textEl.innerHTML = originalContent.replace(/\n/g, '
'); + textEl.innerHTML = parseCustomEmotes(originalContent); } }; return; @@ -814,6 +850,8 @@ document.addEventListener('DOMContentLoaded', () => { data.messages.forEach(msg => { const div = document.createElement('div'); div.className = 'message-item p-2 border-bottom border-secondary'; + div.dataset.id = msg.id; + div.dataset.rawContent = msg.content; div.style.backgroundColor = 'transparent'; const authorStyle = msg.role_color ? `color: ${msg.role_color};` : ''; div.innerHTML = ` @@ -826,7 +864,7 @@ document.addEventListener('DOMContentLoaded', () => { ${msg.time}
- ${escapeHTML(msg.content).replace(/\n/g, '
')} + ${parseCustomEmotes(msg.content)}
@@ -966,8 +1004,8 @@ document.addEventListener('DOMContentLoaded', () => { } else { item.innerHTML = `
-
${res.username}
-
${res.content}
+
${escapeHTML(res.username)}
+
${parseCustomEmotes(res.content)}
`; } @@ -2375,15 +2413,93 @@ document.addEventListener('DOMContentLoaded', () => { // User Settings - Save handled in index.php function escapeHTML(str) { + if (!str) return ""; const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } + function parseMarkdown(text) { + if (!text) return ""; + + // Escape HTML first + let html = escapeHTML(text); + + // Code blocks: ```language\ncontent``` + const codeBlocks = []; + html = html.replace(/```(?:(\w+)\n)?([\s\S]*?)```/g, (match, lang, content) => { + const placeholder = `__CODE_BLOCK_${codeBlocks.length}__`; + codeBlocks.push(`
${content}
`); + return placeholder; + }); + + // Inline code: `content` + const inlineCodes = []; + html = html.replace(/`([^`\n]+)`/g, (match, content) => { + const placeholder = `__INLINE_CODE_${inlineCodes.length}__`; + inlineCodes.push(`${content}`); + return placeholder; + }); + + // Bold: **text** + html = html.replace(/\*\*([^*]+)\*\*/g, '$1'); + + // Italics: *text* or _text_ + html = html.replace(/\*([^*]+)\*/g, '$1'); + html = html.replace(/_([^_]+)_/g, '$1'); + + // Underline: __text__ + html = html.replace(/__([^_]+)__/g, '$1'); + + // Strikethrough: ~~text~~ + html = html.replace(/~~([^~]+)~~/g, '$1'); + + // Spoiler: ||text|| + html = html.replace(/\|\|([^|]+)\|\|/g, '$1'); + + // Headers: # H1, ## H2, ### H3 (must be at start of line) + html = html.replace(/^# (.*$)/gm, '

$1

'); + html = html.replace(/^## (.*$)/gm, '

$1

'); + html = html.replace(/^### (.*$)/gm, '

$1

'); + + // Subtext: -# text (must be at start of line) + html = html.replace(/^-# (.*$)/gm, '$1'); + + // Blockquotes: > text or >>> text (must be at start of line) + html = html.replace(/^> (.*$)/gm, '
$1
'); + html = html.replace(/^>>> ([\s\S]*$)/g, '
$1
'); + + // Hyperlinks: [text](url) + html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); + + // Pure links: + html = html.replace(/<(https?:\/\/[^&]+)>/g, '$1'); + + // Newlines to
(only those not inside placeholders) + html = html.replace(/\n/g, '
'); + + // Remove extra space around headers and blockquotes added by nl2br + html = html.replace(/(
)\s*(

|

|

|
)/gi, '$2'); + html = html.replace(/(<\/h1>|<\/h2>|<\/h3>|<\/blockquote>)\s*(
)/gi, '$1'); + + // Re-insert inline code + inlineCodes.forEach((code, i) => { + html = html.replace(`__INLINE_CODE_${i}__`, code); + }); + + // Re-insert code blocks + codeBlocks.forEach((block, i) => { + html = html.replace(`__CODE_BLOCK_${i}__`, block); + }); + + return html; + } + function parseCustomEmotes(text) { - let parsed = escapeHTML(text); + let parsed = parseMarkdown(text); (window.CUSTOM_EMOTES_CACHE || []).forEach(emote => { const imgHtml = `${emote.name}`; + // Only replace if it's not inside a tag attribute or code block (simplified) parsed = parsed.split(emote.code).join(imgHtml); }); return parsed; @@ -2409,6 +2525,7 @@ document.addEventListener('DOMContentLoaded', () => { const div = document.createElement('div'); div.className = 'message-item'; div.dataset.id = msg.id; + div.dataset.rawContent = msg.content; if (parseInt(msg.id) > lastMessageId) { lastMessageId = parseInt(msg.id); @@ -2483,7 +2600,7 @@ document.addEventListener('DOMContentLoaded', () => { ${msg.timestamp || 'Just now'} ${pinnedBadge} -
${contentWithMentions.replace(/\n/g, '
')}
+
${contentWithMentions}
${attachmentHtml} ${embedHtml}
diff --git a/index.php b/index.php index b263d37..e88629e 100644 --- a/index.php +++ b/index.php @@ -28,8 +28,88 @@ function renderRoleIcon($icon, $size = '14px') { } } +// Helper to parse markdown in content +function parse_markdown($text) { + if (empty($text)) return ""; + + // First escape HTML + $html = htmlspecialchars($text); + + // Code blocks: ```language\ncontent``` + $code_blocks = []; + $html = preg_replace_callback('/```(?:(\w+)\n)?([\s\S]*?)```/', function($matches) use (&$code_blocks) { + $lang = $matches[1] ?? 'text'; + $content = $matches[2]; + $placeholder = "__CODE_BLOCK_" . count($code_blocks) . "__"; + $code_blocks[] = '
' . $content . '
'; + return $placeholder; + }, $html); + + // Inline code: `content` + $inline_codes = []; + $html = preg_replace_callback('/`([^`\n]+)`/', function($matches) use (&$inline_codes) { + $content = $matches[1]; + $placeholder = "__INLINE_CODE_" . count($inline_codes) . "__"; + $inline_codes[] = '' . $content . ''; + return $placeholder; + }, $html); + + // Bold: **text** + $html = preg_replace('/\*\*([^*]+)\*\*/', '$1', $html); + + // Italics: *text* or _text_ + $html = preg_replace('/\*([^*]+)\*/', '$1', $html); + $html = preg_replace('/_([^_]+)_/', '$1', $html); + + // Underline: __text__ + $html = preg_replace('/__([^_]+)__/', '$1', $html); + + // Strikethrough: ~~text~~ + $html = preg_replace('/~~([^~]+)~~/', '$1', $html); + + // Spoiler: ||text|| + $html = preg_replace('/\|\|([^|]+)\|\|/', '$1', $html); + + // Headers: # H1, ## H2, ### H3 (must be at start of line) + $html = preg_replace('/^# (.*$)/m', '

$1

', $html); + $html = preg_replace('/^## (.*$)/m', '

$1

', $html); + $html = preg_replace('/^### (.*$)/m', '

$1

', $html); + + // Subtext: -# text (must be at start of line) + $html = preg_replace('/^-# (.*$)/m', '$1', $html); + + // Blockquotes: > text or >>> text + $html = preg_replace('/^> (.*$)/m', '
$1
', $html); + $html = preg_replace('/^>>> ([\s\S]*$)/', '
$1
', $html); + + // Hyperlinks: [text](url) + $html = preg_replace('/\[([^\]]+)\]\(([^)]+)\)/', '$1', $html); + + // Pure links: + $html = preg_replace('/<(https?:\/\/[^&]+)>/', '$1', $html); + + // Newlines to
(only those not inside placeholders) + $html = nl2br($html); + + // Remove extra space around headers and blockquotes added by nl2br + $html = preg_replace('/(\s*)\s*(|
)/i', '$2', $html); + $html = preg_replace('/(<\/h[1-3]>|<\/blockquote>)\s*(\s*)/i', '$1', $html); + + // Re-insert inline code + foreach ($inline_codes as $i => $code) { + $html = str_replace("__INLINE_CODE_$i" . "__", $code, $html); + } + + // Re-insert code blocks + foreach ($code_blocks as $i => $block) { + $html = str_replace("__CODE_BLOCK_$i" . "__", $block, $html); + } + + return $html; +} + // Helper to parse emotes in content -function parse_emotes($content) { +function parse_emotes($content, $username_to_mention = null) { static $custom_emotes_cache; if ($custom_emotes_cache === null) { try { @@ -39,7 +119,14 @@ function parse_emotes($content) { } } - $result = htmlspecialchars($content); + $result = parse_markdown($content); + + // Parse mentions if username provided + if ($username_to_mention) { + $mention_pattern = '/@' . preg_quote($username_to_mention, '/') . '\b/'; + $result = preg_replace($mention_pattern, '@' . htmlspecialchars($username_to_mention) . '', $result); + } + foreach ($custom_emotes_cache as $ce) { $emote_html = '' . htmlspecialchars($ce['name']) . ''; $result = str_replace($ce['code'], $emote_html, $result); @@ -590,7 +677,7 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? ''; $is_solution = ($active_thread['solution_message_id'] == $m['id']); ?> -
+
">
"> @@ -606,10 +693,18 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? ''; + + + + + + + +
- +
">
. - +
@@ -814,7 +909,7 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? ''; $mention_pattern = '/@' . preg_quote($user['username'], '/') . '\b/'; $is_mentioned = preg_match($mention_pattern, $m['content']); ?> -
+
">
@@ -831,22 +926,7 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
- @' . htmlspecialchars($user['username']) . '', $msg_content); - - // Custom Emotes parsing - static $custom_emotes_cache; - if ($custom_emotes_cache === null) { - $custom_emotes_cache = db()->query("SELECT name, path, code FROM custom_emotes")->fetchAll(); - } - foreach ($custom_emotes_cache as $ce) { -$emote_html = '' . htmlspecialchars($ce['name']) . ''; - $msg_content = str_replace($ce['code'], $emote_html, $msg_content); - } - - echo nl2br($msg_content); - ?> +
- + diff --git a/requests.log b/requests.log index f115ee3..a2a0bca 100644 --- a/requests.log +++ b/requests.log @@ -424,3 +424,20 @@ 2026-02-17 00:34:40 - GET /index.php?server_id=1 - POST: [] 2026-02-17 00:34:45 - GET /index.php?server_id=1&channel_id=20 - POST: [] 2026-02-17 00:35:23 - GET /index.php?server_id=1&channel_id=20 - POST: [] +2026-02-17 00:43:23 - - POST: [] +2026-02-17 00:44:01 - GET /index.php - POST: [] +2026-02-17 00:44:15 - GET /?fl_project=38443 - POST: [] +2026-02-17 00:45:12 - GET /index.php?server_id=1&channel_id=1 - POST: [] +2026-02-17 00:45:14 - GET /index.php?server_id=1&channel_id=20 - POST: [] +2026-02-17 00:45:21 - GET /index.php?server_id=1&channel_id=15 - POST: [] +2026-02-17 00:45:23 - GET /index.php?server_id=1&channel_id=6 - POST: [] +2026-02-17 00:48:04 - GET / - POST: [] +2026-02-17 00:48:39 - GET /?fl_project=38443 - POST: [] +2026-02-17 00:48:50 - GET /index.php?server_id=1&channel_id=6 - POST: [] +2026-02-17 00:57:46 - GET / - POST: [] +2026-02-17 00:58:08 - GET /?fl_project=38443 - POST: [] +2026-02-17 00:58:11 - GET /index.php?server_id=1&channel_id=6 - POST: [] +2026-02-17 00:58:14 - GET /index.php?server_id=1&channel_id=6 - POST: [] +2026-02-17 01:02:05 - GET / - POST: [] +2026-02-17 01:02:20 - GET /?fl_project=38443 - POST: [] +2026-02-17 01:02:36 - GET /index.php?server_id=1&channel_id=6 - POST: []