Skip to content

v0.2.0#2

Merged
NellowTCS merged 15 commits into
mainfrom
v0.2.0
Sep 18, 2025
Merged

v0.2.0#2
NellowTCS merged 15 commits into
mainfrom
v0.2.0

Conversation

@NellowTCS
Copy link
Copy Markdown
Member

@NellowTCS NellowTCS commented Sep 18, 2025

HTMLChat v0.2.0

Highlights

  • Settings modal with toggles:
    • Desktop Notifications, Notification Sounds, Message Sounds
    • Open via the new Settings button in the header
  • Reload chats button next to “Export” to fetch latest messages on demand
  • Lucide icons via npm (no CDNs); programmatic SVG creation
  • Async, non-blocking Search with modal UI
  • File upload support and modal
  • Moderator tools (delete messages)
  • Better UI polish (including modal/toggle styles)
  • Sound toggle fixed and made persistent

Improvements

  • Modularized codebase (Message rendering, Notifications, File upload, PMs, Moderator, etc.)
  • Markdown rendering and sanitization using npm (marked + DOMPurify)
  • Vite + npm project setup and dev workflow
  • Notification UX upgraded (banner → settings modal)

Fixes

  • Sound toggle bug: visual state + actual mute state now consistent
  • Reply formatting corrected
  • Prevented infinite loop on file uploads

Build & Deploy

  • Build with Vite: Build/ project (npm run build creates Build/dist)
  • GitHub Actions deploys Build/dist to gh-pages using peaceiris/actions-gh-pages@v4

Breaking Changes

  • CDN scripts removed; all libs now via npm (lucide, marked, DOMPurify)
  • Icon usage migrated to programmatic creation (no global createIcons, no data-lucide reliance)
  • If embedding, serve the compiled assets from Build/dist

Local Dev

  • From Build/:
    • npm ci
    • npm run dev (Vite dev server)
    • npm run build for production

Notes

  • Desktop notifications require user permission (prompt available in Settings)
  • Minor visual polish to modals and toggles; feedback welcome

Summary by CodeRabbit

  • New Features

    • Adds a Vite-built browser chat app with private message windows, file uploads (drag/drop + previews), threaded replies, fast in-app search, desktop notifications & sounds, context menus, moderator tools, improved message rendering, and chat export.
  • Backend

    • Adds file upload/serving endpoints, private messaging and moderation APIs, enhanced moderation (ban/kick/delete), CORS handling, and a health check.
  • Documentation

    • README bumped to v0.2.0 with build/run instructions and a release checklist.
  • Chores

    • CI now builds and publishes browser assets to GitHub Pages with reduced token permissions and controlled concurrency.

@NellowTCS NellowTCS self-assigned this Sep 18, 2025
@NellowTCS NellowTCS added the enhancement New feature or request label Sep 18, 2025
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Sep 18, 2025

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
htmlchat 3208a14 Sep 18 2025, 08:04 AM

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Sep 18, 2025

Walkthrough

Adds a Vite-built frontend under Build/ (full chat UI, PMs, uploads, search, notifications, moderation), replaces the legacy single-file client with the Build app, updates GitHub Pages workflow to build & publish Build/dist, and extends the Worker backend with routing, uploads, file serving, CORS, moderation, and private messaging.

Changes

Cohort / File(s) Summary of Changes
CI/CD: GitHub Pages workflow
​.github/workflows/static.yml
Renames workflow, reduces permissions, adds concurrency group, removes environment block, runs Node.js 20 → npm cinpm run build in Build/, and deploys Build/dist via peaceiris/actions-gh-pages@v4.
Frontend entry & styles
Build/index.html, Build/src/styles.css
Adds new HTML entry and comprehensive stylesheet for the HTMLChat UI: layout, modals, PM windows, context menu, notifications, audio elements, and responsive styling.
Frontend app & managers
Build/src/main.js, Build/src/messageRenderer.js, Build/src/notifications.js, Build/src/contextMenu.js, Build/src/fileUpload.js, Build/src/privateMessages.js, Build/src/search.js, Build/src/soundManager.js, Build/src/moderatorTools.js
Introduces HTMLChatApp orchestration and modular manager classes for rendering, notifications, context menu actions, file upload flow, private messaging UI, async search, sound control, and local moderator tooling (ban/kick/mod management).
Build tooling & metadata
Build/package.json, Build/vite.config.js, Build/.gitignore, README.md, TODO.md
Adds package manifest (Vite scripts/deps), Vite config, Build/.gitignore, updates README (v0.2.0) describing Build-based frontend, and adds TODO checklist.
Backend routing, uploads & moderation
Worker/src/index.js, Worker/src/chatRoom.js
Adds CORS/OPTIONS handling; explicit routing for /chat, /pm, /mod, /upload, /files, /health; file upload processing with FILE_BUCKET storage and fallback; file serving with proper headers; moderation APIs (ban/unban/kick/add/remove moderator), message deletion, PM handling, presence guards, and moderation enforcement.
Config formatting
Worker/wrangler.toml
Minor formatting change (removed blank line) in v1 migrations; no behavioral change.
Legacy removal
index.html
Deletes previous root single-file client (legacy UI, embedded logic and scripts).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User
  participant FE as HTMLChat (Build)
  participant BE as Worker API
  participant DO as ChatRoom (Durable)

  rect rgb(240,248,255)
  note right of FE: App load & initial fetch
  U->>FE: Open app
  FE->>BE: GET /chat/{room}
  BE->>DO: Forward request
  DO-->>BE: Messages + users + moderator data
  BE-->>FE: JSON response
  FE->>FE: Render messages/users
  end
Loading
sequenceDiagram
  autonumber
  participant U as User
  participant FE as HTMLChat
  participant BE as Worker API
  participant FB as FILE_BUCKET

  rect rgb(255,250,240)
  note right of FE: File upload flow
  U->>FE: Choose/Drop file(s)
  FE->>BE: POST /upload (FormData)
  BE->>BE: Validate size/type
  alt Stored in bucket
    BE->>FB: PUT object
    FB-->>BE: Stored, URL
    BE-->>FE: {url, filename, ...}
  else Fallback (inline)
    BE-->>FE: {dataURL,...}
  end
  FE->>BE: POST /chat/{room} (FILE message)
  BE-->>FE: OK
  FE->>FE: Render attachment
  end
Loading
sequenceDiagram
  autonumber
  participant M as Moderator
  participant FE as HTMLChat
  participant BE as Worker API
  participant DO as ChatRoom

  rect rgb(245,245,255)
  note over FE,DO: Moderation action (ban/kick/delete)
  M->>FE: Trigger action via context menu
  FE->>BE: POST /mod/{room} {action,target,...}
  BE->>DO: Authorize and apply
  alt Authorized
    DO-->>BE: Success
    BE-->>FE: OK
    FE->>BE: GET /chat/{room}
    BE-->>FE: Updated data
  else Forbidden
    DO-->>BE: 403
    BE-->>FE: Error
  end
  end
Loading
sequenceDiagram
  autonumber
  participant A as User A
  participant FE as HTMLChat
  participant BE as Worker API
  participant PM as PM Durable

  rect rgb(250,245,255)
  note right of FE: Private messaging
  A->>FE: Open PM window
  FE->>BE: GET /pm/{conversation}
  BE->>PM: Fetch history
  PM-->>BE: Messages
  BE-->>FE: History
  A->>FE: Send PM
  FE->>BE: POST /pm/{conversation} {to,text}
  BE->>PM: Append message
  PM-->>BE: OK
  BE-->>FE: OK
  FE->>FE: Refresh PM view
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

I thump my paws—v0.2 takes flight,
New burrows built with Vite at night.
Mods patrol, PM windows gleam,
Files upload and searches stream.
Bells ring, pages publish—hop, deploy delight! 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "v0.2.0" is a concise version-bump label that directly corresponds to this release-style changeset (new v0.2.0 frontend features, modularization, Vite build, and GH Pages deploy), so it is related to the main change. It is short and unambiguous about being a release, though it does not describe the key feature highlights.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch v0.2.0

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a911b5d and 3208a14.

📒 Files selected for processing (5)
  • Build/src/contextMenu.js (1 hunks)
  • Build/src/main.js (1 hunks)
  • Build/src/messageRenderer.js (1 hunks)
  • Build/src/privateMessages.js (1 hunks)
  • Build/src/search.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • Build/src/contextMenu.js
  • Build/src/search.js
  • Build/src/messageRenderer.js
  • Build/src/privateMessages.js
  • Build/src/main.js
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Workers Builds: htmlchat

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@NellowTCS
Copy link
Copy Markdown
Member Author

i'm calling it that i'll be yelled at for either XSS or the Open Server

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 21

🧹 Nitpick comments (42)
Build/package.json (1)

1-32: Mark package private and move Vite to devDependencies.

Prevents accidental publish and avoids shipping dev-only deps.

Apply this diff:

 {
   "name": "htmlchat",
   "version": "0.2.0",
   "description": "A retro-styled, browser-only chat client.",
-  "main": "index.js",
+  "main": "index.js",
+  "private": true,
   "scripts": {
     "dev": "vite",
     "build": "vite build",
     "preview": "vite preview"
   },
@@
   "homepage": "https://github.com/HTMLToolkit/HTMLChat#readme",
-  "dependencies": {
+  "dependencies": {
     "dompurify": "^3.2.6",
     "lucide": "^0.544.0",
-    "marked": "^16.3.0",
-    "vite": "^7.1.5"
-  }
+    "marked": "^16.3.0"
+  },
+  "devDependencies": {
+    "vite": "^7.1.5"
+  }
 }
TODO.md (1)

1-26: Add trailing newline.

Keeps tooling happy and avoids “No newline at end of file” diffs.

Apply this diff:

- - [X] Better UI
+ - [X] Better UI
+
README.md (1)

20-26: Fix markdownlint issues (dash vs asterisk, tabs, indent).

Swap dashes to asterisks for the indicated lists and replace tabs with two spaces under “Running it”.

Apply this diff:

- - Export chat logs as JSON (plus a handy Reload button next to Export)
- - File uploads (images/docs) with previews
- - Replies (click to reply, threaded context)
- - Search (fast, non-blocking)
- - Moderator tools (delete/ban)
- - Settings modal (desktop notifications + sounds toggles)
- - Lucide icons via npm (no CDN, crisp SVGs)
+ * Export chat logs as JSON (plus a handy Reload button next to Export)
+ * File uploads (images/docs) with previews
+ * Replies (click to reply, threaded context)
+ * Search (fast, non-blocking)
+ * Moderator tools (delete/ban)
+ * Settings modal (desktop notifications + sounds toggles)
+ * Lucide icons via npm (no CDN, crisp SVGs)
@@
-- Frontend lives in `Build/` and uses Vite.  
-	- Dev: `cd Build && npm install && npm run dev`  
-	- Build: `cd Build && npm install && npm run build`  
+- Frontend lives in `Build/` and uses Vite.  
+  - Dev: `cd Build && npm ci && npm run dev`  
+  - Build: `cd Build && npm ci && npm run build`  

Also applies to: 42-45

Build/src/styles.css (2)

69-71: Remove duplicate CSS blocks (confusing overrides).

.header-btn.muted, .permission-status/#permission-status-text, and .small-btn are defined twice. Keep one source of truth.

Apply this diff to drop the later duplicates:

-/* Sound toggle states */
-.header-btn.muted {
-  opacity: 0.5;
-}
-
-/* Permission Status */
-.permission-status {
-  display: flex;
-  align-items: center;
-  gap: 8px;
-  font-size: 14px;
-}
-
-#permission-status-text {
-  font-weight: 500;
-  padding: 2px 8px;
-  border-radius: 4px;
-  font-size: 12px;
-}
-
-#permission-status-text.granted {
-  background-color: #d4edda;
-  color: #155724;
-}
-
-#permission-status-text.denied {
-  background-color: #f8d7da;
-  color: #721c24;
-}
-
-#permission-status-text.default {
-  background-color: #fff3cd;
-  color: #856404;
-}
-
-.small-btn {
-  padding: 4px 8px;
-  font-size: 12px;
-  background: #007bff;
-  color: white;
-  border: none;
-  border-radius: 4px;
-  cursor: pointer;
-}
-
-.small-btn:hover {
-  background: #0056b3;
-}

Also applies to: 855-857, 931-949, 1021-1049, 950-960, 1051-1063


1-1063: Add reduced‑motion support (a11y).

Respect users’ motion preferences for transitions/animations.

Apply this diff:

+@media (prefers-reduced-motion: reduce) {
+  * {
+    animation-duration: 0.01ms !important;
+    animation-iteration-count: 1 !important;
+    transition-duration: 0.01ms !important;
+    scroll-behavior: auto !important;
+  }
+}
.github/workflows/static.yml (1)

26-46: CI: build on PRs too; keep deploy on main.

Add a PR build job (no deploy) to catch breaks early.

Suggested snippet (separate job):

on:
  push:
    branches: ["main"]
  pull_request:
    branches: ["main"]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: npm
          cache-dependency-path: Build/package-lock.json
      - name: Install
        working-directory: Build
        run: npm ci
      - name: Build
        working-directory: Build
        run: npm run build

  deploy:
    if: github.ref == 'refs/heads/main'
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      # ... keep existing deploy steps
Build/src/messageRenderer.js (5)

96-101: Don’t allow arbitrary style attributes from user content.

Allowing style increases XSS surface. Let CSS handle presentation; drop style from ALLOWED_ATTR.

-      ALLOWED_ATTR: ['href','src','alt','title','target','style']
+      ALLOWED_ATTR: ['href','src','alt','title','target']

158-160: Duplicate class append.

system class is appended twice.

-      if (message.system) messageClass += ' system';
-      if (message.system) messageClass += ' system';
+      if (message.system) messageClass += ' system';

83-89: Unify reply header regex.

processText requires numeric id; renderMessages accepts any. Pick one shape (prefer the latter) so replies parse consistently.

-    const replyMatch = text.match(/^@reply:(\d+):([^:]+):\s*(.*)/);
+    const replyMatch = text.match(/^@reply:([^:]+):([^:]+):\s*(.*)/);

Also applies to: 124-133


42-55: Remove debug logging.

Leaky console logs for user content/ids.

-    console.log('Reply parsed:', { messageId, replyUser, messageText }); // Debug
+    // (debug removed)

Also applies to: 132-133


104-104: Safari lookbehind compatibility.

Negative lookbehind can still bite on older Safari. Consider a DOM walker or a safer URL linker (e.g., marked’s linkify plugin) instead of regex.

Build/src/contextMenu.js (4)

10-16: Guard when #context-menu is missing.

Avoid NPEs if the menu element isn’t present.

   setupEventListeners() {
+    if (!this.menu) {
+      console.warn('Context menu element #context-menu not found');
+      return;
+    }

19-24: Use event delegation with closest() for clicks.

Clicking icon/text inside a menu item won’t bubble a dataset on target.

-    this.menu.addEventListener('click', (e) => {
-      const action = e.target.dataset.action;
+    this.menu.addEventListener('click', (e) => {
+      const el = e.target.closest('[data-action]');
+      const action = el && el.dataset.action;
       if (action && this.currentMessage) {
         this.handleAction(action);
       }
     });

35-76: Position reliably: measure after making the menu renderable.

getBoundingClientRect on display:none returns 0,0. Briefly show invisibly to measure.

   show(event, messageElement) {
     event.preventDefault();
@@
-    const rect = this.menu.getBoundingClientRect();
+    this.menu.style.visibility = 'hidden';
+    this.menu.style.display = 'block';
+    const rect = this.menu.getBoundingClientRect();
@@
-    this.menu.style.display = 'block';
+    this.menu.style.visibility = 'visible';

237-251: Validate ban duration and handle “0” correctly.

Current logic treats "0" as empty and allows NaN through.

-    const reason = prompt(`Ban ${this.currentMessage.user}? Enter reason (optional):`);
-    if (reason !== null) {
-      const duration = prompt('Ban duration (minutes, or leave empty for permanent):');
+    const reason = prompt(`Ban ${this.currentMessage.user}? Enter reason (optional):`);
+    if (reason !== null) {
+      const durationStr = prompt('Ban duration (minutes, or leave empty for permanent):');
+      let duration = null;
+      if (durationStr !== null && durationStr.trim() !== '') {
+        const parsed = parseInt(durationStr, 10);
+        if (Number.isNaN(parsed) || parsed < 0) {
+          alert('Invalid duration');
+          return;
+        }
+        duration = parsed;
+      }
@@
-            duration: duration ? parseInt(duration) : null
+            duration
Build/src/fileUpload.js (1)

118-145: Optional: add input accept attribute + dedupe by lastModified.

Small UX + correctness wins.

Would you like a follow-up diff that sets fileInput.accept to allowed types and also checks lastModified when deduping?

Build/src/privateMessages.js (1)

7-9: Ensure PM container exists.

Guard against null to avoid runtime errors on construction.

Add:

if (!this.windowContainer) console.warn('#pm-windows not found');
Build/src/soundManager.js (1)

50-55: Trim debug log.

Reduce console noise; UI already reflects state.

   toggleSounds() {
     this.soundsEnabled = !this.soundsEnabled;
     this.saveSetting('sounds_enabled', this.soundsEnabled);
-    console.log('Sound manager toggled to:', this.soundsEnabled); // Debug
     return this.soundsEnabled;
   }
Worker/src/index.js (4)

10-20: CORS preflight: echo request headers and add max-age.

Preflight currently allows only Content-Type. Echoing Access-Control-Request-Headers and adding Access-Control-Max-Age improves compatibility and reduces OPTIONS load.

Apply:

   if (request.method === 'OPTIONS') {
     return new Response('', {
       status: 204,
       headers: {
-        'Access-Control-Allow-Origin': '*',
-        'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
-        'Access-Control-Allow-Headers': 'Content-Type'
+        'Access-Control-Allow-Origin': '*',
+        'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
+        'Access-Control-Allow-Headers': request.headers.get('Access-Control-Request-Headers') || 'Content-Type',
+        'Access-Control-Max-Age': '86400'
       }
     });
   }

51-54: /files 404 lacks CORS headers.

Without ACAO on 404, browsers may block error handling.

-      if (!object) {
-        return new Response('File not found', { status: 404 });
-      }
+      if (!object) {
+        return new Response('File not found', {
+          status: 404,
+          headers: {
+            'Content-Type': 'text/plain',
+            'Access-Control-Allow-Origin': '*'
+          }
+        });
+      }

73-85: Version mismatch: health says 2.0.0, UI shows v0.2.0.

Align the reported version or source it from a single env/config.

-        version: '2.0.0'
+        version: env.VERSION || '0.2.0'

And inject VERSION during build/deploy.


166-177: Set contentDisposition during R2 put.

Improves downstream serving and consistent filenames; pairs with nosniff.

-        await env.FILE_BUCKET.put(filename, file.stream(), {
-          httpMetadata: {
-            contentType: file.type
-          },
+        await env.FILE_BUCKET.put(filename, file.stream(), {
+          httpMetadata: {
+            contentType: file.type,
+            contentDisposition: `inline; filename="${sanitizedName}"`
+          },
Worker/src/chatRoom.js (6)

25-29: Avoid setInterval in Durable Objects; use alarms.

Timers may not fire when the DO is idle and keep the instance alive unnecessarily.

-    this.cleanupTimer = setInterval(() => this.cleanupUsers(), 30000);
-    
-    // Initialize moderators and banned users
-    this.initializeModerationData();
+    // Initialize and schedule periodic cleanup via alarms
+    this.initializeModerationData();
+    this.state.storage.getAlarm().then(alarm => {
+      if (!alarm) this.state.storage.setAlarm(Date.now() + 60 * 1000);
+    });

Add an alarm handler:

 export default class ChatRoom {
+  async alarm() {
+    await this.cleanupUsers();
+    await this.cleanupExpiredBans();
+    await this.cleanupExpiredKicks();
+    await this.state.storage.setAlarm(Date.now() + 60 * 1000);
+  }

221-224: Normalize user_messages key to avoid “User” vs “user” bypass.

Case differences create parallel histories and weaken spam checks.

-    const userMessages = await this.state.storage.get(`user_messages:${user}`) || [];
+    const userKey = String(user).toLowerCase();
+    const userMessages = await this.state.storage.get(`user_messages:${userKey}`) || [];
@@
-    await this.state.storage.put(`user_messages:${user}`, recentMessages.slice(-10)); // Keep last 10
+    await this.state.storage.put(`user_messages:${userKey}`, recentMessages.slice(-10)); // Keep last 10

306-315: Presence PUT ignores kicked error. Return 403 when kicked.

Currently you return users even if updateUserPresence rejected with an error.

   if (request.method === 'PUT') {
-      await this.updateUserPresence(room, user);
+      const res = await this.updateUserPresence(room, user);
+      if (res?.error) return textResponse(res.error, 403);
       const users = await this.getUsers(room);
       return jsonResponse({ users, userCount: users.length });
   }

509-512: Case-insensitive removal from presence map on kick.

Presence keys are stored with original case; delete may no-op if cases differ.

-    delete users[targetUser];
+    const keyToDelete = Object.keys(users).find(u => u.toLowerCase() === targetUser.toLowerCase());
+    if (keyToDelete) delete users[keyToDelete];

517-523: Message text says “banned” for a kick.

Use “kicked” to avoid confusion.

-      text: `${targetUser} was kicked by ${moderator}${reason ? ` (${reason})` : ''} - banned for 5 minutes`,
+      text: `${targetUser} was kicked by ${moderator}${reason ? ` (${reason})` : ''} - kicked for 5 minutes`,

348-357: Avoid logging message contents in production.

Logs may leak PII; gate logs behind an env flag or remove.

-    console.log(`Delete request: room=${room}, messageId=${messageId}, user=${user}`); // Debug
-    console.log(`Found message at index: ${messageIndex}`); // Debug
+    if (this.env?.DEBUG) console.log(`Delete request: room=${room}, messageId=${messageId}, user=${user}`);
+    if (this.env?.DEBUG) console.log(`Found message at index: ${messageIndex}`);
@@
-    console.log(`Message to delete:`, message); // Debug
+    if (this.env?.DEBUG) console.log(`Message to delete:`, { id: message.id, user: message.user });
Build/index.html (2)

326-331: CDN script contradicts “no CDN” goal and adds prod risk.

Load eruda only in dev via bundler; remove the CDN include.

-    <script src="https://cdn.jsdelivr.net/npm/eruda"></script>
-    <script>
-      eruda.init();
-    </script>
+    <!-- Dev-only debug should be imported via Vite in src/main.js (e.g., if (import.meta.env.DEV) import('eruda').then(m=>m.default.init())) -->

175-179: Align accept= with server allowlist to reduce UX errors.

Browser may allow selection the server will reject (e.g., audio/* includes unsupported types).

-              accept="image/*,audio/*,.pdf,.doc,.docx,.txt"
+              accept="image/jpeg,image/png,image/gif,image/webp,audio/mpeg,audio/wav,audio/ogg,application/pdf,text/plain,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
Build/src/search.js (4)

95-98: Storage key mismatch may hide messages from search.

Elsewhere export uses htmlchat_messages; here you read htmlchat_.

-      const messages = this.app.loadFromStorage(`htmlchat_${currentRoom}`) || [];
+      const messages =
+        this.app.loadFromStorage(`htmlchat_${currentRoom}`) ||
+        this.app.loadFromStorage('htmlchat_messages') ||
+        [];

Please standardize keys across modules.


155-159: Prefer real message id when available.

Deriving from time+index is brittle and may not match DOM ids.

-      const messageId = `msg-${msg.time}-${index}`;
+      const messageId = msg.id ? String(msg.id) : `msg-${msg.time}-${index}`;

Also update jumpToMessage to try data-message-id matching msg.id first.


106-114: Guard against missing fields.

Optional chaining prevents crashes on system/non-text messages.

-          const textMatch = msg.text.toLowerCase().includes(query) || 
-                           msg.user.toLowerCase().includes(query);
+          const textMatch = (msg.text?.toLowerCase?.() || '').includes(query) ||
+                            (msg.user?.toLowerCase?.() || '').includes(query);

205-225: Avoid fixed 500ms render wait.

Prefer awaiting a render completion hook or requestAnimationFrame loops for a short retry window.

-    this.app.fetchMessages(true).then(() => {
-      setTimeout(() => {
+    this.app.fetchMessages(true).then(async () => {
+      for (let i = 0; i < 5; i++) { // retry for a few frames
+        await new Promise(r => requestAnimationFrame(r));
         // Try to find by messageId again
         let foundMessage = document.getElementById(messageId);
         if (!foundMessage) {
           foundMessage = document.querySelector(`[data-message-id="${messageId}"]`);
         }
         
         if (foundMessage) {
           this.app.messageRenderer.highlightMessage(foundMessage.id);
-        } else {
-          // Fallback: try to find by timestamp
-          const messages = this.app.loadFromStorage(`htmlchat_${this.app.elements.roomSelect.value}`) || [];
-          const messageIndex = messages.findIndex(msg => msg.time === timestamp);
-          if (messageIndex !== -1) {
-            const generatedId = `msg-${timestamp}-${messageIndex}`;
-            this.app.messageRenderer.highlightMessage(generatedId);
-          }
-        }
-      }, 500);
+          return;
+        }
+      }
+      // Fallback by timestamp if still not found
+      const messages = this.app.loadFromStorage(`htmlchat_${this.app.elements.roomSelect.value}`) || [];
+      const messageIndex = messages.findIndex(msg => msg.time === timestamp);
+      if (messageIndex !== -1) {
+        const generatedId = `msg-${timestamp}-${messageIndex}`;
+        this.app.messageRenderer.highlightMessage(generatedId);
+      }
     });
Build/src/moderatorTools.js (3)

1-6: Client-side mod state is not authoritative.

LocalStorage-based moderators/bans are trivially editable and can drift from server state.

  • Treat the Worker (chatRoom.js) as source of truth; reflect its moderators/bans via authenticated API calls.
  • Use this class as a thin cache/UI helper, not as the authority. Sync on load and on change, invalidate on errors.

42-56: Normalize moderator entries and persist through server API.

Ensure adds/removes also call the backend (e.g., /mod endpoints) with proper auth, then update local cache on success.

Would you like a small client helper to wrap POST /mod {action} with x-admin-key and reconcile local state?


154-189: Duplicate spam/banned-word checks vs server; define single policy.

Divergent client/server checks lead to inconsistent UX. Push rules to server and reuse results client-side.

Build/src/notifications.js (1)

99-124: Don’t wipe all classes on permission status element

Resetting className can remove unrelated classes. Remove only what you add.

Apply this diff:

-    if (statusText) {
-      statusText.className = '';
+    if (statusText) {
+      statusText.classList.remove('granted', 'denied', 'default');
Build/src/main.js (4)

266-275: De‑dupe activity tracking listeners

You register the same listeners twice (here and in setupActivityTracking). Keep one.

Apply this diff to remove the duplicate block in setupEventListeners:

-    // Activity tracking
-    ["click", "keypress", "scroll", "mousemove"].forEach((event) => {
-      document.addEventListener(
-        event,
-        () => {
-          this.lastActivity = Date.now();
-        },
-        { passive: true }
-      );
-    });

Also applies to: 297-308


195-227: Guard missing sound toggle element to avoid NPE

If #sound-toggle is absent, querySelector calls will crash.

Apply this diff:

     const soundsEnabled = this.soundManager.isSoundEnabled();
     const soundToggle = this.elements.soundToggle;
 
-  // Wait a bit for icons to initialize, then set the state
-    setTimeout(() => {
+    // Wait a bit for icons to initialize, then set the state
+    if (!soundToggle) return;
+    setTimeout(() => {
       const soundOnIcon = soundToggle.querySelector('.sound-on-icon');
       const soundOffIcon = soundToggle.querySelector('.sound-off-icon');
- 
-  console.log("Initial sound state:", soundsEnabled); // Debug
-  console.log("Initial icons found:", { soundOnIcon, soundOffIcon }); // Debug

203-205: Remove debug logs

Strip noisy console logs before release.

Apply this diff:

-  console.log("Initial sound state:", soundsEnabled); // Debug
-  console.log("Initial icons found:", { soundOnIcon, soundOffIcon }); // Debug
@@
-      console.log("Received data:", data);
@@
-    console.log("Sound toggle clicked, enabled:", isEnabled); // Debug
@@
-    console.log("Found icons:", { soundOnIcon, soundOffIcon }); // Debug
@@
-      console.log("Showing sound ON icon"); // Debug
@@
-      console.log("Showing sound OFF icon"); // Debug

Also applies to: 380-381, 625-656


361-407: Optional: unique notification tags per message

If you want multiple notifications (not one replacing another), pass unique tags or let NotificationManager set none by default (as suggested). Otherwise only the last message may be visible in the OS tray.

No code change required here if you accept the NotificationManager change to drop the default tag.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2fa32b8 and 42e8094.

⛔ Files ignored due to path filters (11)
  • Build/package-lock.json is excluded by !**/package-lock.json
  • Build/public/icons/icon-128x128.png is excluded by !**/*.png
  • Build/public/icons/icon-144x144.png is excluded by !**/*.png
  • Build/public/icons/icon-152x152.png is excluded by !**/*.png
  • Build/public/icons/icon-192x192.png is excluded by !**/*.png
  • Build/public/icons/icon-256x256.png is excluded by !**/*.png
  • Build/public/icons/icon-384x384.png is excluded by !**/*.png
  • Build/public/icons/icon-48x48.png is excluded by !**/*.png
  • Build/public/icons/icon-512x512.png is excluded by !**/*.png
  • Build/public/icons/icon-72x72.png is excluded by !**/*.png
  • Build/public/icons/icon-96x96.png is excluded by !**/*.png
📒 Files selected for processing (21)
  • .github/workflows/static.yml (2 hunks)
  • Build/.gitignore (1 hunks)
  • Build/index.html (1 hunks)
  • Build/package.json (1 hunks)
  • Build/src/contextMenu.js (1 hunks)
  • Build/src/fileUpload.js (1 hunks)
  • Build/src/main.js (1 hunks)
  • Build/src/messageRenderer.js (1 hunks)
  • Build/src/moderatorTools.js (1 hunks)
  • Build/src/notifications.js (1 hunks)
  • Build/src/privateMessages.js (1 hunks)
  • Build/src/search.js (1 hunks)
  • Build/src/soundManager.js (1 hunks)
  • Build/src/styles.css (1 hunks)
  • Build/vite.config.js (1 hunks)
  • README.md (3 hunks)
  • TODO.md (1 hunks)
  • Worker/src/chatRoom.js (6 hunks)
  • Worker/src/index.js (1 hunks)
  • Worker/wrangler.toml (1 hunks)
  • index.html (0 hunks)
💤 Files with no reviewable changes (1)
  • index.html
🧰 Additional context used
🧬 Code graph analysis (7)
Build/src/contextMenu.js (1)
Worker/src/index.js (1)
  • room (109-109)
Build/src/fileUpload.js (1)
Worker/src/index.js (4)
  • allowedTypes (137-143)
  • file (107-107)
  • formData (106-106)
  • room (109-109)
Build/src/privateMessages.js (2)
Worker/src/index.js (1)
  • conversationId (34-34)
Build/src/main.js (1)
  • messages (678-678)
Build/src/messageRenderer.js (1)
Build/src/main.js (1)
  • messages (678-678)
Build/src/search.js (1)
Build/src/main.js (5)
  • messages (678-678)
  • a (694-694)
  • exportData (684-688)
  • blob (690-692)
  • url (693-693)
Worker/src/chatRoom.js (1)
Worker/src/index.js (4)
  • room (109-109)
  • user (108-108)
  • url (7-7)
  • conversationId (34-34)
Build/src/main.js (8)
Build/src/soundManager.js (1)
  • SoundManager (1-69)
Build/src/messageRenderer.js (1)
  • MessageRenderer (5-256)
Build/src/privateMessages.js (1)
  • PrivateMessageManager (3-305)
Build/src/fileUpload.js (1)
  • FileUploadManager (3-351)
Build/src/search.js (1)
  • SearchManager (1-260)
Build/src/notifications.js (1)
  • NotificationManager (1-356)
Build/src/contextMenu.js (1)
  • ContextMenuManager (1-267)
Build/src/moderatorTools.js (1)
  • ModeratorTools (1-299)
🪛 markdownlint-cli2 (0.17.2)
README.md

20-20: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


21-21: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


22-22: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


23-23: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


24-24: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


25-25: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


26-26: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


42-42: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


43-43: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


43-43: Unordered list indentation
Expected: 2; Actual: 1

(MD007, ul-indent)


43-43: Hard tabs
Column: 1

(MD010, no-hard-tabs)


44-44: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


44-44: Unordered list indentation
Expected: 2; Actual: 1

(MD007, ul-indent)


44-44: Hard tabs
Column: 1

(MD010, no-hard-tabs)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Workers Builds: htmlchat
🔇 Additional comments (5)
Build/.gitignore (1)

1-24: LGTM.

Standard ignores fit the new Vite setup and Pages deploy workflow.

README.md (1)

4-4: Version consistency check — confirm Worker /health version

README.md shows Version: v0.2.0; Build/package.json contains "version": "0.2.0" (line 3). No version string was found under Worker/src in the repo search. Ensure the Worker /health version matches README or explicitly document that app and backend use separate versioning.

Worker/wrangler.toml (1)

1-14: Approve — DO SQLite migration and class name match; no conflicting migration tags found.

wrangler.toml declares CHAT_ROOM (class_name = "ChatRoom") and [[migrations]] tag = "v1" with new_sqlite_classes = ["ChatRoom"]; ChatRoom is defined at Worker/src/chatRoom.js:21.

Build/index.html (1)

37-56: Confirmed — icons are created programmatically (no CDN/global createIcons dependency).

Build/src/main.js contains the data-lucide replacement routine (querySelectorAll('[data-lucide]') and helper for creating icons); Build/src/messageRenderer.js, Build/src/fileUpload.js and Build/src/privateMessages.js import icons from 'lucide'; package.json lists the lucide dependency.

Build/src/notifications.js (1)

178-183: Verify icon asset resolution under Vite

Hardcoded 'icons/icon-512x512.png' may 404 after bundling. Prefer a build-resolved URL.

Example alternatives:

  • If file is in public/: use icon: '/icons/icon-512x512.png'
  • If co-located: const appIcon = new URL('../icons/icon-512x512.png', import.meta.url).href

Would you like me to patch this once you confirm the icon location?

Also applies to: 196-203

Comment thread Build/src/fileUpload.js
Comment thread Build/src/fileUpload.js
Comment thread Build/src/fileUpload.js
Comment thread Build/src/main.js
Comment thread Build/src/main.js
Comment thread Worker/src/chatRoom.js Outdated
Comment thread Worker/src/index.js
Comment thread Worker/src/index.js
Comment thread Worker/src/index.js
Comment thread Worker/wrangler.toml
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

♻️ Duplicate comments (2)
Build/src/search.js (1)

193-204: XSS fix: escape-then-highlight is correctly applied

The new escapeHtml + regex highlight flow addresses the prior injection. Keep it consistent across all text fields.

Build/src/main.js (1)

391-410: Welcome banner now built with DOM APIs — resolved XSS

Good switch away from innerHTML; matches the earlier recommendation.

🧹 Nitpick comments (22)
Build/src/fileUpload.js (2)

268-273: Pass auth headers on upload (when AUTH is enabled).

If the Worker is configured with AUTH_SECRET, uploads will lack X-Auth-* headers. Add optional auth headers without setting Content-Type (to not break FormData).

Apply:

-          const uploadRes = await fetch(`${this.app.baseURL}/upload`, {
-            method: 'POST',
-            body: formData
-          });
+          const uploadRes = await fetch(`${this.app.baseURL}/upload`, {
+            method: 'POST',
+            // Ensure getAuthHeaders() does NOT set Content-Type for multipart
+            headers: this.app.getAuthHeaders ? this.app.getAuthHeaders(false) : undefined,
+            body: formData
+          });

128-136: Client/server allowedTypes are identical — verification passed.
Verified Build/src/fileUpload.js and Worker/src/index.js contain the same list (11 MIME types).
Optional: centralize the list or add a CI assertion to prevent future drift.

Build/src/messageRenderer.js (3)

165-166: Duplicate “system” class append.

You append the system class twice.

Apply:

-      if (message.system) messageClass += ' system';
-      if (message.system) messageClass += ' system';
+      if (message.system) messageClass += ' system';

139-139: Remove debug log.

Avoid noisy logs in render path.

-        console.log('Reply parsed:', { messageId, replyUser, messageText }); // Debug
+        // Reply parsed

101-108: Consider forbidding inline styles in user content.

Allowing “style” increases XSS surface; DOMPurify mitigates most issues, but removing it tightens posture.

-      ALLOWED_ATTR: ['href','src','alt','title','target','style']
+      ALLOWED_ATTR: ['href','src','alt','title','target','rel']
Build/src/privateMessages.js (1)

280-282: Include auth headers for PM history fetch (if enabled).

Mirror sendPM headers to avoid 403 when AUTH is configured.

-      const res = await fetch(`${this.app.baseURL}/pm/${encodeURIComponent(conversationId)}?user=${encodeURIComponent(this.app.user)}`);
+      const res = await fetch(`${this.app.baseURL}/pm/${encodeURIComponent(conversationId)}?user=${encodeURIComponent(this.app.user)}`, {
+        headers: this.app.getAuthHeaders ? this.app.getAuthHeaders() : undefined
+      });
Worker/src/chatRoom.js (7)

581-611: Kick system message says “banned” (but it’s a kick).

Text is misleading to users.

-      text: `${targetUser} was kicked by ${moderator}${reason ? ` (${reason})` : ''} - banned for 5 minutes`,
+      text: `${targetUser} was kicked by ${moderator}${reason ? ` (${reason})` : ''} - kicked for 5 minutes`,

Also applies to: 616-620


331-362: Handle kicked user on presence heartbeat.

updateUserPresence can return {error}; PUT path ignores it and still returns success.

-      await this.updateUserPresence(room, user);
+      const pres = await this.updateUserPresence(room, user);
+      if (pres && pres.error) {
+        return textResponse(pres.error, 403);
+      }

595-600: Case-insensitive presence removal on kick.

Presence keys aren’t normalized; deleting with exact case may fail.

-    delete users[targetUser];
+    const k = Object.keys(users).find(u => u.toLowerCase() === targetUser.toLowerCase());
+    if (k) delete users[k];

31-47: Moderator storage is initialized but unused.

You seed/maintain moderators in storage, but isModerator() and GET /mod hardcode NellowTCS. Either use storage or remove the dead paths.

-  async isModerator(username) {
-    // Only NellowTCS is allowed moderator access
-    return username && username.toLowerCase() === 'nellowtcs';
-  }
+  async isModerator(username) {
+    const moderators = (await this.state.storage.get('moderators')) || [];
+    return !!(username && moderators.includes(username.toLowerCase()));
+  }
@@
-        const moderators = ['NellowTCS']; // Only NellowTCS is moderator
+        const moderators = (await this.state.storage.get('moderators')) || [];
@@
-            return textResponse('Cannot modify moderators - NellowTCS is the only moderator', 403);
+            return this.addModerator(targetUser, verifiedUser);
@@
-            return textResponse('Cannot modify moderators - NellowTCS is the only moderator', 403);
+            return this.removeModerator(targetUser, verifiedUser);

Also applies to: 166-169, 536-545, 623-646


232-241: CORS headers: include the same allow-headers as JSON/text helpers.

Align OPTIONS with jsonResponse/textResponse (missing methods/headers parity).

-          'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
-          'Access-Control-Allow-Headers': 'Content-Type, X-Auth-Token, X-Auth-User'
+          'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
+          'Access-Control-Allow-Headers': 'Content-Type, X-Auth-Token, X-Auth-User'

(If you later adopt Authorization, add it here too.)


29-33: Durable Object timers: prefer alarms over setInterval.

setInterval keeps the DO hot; use DO alarms for periodic cleanup to reduce cost and improve reliability.


312-316: Use slice instead of deprecated substr.

Small cleanup.

-        id: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
+        id: `msg_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`,
@@
-        id: `pm_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
+        id: `pm_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`,

Also applies to: 493-499

Build/src/contextMenu.js (1)

19-24: Robust menu click handling.

Use closest('[data-action]') so clicks on child elements work.

-    this.menu.addEventListener('click', (e) => {
-      const action = e.target.dataset.action;
+    this.menu.addEventListener('click', (e) => {
+      const el = e.target.closest('[data-action]');
+      const action = el && el.dataset.action;
       if (action && this.currentMessage) {
         this.handleAction(action);
       }
     });
Build/src/moderatorTools.js (1)

171-182: Banned-words placeholder.

Current list is trivial and may overblock “spam” in normal text. Consider disabling or making it configurable per room.

Build/src/search.js (4)

135-141: Robust sort on timestamp strings or numbers

Handle both numeric epoch and ISO strings to avoid NaN and ensure correct ordering.

Apply this diff:

-      filteredMessages = filteredMessages.sort((a, b) => b.time - a.time);
+      filteredMessages = filteredMessages.sort((a, b) => {
+        const ta = Number(a.time) || Date.parse(a.time) || 0;
+        const tb = Number(b.time) || Date.parse(b.time) || 0;
+        return tb - ta;
+      });

162-162: Prefer canonical message id if present

Use msg.id or msg.messageId when available to improve jump reliability across renderers.

Apply this diff:

-      const messageId = `msg-${msg.time}-${index}`;
+      const messageId = String(
+        msg.id ?? msg.messageId ?? `msg-${Number(msg.time) || Date.parse(msg.time) || 0}-${index}`
+      );

237-241: Timestamp equality check may fail due to type mismatch

Coerce both sides to number for consistent matching.

Apply this diff:

-          const messageIndex = messages.findIndex(msg => msg.time === timestamp);
+          const messageIndex = messages.findIndex(msg => {
+            const t = Number(msg.time) || Date.parse(msg.time) || 0;
+            return t === Number(timestamp);
+          });

154-173: Rendering via innerHTML is still an XSS sink; build DOM nodes instead

You’re escaping text before highlighting (good), but constructing the entire result with innerHTML keeps risk surface area high (style attributes, future edits). Prefer programmatic DOM creation with textContent and setAttribute.

Example rewrite (illustrative):

-    const html = messages.map((msg, index) => { ... return `...${highlightedUser}...${highlightedText}...`; }).join('');
-    this.searchResults.innerHTML = headerHtml + html;
-    this.searchResults.querySelectorAll('.search-result-item').forEach(item => { ... });
+    this.searchResults.textContent = '';
+    const header = document.createElement('div');
+    header.style.cssText = 'padding:8px;background:#f0f0f0;border-bottom:1px solid #ddd;font-weight:bold;';
+    header.textContent = `Found ${messages.length} message${messages.length === 1 ? '' : 's'}`;
+    this.searchResults.appendChild(header);
+    const frag = document.createDocumentFragment();
+    messages.forEach((msg, index) => {
+      const date = new Date(msg.time).toLocaleString();
+      const color = this.app.messageRenderer.getUserColor(String(msg.user ?? ''));
+      const messageId = String(msg.id ?? msg.messageId ?? `msg-${Number(msg.time)||Date.parse(msg.time)||0}-${index}`);
+      const item = document.createElement('div');
+      item.className = 'search-result-item';
+      item.dataset.messageId = messageId;
+      item.dataset.timestamp = String(Number(msg.time) || Date.parse(msg.time) || 0);
+      const top = document.createElement('div');
+      top.style.cssText = 'display:flex;justify-content:space-between;align-items:center;margin-bottom:4px;';
+      const userSpan = document.createElement('span');
+      userSpan.style.color = color;
+      userSpan.style.fontWeight = 'bold';
+      userSpan.innerHTML = this.highlightSearchTerms(String(msg.user ?? ''), query);
+      const timeSpan = document.createElement('span');
+      timeSpan.style.cssText = 'color:#666;font-size:12px;';
+      timeSpan.textContent = date;
+      top.append(userSpan, timeSpan);
+      const textDiv = document.createElement('div');
+      textDiv.innerHTML = this.highlightSearchTerms(String(msg.text ?? ''), query);
+      item.append(top, textDiv);
+      item.addEventListener('click', () => this.jumpToMessage(messageId, Number(item.dataset.timestamp)));
+      frag.appendChild(item);
+    });
+    this.searchResults.appendChild(frag);
Build/src/main.js (3)

294-303: Duplicate activity listeners registered twice

setupEventListeners and setupActivityTracking both attach the same handlers; remove one to avoid double updates.

Apply this diff to keep the dedicated method only:

-    // Activity tracking
-    ["click", "keypress", "scroll", "mousemove"].forEach((event) => {
-      document.addEventListener(
-        event,
-        () => {
-          this.lastActivity = Date.now();
-        },
-        { passive: true }
-      );
-    });

Also applies to: 325-336


487-500: Production logs leak auth presence; downgrade to debug or guard behind a flag

Reduce noisy logs and avoid hinting at auth paths.

Apply this diff:

-      console.log('Adding auth headers:', { user: this.user, hasToken: !!this.authToken, includeContentType });
+      console.debug('Adding auth headers');
@@
-      console.log('No auth headers added:', { 
-        user: this.user, 
-        isNellowTCS: this.user && this.user.toLowerCase() === 'nellowtcs', 
-        hasToken: !!this.authToken 
-      });
+      console.debug('No auth headers added');

721-725: Nit: avoid innerHTML for the static “Loading messages…” stub

Safe as-is, but keep consistency with DOM APIs.

Apply this diff:

-    this.elements.chatBox.innerHTML =
-      '<div class="msg system"><span class="time">[--:--]</span><span class="user">*** System ***</span><span class="text">Loading messages...</span></div>';
+    const sys = document.createElement('div');
+    sys.className = 'msg system';
+    sys.innerHTML = '<span class="time">[--:--]</span><span class="user">*** System ***</span><span class="text">Loading messages...</span>';
+    this.elements.chatBox.textContent = '';
+    this.elements.chatBox.appendChild(sys);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 42e8094 and e232c14.

📒 Files selected for processing (11)
  • Build/src/contextMenu.js (1 hunks)
  • Build/src/fileUpload.js (1 hunks)
  • Build/src/main.js (1 hunks)
  • Build/src/messageRenderer.js (1 hunks)
  • Build/src/moderatorTools.js (1 hunks)
  • Build/src/notifications.js (1 hunks)
  • Build/src/privateMessages.js (1 hunks)
  • Build/src/search.js (1 hunks)
  • Build/vite.config.js (1 hunks)
  • Worker/src/chatRoom.js (7 hunks)
  • Worker/src/index.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • Build/vite.config.js
  • Worker/src/index.js
  • Build/src/notifications.js
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: NellowTCS
PR: HTMLToolkit/HTMLChat#2
File: Worker/src/chatRoom.js:244-248
Timestamp: 2025-09-18T07:10:50.415Z
Learning: NellowTCS prefers to keep the HTMLChat chatroom open and publicly accessible without authentication requirements, declining authentication/authorization suggestions in favor of an open chatroom architecture.
🧬 Code graph analysis (7)
Build/src/privateMessages.js (1)
Worker/src/index.js (1)
  • conversationId (34-34)
Build/src/messageRenderer.js (1)
Build/src/main.js (1)
  • messages (874-874)
Build/src/contextMenu.js (1)
Worker/src/index.js (1)
  • room (125-125)
Build/src/fileUpload.js (1)
Worker/src/index.js (4)
  • allowedTypes (153-159)
  • file (123-123)
  • formData (122-122)
  • room (125-125)
Build/src/search.js (1)
Build/src/main.js (5)
  • messages (874-874)
  • a (890-890)
  • exportData (880-884)
  • blob (886-888)
  • url (889-889)
Worker/src/chatRoom.js (1)
Worker/src/index.js (4)
  • room (125-125)
  • user (124-124)
  • url (7-7)
  • conversationId (34-34)
Build/src/main.js (8)
Build/src/soundManager.js (1)
  • SoundManager (1-69)
Build/src/messageRenderer.js (1)
  • MessageRenderer (5-275)
Build/src/privateMessages.js (1)
  • PrivateMessageManager (3-340)
Build/src/fileUpload.js (1)
  • FileUploadManager (3-381)
Build/src/search.js (1)
  • SearchManager (1-279)
Build/src/notifications.js (1)
  • NotificationManager (1-363)
Build/src/contextMenu.js (1)
  • ContextMenuManager (1-265)
Build/src/moderatorTools.js (1)
  • ModeratorTools (1-306)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Workers Builds: htmlchat
🔇 Additional comments (2)
Build/src/fileUpload.js (1)

167-181: Good: filename escaping + stable upload button.

Preview now escapes file names and uses a dedicated Upload button id. This closes the XSS hole surfaced earlier and prevents targeting the wrong button.

Also applies to: 184-197, 207-214

Build/src/main.js (1)

506-519: Sanitizer verified — renderMessages uses DOMPurify before assigning innerHTML

Build/src/messageRenderer.js calls marked.parse then DOMPurify.sanitize (with ALLOWED_TAGS configured); innerHTML assignments are in Build/src/main.js (around lines 512–515 and 573–576).

Comment thread Build/src/contextMenu.js
Comment thread Build/src/main.js
Comment thread Build/src/main.js
Comment thread Build/src/messageRenderer.js
Comment thread Build/src/messageRenderer.js
Comment thread Build/src/moderatorTools.js
Comment thread Build/src/privateMessages.js
Comment thread Build/src/search.js
Comment thread Worker/src/chatRoom.js
@NellowTCS NellowTCS merged commit 4ecb864 into main Sep 18, 2025
2 checks passed
@NellowTCS NellowTCS deleted the v0.2.0 branch September 18, 2025 08:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant