Skip to content

Avatar and security fixes#221

Merged
matheusfillipe merged 1 commit into
mainfrom
fix/chat-avatars
May 18, 2026
Merged

Avatar and security fixes#221
matheusfillipe merged 1 commit into
mainfrom
fix/chat-avatars

Conversation

@matheusfillipe
Copy link
Copy Markdown
Contributor

@matheusfillipe matheusfillipe commented May 16, 2026

Summary by CodeRabbit

  • Bug Fixes

    • Improved consistency of avatar and media visibility across the app based on user media preferences.
    • Enhanced avatar preview validation to prevent display of invalid URLs.
    • Fixed metadata handling for proper parameter processing from IRC servers.
  • Refactor

    • Standardized media visibility enforcement for avatars and images application-wide.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 16, 2026

📝 Walkthrough

Walkthrough

This PR refactors avatar and media visibility gating across the codebase. A new canShowAvatarUrl() helper and isAbsoluteHttpUrl() validator consolidate scattered isUrlFromFilehost() + media-flag checks into a single mediaSettings-driven pattern. Layout, message, and UI components are updated to use these helpers. Additionally, IRC metadata handlers are hardened to strip double-encoded colons.

Changes

Avatar and Media Visibility Refactoring

Layer / File(s) Summary
Core media utilities and URL validation
src/lib/ircUtils.tsx, src/lib/mediaUtils.ts, tests/lib/mediaUtils.test.ts
Adds isAbsoluteHttpUrl() to validate absolute HTTP(S) URLs, introduces canShowAvatarUrl() for avatar rendering gating with media-level enforcement, hardens canShowMedia() with early URL validation, and adds comprehensive test coverage.
Layout components avatar refactoring
src/components/layout/ChannelList.tsx, src/components/layout/ChatHeader.tsx, src/components/layout/MemberList.tsx, src/components/layout/ServerList.tsx
Derives mediaSettings from mediaLevelToSettings() and gates avatar/icon visibility via canShowAvatarUrl(), eliminating prior filehost URL detection and media-flag destructuring patterns.
Message/event components avatar refactoring
src/components/message/EventMessage.tsx, src/components/message/CollapsedEventMessage.tsx, src/components/message/MessageAvatar.tsx
Computes mediaSettings from store state and server filehost, then gates avatar rendering via canShowAvatarUrl() instead of prior trusted-source and boolean media-flag checks.
Modal/UI components avatar refactoring
src/components/ui/ChannelListModal.tsx, src/components/ui/UserProfileModal.tsx, src/components/ui/ChannelSettingsModal.tsx
Gates avatar visibility via canShowAvatarUrl() and validates avatar preview rendering via isAbsoluteHttpUrl() in the settings modal.

IRC Metadata Handler Hardening

Layer / File(s) Summary
Metadata parameter colon stripping
src/lib/irc/handlers/metadata.ts
Updates handleMetadata and handleMetadataWhoisKeyValue to conditionally strip leading colons from final parameter values, accommodating clients that double-encode trailing parameters.

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly Related PRs

  • ObsidianIRC/ObsidianIRC#82: Introduces the original isUrlFromFilehost/showSafeMedia/showExternalContent gating that this PR refactors into the unified canShowAvatarUrl + mediaSettings pattern.
  • ObsidianIRC/ObsidianIRC#94: Modifies ChannelSettingsModal General tab avatar UI; this PR adds isAbsoluteHttpUrl-based preview validation to the same component.
  • ObsidianIRC/ObsidianIRC#70: Introduces avatarLoadFailed-based load-failure tracking; this PR builds on that pattern by adding media-level gating via canShowAvatarUrl().

Poem

🐰 Avatars once scattered, now unified at last,
From filehost checks and flags of old, we've learned at last,
canShowAvatarUrl brings order, clean and bright,
Media settings flow like magic—every image right!
No more Boolean chaos, just one helper's gentle might. ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Avatar and security fixes' is vague and generic, using non-descriptive terms that don't convey specific information about what was changed despite the PR containing substantial refactoring of media visibility logic. Consider a more specific title like 'Refactor avatar media visibility using canShowAvatarUrl helper' to better reflect the primary change of consolidating media gating logic.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/chat-avatars

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

