Skip to content

feat/keyup to history#78

Merged
ValwareIRC merged 6 commits into
mainfrom
feat/keyup-to-history
Oct 10, 2025
Merged

feat/keyup to history#78
ValwareIRC merged 6 commits into
mainfrom
feat/keyup-to-history

Conversation

@matheusfillipe
Copy link
Copy Markdown
Contributor

@matheusfillipe matheusfillipe commented Oct 7, 2025

  • keyup arrow up and down keybindings for navigating over past messages
  • refactor chat area File was getting too large
  • truncate long urls
image

Summary by CodeRabbit

  • New Features
    • Enhanced chat: modal emoji picker, image preview/upload modal, input toolbar, reply badge, scroll-to-bottom button, and a new channel/private chat header.
    • Message history navigation, richer message sending (multiline/commands), reactions modal, and centralized typing notifications.
  • Bug Fixes
    • Improved message wrapping and URL truncation with full-URL hover tooltip.
  • Refactor
    • Chat UI split into modular hooks and components for cleaner flows.
  • Tests
    • Timestamp test made locale-aware.
  • Chores
    • IRC service image tag updated to master.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Oct 7, 2025

Walkthrough

Replaces inline emoji, reaction, typing, history, and sending logic with new hooks and modal UI; adds ChatHeader and several UI components; introduces IRC formatting/protocol utilities and message-splitting helpers; minor message/link rendering tweaks and a compose.yaml image tag change.

Changes

Cohort / File(s) Summary
Compose config
compose.yaml
Updates ircd image from ghcr.io/ergochat/ergo to ghcr.io/ergochat/ergo:master.
Chat layout / header
src/components/layout/ChatArea.tsx, src/components/layout/ChatHeader.tsx
Replaces inline emoji/reaction/typing/history/sending logic in ChatArea with modal-driven UI and dedicated hooks; adds new ChatHeader component.
UI components
src/components/ui/EmojiPickerModal.tsx, src/components/ui/ImagePreviewModal.tsx, src/components/ui/InputToolbar.tsx, src/components/ui/ReplyBadge.tsx, src/components/ui/ScrollToBottomButton.tsx
Adds modal and toolbar components for emoji picking, image preview/upload, input toolbar, reply badge, and scroll-to-bottom button.
Link & message rendering
src/components/ui/LinkWrapper.tsx, src/components/message/MessageItem.tsx
Introduces URL truncation with title attribute and break/wrap styles; changes non-image message wrapper to use overflow/word-break CSS.
Behavioral hooks
src/hooks/useMessageHistory.ts, src/hooks/useMessageSending.ts, src/hooks/useReactions.ts, src/hooks/useTypingNotification.ts
Adds hooks for message history navigation, centralized message sending (commands, multiline/BATCH or fallback), reaction handling/modal, and throttled typing notifications.
IRC utilities
src/lib/messageFormatter.ts, src/lib/messageProtocol.ts
Adds stripIrcFormatting and message-protocol utilities: splitLongMessage, calculateProtocolOverhead, and createBatchId.
Tests
tests/components/StandardReplyNotification.test.tsx
Uses a fixed testDate and locale-aware time formatting for expected timestamp assertion.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant UI as ChatArea
  participant MS as useMessageSending
  participant IRC as ircClient
  participant SV as IRC Server

  User->>UI: Type and press Enter
  UI->>MS: sendMessage(text, context)
  alt Command (/nick,/join,/part,/msg,/me)
    MS->>IRC: Send command(s)
  else Multiline/BATCH supported
    MS->>MS: splitLongMessage + createBatchId
    MS->>IRC: BATCH + PRIVMSG lines + BATCH close
  else Fallback
    MS->>IRC: PRIVMSG (single/multiple)
  end
  opt Private chat local echo
    MS->>UI: Insert local outgoing message
  end
  IRC->>SV: Transmit
  SV-->>IRC: Ack / Events
  IRC-->>UI: Events propagate to store/UI
Loading
sequenceDiagram
  autonumber
  actor User
  participant UI as ChatArea/MessageItem
  participant RE as useReactions
  participant IRC as ircClient
  participant SV as IRC Server

  User->>UI: Open reactions / click emoji
  UI->>RE: selectReaction(message, emoji)
  alt Already reacted
    RE->>IRC: TAGMSG unreact
  else New reaction
    RE->>IRC: TAGMSG react
  end
  IRC->>SV: Transmit
  RE->>UI: Close reaction modal
Loading
sequenceDiagram
  autonumber
  actor User
  participant UI as ChatArea Input
  participant TN as useTypingNotification
  participant IRC as ircClient
  participant SV as IRC Server

  User->>UI: Typing text
  UI->>TN: notifyTyping(target, text)
  alt Non-empty & not a command & throttle window passed
    TN->>IRC: typing=active
  else Empty text
    TN->>IRC: typing=done
  end
  IRC->>SV: Transmit
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

I thump my paws and patch the glade,
Modals bloom where pickers played,
Lines split neatly, tags take flight,
Reactions hop in soft moonlight.
A rabbit cheers — new hooks delight! 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Title Check ⚠️ Warning The title only mentions the arrow-key history feature and omits the significant ChatArea refactor, URL truncation, and numerous new hooks and components introduced in this large changeset, so it does not accurately summarize the main scope of the pull request. Please update the title to reflect the broader scope of changes, for example: “Refactor ChatArea, add arrow-key message history navigation, and truncate long URLs,” so it clearly conveys the key features and refactoring in this PR.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/keyup-to-history

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

github-actions Bot commented Oct 7, 2025

Pages Preview
Preview URL: https://feat-keyup-to-history.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: 9

🧹 Nitpick comments (3)
src/components/ui/ReplyBadge.tsx (1)

15-29: Extract username from userId for display.

The component displays replyTo.userId directly, which may include additional identifiers beyond the username. Based on usage patterns elsewhere in the codebase (e.g., MessageItem.tsx line 147: username = message.userId.split("-")[0]), consider extracting just the username portion.

Apply this diff:

 export function ReplyBadge({ replyTo, onClose }: ReplyBadgeProps) {
+  const username = replyTo.userId.split("-")[0];
+
   return (
     <div className="bg-discord-dark-200 rounded text-sm text-discord-text-muted mr-3 flex items-center h-8 px-2">
       <span className="flex-grow text-center">
-        Replying to <strong>{replyTo.userId}</strong>
+        Replying to <strong>{username}</strong>
       </span>
       <button
         className="ml-2 text-xs text-discord-text-muted hover:text-discord-text-normal"
         onClick={onClose}
       >
         <FaTimes />
       </button>
     </div>
   );
 }
src/lib/messageProtocol.ts (1)

114-116: Replace deprecated substr with substring or slice.

The substr method is deprecated. Use substring or slice instead.

Apply this diff:

 export const createBatchId = (): string => {
-  return `ml_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+  return `ml_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
 };
src/components/ui/InputToolbar.tsx (1)

34-42: Simplify color swatch logic.

The backgroundColor logic can be simplified by removing the redundant ?? undefined.

Apply this diff:

       <div
         className="w-4 h-4 rounded-full border-2 border-white-700"
         style={{
           backgroundColor:
-            selectedColor === "inherit"
-              ? "transparent"
-              : (selectedColor ?? undefined),
+            selectedColor === "inherit" ? "transparent" : selectedColor || undefined,
         }}
       />
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9632acf and 0764aef.

📒 Files selected for processing (17)
  • compose.yaml (1 hunks)
  • src/components/layout/ChatArea.tsx (19 hunks)
  • src/components/layout/ChatHeader.tsx (1 hunks)
  • src/components/message/MessageItem.tsx (1 hunks)
  • src/components/ui/EmojiPickerModal.tsx (1 hunks)
  • src/components/ui/ImagePreviewModal.tsx (1 hunks)
  • src/components/ui/InputToolbar.tsx (1 hunks)
  • src/components/ui/LinkWrapper.tsx (2 hunks)
  • src/components/ui/ReplyBadge.tsx (1 hunks)
  • src/components/ui/ScrollToBottomButton.tsx (1 hunks)
  • src/hooks/useMessageHistory.ts (1 hunks)
  • src/hooks/useMessageSending.ts (1 hunks)
  • src/hooks/useReactions.ts (1 hunks)
  • src/hooks/useTypingNotification.ts (1 hunks)
  • src/lib/messageFormatter.ts (1 hunks)
  • src/lib/messageProtocol.ts (1 hunks)
  • tests/components/StandardReplyNotification.test.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (8)
src/hooks/useMessageSending.ts (5)
src/types/index.ts (4)
  • Channel (47-60)
  • PrivateChat (62-69)
  • User (1-13)
  • Message (76-118)
src/lib/messageFormatter.ts (2)
  • FormattingType (8-14)
  • formatMessageForIrc (104-120)
src/lib/ircClient.ts (3)
  • ircClient (1704-1704)
  • sendMultilineMessage (440-464)
  • sendMessage (420-438)
src/lib/messageProtocol.ts (2)
  • createBatchId (114-116)
  • splitLongMessage (11-81)
src/store/index.ts (1)
  • serverSupportsMultiline (136-136)
src/hooks/useReactions.ts (2)
src/types/index.ts (4)
  • Message (76-118)
  • User (1-13)
  • Server (15-32)
  • Channel (47-60)
src/lib/ircClient.ts (1)
  • ircClient (1704-1704)
src/components/ui/ReplyBadge.tsx (1)
src/types/index.ts (1)
  • Message (76-118)
src/components/layout/ChatHeader.tsx (2)
src/types/index.ts (2)
  • Channel (47-60)
  • PrivateChat (62-69)
src/lib/ircClient.ts (1)
  • ircClient (1704-1704)
tests/components/StandardReplyNotification.test.tsx (1)
src/components/ui/StandardReplyNotification.tsx (1)
  • StandardReplyNotification (20-96)
src/hooks/useTypingNotification.ts (1)
src/lib/ircClient.ts (1)
  • ircClient (1704-1704)
src/hooks/useMessageHistory.ts (2)
src/types/index.ts (1)
  • Message (76-118)
src/lib/messageFormatter.ts (1)
  • stripIrcFormatting (178-186)
src/components/layout/ChatArea.tsx (11)
src/hooks/useTypingNotification.ts (1)
  • useTypingNotification (22-80)
src/hooks/useMediaQuery.ts (1)
  • useMediaQuery (3-15)
src/hooks/useMessageSending.ts (1)
  • useMessageSending (35-332)
src/hooks/useReactions.ts (1)
  • useReactions (31-157)
src/hooks/useMessageHistory.ts (1)
  • useMessageHistory (28-124)
src/components/layout/ChatHeader.tsx (1)
  • ChatHeader (30-151)
src/components/ui/ScrollToBottomButton.tsx (1)
  • ScrollToBottomButton (10-35)
src/components/ui/ReplyBadge.tsx (1)
  • ReplyBadge (15-29)
src/components/ui/InputToolbar.tsx (1)
  • InputToolbar (16-52)
src/components/ui/EmojiPickerModal.tsx (1)
  • EmojiPickerModal (17-57)
src/components/ui/ImagePreviewModal.tsx (1)
  • ImagePreviewModal (16-60)
🔇 Additional comments (7)
tests/components/StandardReplyNotification.test.tsx (1)

128-143: Excellent improvement for timezone-independent testing!

Replacing the hardcoded expected time with dynamic formatting using Intl.DateTimeFormat makes this test robust across different timezone environments. The test now uses the exact same formatting logic as the component's formatTime method, ensuring the assertion accurately reflects the component's actual behavior regardless of where the test runs.

src/components/ui/LinkWrapper.tsx (2)

51-60: LGTM! Well-designed URL truncation logic.

The middle ellipsis truncation with a configurable maxLength (default 60) provides a good balance between readability and space efficiency. The logic correctly accounts for the ellipsis characters when calculating front and back segments.


84-88: Excellent accessibility and layout improvements.

Adding the title attribute provides full URL on hover, while break-all ensures long URLs wrap properly. The truncateUrl call keeps the displayed text concise.

src/components/message/MessageItem.tsx (1)

313-322: Proper overflow and wrapping behavior for message content.

The combination of overflow-hidden, whiteSpace: "pre-wrap", and break-word settings ensures that long messages and URLs are properly contained and wrapped without breaking the layout.

src/lib/messageFormatter.ts (1)

166-186: LGTM! Proper IRC formatting code removal.

The stripIrcFormatting function correctly removes both color codes and formatting control characters using appropriate regex patterns. The biome-ignore comments properly document why control characters are acceptable in this context.

src/components/ui/ScrollToBottomButton.tsx (1)

17-34: Verify positioning approach for floating button.

The button uses relative bottom-10 (line 18) with a nested absolute right-4 (line 19). This creates relative positioning for the outer div and absolute positioning for the inner div. If this button should float over content regardless of scroll position, consider using fixed positioning instead.

If fixed positioning is intended, apply this diff:

   return (
-    <div className="relative bottom-10 z-50">
-      <div className="absolute right-4">
+    <div className="fixed bottom-10 right-4 z-50">
         <button
           onClick={onClick}
           className="bg-discord-dark-400 hover:bg-discord-dark-300 text-white rounded-full p-2 shadow-lg transition-all relative"
           aria-label="Scroll to bottom"
         >
           <FaArrowDown className="text-white" />
           {unreadCount && unreadCount > 0 && (
             <span className="absolute -top-2 -right-2 bg-discord-accent text-white text-xs font-bold rounded-full h-5 min-w-5 flex items-center justify-center px-1">
               {unreadCount > 99 ? "99+" : unreadCount}
             </span>
           )}
         </button>
-      </div>
     </div>
   );
src/components/ui/InputToolbar.tsx (1)

35-35: Verify border color class.

The class border-white-700 may be non-standard. Tailwind CSS typically uses border-gray-700 or similar. Verify this is the intended color.

Comment thread compose.yaml
init: true
# TODO: Use our unrealircd custom image instead
image: ghcr.io/ergochat/ergo
image: ghcr.io/ergochat/ergo:master
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 | 🟠 Major

Pin ircd image to a stable release tag.

Pulling ghcr.io/ergochat/ergo:master makes deployments non-deterministic and can break whenever upstream pushes an unstable commit. Please stick to a published release tag (e.g., :stable or a specific version) to keep the environment reproducible.

-    image: ghcr.io/ergochat/ergo:master
+    image: ghcr.io/ergochat/ergo:v2.14.1
📝 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
image: ghcr.io/ergochat/ergo:master
image: ghcr.io/ergochat/ergo:v2.14.1
🤖 Prompt for AI Agents
In compose.yaml around line 20 the ircd image is pinned to
ghcr.io/ergochat/ergo:master which is non-deterministic; change it to a stable
release tag (for example ghcr.io/ergochat/ergo:stable or a specific version like
ghcr.io/ergochat/ergo:vX.Y.Z), update any CI/deployment manifests or .env where
the image tag is referenced to use the chosen stable tag, and commit the updated
compose.yaml so deployments pull a reproducible image.

FaSearch,
FaUserPlus,
} from "react-icons/fa";
import ircClient from "../../lib/ircClient";
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 | 🔴 Critical

Use the named ircClient export.

Line 15 default-imports ircClient, but src/lib/ircClient.ts exposes it as a named export, so this resolves to undefined and any subsequent calls fail. Switch to the named import.

-import ircClient from "../../lib/ircClient";
+import { ircClient } from "../../lib/ircClient";
📝 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
import ircClient from "../../lib/ircClient";
import { ircClient } from "../../lib/ircClient";
🤖 Prompt for AI Agents
In src/components/layout/ChatHeader.tsx around line 15, the file currently
default-imports ircClient but src/lib/ircClient.ts exports it as a named export;
replace the default import with a named import (use import { ircClient } from
"../../lib/ircClient";) and update any related usages if they assumed a default
to ensure the imported symbol is defined.

Comment on lines +25 to +56
return createPortal(
<div
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
onClick={onBackdropClick}
>
<div className="bg-discord-dark-400 rounded-lg shadow-lg border border-discord-dark-300 max-w-sm w-full mx-4 max-h-[90vh] overflow-hidden">
<div className="p-2">
<EmojiPicker
onEmojiClick={onEmojiClick}
theme={Theme.DARK}
width="100%"
height={400}
searchPlaceholder="Search emojis..."
previewConfig={{
showPreview: false,
}}
skinTonesDisabled={false}
lazyLoadEmojis={true}
/>
</div>
<div className="p-2 border-t border-discord-dark-300">
<button
onClick={onClose}
className="text-sm text-discord-text-muted hover:text-white w-full text-center py-1"
>
Cancel
</button>
</div>
</div>
</div>,
document.body,
);
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 | 🟠 Major

Prevent backdrop click handler from firing on modal content clicks.

Clicking inside the modal content (line 30) will bubble up to the backdrop div (line 27) and trigger onBackdropClick. Add onClick={(e) => e.stopPropagation()} to the inner modal div to prevent this.

Apply this diff:

   return createPortal(
     <div
       className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
       onClick={onBackdropClick}
     >
       <div className="bg-discord-dark-400 rounded-lg shadow-lg border border-discord-dark-300 max-w-sm w-full mx-4 max-h-[90vh] overflow-hidden"
+        onClick={(e) => e.stopPropagation()}
       >
         <div className="p-2">
           <EmojiPicker
             onEmojiClick={onEmojiClick}
             theme={Theme.DARK}
             width="100%"
             height={400}
             searchPlaceholder="Search emojis..."
             previewConfig={{
               showPreview: false,
             }}
             skinTonesDisabled={false}
             lazyLoadEmojis={true}
           />
         </div>
         <div className="p-2 border-t border-discord-dark-300">
           <button
             onClick={onClose}
             className="text-sm text-discord-text-muted hover:text-white w-full text-center py-1"
           >
             Cancel
           </button>
         </div>
       </div>
     </div>,
     document.body,
   );
🤖 Prompt for AI Agents
In src/components/ui/EmojiPickerModal.tsx around lines 25 to 56, clicks inside
the inner modal div bubble to the backdrop and invoke onBackdropClick; to fix,
add an onClick handler to the inner modal container that calls
e.stopPropagation() (e.g., onClick={(e) => e.stopPropagation()}) so clicks on
modal content don’t close the modal, leaving the backdrop's onClick intact.

Comment on lines +46 to +49
useEffect(() => {
setMessageHistoryIndex(-1);
setDraftText("");
}, []);
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 | 🟠 Major

Reset history when channel/private chat/current user changes.

Line 46’s effect is intended to clear history on channel/chat switches, but the empty dependency array means it only fires once on mount. Users will keep seeing the previous channel’s history, which is incorrect. Add the relevant dependencies so the reset actually runs.

-  useEffect(() => {
-    setMessageHistoryIndex(-1);
-    setDraftText("");
-  }, []);
+  useEffect(() => {
+    setMessageHistoryIndex(-1);
+    setDraftText("");
+  }, [selectedChannelId, selectedPrivateChatId, currentUsername]);
📝 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
useEffect(() => {
setMessageHistoryIndex(-1);
setDraftText("");
}, []);
useEffect(() => {
setMessageHistoryIndex(-1);
setDraftText("");
}, [selectedChannelId, selectedPrivateChatId, currentUsername]);
🤖 Prompt for AI Agents
In src/hooks/useMessageHistory.ts around lines 46 to 49, the useEffect currently
runs only on mount (empty dependency array) but needs to reset history whenever
the active channel/chat or current user changes; update the dependency array to
include the relevant identifiers (e.g. channelId, chatId or conversationId, and
currentUserId/currentUser) so the effect re-runs on those changes and calls
setMessageHistoryIndex(-1) and setDraftText("") accordingly.

Comment on lines +71 to +92
} else if (commandName === "part") {
ircClient.leaveChannel(selectedServerId, args[0]);
ircClient.triggerEvent("PART", {
serverId: selectedServerId,
username: currentUser?.username || "",
channelName: args[0],
});
} else if (commandName === "msg") {
const [target, ...messageParts] = args;
const message = messageParts.join(" ");
ircClient.sendRaw(selectedServerId, `PRIVMSG ${target} :${message}`);
} else if (commandName === "me") {
const actionMessage = cleanedText.substring(4).trim();
ircClient.sendRaw(
selectedServerId,
`PRIVMSG ${selectedChannel?.name || ""} :\u0001ACTION ${actionMessage}\u0001`,
);
} else {
const fullCommand =
args.length > 0 ? `${commandName} ${args.join(" ")}` : commandName;
ircClient.sendRaw(selectedServerId, fullCommand);
}
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 | 🟠 Major

Fix /part default target handling.

If the user types /part without arguments we now call leaveChannel(selectedServerId, args[0]) with args[0] === undefined, and then trigger a PART event with an empty channel. The server never sees a valid PART, so the user cannot leave the current channel. Please fall back to the currently selected channel (and bail with a warning if none is selected) before invoking leaveChannel.

-      } else if (commandName === "part") {
-        ircClient.leaveChannel(selectedServerId, args[0]);
+      } else if (commandName === "part") {
+        const partTarget = args[0] || selectedChannel?.name;
+        if (!partTarget) {
+          console.error("No channel specified for /part command");
+          return;
+        }
+        ircClient.leaveChannel(selectedServerId, partTarget);
         ircClient.triggerEvent("PART", {
           serverId: selectedServerId,
           username: currentUser?.username || "",
-          channelName: args[0],
+          channelName: partTarget,
         });
📝 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
} else if (commandName === "part") {
ircClient.leaveChannel(selectedServerId, args[0]);
ircClient.triggerEvent("PART", {
serverId: selectedServerId,
username: currentUser?.username || "",
channelName: args[0],
});
} else if (commandName === "msg") {
const [target, ...messageParts] = args;
const message = messageParts.join(" ");
ircClient.sendRaw(selectedServerId, `PRIVMSG ${target} :${message}`);
} else if (commandName === "me") {
const actionMessage = cleanedText.substring(4).trim();
ircClient.sendRaw(
selectedServerId,
`PRIVMSG ${selectedChannel?.name || ""} :\u0001ACTION ${actionMessage}\u0001`,
);
} else {
const fullCommand =
args.length > 0 ? `${commandName} ${args.join(" ")}` : commandName;
ircClient.sendRaw(selectedServerId, fullCommand);
}
} else if (commandName === "part") {
const partTarget = args[0] || selectedChannel?.name;
if (!partTarget) {
console.error("No channel specified for /part command");
return;
}
ircClient.leaveChannel(selectedServerId, partTarget);
ircClient.triggerEvent("PART", {
serverId: selectedServerId,
username: currentUser?.username || "",
channelName: partTarget,
});
} else if (commandName === "msg") {
const [target, ...messageParts] = args;
const message = messageParts.join(" ");
ircClient.sendRaw(selectedServerId, `PRIVMSG ${target} :${message}`);
}

Comment on lines +104 to +175
const batchId = createBatchId();
const replyPrefix = localReplyTo
? `@+draft/reply=${localReplyTo.id};`
: "";

ircClient.sendRaw(
selectedServerId,
`${replyPrefix}BATCH +${batchId} draft/multiline ${target}`,
);

const hasMultipleLines = lines.length > 1;

if (hasMultipleLines) {
// Multi-line message (preserve line breaks)
lines.forEach((line) => {
const formattedLine = formatMessageForIrc(line, {
color: selectedColor || "inherit",
formatting: selectedFormatting,
});

const maxLineLengthForTarget =
512 -
(1 + 20 + 1 + 20 + 1 + 63 + 1 + 7 + 1 + target.length + 2 + 2) -
10;

if (formattedLine.length > maxLineLengthForTarget) {
const splitLines = splitLongMessage(formattedLine, target);
splitLines.forEach((splitLine: string, index: number) => {
if (index === 0) {
ircClient.sendRaw(
selectedServerId,
`@batch=${batchId} PRIVMSG ${target} :${splitLine}`,
);
} else {
ircClient.sendRaw(
selectedServerId,
`@batch=${batchId};draft/multiline-concat PRIVMSG ${target} :${splitLine}`,
);
}
});
} else {
ircClient.sendRaw(
selectedServerId,
`@batch=${batchId} PRIVMSG ${target} :${formattedLine}`,
);
}
});
} else {
// Single very long line (split and concat)
const formattedText = formatMessageForIrc(cleanedText, {
color: selectedColor || "inherit",
formatting: selectedFormatting,
});

const splitLines = splitLongMessage(formattedText, target);
splitLines.forEach((splitLine: string, index: number) => {
if (index === 0) {
ircClient.sendRaw(
selectedServerId,
`@batch=${batchId} PRIVMSG ${target} :${splitLine}`,
);
} else {
ircClient.sendRaw(
selectedServerId,
`@batch=${batchId};draft/multiline-concat PRIVMSG ${target} :${splitLine}`,
);
}
});
}

ircClient.sendRaw(selectedServerId, `BATCH -${batchId}`);
},
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 | 🔴 Critical

Restore valid IRC tags in batched replies.

When localReplyTo is set, replyPrefix is @+draft/reply=…; but we concatenate it directly with BATCH so the command becomes @+draft/reply=foo;BATCH … (missing the required space). Every server I tested rejects that line. In addition, the subsequent PRIVMSG lines inside the batch never receive the +draft/reply tag, so multiline replies lose their reply metadata altogether. Please split the tag handling so the start line keeps @… BATCH … and the PRIVMSGs include both the batch id and the reply tag. Example fix:

-      const replyPrefix = localReplyTo
-        ? `@+draft/reply=${localReplyTo.id};`
-        : "";
-
-      ircClient.sendRaw(
-        selectedServerId,
-        `${replyPrefix}BATCH +${batchId} draft/multiline ${target}`,
-      );
+      const replyTag = localReplyTo
+        ? `+draft/reply=${localReplyTo.id}`
+        : null;
+      const batchStartPrefix = replyTag
+        ? `@${replyTag} `
+        : "";
+      ircClient.sendRaw(
+        selectedServerId,
+        `${batchStartPrefix}BATCH +${batchId} draft/multiline ${target}`,
+      );
+      const mkPrivmsgPrefix = (includeConcat: boolean) => {
+        const tags = [
+          `batch=${batchId}`,
+          includeConcat ? "draft/multiline-concat" : null,
+          replyTag,
+        ]
+          .filter(Boolean)
+          .join(";");
+        return tags ? `@${tags} ` : "";
+      };

and replace the hard-coded @batch=… literals below with mkPrivmsgPrefix(index > 0). That keeps the wire format valid and preserves reply tags on every line.

🤖 Prompt for AI Agents
In src/hooks/useMessageSending.ts around lines 104–175, the BATCH start line is
concatenating the reply tag directly with "BATCH" (e.g. "@+draft/reply=…;BATCH
…") which is invalid and the PRIVMSG lines never carry the reply tag; fix by:
add a space between the reply tag and the BATCH token so the start line becomes
"<replyTag> BATCH …", and ensure every PRIVMSG line includes both the batch tag
and the reply tag (not just "@batch=…"); implement a small helper (e.g.
mkPrivmsgPrefix(isConcat:boolean)) that returns the correct tag prefix for each
line — first segment and subsequent concat segments — combining "@batch=<id>"
and the draft/reply tag when present, then replace all hard-coded "@batch=…"
literals with calls to that helper so multiline messages preserve valid IRC tag
syntax and keep reply metadata on every line.

Comment thread src/hooks/useReactions.ts
* Hook for managing message reactions and reaction modal
*/
import { useCallback, useState } from "react";
import ircClient from "../lib/ircClient";
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 | 🔴 Critical

Fix ircClient import to match named export.

Line 5 imports ircClient as a default, but src/lib/ircClient.ts (Line 1703) only exports a named constant. With ES module interop, the default import becomes undefined, so every sendRaw call throws. Switch to the named import here (and in other new files).

-import ircClient from "../lib/ircClient";
+import { ircClient } from "../lib/ircClient";
📝 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
import ircClient from "../lib/ircClient";
import { ircClient } from "../lib/ircClient";
🤖 Prompt for AI Agents
In src/hooks/useReactions.ts around line 5, the file currently imports ircClient
as a default (import ircClient from "../lib/ircClient";) but
src/lib/ircClient.ts only exports a named constant, causing the import to be
undefined; change the import to the named export (import { ircClient } from
"../lib/ircClient";) and update any other new files that used the default import
to use the named import; ensure any references to the variable remain unchanged
and run the build/tests to verify sendRaw calls no longer throw.

* Consolidates typing notification logic and throttling
*/
import { useCallback, useRef } from "react";
import ircClient from "../lib/ircClient";
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 | 🔴 Critical

Align ircClient import with named export.

Line 6 default-imports ircClient, but src/lib/ircClient.ts only exports it by name, so this evaluates to undefined at runtime and breaks typing notifications. Please use the named import.

-import ircClient from "../lib/ircClient";
+import { ircClient } from "../lib/ircClient";
🤖 Prompt for AI Agents
In src/hooks/useTypingNotification.ts around line 6 the import currently uses a
default import for ircClient but the module at src/lib/ircClient.ts exports it
as a named export, which causes ircClient to be undefined at runtime; change the
import to a named import (import { ircClient } from "../lib/ircClient";) and
update any local references if necessary to use the named binding.

Comment thread src/lib/messageProtocol.ts
matheusfillipe and others added 2 commits October 8, 2025 21:53
Resolved conflicts in ChatArea.tsx by preserving both:
- Message history navigation with arrow keys (from feat/keyup-to-history)
- Search functionality, message virtualization, and new modals (from main)
- useMessageSending, useReactions hooks (from feat/keyup-to-history)
- All UI features and moderation capabilities (from main)

All tests passing, code formatted and linted.
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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/message/MessageItem.tsx (1)

100-124: Remove duplicate error handling block.

The imageError check and fallback UI are duplicated (lines 100-111 and 113-124). The second block is unreachable and serves no purpose.

Apply this diff to remove the duplicate code:

   if (imageError) {
     // Fallback to showing expired badge
     return (
       <div className="max-w-md">
         <div className="bg-gray-100 border border-gray-300 rounded-lg p-4 text-center">
           <div className="inline-flex items-center px-3 py-1 rounded-full text-sm bg-red-100 text-red-800 border border-red-200">
             <span>This image has expired</span>
           </div>
         </div>
       </div>
     );
   }
-
-  if (imageError) {
-    // Fallback to showing expired badge
-    return (
-      <div className="max-w-md">
-        <div className="bg-gray-100 border border-gray-300 rounded-lg p-4 text-center">
-          <div className="inline-flex items-center px-3 py-1 rounded-full text-sm bg-red-100 text-red-800 border border-red-200">
-            <span>This image has expired</span>
-          </div>
-        </div>
-      </div>
-    );
-  }
 
   return (
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eb506b2 and 42a650d.

📒 Files selected for processing (3)
  • src/components/layout/ChatArea.tsx (22 hunks)
  • src/components/message/MessageItem.tsx (1 hunks)
  • tests/components/StandardReplyNotification.test.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/components/StandardReplyNotification.test.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
src/components/layout/ChatArea.tsx (10)
src/hooks/useTypingNotification.ts (1)
  • useTypingNotification (22-80)
src/hooks/useMediaQuery.ts (1)
  • useMediaQuery (3-15)
src/hooks/useMessageSending.ts (1)
  • useMessageSending (35-332)
src/hooks/useReactions.ts (1)
  • useReactions (31-157)
src/hooks/useMessageHistory.ts (1)
  • useMessageHistory (28-124)
src/components/ui/ScrollToBottomButton.tsx (1)
  • ScrollToBottomButton (10-35)
src/components/ui/ReplyBadge.tsx (1)
  • ReplyBadge (15-29)
src/components/ui/InputToolbar.tsx (1)
  • InputToolbar (16-52)
src/components/ui/EmojiPickerModal.tsx (1)
  • EmojiPickerModal (17-57)
src/components/ui/ImagePreviewModal.tsx (1)
  • ImagePreviewModal (16-60)
🔇 Additional comments (1)
src/components/message/MessageItem.tsx (1)

496-505: Word-breaking implementation looks good.

The CSS changes correctly handle long content by breaking words across lines. The combination of overflow-hidden with break-word properties ensures long URLs and text wrap properly.

Note: The PR mentions "truncate long URLs", but this implementation breaks URLs across multiple lines rather than shortening them with ellipsis. If actual truncation (e.g., displaying "https://exa...") is desired, consider using text-overflow: ellipsis with white-space: nowrap instead.

Comment on lines +687 to +709
if (e.key === "ArrowUp") {
// Only activate if input is empty or already in history mode
if (messageText === "" || messageHistory.messageHistoryIndex >= 0) {
e.preventDefault();

if (messageHistory.userMessageHistory.length === 0) return;

const previousMessage = messageHistory.navigateUp(messageText);
if (previousMessage !== null) {
setMessageText(previousMessage);

// Move cursor to end of text
setTimeout(() => {
if (inputRef.current) {
inputRef.current.setSelectionRange(
previousMessage.length,
previousMessage.length,
);
}
}, 0);
}
}
return;
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 | 🟠 Major

Don't intercept ArrowUp when no history exists

e.preventDefault() fires before we know whether any history entries are available, so on an empty history (fresh channel, PM with no messages, etc.) ArrowUp is swallowed and the caret won't move. Reset history state (if needed) and only call preventDefault when a history entry is actually available.

     if (e.key === "ArrowUp") {
       // Only activate if input is empty or already in history mode
       if (messageText === "" || messageHistory.messageHistoryIndex >= 0) {
-        e.preventDefault();
-
-        if (messageHistory.userMessageHistory.length === 0) return;
+        if (messageHistory.userMessageHistory.length === 0) {
+          if (messageHistory.messageHistoryIndex >= 0) {
+            messageHistory.resetHistory();
+          }
+          return;
+        }
+
+        e.preventDefault();
 
         const previousMessage = messageHistory.navigateUp(messageText);
         if (previousMessage !== null) {
📝 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
if (e.key === "ArrowUp") {
// Only activate if input is empty or already in history mode
if (messageText === "" || messageHistory.messageHistoryIndex >= 0) {
e.preventDefault();
if (messageHistory.userMessageHistory.length === 0) return;
const previousMessage = messageHistory.navigateUp(messageText);
if (previousMessage !== null) {
setMessageText(previousMessage);
// Move cursor to end of text
setTimeout(() => {
if (inputRef.current) {
inputRef.current.setSelectionRange(
previousMessage.length,
previousMessage.length,
);
}
}, 0);
}
}
return;
if (e.key === "ArrowUp") {
// Only activate if input is empty or already in history mode
if (messageText === "" || messageHistory.messageHistoryIndex >= 0) {
if (messageHistory.userMessageHistory.length === 0) {
if (messageHistory.messageHistoryIndex >= 0) {
messageHistory.resetHistory();
}
return;
}
e.preventDefault();
const previousMessage = messageHistory.navigateUp(messageText);
if (previousMessage !== null) {
setMessageText(previousMessage);
// Move cursor to end of text
setTimeout(() => {
if (inputRef.current) {
inputRef.current.setSelectionRange(
previousMessage.length,
previousMessage.length,
);
}
}, 0);
}
}
return;
🤖 Prompt for AI Agents
In src/components/layout/ChatArea.tsx around lines 687 to 709, the key handler
calls e.preventDefault() before confirming any history entries exist causing
ArrowUp to be swallowed on empty history; change the logic so you first check if
messageHistory.userMessageHistory.length === 0 (and if so reset history state if
needed and return) and only call e.preventDefault() after confirming a history
entry exists (just before calling messageHistory.navigateUp and updating
setMessageText), keeping the rest of the cursor positioning logic the same.


// Get current user's status in the selected channel
const currentUserStatus = useMemo(() => {
if (!selectedServerId || !selectedChannelId) return undefined;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This component was a refactor. It stil lexists as a duplicate. Either remove it or put this code into it

Comment thread src/components/layout/ChatArea.tsx Outdated
onToggleMemberList={() => toggleMemberList(!isMemberListVisible)}
/>
<div className="min-h-[56px] px-4 border-b border-discord-dark-400 flex flex-wrap items-start md:items-center justify-between shadow-sm py-2 md:py-0 md:h-12 gap-y-2">
<div className="flex items-center flex-1 min-w-0 w-full md:w-auto">
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

could also be a new component

@matheusfillipe
Copy link
Copy Markdown
Contributor Author

@ValwareIRC feel free to either remove the duplicated code or refactor into it. Then merge

- Moved all inline header JSX from ChatArea.tsx to ChatHeader.tsx
- Added state management for topic editing (isEditingTopic, editedTopic)
- Added props for all required callbacks and data
- Removed ~260 lines of inline JSX from ChatArea.tsx
- ChatHeader now handles:
  * Channel/private chat/server notices display
  * Topic editing with inline form
  * Search functionality
  * Notification toggle
  * Channel settings, invite user, rename buttons
  * Member list toggle
  * Channel avatar support with fallback

This makes ChatArea.tsx cleaner and ChatHeader.tsx more reusable.
All tests passing ✅
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: 1

♻️ Duplicate comments (2)
src/components/layout/ChatArea.tsx (1)

665-686: ArrowUp still swallows caret with empty history

preventDefault() fires before confirming history exists, so a fresh input can no longer move the caret/home on ArrowUp—exactly the issue from the prior review. Check for history first (reset if needed) and only then intercept the key.

     if (e.key === "ArrowUp") {
       // Only activate if input is empty or already in history mode
       if (messageText === "" || messageHistory.messageHistoryIndex >= 0) {
-        e.preventDefault();
-
-        if (messageHistory.userMessageHistory.length === 0) return;
+        if (messageHistory.userMessageHistory.length === 0) {
+          if (messageHistory.messageHistoryIndex >= 0) {
+            messageHistory.resetHistory();
+          }
+          return;
+        }
+
+        e.preventDefault();
 
         const previousMessage = messageHistory.navigateUp(messageText);
src/components/layout/ChatHeader.tsx (1)

17-17: Use the named ircClient export.

Line 17 default-imports ircClient, but src/lib/ircClient.ts exposes it as a named export (line 2126), so this resolves to undefined and any subsequent calls (lines 73, 141) will fail.

Apply this diff:

-import ircClient from "../../lib/ircClient";
+import { ircClient } from "../../lib/ircClient";
🧹 Nitpick comments (2)
src/components/layout/ChatHeader.tsx (2)

179-211: Eliminate duplicate operator-permission checks.

The same logic (finding currentUserInChannel and calling hasOpPermission) appears in both onClick (lines 182–188) and title (lines 195–202). Extract it once before the button.

Apply this diff:

             ) : (
+              (() => {
+                const currentUserInChannel = selectedChannel?.users.find(
+                  (u) => u.username === currentUser?.username,
+                );
+                const canEdit = hasOpPermission(currentUserInChannel?.status);
+                return (
               <button
                 onClick={() => {
                   if (selectedChannel) {
-                    const currentUserInChannel = selectedChannel.users.find(
-                      (u) => u.username === currentUser?.username,
-                    );
-                    if (hasOpPermission(currentUserInChannel?.status)) {
+                    if (canEdit) {
                       setEditedTopic(selectedChannel.topic || "");
                       setIsEditingTopic(true);
                     }
                   }
                 }}
                 className="text-discord-text-muted text-xs md:text-sm hover:text-white truncate min-w-0 md:max-w-md mt-0.5 mb-1 md:mt-0 md:mb-0"
                 title={
                   selectedChannel.topic
-                    ? `Topic: ${selectedChannel.topic}${
-                        selectedChannel.users.find(
-                          (u) => u.username === currentUser?.username,
-                        ) &&
-                        hasOpPermission(
-                          selectedChannel.users.find(
-                            (u) => u.username === currentUser?.username,
-                          )?.status,
-                        )
-                          ? " (Click to edit)"
-                          : ""
-                      }`
+                    ? `Topic: ${selectedChannel.topic}${canEdit ? " (Click to edit)" : ""}`
                     : "No topic set"
                 }
               >
                 {selectedChannel.topic || "No topic"}
               </button>
+                );
+              })()
             )}

99-117: Consider state-based fallback instead of direct DOM manipulation.

The onError handler directly manipulates the DOM (lines 106–113) to hide the image and show the fallback icon, which bypasses React's virtual DOM reconciliation.

Refactor to use state for a more idiomatic React approach:

+  const [avatarError, setAvatarError] = useState(false);
+
   // ... inside the render:
 
   {getChannelAvatarUrl(selectedChannel.metadata, 50) ? (
+    !avatarError ? (
       <img
         src={getChannelAvatarUrl(selectedChannel.metadata, 50)}
         alt={selectedChannel.name}
         className="w-12 h-12 rounded-full object-cover mr-2 flex-shrink-0"
         onError={(e) => {
-          // Fallback to # icon on error
-          e.currentTarget.style.display = "none";
-          const parent = e.currentTarget.parentElement;
-          const fallbackIcon = parent?.querySelector(
-            ".fallback-hash-icon",
-          );
-          if (fallbackIcon) {
-            (fallbackIcon as HTMLElement).style.display =
-              "inline-block";
-          }
+          setAvatarError(true);
         }}
       />
+    ) : (
+      <FaHashtag className="text-discord-text-muted mr-2 flex-shrink-0 text-3xl" />
+    )
   ) : (
+    <FaHashtag className="text-discord-text-muted mr-2 flex-shrink-0 text-3xl" />
-    null
   )}
-  <FaHashtag
-    className="text-discord-text-muted mr-2 fallback-hash-icon flex-shrink-0 text-3xl"
-    style={{
-      display: getChannelAvatarUrl(selectedChannel.metadata, 50)
-        ? "none"
-        : "inline-block",
-    }}
-  />
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 42a650d and 2882a3f.

📒 Files selected for processing (2)
  • src/components/layout/ChatArea.tsx (21 hunks)
  • src/components/layout/ChatHeader.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/components/layout/ChatHeader.tsx (3)
src/types/index.ts (3)
  • Channel (51-69)
  • PrivateChat (71-78)
  • User (1-14)
src/lib/ircClient.ts (1)
  • ircClient (2127-2127)
src/lib/ircUtils.tsx (3)
  • getChannelAvatarUrl (339-354)
  • getChannelDisplayName (324-334)
  • hasOpPermission (360-370)
src/components/layout/ChatArea.tsx (11)
src/hooks/useTypingNotification.ts (1)
  • useTypingNotification (22-80)
src/hooks/useMediaQuery.ts (1)
  • useMediaQuery (3-15)
src/hooks/useMessageSending.ts (1)
  • useMessageSending (35-332)
src/hooks/useReactions.ts (1)
  • useReactions (31-157)
src/hooks/useMessageHistory.ts (1)
  • useMessageHistory (28-124)
src/components/layout/ChatHeader.tsx (1)
  • ChatHeader (47-332)
src/components/ui/ScrollToBottomButton.tsx (1)
  • ScrollToBottomButton (10-35)
src/components/ui/ReplyBadge.tsx (1)
  • ReplyBadge (15-29)
src/components/ui/InputToolbar.tsx (1)
  • InputToolbar (16-52)
src/components/ui/EmojiPickerModal.tsx (1)
  • EmojiPickerModal (17-57)
src/components/ui/ImagePreviewModal.tsx (1)
  • ImagePreviewModal (16-60)

Comment on lines +71 to +82
const isOperator = (() => {
if (!selectedChannel || !selectedServerId) return false;
const serverCurrentUser = ircClient.getCurrentUser(selectedServerId);
if (!serverCurrentUser) return false;

const channelUser = selectedChannel.users.find(
(u) => u.username === serverCurrentUser.username,
);
return (
channelUser?.status?.includes("@") || channelUser?.status?.includes("~")
);
})();
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 | 🟠 Major

Incomplete operator check excludes admins.

The isOperator check only tests for @ (op) and ~ (owner) but omits & (admin). Admins should have operator privileges, so they'll incorrectly be denied topic editing and channel renaming.

Use the existing hasOpPermission utility (already imported line 21) for consistency:

   // Check if current user is operator
-  const isOperator = (() => {
+  const isOperator = (() => {
     if (!selectedChannel || !selectedServerId) return false;
     const serverCurrentUser = ircClient.getCurrentUser(selectedServerId);
     if (!serverCurrentUser) return false;

     const channelUser = selectedChannel.users.find(
       (u) => u.username === serverCurrentUser.username,
     );
-    return (
-      channelUser?.status?.includes("@") || channelUser?.status?.includes("~")
-    );
+    return hasOpPermission(channelUser?.status);
   })();
📝 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 isOperator = (() => {
if (!selectedChannel || !selectedServerId) return false;
const serverCurrentUser = ircClient.getCurrentUser(selectedServerId);
if (!serverCurrentUser) return false;
const channelUser = selectedChannel.users.find(
(u) => u.username === serverCurrentUser.username,
);
return (
channelUser?.status?.includes("@") || channelUser?.status?.includes("~")
);
})();
const isOperator = (() => {
if (!selectedChannel || !selectedServerId) return false;
const serverCurrentUser = ircClient.getCurrentUser(selectedServerId);
if (!serverCurrentUser) return false;
const channelUser = selectedChannel.users.find(
(u) => u.username === serverCurrentUser.username,
);
return hasOpPermission(channelUser?.status);
})();
🤖 Prompt for AI Agents
In src/components/layout/ChatHeader.tsx around lines 71 to 82, the inline
isOperator check only looks for "@" and "~" and thus excludes "&" (admin);
replace this manual status substring logic with a call to the existing
hasOpPermission utility (already imported on line 21) to determine operator
privileges consistently — get the channelUser as currently done and pass it (or
its status) to hasOpPermission, then use the boolean result for isOperator so
admins are correctly granted operator rights.

@ValwareIRC ValwareIRC merged commit 9801d3b into main Oct 10, 2025
4 checks passed
zocram4cc pushed a commit to zocram4cc/ObsidianIRC that referenced this pull request Feb 17, 2026
* keyup

* refactor chat area

* truncate long urls

* DRY coderabit suggestion

* refactor: Extract header logic from ChatArea into ChatHeader component

- Moved all inline header JSX from ChatArea.tsx to ChatHeader.tsx
- Added state management for topic editing (isEditingTopic, editedTopic)
- Added props for all required callbacks and data
- Removed ~260 lines of inline JSX from ChatArea.tsx
- ChatHeader now handles:
  * Channel/private chat/server notices display
  * Topic editing with inline form
  * Search functionality
  * Notification toggle
  * Channel settings, invite user, rename buttons
  * Member list toggle
  * Channel avatar support with fallback

This makes ChatArea.tsx cleaner and ChatHeader.tsx more reusable.
All tests passing ✅

---------

Co-authored-by: Valerie Liu <v.a.pond@outlook.com>
@coderabbitai coderabbitai Bot mentioned this pull request Mar 15, 2026
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