src/components/layout/ServerList.tsx

Oops! Something went wrong! :(

ESLint: 9.39.1

ESLint couldn't find an eslint.config.(js|mjs|cjs) file.

From ESLint v9.0.0, the default configuration file is now eslint.config.js.
If you are using a .eslintrc.* file, please follow the migration guide
to update your configuration file to the new format:

https://eslint.org/docs/latest/use/configure/migration-guide

If you still have problems after following the migration guide, please stop by
https://eslint.org/chat/help to chat with the team.

src/components/layout/ChannelList.tsx

Oops! Something went wrong! :(

ESLint: 9.39.1

ESLint couldn't find an eslint.config.(js|mjs|cjs) file.

From ESLint v9.0.0, the default configuration file is now eslint.config.js.
If you are using a .eslintrc.* file, please follow the migration guide
to update your configuration file to the new format:

https://eslint.org/docs/latest/use/configure/migration-guide

If you still have problems after following the migration guide, please stop by
https://eslint.org/chat/help to chat with the team.

src/components/message/EventMessage.tsx

Oops! Something went wrong! :(

ESLint: 9.39.1

ESLint couldn't find an eslint.config.(js|mjs|cjs) file.

From ESLint v9.0.0, the default configuration file is now eslint.config.js.
If you are using a .eslintrc.* file, please follow the migration guide
to update your configuration file to the new format:

https://eslint.org/docs/latest/use/configure/migration-guide

If you still have problems after following the migration guide, please stop by
https://eslint.org/chat/help to chat with the team.

  • 11 others

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

@github-actions
Copy link
Copy Markdown

Pages Preview
Preview URL: https://fix-chat-avatars.obsidianirc.pages.dev

Automated deployment preview for the PR in the Cloudflare Pages.

Copy link
Copy Markdown
Contributor

@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: 2

🧹 Nitpick comments (1)
src/components/message/CollapsedEventMessage.tsx (1)

33-36: 💤 Low value

Consider using a selector for server to ensure reactivity.

useStore.getState() performs a synchronous read that won't trigger re-renders if servers changes. If server.filehost could change during the component's lifetime, use a selector instead:

 const mediaSettings = mediaLevelToSettings(
   useStore((state) => state.globalSettings.mediaVisibilityLevel),
 );
-const server = useStore.getState().servers.find((s) => s.id === serverId);
+const server = useStore((state) => state.servers.find((s) => s.id === serverId));

That said, if filehost is effectively immutable after connection, this is acceptable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/message/CollapsedEventMessage.tsx` around lines 33 - 36, The
code uses useStore.getState().servers.find(...) to read the server synchronously
which won't trigger re-renders; replace that with a reactive selector like
useStore(state => state.servers.find(s => s.id === serverId)) so the component
(CollapsedEventMessage) updates if the servers array or server.filehost changes
— locate the server lookup in CollapsedEventMessage.tsx and swap the getState()
call for the selector form using serverId.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/message/EventMessage.tsx`:
- Around line 34-36: The server lookup in EventMessage.tsx uses a snapshot read
via useStore.getState() which prevents reactive updates; change it to subscribe
to the store using useStore(...) so the component re-renders when
servers/filehost change. Replace the useStore.getState().servers.find(...)
access used for the server constant with a selector passed into useStore
(matching how mediaSettings is retrieved) that returns servers.find(s => s.id
=== message.serverId) to ensure avatar gating updates reactively.

In `@src/lib/irc/handlers/metadata.ts`:
- Around line 14-16: Add unit tests for the colon-stripping behavior in the
metadata handlers: create tests targeting handleMetadata and
handleMetadataWhoisKeyValue that assert (1) values without leading ":" are
preserved, (2) double-encoded values (e.g., "::value" and ":value" variants) are
normalized to a single unprefixed value, (3) empty trailing params (parv length
< 4 or last param equals ":" or "") produce empty strings, and (4) overall
behavior matches draft/metadata-2 examples; exercise the handlers by supplying
representative parv arrays and assert the final stored/returned metadata equals
the expected value for each case.

---

Nitpick comments:
In `@src/components/message/CollapsedEventMessage.tsx`:
- Around line 33-36: The code uses useStore.getState().servers.find(...) to read
the server synchronously which won't trigger re-renders; replace that with a
reactive selector like useStore(state => state.servers.find(s => s.id ===
serverId)) so the component (CollapsedEventMessage) updates if the servers array
or server.filehost changes — locate the server lookup in
CollapsedEventMessage.tsx and swap the getState() call for the selector form
using serverId.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6fe5f406-b6fd-4904-82c2-1b7f9999101f

📥 Commits

Reviewing files that changed from the base of the PR and between e153b25 and 3448014.

📒 Files selected for processing (14)
  • src/components/layout/ChannelList.tsx
  • src/components/layout/ChatHeader.tsx
  • src/components/layout/MemberList.tsx
  • src/components/layout/ServerList.tsx
  • src/components/message/CollapsedEventMessage.tsx
  • src/components/message/EventMessage.tsx
  • src/components/message/MessageAvatar.tsx
  • src/components/ui/ChannelListModal.tsx
  • src/components/ui/ChannelSettingsModal.tsx
  • src/components/ui/UserProfileModal.tsx
  • src/lib/irc/handlers/metadata.ts
  • src/lib/ircUtils.tsx
  • src/lib/mediaUtils.ts
  • tests/lib/mediaUtils.test.ts

Comment on lines +34 to +36
const server = useStore
.getState()
.servers.find((s) => s.id === message.serverId);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find non-reactive server lookups through Zustand snapshots
rg -nP --type=ts --type=tsx 'useStore\.getState\(\)\.servers\.find'

Repository: ObsidianIRC/ObsidianIRC

Length of output: 95


🏁 Script executed:

cat -n src/components/message/EventMessage.tsx | head -60

Repository: ObsidianIRC/ObsidianIRC

Length of output: 2181


🏁 Script executed:

#!/bin/bash
# Search for the file and its contents to understand context
find . -name "EventMessage.tsx" -type f

Repository: ObsidianIRC/ObsidianIRC

Length of output: 109


🏁 Script executed:

#!/bin/bash
# Search for useStore usage patterns - use correct rg syntax
rg -n 'useStore\.getState\(\)' --type ts -A 3 -B 1 | head -50

Repository: ObsidianIRC/ObsidianIRC

Length of output: 3193


🏁 Script executed:

#!/bin/bash
# Search for useStore with selector pattern to see best practices in codebase
rg -n 'useStore\(\(' --type ts -A 2 -B 1 | head -50

Repository: ObsidianIRC/ObsidianIRC

Length of output: 3824


🏁 Script executed:

cat -n src/components/message/EventMessage.tsx | tail -50

Repository: ObsidianIRC/ObsidianIRC

Length of output: 2268


🏁 Script executed:

rg -n 'server' src/components/message/EventMessage.tsx -A 1 -B 1

Repository: ObsidianIRC/ObsidianIRC

Length of output: 758


Use a reactive selector for server lookup.

Line 34 uses useStore.getState() (snapshot read), so avatar gating won't update when servers/filehost changes. Subscribe via useStore(...) instead, matching the pattern used for mediaSettings on line 32.

Suggested fix
-  const server = useStore
-    .getState()
-    .servers.find((s) => s.id === message.serverId);
+  const server = useStore((state) =>
+    state.servers.find((s) => s.id === message.serverId),
+  );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const server = useStore
.getState()
.servers.find((s) => s.id === message.serverId);
const server = useStore((state) =>
state.servers.find((s) => s.id === message.serverId),
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/message/EventMessage.tsx` around lines 34 - 36, The server
lookup in EventMessage.tsx uses a snapshot read via useStore.getState() which
prevents reactive updates; change it to subscribe to the store using
useStore(...) so the component re-renders when servers/filehost change. Replace
the useStore.getState().servers.find(...) access used for the server constant
with a selector passed into useStore (matching how mediaSettings is retrieved)
that returns servers.find(s => s.id === message.serverId) to ensure avatar
gating updates reactively.

Comment on lines +14 to +16
// Strip leading ":" — some clients double-encode the trailing param, leaving one colon after the parser strips its own.
const rawValue = parv.length >= 4 ? parv[parv.length - 1] : "";
const value = rawValue.startsWith(":") ? rawValue.substring(1) : rawValue;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Search for tests covering metadata handler colon-stripping behavior

# Test: Look for test files covering metadata handlers
fd -e test.ts -e test.tsx -e spec.ts . | xargs rg -l 'metadata' -i

# Test: Search for any tests calling handleMetadata or testing colon behavior
rg -n "handleMetadata|double.*encod|leading.*colon|startsWith.*:" --type ts -g '*test*' -g '*spec*'

Repository: ObsidianIRC/ObsidianIRC

Length of output: 931


🏁 Script executed:

#!/bin/bash

# Search for IRC handler test files
echo "=== Looking for IRC handler test structure ==="
find tests -type d -name "handlers" -o -name "irc" 2>/dev/null | head -20

echo ""
echo "=== All test files in tests/lib ==="
fd -type f . tests/lib/ 2>/dev/null | head -20

echo ""
echo "=== Searching for any reference to handleMetadata in tests ==="
rg "handleMetadata" tests/ --type ts --type tsx 2>/dev/null | head -20

echo ""
echo "=== Checking handlers index to see registration ==="
grep -n "metadata" src/lib/irc/handlers/index.ts 2>/dev/null | head -10

Repository: ObsidianIRC/ObsidianIRC

Length of output: 370


🏁 Script executed:

#!/bin/bash

echo "=== Checking full metadata.ts implementation (lines 1-50) ==="
head -50 src/lib/irc/handlers/metadata.ts | cat -n

echo ""
echo "=== Checking handlers/index.ts dispatch for metadata ==="
grep -B2 -A2 "METADATA\|metadata" src/lib/irc/handlers/index.ts | head -30

echo ""
echo "=== Looking for any integration tests or test patterns ==="
find tests -type f -name "*.test.ts" -o -name "*.test.tsx" | wc -l

echo ""
echo "=== Check if there are handler-related tests elsewhere ==="
rg "ctx\.triggerEvent|IRCClientContext" tests/ --type ts --type tsx 2>/dev/null | head -10

Repository: ObsidianIRC/ObsidianIRC

Length of output: 2749


Add test coverage for colon-stripping edge cases in metadata handlers.

The logic correctly handles double-encoded trailing parameters by conditionally stripping leading colons. This implementation is consistent across handleMetadata (lines 14–16), handleMetadataWhoisKeyValue (lines 37–40), and aligns with the draft/metadata-2 specification.

However, no test coverage exists for this behavior. Recommend adding tests to verify:

  1. Normal values without leading colons are preserved
  2. Double-encoded values with extra colons are correctly stripped
  3. Empty values are handled properly
  4. Behavior matches the draft/metadata-2 specification
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/irc/handlers/metadata.ts` around lines 14 - 16, Add unit tests for
the colon-stripping behavior in the metadata handlers: create tests targeting
handleMetadata and handleMetadataWhoisKeyValue that assert (1) values without
leading ":" are preserved, (2) double-encoded values (e.g., "::value" and
":value" variants) are normalized to a single unprefixed value, (3) empty
trailing params (parv length < 4 or last param equals ":" or "") produce empty
strings, and (4) overall behavior matches draft/metadata-2 examples; exercise
the handlers by supplying representative parv arrays and assert the final
stored/returned metadata equals the expected value for each case.

@matheusfillipe matheusfillipe merged commit 2b362bd into main May 18, 2026
5 checks passed
@matheusfillipe matheusfillipe deleted the fix/chat-avatars branch May 18, 2026 07:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants