feat: implement pinned private messages with MONITOR support and UI improvements#81
Conversation
…itor support Features: - Add pinned private messages with localStorage persistence - Implement drag-and-drop reordering for pinned PMs - Add full MONITOR protocol support (IRCv3) - Implement extended-monitor for away-notify integration - Add status indicators (online/away/offline) to PM tabs - Auto-MONITOR users when PM tabs are opened - Auto-cleanup MONITOR entries when unpinned PMs are closed - Restore pinned PMs and MONITOR subscriptions on reconnect - Server-specific MONITOR lists (not global) Bug Fixes: - Fix PM persistence through page refreshes - Add user avatar display in private message chat header - Fix status indicator colors based on WHO response (H=green, G=yellow) - Remove incorrect global METADATA UNSUB when closing PMs - Update private chat status from WHO_REPLY events - Handle WHO_END for individual users (not just channels) - Send WHO request when opening existing PM tabs - Update METADATA handler to refresh private chat state Implements IRCv3 MONITOR (730-734 numerics) and extended-monitor specs
- Fix typing notification cleanup on user disconnect (PART/KICK/QUIT) - Add 6-second auto-clear timer for typing notifications with reset on new activity - Update typing notification rate limit to 3 seconds - Make channel and PM tab avatars consistent size (24px when deselected) - Add darker gray hover background to chat messages - Improve member list styling to match PM tabs - Add status prefix badges with distinct blue background - Fix WHOX parsing to extract op levels from flags field - Update message spacing for better readability
- Remove useless React fragment in ChannelList.tsx - Replace explicit 'any' types with proper TypeScript types - Fix optional chain usage in store/index.ts - Replace non-null assertion with type assertion - Ensure all biome linting rules pass
- Remove left margins from channel tabs for tighter layout - Adjust channel list default width to compensate for removed margins - Apply consistent horizontal padding to both channel and member lists - Fine-tune member list spacing for optimal visual balance
- Make all IRC channel operations case-insensitive to prevent duplicates - Auto-rename channels to match server canonical case - Optimize UI spacing for channel and member lists
- Change mobile channel list width from fixed w-64 to calculated w-[calc(100vw-72px)] - This ensures the channel list spans the full available width minus the server list width on mobile - Prevents overflow while maximizing usable space for channel navigation
- Set mobile channel list width to w-[calc(100vw-72px)] to span available screen space - Add max-w-full classes to prevent text overflow in channel names and UI elements - Ensure proper responsive behavior on mobile devices
WalkthroughAdds private chat pinning, reordering, and richer metadata/UI across ChannelList, ChatHeader, and MemberList. Integrates IRC MONITOR, WHO/WHOX, and away handling in ircClient and store with persistence in localStorage. Adjusts typing throttle, layout widths, message hover color, and test expectations for display-name and status rendering. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor U as User
participant CL as ChannelList
participant CH as ChatHeader
participant ST as Store
participant LS as localStorage
participant IRC as ircClient
rect rgba(230,245,255,0.6)
note over U,ST: Pin/Unpin private chat
U->>CL: Click pin/unpin on PM
CL->>ST: pinPrivateChat/unpinPrivateChat(serverId, pmId)
ST->>LS: Save pinned PMs
ST->>IRC: monitorAdd/monitorRemove(target)
ST-->>CL: State update (isPinned/order)
ST-->>CH: Header pin state update
end
sequenceDiagram
autonumber
actor U as User
participant PM as UserProfileModal
participant ST as Store
participant IRC as ircClient
rect rgba(235,255,235,0.6)
note over U,ST: Open PM from profile
U->>PM: Click "PM User"
PM->>ST: openPrivateChat(serverId, username)
ST->>IRC: CHATHISTORY LATEST (optional)
ST->>IRC: monitorAdd(username)
ST-->>PM: Close modal
ST-->>U: Selects/creates PM view
end
sequenceDiagram
autonumber
participant IRC as ircClient
participant ST as Store
participant UI as UI (ChannelList/MemberList/ChatHeader)
rect rgba(255,245,230,0.6)
note over IRC,ST: Presence & WHOX enrichment
IRC-->>ST: WHOX_REPLY / WHO_REPLY (account, realname, flags, isAway)
IRC-->>ST: MONONLINE / MONOFFLINE
IRC-->>ST: RPL_AWAY (awayMessage)
ST-->>UI: Update PM/channels (isOnline, isAway, realname, status)
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
|
Automated deployment preview for the PR in the Cloudflare Pages. |
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/components/message/MessageItem.tsx (1)
113-124: Remove unreachable duplicate code block.Lines 113-124 duplicate the exact same imageError handling from lines 100-111. Since the first block returns early, this second block is unreachable dead code.
Apply this diff to remove the duplicate:
- 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> - ); - } -src/components/layout/ChatHeader.tsx (1)
3-330: ClearavatarLoadFailedwhen the avatar metadata changes.After a single load failure,
avatarLoadFailedstays true because we never reset it when new metadata arrives. Subsequent avatar updates (e.g., user sets a new image) remain hidden until a full reload. ImportuseEffectand depend onprivateChatAvatarso the state clears on each avatar change.-import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; @@ const [avatarLoadFailed, setAvatarLoadFailed] = useState(false); @@ const privateChatAvatar = privateChatUserMetadata?.avatar?.value; + +useEffect(() => { + setAvatarLoadFailed(false); +}, [privateChatAvatar]);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (17)
BUG_FIXES.md(1 hunks)PINNED_PMS_AND_MONITOR.md(1 hunks)ai_docs/extended-monitor.md(1 hunks)ai_docs/metadata-2.md(1 hunks)ai_docs/monitor.md(1 hunks)src/components/layout/AppLayout.tsx(1 hunks)src/components/layout/ChannelList.tsx(14 hunks)src/components/layout/ChatHeader.tsx(5 hunks)src/components/layout/MemberList.tsx(4 hunks)src/components/message/MessageItem.tsx(1 hunks)src/components/ui/UserProfileModal.tsx(4 hunks)src/hooks/useTypingNotification.ts(2 hunks)src/lib/ircClient.ts(8 hunks)src/store/index.ts(39 hunks)src/types/index.ts(2 hunks)tailwind.config.js(1 hunks)tests/components/MetadataDisplay.test.tsx(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
tests/components/MetadataDisplay.test.tsx (1)
src/components/layout/MemberList.tsx (1)
MemberList(205-523)
src/components/layout/MemberList.tsx (1)
src/lib/ircUtils.tsx (2)
getColorStyle(298-318)mircToHtml(183-295)
src/components/layout/ChannelList.tsx (3)
src/store/index.ts (1)
loadSavedMetadata(113-115)src/types/index.ts (2)
PrivateChat(72-87)User(1-15)src/lib/ircUtils.tsx (3)
getChannelAvatarUrl(339-354)getChannelDisplayName(324-334)mircToHtml(183-295)
src/lib/ircClient.ts (1)
src/types/index.ts (1)
BaseIRCEvent(187-189)
src/components/layout/ChatHeader.tsx (1)
src/store/index.ts (1)
loadSavedMetadata(113-115)
src/store/index.ts (3)
src/lib/ircClient.ts (1)
ircClient(2296-2296)src/types/index.ts (3)
Message(94-146)PrivateChat(72-87)User(1-15)src/lib/notificationSounds.ts (2)
shouldPlayNotificationSound(68-120)playNotificationSound(6-65)
🪛 markdownlint-cli2 (0.18.1)
PINNED_PMS_AND_MONITOR.md
41-41: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
ai_docs/metadata-2.md
271-271: Hard tabs
Column: 6
(MD010, no-hard-tabs)
272-272: Hard tabs
Column: 16
(MD010, no-hard-tabs)
273-273: Hard tabs
Column: 13
(MD010, no-hard-tabs)
274-274: Hard tabs
Column: 20
(MD010, no-hard-tabs)
275-275: Hard tabs
Column: 19
(MD010, no-hard-tabs)
276-276: Hard tabs
Column: 13
(MD010, no-hard-tabs)
277-277: Hard tabs
Column: 15
(MD010, no-hard-tabs)
278-278: Hard tabs
Column: 14
(MD010, no-hard-tabs)
279-279: Hard tabs
Column: 15
(MD010, no-hard-tabs)
280-280: Hard tabs
Column: 15
(MD010, no-hard-tabs)
283-283: Hard tabs
Column: 6
(MD010, no-hard-tabs)
283-283: Hard tabs
Column: 11
(MD010, no-hard-tabs)
283-283: Hard tabs
Column: 17
(MD010, no-hard-tabs)
283-283: Hard tabs
Column: 22
(MD010, no-hard-tabs)
283-283: Hard tabs
Column: 29
(MD010, no-hard-tabs)
283-283: Hard tabs
Column: 34
(MD010, no-hard-tabs)
283-283: Hard tabs
Column: 41
(MD010, no-hard-tabs)
283-283: Hard tabs
Column: 47
(MD010, no-hard-tabs)
283-283: Hard tabs
Column: 53
(MD010, no-hard-tabs)
284-284: Hard tabs
Column: 16
(MD010, no-hard-tabs)
284-284: Hard tabs
Column: 19
(MD010, no-hard-tabs)
284-284: Hard tabs
Column: 22
(MD010, no-hard-tabs)
284-284: Hard tabs
Column: 25
(MD010, no-hard-tabs)
284-284: Hard tabs
Column: 28
(MD010, no-hard-tabs)
284-284: Hard tabs
Column: 31
(MD010, no-hard-tabs)
284-284: Hard tabs
Column: 34
(MD010, no-hard-tabs)
284-284: Hard tabs
Column: 37
(MD010, no-hard-tabs)
284-284: Hard tabs
Column: 40
(MD010, no-hard-tabs)
284-284: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
284-284: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
284-284: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
284-284: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
284-284: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
284-284: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
284-284: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
284-284: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
285-285: Hard tabs
Column: 13
(MD010, no-hard-tabs)
285-285: Hard tabs
Column: 16
(MD010, no-hard-tabs)
285-285: Hard tabs
Column: 19
(MD010, no-hard-tabs)
285-285: Hard tabs
Column: 22
(MD010, no-hard-tabs)
285-285: Hard tabs
Column: 25
(MD010, no-hard-tabs)
285-285: Hard tabs
Column: 28
(MD010, no-hard-tabs)
285-285: Hard tabs
Column: 31
(MD010, no-hard-tabs)
285-285: Hard tabs
Column: 34
(MD010, no-hard-tabs)
285-285: Hard tabs
Column: 37
(MD010, no-hard-tabs)
285-285: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
285-285: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
285-285: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
285-285: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
286-286: Hard tabs
Column: 15
(MD010, no-hard-tabs)
286-286: Hard tabs
Column: 18
(MD010, no-hard-tabs)
286-286: Hard tabs
Column: 21
(MD010, no-hard-tabs)
286-286: Hard tabs
Column: 24
(MD010, no-hard-tabs)
286-286: Hard tabs
Column: 27
(MD010, no-hard-tabs)
286-286: Hard tabs
Column: 30
(MD010, no-hard-tabs)
286-286: Hard tabs
Column: 33
(MD010, no-hard-tabs)
286-286: Hard tabs
Column: 36
(MD010, no-hard-tabs)
286-286: Hard tabs
Column: 39
(MD010, no-hard-tabs)
287-287: Hard tabs
Column: 20
(MD010, no-hard-tabs)
287-287: Hard tabs
Column: 23
(MD010, no-hard-tabs)
287-287: Hard tabs
Column: 26
(MD010, no-hard-tabs)
287-287: Hard tabs
Column: 29
(MD010, no-hard-tabs)
287-287: Hard tabs
Column: 32
(MD010, no-hard-tabs)
287-287: Hard tabs
Column: 35
(MD010, no-hard-tabs)
287-287: Hard tabs
Column: 38
(MD010, no-hard-tabs)
287-287: Hard tabs
Column: 41
(MD010, no-hard-tabs)
287-287: Hard tabs
Column: 44
(MD010, no-hard-tabs)
287-287: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
288-288: Hard tabs
Column: 19
(MD010, no-hard-tabs)
288-288: Hard tabs
Column: 22
(MD010, no-hard-tabs)
288-288: Hard tabs
Column: 25
(MD010, no-hard-tabs)
288-288: Hard tabs
Column: 28
(MD010, no-hard-tabs)
288-288: Hard tabs
Column: 31
(MD010, no-hard-tabs)
288-288: Hard tabs
Column: 34
(MD010, no-hard-tabs)
288-288: Hard tabs
Column: 37
(MD010, no-hard-tabs)
288-288: Hard tabs
Column: 40
(MD010, no-hard-tabs)
288-288: Hard tabs
Column: 43
(MD010, no-hard-tabs)
288-288: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
288-288: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
288-288: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
288-288: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
289-289: Hard tabs
Column: 13
(MD010, no-hard-tabs)
289-289: Hard tabs
Column: 16
(MD010, no-hard-tabs)
289-289: Hard tabs
Column: 19
(MD010, no-hard-tabs)
289-289: Hard tabs
Column: 22
(MD010, no-hard-tabs)
289-289: Hard tabs
Column: 25
(MD010, no-hard-tabs)
289-289: Hard tabs
Column: 28
(MD010, no-hard-tabs)
289-289: Hard tabs
Column: 31
(MD010, no-hard-tabs)
289-289: Hard tabs
Column: 34
(MD010, no-hard-tabs)
289-289: Hard tabs
Column: 37
(MD010, no-hard-tabs)
289-289: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
290-290: Hard tabs
Column: 15
(MD010, no-hard-tabs)
290-290: Hard tabs
Column: 18
(MD010, no-hard-tabs)
290-290: Hard tabs
Column: 21
(MD010, no-hard-tabs)
290-290: Hard tabs
Column: 24
(MD010, no-hard-tabs)
290-290: Hard tabs
Column: 27
(MD010, no-hard-tabs)
290-290: Hard tabs
Column: 30
(MD010, no-hard-tabs)
290-290: Hard tabs
Column: 33
(MD010, no-hard-tabs)
290-290: Hard tabs
Column: 36
(MD010, no-hard-tabs)
290-290: Hard tabs
Column: 39
(MD010, no-hard-tabs)
291-291: Hard tabs
Column: 14
(MD010, no-hard-tabs)
291-291: Hard tabs
Column: 17
(MD010, no-hard-tabs)
291-291: Hard tabs
Column: 20
(MD010, no-hard-tabs)
291-291: Hard tabs
Column: 23
(MD010, no-hard-tabs)
291-291: Hard tabs
Column: 26
(MD010, no-hard-tabs)
291-291: Hard tabs
Column: 29
(MD010, no-hard-tabs)
291-291: Hard tabs
Column: 32
(MD010, no-hard-tabs)
291-291: Hard tabs
Column: 35
(MD010, no-hard-tabs)
291-291: Hard tabs
Column: 38
(MD010, no-hard-tabs)
291-291: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
292-292: Hard tabs
Column: 15
(MD010, no-hard-tabs)
292-292: Hard tabs
Column: 18
(MD010, no-hard-tabs)
292-292: Hard tabs
Column: 21
(MD010, no-hard-tabs)
292-292: Hard tabs
Column: 24
(MD010, no-hard-tabs)
292-292: Hard tabs
Column: 27
(MD010, no-hard-tabs)
292-292: Hard tabs
Column: 30
(MD010, no-hard-tabs)
292-292: Hard tabs
Column: 33
(MD010, no-hard-tabs)
292-292: Hard tabs
Column: 36
(MD010, no-hard-tabs)
292-292: Hard tabs
Column: 39
(MD010, no-hard-tabs)
305-305: Hard tabs
Column: 5
(MD010, no-hard-tabs)
305-305: Hard tabs
Column: 12
(MD010, no-hard-tabs)
306-306: Hard tabs
Column: 5
(MD010, no-hard-tabs)
306-306: Hard tabs
Column: 24
(MD010, no-hard-tabs)
307-307: Hard tabs
Column: 5
(MD010, no-hard-tabs)
307-307: Hard tabs
Column: 19
(MD010, no-hard-tabs)
308-308: Hard tabs
Column: 5
(MD010, no-hard-tabs)
308-308: Hard tabs
Column: 20
(MD010, no-hard-tabs)
309-309: Hard tabs
Column: 5
(MD010, no-hard-tabs)
309-309: Hard tabs
Column: 24
(MD010, no-hard-tabs)
310-310: Hard tabs
Column: 5
(MD010, no-hard-tabs)
310-310: Hard tabs
Column: 26
(MD010, no-hard-tabs)
311-311: Hard tabs
Column: 5
(MD010, no-hard-tabs)
311-311: Hard tabs
Column: 23
(MD010, no-hard-tabs)
312-312: Hard tabs
Column: 5
(MD010, no-hard-tabs)
312-312: Hard tabs
Column: 28
(MD010, no-hard-tabs)
315-315: Hard tabs
Column: 7
(MD010, no-hard-tabs)
315-315: Hard tabs
Column: 12
(MD010, no-hard-tabs)
315-315: Hard tabs
Column: 18
(MD010, no-hard-tabs)
315-315: Hard tabs
Column: 23
(MD010, no-hard-tabs)
315-315: Hard tabs
Column: 30
(MD010, no-hard-tabs)
315-315: Hard tabs
Column: 35
(MD010, no-hard-tabs)
315-315: Hard tabs
Column: 42
(MD010, no-hard-tabs)
315-315: Hard tabs
Column: 48
(MD010, no-hard-tabs)
315-315: Hard tabs
Column: 54
(MD010, no-hard-tabs)
316-316: Hard tabs
Column: 19
(MD010, no-hard-tabs)
316-316: Hard tabs
Column: 22
(MD010, no-hard-tabs)
316-316: Hard tabs
Column: 25
(MD010, no-hard-tabs)
316-316: Hard tabs
Column: 28
(MD010, no-hard-tabs)
316-316: Hard tabs
Column: 31
(MD010, no-hard-tabs)
316-316: Hard tabs
Column: 34
(MD010, no-hard-tabs)
316-316: Hard tabs
Column: 37
(MD010, no-hard-tabs)
316-316: Hard tabs
Column: 40
(MD010, no-hard-tabs)
316-316: Hard tabs
Column: 43
(MD010, no-hard-tabs)
317-317: Hard tabs
Column: 14
(MD010, no-hard-tabs)
317-317: Hard tabs
Column: 17
(MD010, no-hard-tabs)
317-317: Hard tabs
Column: 20
(MD010, no-hard-tabs)
317-317: Hard tabs
Column: 23
(MD010, no-hard-tabs)
317-317: Hard tabs
Column: 26
(MD010, no-hard-tabs)
317-317: Hard tabs
Column: 29
(MD010, no-hard-tabs)
317-317: Hard tabs
Column: 32
(MD010, no-hard-tabs)
317-317: Hard tabs
Column: 35
(MD010, no-hard-tabs)
317-317: Hard tabs
Column: 38
(MD010, no-hard-tabs)
317-317: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
317-317: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
317-317: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
317-317: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
318-318: Hard tabs
Column: 15
(MD010, no-hard-tabs)
318-318: Hard tabs
Column: 18
(MD010, no-hard-tabs)
318-318: Hard tabs
Column: 21
(MD010, no-hard-tabs)
318-318: Hard tabs
Column: 24
(MD010, no-hard-tabs)
318-318: Hard tabs
Column: 27
(MD010, no-hard-tabs)
318-318: Hard tabs
Column: 30
(MD010, no-hard-tabs)
318-318: Hard tabs
Column: 33
(MD010, no-hard-tabs)
318-318: Hard tabs
Column: 36
(MD010, no-hard-tabs)
318-318: Hard tabs
Column: 39
(MD010, no-hard-tabs)
319-319: Hard tabs
Column: 19
(MD010, no-hard-tabs)
319-319: Hard tabs
Column: 22
(MD010, no-hard-tabs)
319-319: Hard tabs
Column: 25
(MD010, no-hard-tabs)
319-319: Hard tabs
Column: 28
(MD010, no-hard-tabs)
319-319: Hard tabs
Column: 31
(MD010, no-hard-tabs)
319-319: Hard tabs
Column: 34
(MD010, no-hard-tabs)
319-319: Hard tabs
Column: 37
(MD010, no-hard-tabs)
319-319: Hard tabs
Column: 40
(MD010, no-hard-tabs)
319-319: Hard tabs
Column: 43
(MD010, no-hard-tabs)
319-319: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
320-320: Hard tabs
Column: 21
(MD010, no-hard-tabs)
320-320: Hard tabs
Column: 24
(MD010, no-hard-tabs)
320-320: Hard tabs
Column: 27
(MD010, no-hard-tabs)
320-320: Hard tabs
Column: 30
(MD010, no-hard-tabs)
320-320: Hard tabs
Column: 33
(MD010, no-hard-tabs)
320-320: Hard tabs
Column: 36
(MD010, no-hard-tabs)
320-320: Hard tabs
Column: 39
(MD010, no-hard-tabs)
320-320: Hard tabs
Column: 42
(MD010, no-hard-tabs)
320-320: Hard tabs
Column: 45
(MD010, no-hard-tabs)
321-321: Hard tabs
Column: 18
(MD010, no-hard-tabs)
321-321: Hard tabs
Column: 21
(MD010, no-hard-tabs)
321-321: Hard tabs
Column: 24
(MD010, no-hard-tabs)
321-321: Hard tabs
Column: 27
(MD010, no-hard-tabs)
321-321: Hard tabs
Column: 30
(MD010, no-hard-tabs)
321-321: Hard tabs
Column: 33
(MD010, no-hard-tabs)
321-321: Hard tabs
Column: 36
(MD010, no-hard-tabs)
321-321: Hard tabs
Column: 39
(MD010, no-hard-tabs)
321-321: Hard tabs
Column: 42
(MD010, no-hard-tabs)
321-321: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
322-322: Hard tabs
Column: 23
(MD010, no-hard-tabs)
322-322: Hard tabs
Column: 26
(MD010, no-hard-tabs)
322-322: Hard tabs
Column: 29
(MD010, no-hard-tabs)
322-322: Hard tabs
Column: 32
(MD010, no-hard-tabs)
322-322: Hard tabs
Column: 35
(MD010, no-hard-tabs)
322-322: Hard tabs
Column: 38
(MD010, no-hard-tabs)
322-322: Hard tabs
Column: 41
(MD010, no-hard-tabs)
322-322: Hard tabs
Column: 44
(MD010, no-hard-tabs)
322-322: Hard tabs
Column: 47
(MD010, no-hard-tabs)
322-322: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
322-322: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
348-348: Bare URL used
(MD034, no-bare-urls)
348-348: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
349-349: Bare URL used
(MD034, no-bare-urls)
349-349: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
353-353: Bare URL used
(MD034, no-bare-urls)
358-358: Bare URL used
(MD034, no-bare-urls)
363-363: Bare URL used
(MD034, no-bare-urls)
364-364: Bare URL used
(MD034, no-bare-urls)
373-373: Bare URL used
(MD034, no-bare-urls)
378-378: Bare URL used
(MD034, no-bare-urls)
378-378: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
379-379: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
383-383: Bare URL used
(MD034, no-bare-urls)
383-383: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
384-384: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
394-394: Bare URL used
(MD034, no-bare-urls)
394-394: Bare URL used
(MD034, no-bare-urls)
398-398: Bare URL used
(MD034, no-bare-urls)
405-405: Bare URL used
(MD034, no-bare-urls)
405-405: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
406-406: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
422-422: Bare URL used
(MD034, no-bare-urls)
429-429: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
430-430: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
431-431: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
432-432: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
443-443: Bare URL used
(MD034, no-bare-urls)
460-460: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
461-461: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
462-462: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
463-463: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
464-464: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
465-465: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
473-473: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
475-475: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
495-495: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
497-497: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
506-506: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
508-508: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
518-518: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
520-520: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
530-530: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
532-532: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
539-539: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
541-541: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
555-555: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
557-557: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
559-559: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
561-561: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
566-566: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
568-568: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
570-570: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
572-572: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
583-583: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
585-585: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
590-590: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
592-592: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
597-597: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
598-598: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
600-600: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
601-601: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
612-612: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
614-614: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
619-619: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
621-621: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
626-626: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
629-629: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
634-634: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
640-640: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
648-648: Bare URL used
(MD034, no-bare-urls)
BUG_FIXES.md
41-41: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🔇 Additional comments (14)
tailwind.config.js (1)
77-77: LGTM! Subtle hover color refinement.The color change from
#32353Bto#2A2D33slightly darkens the message hover background, which should improve visual feedback consistency.src/components/message/MessageItem.tsx (1)
448-450: Verify intentional removal of theme flexibility.The hover background was changed from a theme-based dynamic class (
hover:bg-${theme}-message-hover) to a hardcoded Discord color (hover:bg-discord-message-hover). This aligns with the Tailwind config update but removes support for other themes.The conditional padding logic based on
showHeaderlooks good and improves layout consistency.Confirm that hardcoding to Discord theme for message hover is intentional and that multi-theme support for this component is no longer required.
src/components/layout/AppLayout.tsx (2)
88-88: LGTM! Sidebar width increase accommodates enhanced UI.The defaultWidth increase from 240 to 264 aligns with the PR's layout improvements for pinned private messages and enhanced channel list features.
95-95: LGTM! Conditional width properly handles narrow view.The conditional width logic correctly applies
w-[calc(100vw-72px)]in narrow view (accounting for server list width) andw-fullotherwise. The use ofcalc()with viewport units is appropriate here.src/types/index.ts (2)
5-5: LGTM! Realname field addition is appropriate.The optional
realnamefield properly captures the gecos/realname field from WHO responses, aligning with IRC protocol standards.
79-87: LGTM! PrivateChat type extensions support new features.The new optional fields comprehensively support the PR's pinned PM and MONITOR features:
isPinned,order: pinning and reordering functionalityisOnline,isAway,awayMessage: presence tracking via MONITOR/extended-monitorrealname,account: identity metadata from WHO/WHOXisBot: bot detectionAll fields are appropriately optional for backward compatibility.
src/hooks/useTypingNotification.ts (1)
30-43: LGTM! Typing throttle reduction improves responsiveness.The throttle reduction from 5 seconds to 3 seconds makes typing indicators more responsive while still preventing notification spam. This aligns with the PR description's mention of a "3-second rate limit" for typing notifications.
The change is straightforward and maintains the existing logic flow.
ai_docs/extended-monitor.md (1)
1-42: LGTM! Clear and well-structured documentation.This documentation file is clean with no linting issues (unlike monitor.md and metadata-2.md). The content clearly explains the extended-monitor capability and importantly includes a privacy considerations section about nick-change tracking.
The documentation effectively supports the PR's MONITOR/extended-monitor implementation.
src/components/ui/UserProfileModal.tsx (3)
13-13: LGTM! Icon import for PM button.The FaUser icon import supports the new PM User button functionality.
88-88: LGTM! Store integration for PM functionality.The openPrivateChat action is properly retrieved from the store to support the PM User button.
612-636: PM User button and openPrivateChat behavior verified
openPrivateChatselects an existing chat rather than creating duplicates (src/store/index.ts:1424–1434)- Offline users still get a PM tab opened and are correctly marked offline (WHO_END logic; src/store/index.ts:6340–6350)
- Behavior is consistent when invoked from the profile modal or any other component
tests/components/MetadataDisplay.test.tsx (3)
169-176: LGTM! Test correctly validates display name usage.The test properly validates that the display name "Alice Wonderland" is shown and colored correctly (#ff0000). The comment clarifies that display name is shown for Alice, while username is shown for Bob who lacks a display name.
193-203: LGTM! Test improvements for status display.The test refactoring improves validation by checking for actual status text content ("Working on something cool!" and "Away from keyboard") rather than counting lightbulb icons. This provides more robust verification of the status display feature.
356-356: LGTM! Action message format correctly tested.The test properly validates the new action message format that includes both display name and username: "Alice Wonderland (alice) waves hello". This ensures users can see both the friendly display name and the IRC username for context.
|
|
||
| IRCv3 | ||
|
|
||
| Home Working Group FAQs Specifications Registry Support Tables | ||
|
|
||
| Metadata | ||
|
|
||
| Copyright © 2012 Kiyoshi Aman <kiyoshi.aman@gmail.com> | ||
|
|
||
| Copyright © 2015-2016 Attila Molnar <attilamolnar@hush.com> | ||
|
|
||
| Copyright © 2018 James Wheare <james@irccloud.com> | ||
|
|
||
| Copyright © 2021 Daniel Oaks <daniel@danieloaks.net> | ||
|
|
||
| Copyright © 2022 Valerie Pond <v.a.pond@outlook.com> | ||
|
|
||
| Copyright © 2022-2023 Val Lorentz <progval+ircv3@progval.net> | ||
|
|
||
| Unlimited redistribution and modification of this document is allowed provided that the above copyright notice and this permission notice remains intact. | ||
|
|
||
| This specification is a work-in-progress and may have major incompatible changes without warning. | ||
|
|
||
| This specification may change at any time and we do not recommend implementing it in a production environment. | ||
| Notes for implementing work-in-progress version | ||
|
|
||
| This is a work-in-progress specification. | ||
|
|
||
| Software implementing this work-in-progress specification MUST NOT use the unprefixed metadata-2 capability name. Instead, implementations SHOULD use the draft/metadata-2 capability name to be interoperable with other software implementing a compatible work-in-progress version. | ||
|
|
||
| The final version of the specification will use an unprefixed capability name. | ||
| Introduction | ||
|
|
||
| It is useful to associate metadata with one’s IRC presence, e.g. to make one’s homepage or non-IRC contact details more discoverable. There are several mechanisms for doing this, but they normally rely on the presence of services and aren’t really suitable for transient metadata like a user’s current location. | ||
|
|
||
| This feature lays out a command that can be used to set metadata, and a message that can be used to receive metadata updates from the server. | ||
| Concepts | ||
|
|
||
| Clients and channels can both contain metadata. Metadata acts as a key:value store (for example, the display-name key on a user, or the url key on a channel). | ||
|
|
||
| Clients can set/delete metadata on themselves, and on channels they administer. Privileged users (e.g. network admins) may be able to set certain metadata on other users, and set special keys on themselves or channels. | ||
|
|
||
| Server administrators can setup lists of allowed or blocked keys, and may also restrict the setting/viewing of keys depending on whether the user is an admin, what kind of admin they are, etc (see the Visibility field). | ||
|
|
||
| On joining a server, clients have to configure their ‘metadata key subscriptions’. This is a list of which keys the client understands and wants to get updates about (for example, they may subscribe to a status key if they support user-set statuses). By default, this subscription list is empty. | ||
|
|
||
| When metadata is updated, users who have subscribed to the changed key will receive a METADATA message notifying them of the change. | ||
|
|
||
| For channel metadata, this applies only to subscribers who are members of the updated channel. For user metadata, this applies only to subscribers who either share a channel with the updated user or are monitoring them. | ||
|
|
||
| On joining a channel, users will get the channel’s current metadata sent to them with METADATA messages, and get the same information for all users who are in the channel. Specifically, they get that information for the keys they are subscribed to. The server may also tell them to request that information at a later time. | ||
| Relation with other specifications | ||
|
|
||
| This specification depends on the batch capability which MUST be negotiated to use draft/metadata-2. The order of capability negotiation is not significant and MUST not be enforced. | ||
|
|
||
| This specification also uses the standard replies framework. | ||
|
|
||
| Clients MUST NOT request both metadata-notify and draft/metadata-2. Servers MUST NOT accept these requests either. | ||
| draft/metadata-2 Capability | ||
|
|
||
| The draft/metadata-2 capability indicates that a server supports metadata, and provides any limits and information about the system that clients must be aware of. Clients MUST request this capability in order to receive METADATA notifications. | ||
|
|
||
| The ABNF format of the metadata capability is: | ||
|
|
||
| capability ::= 'metadata' ['=' tokens] | ||
| tokens ::= token [',' token]* | ||
| token ::= key ['=' value] | ||
| key ::= <sequence of a-zA-Z0-9_./:-> | ||
| value ::= <utf8> | ||
|
|
||
| These are the defined tokens: | ||
|
|
||
| before-connect: if present, indicates the server supports METADATA commands during connection registration | ||
| max-subs: the maximum number of keys a client is allowed in its subscription list. See the SUB subcommand for more details. | ||
| max-keys: the maximum number of keys a client is allowed to set on its own nickname. | ||
| max-value-bytes: the maximum size of values a client is allowed to set. Servers MAY send longer values. | ||
|
|
||
| Clients MUST silently ignore any unknown tokens. | ||
| Keys and Values | ||
|
|
||
| Key names follow this grammar: | ||
|
|
||
| be restricted to the ranges A-Z, a-z, 0-9, and _./:- and are case-insensitive. Key names MUST NOT start with a colon (:). They follow the same rules as message tag names. | ||
|
|
||
| Values can take any form, but MUST be encoded using UTF-8. | ||
|
|
||
| The expected handling of individual metadata keys SHOULD be defined and listed in the IRCv3 extension registry. | ||
| Batch type | ||
|
|
||
| This specification adds the metadata batch type. | ||
|
|
||
| This batch MUST be sent to clients on connection and as reply to successful METADATA GET, METADATA LIST, and METADATA SYNC subcommands. | ||
|
|
||
| This batch type does not take any parameter, and clients MUST ignore them if any. | ||
| Notifications | ||
|
|
||
| Clients can either be subscribed to a key, or not subscribed to it. By default, clients are not subscribed to any keys. They can subscribe and unsubscribe from keys using the SUB and UNSUB subcommands. The server MUST allow clients to subscribe to any valid keys, including privileged keys the client cannot access at the time of subscription. | ||
|
|
||
| The client will receive METADATA messages about the keys they are subscribed to. They can also use the GET and LIST subcommands to receive information about keys they are not subscribed to. | ||
|
|
||
| Clients automatically receive metadata updates for: | ||
|
|
||
| themselves (excluding changes they make themselves) | ||
| channels they are joined to, | ||
| other clients in the channels they are joined to, and | ||
| users they are currently monitoring. | ||
|
|
||
| Servers MUST reply to erroneous requests using Standard Replies | ||
|
|
||
| If a channel/user the client is receiving updates for changes one of the keys the client is subscribed to, they will receive a METADATA message notifying them of the change. Clients MAY also receive metadata notifications for keys they have not subscribed to, or even when they have not subscribed to any keys. | ||
|
|
||
| Here are additional cases where clients will receive METADATA messages: | ||
|
|
||
| Upon requesting the metadata capability, clients receive their non-transient metadata (for example, metadata stored by the server or by services) in a metadata batch with their own nick as target. If none exists, the server MUST send an empty batch instead. | ||
| When subscribing to a key, clients SHOULD receive the current value of that key for channels/users they are receiving updates for. | ||
| Clients SHOULD receive the current values of keys they are subscribed to when they MONITOR a user, or when one of their monitored users comes online. | ||
|
|
||
| Postponed synchronization | ||
|
|
||
| If the client joins a large channel, or the client is already on some channels and enables the metadata capability, the server may not be able to send the client all current metadata for their targets. | ||
|
|
||
| In this case, the server MUST respond with a RPL_METADATASYNCLATER numeric instead of propagating the current metadata of the targets. This numeric indicates that the specified target has some metadata set that the client SHOULD request synchronization of at a later time. | ||
|
|
||
| The client can use the SYNC subcommand to request the sync of metadata for the given target. If the [<RetryAfter>] is given, the client SHOULD wait at least that many seconds before sending the sync request. | ||
| METADATA server message | ||
|
|
||
| Clients that request the metadata capability MUST be able to handle incoming METADATA messages. After negotiating this capability, servers MAY send this message to clients at any time. | ||
|
|
||
| The format of the METADATA server message is: | ||
|
|
||
| METADATA <Target> <Key> <Visibility> <Value> | ||
|
|
||
| Target is a valid nickname or channel name. | ||
|
|
||
| Key is a valid key name | ||
|
|
||
| Visibility is an asterisk (*) for keys visible to everyone, or an implementation-defined value which describes the key’s visibility status; for instance, it MAY be a permission level or flag. (TODO should we define a prefix like STATUSMSG for this value?) | ||
|
|
||
| Value is a UTF-8 encoded value. | ||
| METADATA client command | ||
|
|
||
| METADATA <Target> <Subcommand> [<Param 1> ... [<Param n>]] | ||
|
|
||
| Target is a valid nickname or channel name. Clients SHOULD use the asterisk symbol (*) when targeting their own nickname. | ||
|
|
||
| Subcommand is one of the subcommands listed below. The allowed params are described in each subcommand description. | ||
|
|
||
| Clients MAY use this command during connection registration if the server advertises the before-connect token. | ||
| METADATA GET | ||
|
|
||
| METADATA <Target> GET key1 key2 ... | ||
|
|
||
| This subcommand lets clients lookup keys on the given target. | ||
|
|
||
| Multiple keys may be given. | ||
|
|
||
| The response will be a metadata batch containing: | ||
|
|
||
| RPL_KEYVALUE | ||
| RPL_KEYNOTSET | ||
| FAIL METADATA KEY_INVALID | ||
|
|
||
| for every key in order. | ||
|
|
||
| Servers MAY replace metadata which is considered not visible for the requesting user, with RPL_KEYNOTSET or with FAIL METADATA KEY_NO_PERMISSION. | ||
|
|
||
| Failures: KEY_INVALID, KEY_NO_PERMISSION | ||
| METADATA LIST | ||
|
|
||
| METADATA <Target> LIST | ||
|
|
||
| This subcommand lists all of the target’s currently-set metadata keys along with their values. | ||
|
|
||
| If the target is valid, the response is a metadata batch containing zero or more RPL_KEYVALUE events. If the target is not valid, ONLY a FAIL METADATA INVALID_TARGET reply is sent. | ||
|
|
||
| Servers MAY omit metadata which is considered not visible for the requesting user, or replace it with FAIL METADATA KEY_NO_PERMISSION within the batch. | ||
|
|
||
| Failures: FAIL METADATA KEY_NO_PERMISSION. | ||
| METADATA SET | ||
|
|
||
| METADATA <Target> SET <Key> [:Value] | ||
|
|
||
| This subcommand sets the key on the target to the given value. If no value is given, the key is removed. | ||
|
|
||
| If the key is invalid, the server responsds with FAIL METADATA KEY_INVALID and fails the request. | ||
|
|
||
| If the key is valid, but not set, and the client tries to remove the key, the server responds with FAIL METADATA KEY_NOT_SET and fails the request. | ||
|
|
||
| If the user cannot set keys on the given target, the server responds with FAIL METADATA KEY_NO_PERMISSION and fails the request. | ||
|
|
||
| Servers MAY respond to certain keys considered not settable by the requesting user, or otherwise disallowed by the server, with FAIL METADATA KEY_NO_PERMISSION and fail the request. | ||
|
|
||
| Servers MAY respond with FAIL METADATA RATE_LIMITED and fail the request. When a client receives FAIL METADATA RATE_LIMITED, it SHOULD retry the METADATA SET request at a later time. If the FAIL METADATA RATE_LIMITED event contains the <RetryAfter> parameter, the parameter value MUST be a positive integer indicating the minimum number of seconds the client should wait before retrying the request. | ||
|
|
||
| If the request is successful, the server carries out the requested change and responds with one RPL_KEYVALUE event, representing the new value if any, or RPL_KEYNOTSET if not. This new value MAY differ from the one sent by the client. | ||
|
|
||
| Failures: FAIL METADATA LIMIT_REACHED, FAIL METADATA KEY_INVALID, FAIL METADATA KEY_NO_PERMISSION, FAIL METADATA RATE_LIMITED, FAIL METADATA KEY_NOT_SET, FAIL METADATA VALUE_INVALID | ||
| METADATA CLEAR | ||
|
|
||
| METADATA <Target> CLEAR | ||
|
|
||
| This subcommand removes all metadata from the target, equivalently to using METADATA SET on all currently-set keys with an empty value. | ||
|
|
||
| If the user cannot clear keys on the given target, the server responds with FAIL METADATA KEY_NO_PERMISSION with an asterisk (*) in the <Key> field and fails the request. | ||
|
|
||
| Servers MAY omit certain keys which are considered not settable by the requesting user, or respond with FAIL METADATA KEY_NO_PERMISSION for each of those keys. | ||
|
|
||
| If the request is successful, the server responds with a metadata batch containing one RPL_KEYVALUE event per cleared key, and failure messages if any. | ||
|
|
||
| Failures: FAIL METADATA KEY_NO_PERMISSION | ||
| METADATA SUB | ||
|
|
||
| METADATA * SUB <key1> [<key2> ...] | ||
|
|
||
| This subcommand lets clients subscribe to updates for the given keys. | ||
|
|
||
| The server MUST process each key in order, as the client uses this order to determine which keys they were and were not able to subscribe to. Clients SHOULD send keys in order of preference. | ||
|
|
||
| The server processes each key in order, and: | ||
|
|
||
| If the client has subscribed to too many keys, the server does not process any further keys in the subcommand. The <key> parameter of the FAIL METADATA TOO_MANY_SUBS reply MUST be this first key that the client could not subscribe to (so the client knows all keys sent after that one were not processed). | ||
|
|
||
| If the client successfully subscribes to a key, or is already subscribed to a requested key, that key MUST appear in a RPL_METADATASUBOK reply numeric. | ||
|
|
||
| If the key’s name is invalid, the server sends a FAIL METADATA KEY_INVALID reply to the client and continues processing keys. | ||
|
|
||
| If the client does not have permission to view a given key, the server sends a FAIL METADATA KEY_NO_PERMISSION reply to the client and continues processing keys. However, the subscription MUST still be successful, and that key MUST appear in a RPL_METADATASUBOK reply numeric. In this case, the FAIL METADATA KEY_NO_PERMISSION reply serves as a warning indicating that the client will not receive METADATA messages about this key unless it gains the necessary (implementation defined) privileges later. | ||
|
|
||
| Once the server is finished processing keys, it responds with: | ||
|
|
||
| zero or more of this numeric in any order: RPL_METADATASUBOK, | ||
| zero or more of these standard reply codes: FAIL METADATA KEY_INVALID, FAIL METADATA KEY_NO_PERMISSION | ||
| and MAY respond with one FAIL METADATA TOO_MANY_SUBS reply. | ||
|
|
||
| METADATA UNSUB | ||
|
|
||
| METADATA * UNSUB <key1> [<key2> ...] | ||
|
|
||
| This subcommand lets clients unsubscribe from updates for the given keys. | ||
|
|
||
| Servers process the given keys, and: | ||
|
|
||
| If the client successfully unsubscribes from a key, or is not subscribed to a requested key, that key MUST appear in a RPL_METADATAUNSUBOK reply numeric. | ||
|
|
||
| If the key’s name is invalid, the server sends a FAIL METADATA KEY_INVALID reply to the client and continues processing keys. | ||
|
|
||
| Once the server is finished processing keys, it responds with: | ||
|
|
||
| zero or more of this numeric in any order: RPL_METADATAUNSUBOK | ||
| zero of more of this standard reply: FAIL METADATA KEY_INVALID | ||
|
|
||
| METADATA SUBS | ||
|
|
||
| METADATA * SUBS | ||
|
|
||
| This subcommand returns which keys the client is currently subscribed to. | ||
|
|
||
| The server responds with zero or more RPL_METADATASUBS numerics. The server MAY return the keys in any order. The server MUST NOT list the same key multiple times in a response to this subcommand. | ||
| METADATA SYNC | ||
|
|
||
| METADATA <Target> SYNC | ||
|
|
||
| Clients use this subcommand to receive all subscribed metadata from the given target. If the target is a channel, it also syncs the metadata for all other users in that channel. | ||
|
|
||
| If the sync cannot be performed at this time (due to load or other implementation-defined details), the server responds with a RPL_METADATASYNCLATER. If the sync can be performed, the server responds with a metadata batch containing zero or more METADATA events. | ||
|
|
||
| For details, please see the postponed synchronization section. | ||
| Standard Replies | ||
|
|
||
| The following Standard Replies codes are defined with these parameters: | ||
| Code Parameters | ||
| INVALID_TARGET <Target> :invalid metadata target | ||
| KEY_INVALID <InvalidKey> :invalid key | ||
| SUBCOMMAND_INVALID <SubCommand> :invalid subcommand | ||
| KEY_NO_PERMISSION <Target> <Key> :permission denied | ||
| KEY_NOT_SET <Target> <Key> :key not set | ||
| LIMIT_REACHED <Target> :metadata limit reached | ||
| RATE_LIMITED <Target> <Key> <RetryAfter> :too many changes | ||
| TOO_MANY_SUBS <Key> :too many subscriptions | ||
| VALUE_INVALID :value is too long or not UTF8 | ||
|
|
||
| Reference table of Standard Replies codes and the METADATA subcommands or any other commands that produce them: | ||
| Code GET LIST SET CLEAR SUB UNSUB SUBS SYNC Other | ||
| INVALID_TARGET * * * * * * * * | ||
| KEY_INVALID * * * * | ||
| VALUE_INVALID * | ||
| SUBCOMMAND_INVALID * | ||
| KEY_NO_PERMISSION * * * * * | ||
| KEY_NOT_SET * | ||
| LIMIT_REACHED * | ||
| RATE_LIMITED * | ||
| TOO_MANY_SUBS * | ||
|
|
||
| Each subcommand section describes the reply and error numerics it expects from the server, but here are brief descriptions of numerics that are used for multiple subcommands: | ||
| Examples | ||
|
|
||
| FAIL METADATA TARGET_INVALID ExampleUser!lol :Invalid target. when a client refers to an invalid target. | ||
| FAIL METADATA KEY_INVALID %key% :That is not a valid key. when a client refers to an invalid key. | ||
| FAIL METADATA KEY_NO_PERMISSION %target% %key% :You do not have permission to set %key% on %target% when a client attempts to access or set a key on a target when they lack sufficient permission. | ||
| FAIL METADATA SUBCOMMAND_INVALID destr0y :Invalid subcommand. when a client calls a METADATA subcommand which is not defined. | ||
|
|
||
| Numerics | ||
|
|
||
| The following numerics 760 through 775 are reserved for metadata, with these labels and parameters: | ||
| No. Label Parameters | ||
| 760 RPL_WHOISKEYVALUE <Target> <Key> <Visibility> :<Value> | ||
| 761 RPL_KEYVALUE <Target> <Key> <Visibility> :<Value> | ||
| 766 RPL_KEYNOTSET <Target> <Key> :key not set | ||
| 770 RPL_METADATASUBOK <Key1> [<Key2> ...] | ||
| 771 RPL_METADATAUNSUBOK <Key1> [<Key2> ...] | ||
| 772 RPL_METADATASUBS <Key1> [<Key2> ...] | ||
| 774 RPL_METADATASYNCLATER <Target> [<RetryAfter>] | ||
|
|
||
| Reference table of numerics and the METADATA subcommands or any other commands that produce them: | ||
| Label GET LIST SET CLEAR SUB UNSUB SUBS SYNC Other | ||
| RPL_WHOISKEYVALUE WHOIS | ||
| RPL_KEYVALUE * * * * | ||
| RPL_KEYNOTSET * | ||
| RPL_METADATASUBOK * | ||
| RPL_METADATAUNSUBOK * | ||
| RPL_METADATASUBS * | ||
| RPL_METADATASYNCLATER * * JOIN | ||
|
|
||
| Replies: | ||
|
|
||
| RPL_KEYVALUE reports the values of metadata keys. The Visibility parameter is defined in the server message section. | ||
|
|
||
| RPL_WHOISKEYVALUE numeric | ||
|
|
||
| When a user runs WHOIS on a user with metadata, a subset of that metadata MAY be sent with RPL_WHOISKEYVALUE numerics. This subset MUST be chosen explicitly, and optimised for keys that can be easily read by users. For a complete view of user metadata, see the LIST subcommand. | ||
| Examples | ||
|
|
||
| All examples begin with the client not being subscribed to any keys. | ||
| Capability Examples | ||
| Capability value in CAP LS 1 | ||
|
|
||
| C: CAP LS 302 | ||
| S: CAP * LS :userhost-in-names draft/metadata-2=foo,max-subs=50,bar multi-prefix | ||
|
|
||
| Capability value in CAP LS 2 | ||
|
|
||
| C: CAP LS 302 | ||
| S: CAP * LS :draft/metadata-2=max-subs=25 multi-prefix invite-notify | ||
|
|
||
| METADATA command examples | ||
| Setting metadata on self | ||
|
|
||
| C: METADATA * SET url :http://www.example.com | ||
| S: :irc.example.com 761 client * url * :http://www.example.com | ||
|
|
||
| Setting metadata on self, but the limit has been reached | ||
|
|
||
| C: METADATA * SET url :http://www.example.com | ||
| S: FAIL METADATA LIMIT_REACHED :Metadata limit reached | ||
|
|
||
| Setting metadata on another user, without permission | ||
|
|
||
| C: METADATA user1 SET url :http://www.example.com | ||
| S: FAIL METADATA KEY_NO_PERMISSION user1 url :You do not have permission to set 'url' on 'user1' | ||
|
|
||
| Setting metadata on channel | ||
|
|
||
| C: METADATA #example SET url :http://www.example.com | ||
| S: :irc.example.com 761 client #example url * :http://www.example.com | ||
|
|
||
| Setting metadata on an invalid target | ||
|
|
||
| C: METADATA $a:user SET url :http://www.example.com | ||
| S: FAIL METADATA INVALID_TARGET $a:user :Invalid target. | ||
|
|
||
| Setting metadata with an invalid key | ||
|
|
||
| C: METADATA user1 SET $url$ :http://www.example.com | ||
| S: FAIL METADATA KEY_INVALID $url$ :Invalid key. | ||
|
|
||
| Server rate-limits setting metadata and provides a RetryAfter value | ||
|
|
||
| C: METADATA * SET url :http://www.example.com | ||
| S: FAIL METADATA RATE_LIMITED * url 5 :Rate-limit reached. You're going too fast! Try again in 5 seconds. | ||
|
|
||
| Server rate-limits setting metadata with no RetryAfter value | ||
|
|
||
| C: METADATA * SET url :http://www.example.com | ||
| S: FAIL METADATA RATE_LIMITED * url * :Rate-limit reached. You're going too fast! | ||
|
|
||
| Thought: A non-normative retry value helps against automated spam while still being descriptive for the end-user. | ||
| METADATA message examples | ||
| External server sets metadata on a user | ||
|
|
||
| S: :irc.example.com METADATA user1 account * :user1 | ||
|
|
||
| User sets metadata on a channel | ||
|
|
||
| S: :user1!~user@somewhere.example.com METADATA #example url * :http://www.example.com | ||
|
|
||
| External server updates metadata on a channel | ||
|
|
||
| S: :irc.example.com METADATA #example wiki-url * :http://wiki.example.com | ||
|
|
||
| Listing and Viewing Metadata Examples | ||
| Listing metadata, with an implementation-defined visibility field | ||
|
|
||
| C: METADATA user1 LIST | ||
| S: :irc.example.com BATCH +VUN2ot metadata | ||
| S: @batch=VUN2ot :irc.example.com 761 client user1 url * :http://www.example.com | ||
| S: @batch=VUN2ot :irc.example.com 761 client user1 im.xmpp * :user1@xmpp.example.com | ||
| S: @batch=VUN2ot :irc.example.com 761 client user1 bot-likeliness-score visible-only-for-admin :42 | ||
| S: :irc.example.com BATCH -VUN2ot | ||
|
|
||
| Getting several metadata keys from a user | ||
|
|
||
| C: METADATA user1 GET blargh splot im.xmpp | ||
| S: :irc.example.com BATCH +gWkCiV metadata | ||
| S: @batch=gWkCiV 766 client user1 blargh :No matching key | ||
| S: @batch=gWkCiV 766 client user1 splot :No matching key | ||
| S: @batch=gWkCiV :irc.example.com 761 client user1 im.xmpp * :user1@xmpp.example.com | ||
| S: :irc.example.com BATCH -gWkCiV | ||
|
|
||
| Client joins a channel and syncs metadata immediately | ||
|
|
||
| C: JOIN #smallchan | ||
| S: :modernclient!modernclient@example.com JOIN #smallchan | ||
| S: :irc.example.com 353 modernclient @ #smallchan :user1 user2 user3 user4 user5 ... | ||
| S: :irc.example.com 353 modernclient @ #smallchan :user51 user52 user53 user54 ... | ||
| S: :irc.example.com 353 modernclient @ #smallchan :user101 user102 user103 user104 ... | ||
| S: :irc.example.com 353 modernclient @ #smallchan :user151 user152 user153 user154 ... | ||
| S: :irc.example.com 366 modernclient #smallchan :End of /NAMES list. | ||
| S: :irc.example.com BATCH +UwZ67M metadata | ||
| S: @batch=UwZ67M :irc.example.com METADATA user2 bar * :second example value | ||
| S: @batch=UwZ67M :irc.example.com METADATA user1 foo * :third example value | ||
| S: @batch=UwZ67M :irc.example.com METADATA user1 bar * :this is another example value | ||
| S: @batch=UwZ67M :irc.example.com METADATA user3 website * :www.example.com | ||
| ...and some more metadata messages | ||
| S: :irc.example.com BATCH -UwZ67M | ||
|
|
||
| Client joins a channel and but needs to sync metadata later | ||
|
|
||
| To-do: Think of a better sync-later flow | ||
|
|
||
| Client joins channel: | ||
|
|
||
| C: JOIN #bigchan | ||
| S: :modernclient!modernclient@example.com JOIN #bigchan | ||
| S: :irc.example.com 353 modernclient @ #bigchan :user1 user2 user3 user4 user5 ... | ||
| S: :irc.example.com 353 modernclient @ #bigchan :user51 user52 user53 user54 ... | ||
| S: :irc.example.com 353 modernclient @ #bigchan :user101 user102 user103 user104 ... | ||
| S: :irc.example.com 353 modernclient @ #bigchan :user151 user152 user153 user154 ... | ||
| S: :irc.example.com 366 modernclient #bigchan :End of /NAMES list. | ||
| S: :irc.example.com 774 modernclient #bigchan 4 | ||
|
|
||
| Client waits 4 seconds: | ||
|
|
||
| C: METADATA #bigchan SYNC | ||
| S: :irc.example.com 774 modernclient #bigchan 6 | ||
|
|
||
| Client waits 6 more seconds: | ||
|
|
||
| C: METADATA #bigchan SYNC | ||
| S: :irc.example.com BATCH +O5J6rk metadata | ||
| S: @batch=O5J6rk :irc.example.com METADATA user52 foo * :example value 1 | ||
| S: @batch=O5J6rk :irc.example.com METADATA user2 bar * :second example value | ||
| S: @batch=O5J6rk :irc.example.com METADATA user1 foo * :third example value | ||
| S: @batch=O5J6rk :irc.example.com METADATA user1 bar * :this is another example value | ||
| S: @batch=O5J6rk :irc.example.com METADATA user152 baz * :Lorem ipsum | ||
| S: @batch=O5J6rk :irc.example.com METADATA user3 website * :www.example.com | ||
| S: @batch=O5J6rk :irc.example.com METADATA user152 bar * :dolor sit amet | ||
| ...and many more metadata messages | ||
| S: :irc.example.com BATCH -O5J6rk | ||
|
|
||
| Subscription Examples | ||
| Basic subscriping and unsubscribing | ||
|
|
||
| C: METADATA * SUB avatar website foo bar | ||
| S: :irc.example.com 770 modernclient avatar website foo bar | ||
| C: METADATA * UNSUB foo bar | ||
| S: :irc.example.com 771 modernclient bar foo | ||
|
|
||
| Multiple RPL_METADATASUBOK numerics in reply to METADATA SUB | ||
|
|
||
| C: METADATA * SUB avatar website foo bar baz | ||
| S: :irc.example.com 770 modernclient avatar website | ||
| S: :irc.example.com 770 modernclient foo | ||
| S: :irc.example.com 770 modernclient bar baz | ||
|
|
||
| Invalid key name in reply to subscription | ||
|
|
||
| C: METADATA * SUB foo $url bar | ||
| S: :irc.example.com 770 modernclient foo bar | ||
| S: FAIL METADATA KEY_INVALID $url :Invalid key | ||
|
|
||
| “Subscribed to too many keys” error in reply to subscription 1 | ||
|
|
||
| The client first successfully subscribes to some keys and later it tries to subscribe to some more keys, unsuccessfully. | ||
|
|
||
| C: METADATA * SUB website avatar foo bar baz | ||
| S: :irc.example.com 770 modernclient website avatar foo bar baz | ||
| C: METADATA * SUB email city | ||
| S: FAIL METADATA TOO_MANY_SUBS email :Too many subscriptions! | ||
| C: METADATA * SUBS | ||
| S: :irc.example.com 770 modernclient website avatar foo bar baz | ||
|
|
||
| “Subscribed to too many keys” error in reply to subscription 2 | ||
|
|
||
| This is like the previous case, except when the second METADATA SUB happens the server accepts the first 2 keys (email, city) but not the rest (country, bar, baz). | ||
|
|
||
| C: METADATA * SUB website avatar foo | ||
| S: :irc.example.com 770 modernclient website avatar foo | ||
| C: METADATA * SUB email city country bar baz | ||
| S: FAIL METADATA TOO_MANY_SUBS country :Too many subscriptions! | ||
| S: :irc.example.com 770 modernclient email city | ||
| C: METADATA * SUBS | ||
| S: :irc.example.com 770 modernclient website avatar city foo email | ||
|
|
||
| “Subscribed to too many keys” error in reply to subscription 3 | ||
|
|
||
| In this case, the client is trying to subscribe to a key that it is already subscribed to (website), but the key is not processed because the limit imposed by the server on the number of subscribed keys is reached before the website key is processed by the server. The client, however, successfully subscribes to the foo key which was also in the second request, but it appeared before the website key. | ||
|
|
||
| C: METADATA * SUB avatar website | ||
| S: :irc.example.com 770 modernclient avatar website | ||
| C: METADATA * SUB foo website avatar | ||
| S: FAIL METADATA TOO_MANY_SUBS website :Too many subscriptions! | ||
| S: :irc.example.com 770 modernclient :foo | ||
| C: METADATA * SUBS | ||
| S: :irc.example.com 772 modernclient avatar foo website | ||
|
|
||
| Querying the list of subscribed keys 1 | ||
|
|
||
| The server replies with a single RPL_METADATASUBS (772) numeric. | ||
|
|
||
| C: METADATA * SUB website avatar foo bar baz | ||
| S: :irc.example.com 770 modernclient website avatar foo bar baz | ||
| C: METADATA * SUBS | ||
| S: :irc.example.com 772 modernclient avatar bar baz foo website | ||
|
|
||
| Querying the list of subscribed keys 2 | ||
|
|
||
| The server replies with multiple RPL_METADATASUBS (772) numerics. | ||
|
|
||
| C: METADATA * SUB website avatar foo bar baz | ||
| S: :irc.example.com 770 modernclient website avatar foo bar baz | ||
| C: METADATA * SUBS | ||
| S: :irc.example.com 772 modernclient avatar | ||
| S: :irc.example.com 772 modernclient bar baz | ||
| S: :irc.example.com 772 modernclient foo website | ||
|
|
||
| Empty list of subscribed keys | ||
|
|
||
| In this case, there are no RPL_METADATASUB numerics sent. | ||
|
|
||
| C: METADATA * SUBS | ||
| S: *no replies* | ||
|
|
||
| Unsubscribing | ||
|
|
||
| C: METADATA * SUB website avatar foo bar baz | ||
| S: :irc.example.com 770 modernclient website avatar foo bar baz | ||
| C: METADATA * SUBS | ||
| S: :irc.example.com 772 modernclient avatar bar baz foo website | ||
| C: METADATA * UNSUB bar foo baz | ||
| S: :irc.example.com 771 modernclient baz foo bar | ||
| C: METADATA * SUBS | ||
| S: :irc.example.com 772 modernclient avatar website | ||
|
|
||
| Subscribing to the same key multiple times 1 | ||
|
|
||
| C: METADATA * SUB website avatar foo bar baz | ||
| S: :irc.example.com 779 modernclient website avatar foo bar baz | ||
| C: METADATA * SUBS | ||
| S: :irc.example.com 772 modernclient avatar bar baz foo website | ||
| C: METADATA * SUB avatar website | ||
| S: :irc.example.com 770 modernclient avatar website | ||
| C: METADATA * SUBS | ||
| S: :irc.example.com 772 modernclient avatar bar baz foo website | ||
|
|
||
| Subscribing to the same key multiple times 2 | ||
|
|
||
| The client (erroneously) subscribes to the same key twice in the same command. The server is free to include the key being subscribed to in the RPL_METADATASUBOK (770) numeric once or twice. | ||
|
|
||
| In both cases, the key will only appear once in the reply to a following METADATA SUBS command. | ||
|
|
||
| Once: | ||
|
|
||
| C: METADATA * SUB avatar avatar | ||
| S: :irc.example.com 770 modernclient avatar | ||
| C: METADATA * SUBS | ||
| S: :irc.example.com 772 modernclient avatar | ||
|
|
||
| Twice: | ||
|
|
||
| C: METADATA * SUB avatar avatar | ||
| S: :irc.example.com 770 modernclient avatar avatar | ||
| C: METADATA * SUBS | ||
| S: :irc.example.com 772 modernclient avatar | ||
|
|
||
| Unsubscribing from a non-subscribed key 1 | ||
|
|
||
| C: METADATA * SUBS | ||
| C: METADATA * UNSUB website | ||
| S: :irc.example.com 771 modernclient website | ||
| C: METADATA * SUBS | ||
| C: METADATA * SUB website | ||
| S: :irc.example.com 771 modernclient website | ||
| C: METADATA * SUBS | ||
| S: :irc.example.com 772 modernclient website | ||
|
|
||
| Unsubscribing from a non-subscribed key 2 | ||
|
|
||
| The client (erroneously) unsubscribes from the same key twice in the same command. The server is free to include the key being unsubscribed from in the RPL_METADATAUNSUBOK numeric once or twice. | ||
|
|
||
| Once: | ||
|
|
||
| C: METADATA * SUBS | ||
| S: :irc.example.com 772 modernclient website | ||
| C: METADATA * UNSUB website website | ||
| S: :irc.example.com 772 modernclient website | ||
|
|
||
| Twice: | ||
|
|
||
| C: METADATA * SUBS | ||
| S: :irc.example.com 772 modernclient website | ||
| C: METADATA * UNSUB website website | ||
| S: :irc.example.com 771 modernclient website website | ||
|
|
||
| Subscribing to a key which requires privileges but without privileges | ||
|
|
||
| C: METADATA * SUB avatar secretkey website | ||
| S: FAIL METADATA KEY_NO_PERMISSION modernclient secretkey :You do not have permission to do that. | ||
| S: :irc.example.com 770 modernclient avatar website | ||
| C: METADATA * SUBS | ||
| S: :irc.example.com 772 modernclient avatar website | ||
|
|
||
| Subscribing to invalid keys and a key which requires privileges but without privileges | ||
|
|
||
| C: METADATA * SUB $invalid1 secretkey1 $invalid2 secretkey2 website | ||
| S: FAIL METADATA KEY_NO_PERMISSION modernclient secretkey1 :You do not have permission to do that. | ||
| S: FAIL METADATA KEY_INVALID $invalid1 :Invalid key | ||
| S: FAIL METADATA KEY_NO_PERMISSION modernclient secretkey2 :You do not have permission to do that. | ||
| S: FAIL METADATA KEY_INVALID $invalid2 :Invalid key | ||
| S: :irc.example.com 770 modernclient website | ||
| C: METADATA * SUBS | ||
| S: :irc.example.com 772 modernclient website | ||
|
|
||
| Non-normative examples | ||
|
|
||
| The following examples describe how an implementation might use certain features. Unlike previous examples, they are in no way intended to guide implementations’ behaviour. | ||
| Notification for a user becoming an operator: | ||
|
|
||
| :OperServ!OperServ@services.int METADATA user1 services.operclass oper:auspex :services-root | ||
|
|
||
| Client implementation considerations | ||
|
|
||
| This section is not normative | ||
|
|
||
| While this is true of any batch, clients should take particular care not to pause processing of other messages while a metadata batch is open. As these batches can be potentially large, servers are likely to produce them asynchronously in order to avoid freezing delivery of more important messages. | ||
|
|
||
| As servers may rewrite values set by clients with METADATA SET, clients should check the response before storing it in any local cache. | ||
| Differences between this specification and metadata-notify | ||
|
|
||
| This section is not normative | ||
|
|
||
| This specification replaces an earlier deprecated metadata-notify specification, by adding the following incompatible changes: | ||
|
|
||
| The metadata-notify cap subscribed you to all keys. With the addition of explicit SUB and UNSUB subcommands, this is no longer the case. | ||
| Rate limiting protocol mechanics. | ||
| Support for delayed synchronization and METADATA SYNC. | ||
| Moved ERR_* replies to Standard Replies format | ||
| Use of batch instead of RPL_METADATAEND in situations where more than one RPL_KEYVALUE is sent. | ||
| Non-standard keys should now use a vendor prefix | ||
|
|
||
| Software supporting Metadata: Ergo, soju (as Server) | ||
| IRCv3 Working Group © 2025 · RSS | ||
|
|
||
| Home Working Group FAQs Specifications | ||
|
|
There was a problem hiding this comment.
Fix markdown formatting issues flagged by linter.
Similar to monitor.md, this file has extensive markdown formatting issues:
- Hard tabs in tables and code blocks
- Bare URLs throughout examples
- Spaces inside emphasis markers
These should be corrected to pass linting and maintain consistency.
Additionally, consider adding a note at the top of the file clearly indicating this is reference documentation copied from the IRCv3 specification, to avoid confusion about authorship and maintenance responsibility.
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)
271-271: Hard tabs
Column: 6
(MD010, no-hard-tabs)
272-272: Hard tabs
Column: 16
(MD010, no-hard-tabs)
273-273: Hard tabs
Column: 13
(MD010, no-hard-tabs)
274-274: Hard tabs
Column: 20
(MD010, no-hard-tabs)
275-275: Hard tabs
Column: 19
(MD010, no-hard-tabs)
276-276: Hard tabs
Column: 13
(MD010, no-hard-tabs)
277-277: Hard tabs
Column: 15
(MD010, no-hard-tabs)
278-278: Hard tabs
Column: 14
(MD010, no-hard-tabs)
279-279: Hard tabs
Column: 15
(MD010, no-hard-tabs)
280-280: Hard tabs
Column: 15
(MD010, no-hard-tabs)
283-283: Hard tabs
Column: 6
(MD010, no-hard-tabs)
283-283: Hard tabs
Column: 11
(MD010, no-hard-tabs)
283-283: Hard tabs
Column: 17
(MD010, no-hard-tabs)
283-283: Hard tabs
Column: 22
(MD010, no-hard-tabs)
283-283: Hard tabs
Column: 29
(MD010, no-hard-tabs)
283-283: Hard tabs
Column: 34
(MD010, no-hard-tabs)
283-283: Hard tabs
Column: 41
(MD010, no-hard-tabs)
283-283: Hard tabs
Column: 47
(MD010, no-hard-tabs)
283-283: Hard tabs
Column: 53
(MD010, no-hard-tabs)
284-284: Hard tabs
Column: 16
(MD010, no-hard-tabs)
284-284: Hard tabs
Column: 19
(MD010, no-hard-tabs)
284-284: Hard tabs
Column: 22
(MD010, no-hard-tabs)
284-284: Hard tabs
Column: 25
(MD010, no-hard-tabs)
284-284: Hard tabs
Column: 28
(MD010, no-hard-tabs)
284-284: Hard tabs
Column: 31
(MD010, no-hard-tabs)
284-284: Hard tabs
Column: 34
(MD010, no-hard-tabs)
284-284: Hard tabs
Column: 37
(MD010, no-hard-tabs)
284-284: Hard tabs
Column: 40
(MD010, no-hard-tabs)
284-284: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
284-284: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
284-284: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
284-284: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
284-284: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
284-284: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
284-284: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
284-284: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
285-285: Hard tabs
Column: 13
(MD010, no-hard-tabs)
285-285: Hard tabs
Column: 16
(MD010, no-hard-tabs)
285-285: Hard tabs
Column: 19
(MD010, no-hard-tabs)
285-285: Hard tabs
Column: 22
(MD010, no-hard-tabs)
285-285: Hard tabs
Column: 25
(MD010, no-hard-tabs)
285-285: Hard tabs
Column: 28
(MD010, no-hard-tabs)
285-285: Hard tabs
Column: 31
(MD010, no-hard-tabs)
285-285: Hard tabs
Column: 34
(MD010, no-hard-tabs)
285-285: Hard tabs
Column: 37
(MD010, no-hard-tabs)
285-285: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
285-285: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
285-285: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
285-285: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
286-286: Hard tabs
Column: 15
(MD010, no-hard-tabs)
286-286: Hard tabs
Column: 18
(MD010, no-hard-tabs)
286-286: Hard tabs
Column: 21
(MD010, no-hard-tabs)
286-286: Hard tabs
Column: 24
(MD010, no-hard-tabs)
286-286: Hard tabs
Column: 27
(MD010, no-hard-tabs)
286-286: Hard tabs
Column: 30
(MD010, no-hard-tabs)
286-286: Hard tabs
Column: 33
(MD010, no-hard-tabs)
286-286: Hard tabs
Column: 36
(MD010, no-hard-tabs)
286-286: Hard tabs
Column: 39
(MD010, no-hard-tabs)
287-287: Hard tabs
Column: 20
(MD010, no-hard-tabs)
287-287: Hard tabs
Column: 23
(MD010, no-hard-tabs)
287-287: Hard tabs
Column: 26
(MD010, no-hard-tabs)
287-287: Hard tabs
Column: 29
(MD010, no-hard-tabs)
287-287: Hard tabs
Column: 32
(MD010, no-hard-tabs)
287-287: Hard tabs
Column: 35
(MD010, no-hard-tabs)
287-287: Hard tabs
Column: 38
(MD010, no-hard-tabs)
287-287: Hard tabs
Column: 41
(MD010, no-hard-tabs)
287-287: Hard tabs
Column: 44
(MD010, no-hard-tabs)
287-287: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
288-288: Hard tabs
Column: 19
(MD010, no-hard-tabs)
288-288: Hard tabs
Column: 22
(MD010, no-hard-tabs)
288-288: Hard tabs
Column: 25
(MD010, no-hard-tabs)
288-288: Hard tabs
Column: 28
(MD010, no-hard-tabs)
288-288: Hard tabs
Column: 31
(MD010, no-hard-tabs)
288-288: Hard tabs
Column: 34
(MD010, no-hard-tabs)
288-288: Hard tabs
Column: 37
(MD010, no-hard-tabs)
288-288: Hard tabs
Column: 40
(MD010, no-hard-tabs)
288-288: Hard tabs
Column: 43
(MD010, no-hard-tabs)
288-288: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
288-288: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
288-288: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
288-288: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
289-289: Hard tabs
Column: 13
(MD010, no-hard-tabs)
289-289: Hard tabs
Column: 16
(MD010, no-hard-tabs)
289-289: Hard tabs
Column: 19
(MD010, no-hard-tabs)
289-289: Hard tabs
Column: 22
(MD010, no-hard-tabs)
289-289: Hard tabs
Column: 25
(MD010, no-hard-tabs)
289-289: Hard tabs
Column: 28
(MD010, no-hard-tabs)
289-289: Hard tabs
Column: 31
(MD010, no-hard-tabs)
289-289: Hard tabs
Column: 34
(MD010, no-hard-tabs)
289-289: Hard tabs
Column: 37
(MD010, no-hard-tabs)
289-289: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
290-290: Hard tabs
Column: 15
(MD010, no-hard-tabs)
290-290: Hard tabs
Column: 18
(MD010, no-hard-tabs)
290-290: Hard tabs
Column: 21
(MD010, no-hard-tabs)
290-290: Hard tabs
Column: 24
(MD010, no-hard-tabs)
290-290: Hard tabs
Column: 27
(MD010, no-hard-tabs)
290-290: Hard tabs
Column: 30
(MD010, no-hard-tabs)
290-290: Hard tabs
Column: 33
(MD010, no-hard-tabs)
290-290: Hard tabs
Column: 36
(MD010, no-hard-tabs)
290-290: Hard tabs
Column: 39
(MD010, no-hard-tabs)
291-291: Hard tabs
Column: 14
(MD010, no-hard-tabs)
291-291: Hard tabs
Column: 17
(MD010, no-hard-tabs)
291-291: Hard tabs
Column: 20
(MD010, no-hard-tabs)
291-291: Hard tabs
Column: 23
(MD010, no-hard-tabs)
291-291: Hard tabs
Column: 26
(MD010, no-hard-tabs)
291-291: Hard tabs
Column: 29
(MD010, no-hard-tabs)
291-291: Hard tabs
Column: 32
(MD010, no-hard-tabs)
291-291: Hard tabs
Column: 35
(MD010, no-hard-tabs)
291-291: Hard tabs
Column: 38
(MD010, no-hard-tabs)
291-291: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
292-292: Hard tabs
Column: 15
(MD010, no-hard-tabs)
292-292: Hard tabs
Column: 18
(MD010, no-hard-tabs)
292-292: Hard tabs
Column: 21
(MD010, no-hard-tabs)
292-292: Hard tabs
Column: 24
(MD010, no-hard-tabs)
292-292: Hard tabs
Column: 27
(MD010, no-hard-tabs)
292-292: Hard tabs
Column: 30
(MD010, no-hard-tabs)
292-292: Hard tabs
Column: 33
(MD010, no-hard-tabs)
292-292: Hard tabs
Column: 36
(MD010, no-hard-tabs)
292-292: Hard tabs
Column: 39
(MD010, no-hard-tabs)
305-305: Hard tabs
Column: 5
(MD010, no-hard-tabs)
305-305: Hard tabs
Column: 12
(MD010, no-hard-tabs)
306-306: Hard tabs
Column: 5
(MD010, no-hard-tabs)
306-306: Hard tabs
Column: 24
(MD010, no-hard-tabs)
307-307: Hard tabs
Column: 5
(MD010, no-hard-tabs)
307-307: Hard tabs
Column: 19
(MD010, no-hard-tabs)
308-308: Hard tabs
Column: 5
(MD010, no-hard-tabs)
308-308: Hard tabs
Column: 20
(MD010, no-hard-tabs)
309-309: Hard tabs
Column: 5
(MD010, no-hard-tabs)
309-309: Hard tabs
Column: 24
(MD010, no-hard-tabs)
310-310: Hard tabs
Column: 5
(MD010, no-hard-tabs)
310-310: Hard tabs
Column: 26
(MD010, no-hard-tabs)
311-311: Hard tabs
Column: 5
(MD010, no-hard-tabs)
311-311: Hard tabs
Column: 23
(MD010, no-hard-tabs)
312-312: Hard tabs
Column: 5
(MD010, no-hard-tabs)
312-312: Hard tabs
Column: 28
(MD010, no-hard-tabs)
315-315: Hard tabs
Column: 7
(MD010, no-hard-tabs)
315-315: Hard tabs
Column: 12
(MD010, no-hard-tabs)
315-315: Hard tabs
Column: 18
(MD010, no-hard-tabs)
315-315: Hard tabs
Column: 23
(MD010, no-hard-tabs)
315-315: Hard tabs
Column: 30
(MD010, no-hard-tabs)
315-315: Hard tabs
Column: 35
(MD010, no-hard-tabs)
315-315: Hard tabs
Column: 42
(MD010, no-hard-tabs)
315-315: Hard tabs
Column: 48
(MD010, no-hard-tabs)
315-315: Hard tabs
Column: 54
(MD010, no-hard-tabs)
316-316: Hard tabs
Column: 19
(MD010, no-hard-tabs)
316-316: Hard tabs
Column: 22
(MD010, no-hard-tabs)
316-316: Hard tabs
Column: 25
(MD010, no-hard-tabs)
316-316: Hard tabs
Column: 28
(MD010, no-hard-tabs)
316-316: Hard tabs
Column: 31
(MD010, no-hard-tabs)
316-316: Hard tabs
Column: 34
(MD010, no-hard-tabs)
316-316: Hard tabs
Column: 37
(MD010, no-hard-tabs)
316-316: Hard tabs
Column: 40
(MD010, no-hard-tabs)
316-316: Hard tabs
Column: 43
(MD010, no-hard-tabs)
317-317: Hard tabs
Column: 14
(MD010, no-hard-tabs)
317-317: Hard tabs
Column: 17
(MD010, no-hard-tabs)
317-317: Hard tabs
Column: 20
(MD010, no-hard-tabs)
317-317: Hard tabs
Column: 23
(MD010, no-hard-tabs)
317-317: Hard tabs
Column: 26
(MD010, no-hard-tabs)
317-317: Hard tabs
Column: 29
(MD010, no-hard-tabs)
317-317: Hard tabs
Column: 32
(MD010, no-hard-tabs)
317-317: Hard tabs
Column: 35
(MD010, no-hard-tabs)
317-317: Hard tabs
Column: 38
(MD010, no-hard-tabs)
317-317: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
317-317: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
317-317: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
317-317: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
318-318: Hard tabs
Column: 15
(MD010, no-hard-tabs)
318-318: Hard tabs
Column: 18
(MD010, no-hard-tabs)
318-318: Hard tabs
Column: 21
(MD010, no-hard-tabs)
318-318: Hard tabs
Column: 24
(MD010, no-hard-tabs)
318-318: Hard tabs
Column: 27
(MD010, no-hard-tabs)
318-318: Hard tabs
Column: 30
(MD010, no-hard-tabs)
318-318: Hard tabs
Column: 33
(MD010, no-hard-tabs)
318-318: Hard tabs
Column: 36
(MD010, no-hard-tabs)
318-318: Hard tabs
Column: 39
(MD010, no-hard-tabs)
319-319: Hard tabs
Column: 19
(MD010, no-hard-tabs)
319-319: Hard tabs
Column: 22
(MD010, no-hard-tabs)
319-319: Hard tabs
Column: 25
(MD010, no-hard-tabs)
319-319: Hard tabs
Column: 28
(MD010, no-hard-tabs)
319-319: Hard tabs
Column: 31
(MD010, no-hard-tabs)
319-319: Hard tabs
Column: 34
(MD010, no-hard-tabs)
319-319: Hard tabs
Column: 37
(MD010, no-hard-tabs)
319-319: Hard tabs
Column: 40
(MD010, no-hard-tabs)
319-319: Hard tabs
Column: 43
(MD010, no-hard-tabs)
319-319: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
320-320: Hard tabs
Column: 21
(MD010, no-hard-tabs)
320-320: Hard tabs
Column: 24
(MD010, no-hard-tabs)
320-320: Hard tabs
Column: 27
(MD010, no-hard-tabs)
320-320: Hard tabs
Column: 30
(MD010, no-hard-tabs)
320-320: Hard tabs
Column: 33
(MD010, no-hard-tabs)
320-320: Hard tabs
Column: 36
(MD010, no-hard-tabs)
320-320: Hard tabs
Column: 39
(MD010, no-hard-tabs)
320-320: Hard tabs
Column: 42
(MD010, no-hard-tabs)
320-320: Hard tabs
Column: 45
(MD010, no-hard-tabs)
321-321: Hard tabs
Column: 18
(MD010, no-hard-tabs)
321-321: Hard tabs
Column: 21
(MD010, no-hard-tabs)
321-321: Hard tabs
Column: 24
(MD010, no-hard-tabs)
321-321: Hard tabs
Column: 27
(MD010, no-hard-tabs)
321-321: Hard tabs
Column: 30
(MD010, no-hard-tabs)
321-321: Hard tabs
Column: 33
(MD010, no-hard-tabs)
321-321: Hard tabs
Column: 36
(MD010, no-hard-tabs)
321-321: Hard tabs
Column: 39
(MD010, no-hard-tabs)
321-321: Hard tabs
Column: 42
(MD010, no-hard-tabs)
321-321: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
322-322: Hard tabs
Column: 23
(MD010, no-hard-tabs)
322-322: Hard tabs
Column: 26
(MD010, no-hard-tabs)
322-322: Hard tabs
Column: 29
(MD010, no-hard-tabs)
322-322: Hard tabs
Column: 32
(MD010, no-hard-tabs)
322-322: Hard tabs
Column: 35
(MD010, no-hard-tabs)
322-322: Hard tabs
Column: 38
(MD010, no-hard-tabs)
322-322: Hard tabs
Column: 41
(MD010, no-hard-tabs)
322-322: Hard tabs
Column: 44
(MD010, no-hard-tabs)
322-322: Hard tabs
Column: 47
(MD010, no-hard-tabs)
322-322: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
322-322: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
348-348: Bare URL used
(MD034, no-bare-urls)
348-348: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
349-349: Bare URL used
(MD034, no-bare-urls)
349-349: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
353-353: Bare URL used
(MD034, no-bare-urls)
358-358: Bare URL used
(MD034, no-bare-urls)
363-363: Bare URL used
(MD034, no-bare-urls)
364-364: Bare URL used
(MD034, no-bare-urls)
373-373: Bare URL used
(MD034, no-bare-urls)
378-378: Bare URL used
(MD034, no-bare-urls)
378-378: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
379-379: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
383-383: Bare URL used
(MD034, no-bare-urls)
383-383: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
384-384: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
394-394: Bare URL used
(MD034, no-bare-urls)
394-394: Bare URL used
(MD034, no-bare-urls)
398-398: Bare URL used
(MD034, no-bare-urls)
405-405: Bare URL used
(MD034, no-bare-urls)
405-405: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
406-406: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
422-422: Bare URL used
(MD034, no-bare-urls)
429-429: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
430-430: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
431-431: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
432-432: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
443-443: Bare URL used
(MD034, no-bare-urls)
460-460: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
461-461: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
462-462: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
463-463: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
464-464: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
465-465: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
473-473: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
475-475: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
495-495: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
497-497: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
506-506: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
508-508: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
518-518: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
520-520: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
530-530: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
532-532: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
539-539: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
541-541: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
555-555: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
557-557: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
559-559: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
561-561: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
566-566: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
568-568: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
570-570: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
572-572: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
583-583: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
585-585: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
590-590: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
592-592: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
597-597: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
598-598: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
600-600: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
601-601: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
612-612: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
614-614: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
619-619: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
621-621: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
626-626: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
629-629: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
634-634: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
640-640: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
648-648: Bare URL used
(MD034, no-bare-urls)
🤖 Prompt for AI Agents
In ai_docs/metadata-2.md (lines 1-674), the linter flagged Markdown issues: hard
tabs in tables/code blocks, bare URLs in examples, and spaces inside emphasis
markers; also add a top-note about the file being reference material from the
IRCv3 spec. Fix by replacing hard tabs with 2-4 spaces everywhere (especially in
tables and fenced code blocks), convert bare URLs to explicit Markdown links or
wrap them in inline code where appropriate, remove stray spaces inside
*emphasis* and _emphasis_ markers so formatting is correct, ensure tables use
proper pipe-separated rows or code fences for blocks, and add a short
non-normative header at the top stating the document is reference/copied from
the IRCv3 specification with copyright and maintenance guidance.
|
|
||
| IRCv3 | ||
|
|
||
| Home Working Group FAQs Specifications Registry Support Tables | ||
|
|
||
| Monitor | ||
|
|
||
| Copyright © 2004-2015 Lee Hardy <lee@leeh.co.uk> | ||
|
|
||
| Copyright © 2013-2015 Kiyoshi Aman <kiyoshi.aman@gmail.com> | ||
|
|
||
| Copyright © 2015 William Pitcock <nenolod@dereferenced.org> | ||
|
|
||
| Unlimited redistribution and modification of this document is allowed provided that the above copyright notice and this permission notice remains intact. | ||
|
|
||
| A protocol for notification of when clients become online/offline | ||
| Introduction | ||
|
|
||
| Currently, ISON requests by clients use a large amount of bandwidth. It is expected that it is more efficient for this to be done by the server at the expense of cpu cycles. This specification deprecates both the ISON and legacy WATCH extensions. | ||
| WATCH vs. MONITOR | ||
|
|
||
| The WATCH implementation suffers from quite a few problems. First, the implementation of the WATCH command is non-standard, and differs between different vendor implementations of the WATCH command. | ||
|
|
||
| The MONITOR extension enhances the legacy WATCH command by providing documented, standardized, ISON style notifications instead of one numeric per watch-list entry as with WATCH. Further, the MONITOR implementation is allowed to multicast notifications to every client which has a subscription to a target whom is subject to a state change. The MONITOR implementation also enhances user privacy by disallowing subscription to hostmasks, allowing users to avoid nick-change stalking. | ||
| MONITOR Command | ||
|
|
||
| The command used throughout this specification is MONITOR. | ||
|
|
||
| Each use of the MONITOR command takes a special modifier, indicating the operation being performed. The client MUST NOT attempt to specify more than one modifier. Only one special modifier may be used per MONITOR command. | ||
|
|
||
| Thus it is impossible to combine additions to the list with removals from the list – these MUST be done with two separate commands. | ||
|
|
||
| In commands and numerics where multiple targets may occur, the length of the target list is limited only by the buffer size of 512 chars, as defined in RFC1459. | ||
|
|
||
| Support of this specification is indicated by the MONITOR token in RPL_ISUPPORT (005). This token takes an optional parameter, of the maximum amount of targets a client may have in their monitor list. If no parameter is specified, there is no limit. A typical token would be: | ||
|
|
||
| MONITOR=100 | ||
|
|
||
| For this specification, ‘target’ MUST be a valid nick as determined by the IRC daemon. | ||
| MONITOR + target[,target2]* | ||
|
|
||
| Adds the given list of targets to the list of targets being monitored. Targets already in the list MUST NOT be added again. | ||
|
|
||
| If any of the targets being added are online, the server will generate RPL_MONONLINE numerics listing those targets that are online. | ||
|
|
||
| If any of the targets being added are offline, the server will generate RPL_MONOFFLINE numerics listing those targets that are offline. | ||
| MONITOR - target[,target2]* | ||
|
|
||
| Removes the given list of targets from the list of targets being monitored. No output will be returned for use of this command. | ||
| MONITOR C | ||
|
|
||
| Clears the list of targets being monitored. No output will be returned for use of this command. | ||
| MONITOR L | ||
|
|
||
| Outputs the current list of targets being monitored. All output will use RPL_MONLIST, and the output will be terminated with RPL_ENDOFMONLIST. | ||
| MONITOR S | ||
|
|
||
| Outputs for each target in the list being monitored, whether the client is online or offline. All targets that are online will be sent using RPL_MONONLINE, all targets that are offline will be sent using RPL_MONOFFLINE. | ||
| Numeric replies | ||
| 730 - RPL_MONONLINE | ||
|
|
||
| :<server> 730 <nick> :target[!user@host][,target[!user@host]]* | ||
|
|
||
| This numeric is used to indicate to a client that either a target has just become online, or that a target they have added to their monitor list is online. | ||
|
|
||
| The server MAY send a hostmask with the target. | ||
|
|
||
| The server may send “*” instead of the nick (<nick>). (This makes it possible to send the exact same message to all clients monitoring a certain target.) | ||
| 731 - RPL_MONOFFLINE | ||
|
|
||
| :<server> 731 <nick> :target[,target2]* | ||
|
|
||
| This numeric is used to indicate to a client that either a target has just left the irc network, or that a target they have added to their monitor list is offline. | ||
|
|
||
| The argument is a chained list of targets that are offline. | ||
|
|
||
| As with 730, the server may send “*” instead of the nick (<nick>). | ||
| 732 - RPL_MONLIST | ||
|
|
||
| :<server> 732 <nick> :target[,target2]* | ||
|
|
||
| This numeric is used to indicate to a client the list of targets they have in their monitor list. | ||
| 733 - RPL_ENDOFMONLIST | ||
|
|
||
| :<server> 733 <nick> :End of MONITOR list | ||
|
|
||
| This numeric is used to indicate to a client the end of a monitor list. | ||
| 734 - ERR_MONLISTFULL | ||
|
|
||
| :<server> 734 <nick> <limit> <targets> :Monitor list is full. | ||
|
|
||
| This numeric is used to indicate to a client that their monitor list is full, so the command failed. The <limit> parameter is the maximum number of targets a client may have in their list, the <targets> parameter is the list of targets, as the client sent them, that cannot be added. | ||
| Errata | ||
|
|
||
| Earlier version of this spec had RPL_MONONLINE only sending nicknames, This did not match existing implementations. | ||
|
|
||
| Software supporting Monitor: Ergo, IRCCloud Teams, ircd-hybrid, InspIRCd, Solanum, txircd, UnrealIRCd, AdiIRC, Ambassador, Colloquy, glirc, Halloy, HexChat, IceChat, mIRC, Mozilla Thunderbird, senpai, Textual, WeeChat, gamja, IRCCloud, Colloquy, CoreIRC, Palaver, Goguma, IRCCloud (as Server), pounce (as Server), pounce (as Client), soju (as Server), soju (as Client), BitBot, Eggdrop, Limnoria, Communi, girc, ircrobots, Kitteh IRC Client Library, pydle, Rust irc, Warren | ||
| IRCv3 Working Group © 2025 · RSS | ||
|
|
||
| Home Working Group FAQs Specifications |
There was a problem hiding this comment.
Fix markdown formatting issues flagged by linter.
The static analysis tool reports numerous markdown formatting issues throughout this file:
- Hard tabs (should use spaces)
- Bare URLs (should be wrapped in angle brackets or markdown link syntax)
- Spaces inside emphasis markers
These formatting issues should be addressed to maintain code quality standards and ensure the linter passes.
Example fixes:
-MONITOR=100
+`MONITOR=100`
-:<server> 730 <nick> :target[!user@host][,target[!user@host]]*
+`:<server> 730 <nick> :target[!user@host][,target[!user@host]]*`For tables, replace hard tabs with spaces or use proper markdown table syntax.
🤖 Prompt for AI Agents
In ai_docs/monitor.md around lines 1 to 100, the file contains markdown lint
issues: replace all hard tabs with spaces (use 2 or 4 spaces consistently),
convert bare URLs to angle-bracketed or standard markdown link syntax
(<https://...> or [text](https://...)), remove spaces inside emphasis markers so
emphasis is like *word* or _word_, and for any tab-delimited content convert to
proper markdown table syntax using pipes or align with spaces; after edits run
the repository markdown linter to verify no remaining warnings.
| ``` | ||
| H = Here (green indicator) | ||
| G = Gone/Away (yellow indicator) | ||
| No response = Offline (grey indicator) | ||
| ``` |
There was a problem hiding this comment.
Add the missing language tag for this fence.
markdownlint (MD040) is also flagging this block. Tag it (e.g., text) so linting passes.
-```
+```text
H = Here (green indicator)
G = Gone/Away (yellow indicator)
No response = Offline (grey indicator)
-```
+```🧰 Tools
🪛 markdownlint-cli2 (0.18.1)
41-41: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
In BUG_FIXES.md around lines 41 to 45, the fenced code block is missing a
language tag which triggers markdownlint MD040; add a language tag (e.g., text)
after the opening backticks so the block becomes ```text, leaving the content
and closing backticks unchanged so the linter no longer flags it.
| ``` | ||
| User Action → Store Action → IRC Command → Server Response → Event Handler → State Update → UI Update | ||
| ``` | ||
|
|
There was a problem hiding this comment.
Specify the fence language to satisfy markdownlint.
markdownlint (MD040) is complaining because this code block has no language tag. Add one (e.g., text) so the docs build and lint pass.
-```
+```text
User Action → Store Action → IRC Command → Server Response → Event Handler → State Update → UI Update
-```
+```🤖 Prompt for AI Agents
In PINNED_PMS_AND_MONITOR.md around lines 111 to 114, the fenced code block
lacks a language tag causing markdownlint MD040; update the opening fence to
include a language (e.g., change ``` to ```text) so the block becomes a
text-fenced code block and the linter will pass.
| useEffect(() => { | ||
| setAvatarLoadFailed(false); | ||
| }, []); |
There was a problem hiding this comment.
Reset the avatar fallback when the URL updates.
This effect runs only once, so after one load error the fallback sticks even if new metadata delivers a valid avatar later. That keeps the avatar permanently broken until a full reload. Depend on avatarUrl so the failure state clears whenever a new image arrives.
-useEffect(() => {
- setAvatarLoadFailed(false);
-}, []);
+useEffect(() => {
+ setAvatarLoadFailed(false);
+}, [avatarUrl]);📝 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.
| useEffect(() => { | |
| setAvatarLoadFailed(false); | |
| }, []); | |
| useEffect(() => { | |
| setAvatarLoadFailed(false); | |
| }, [avatarUrl]); |
🤖 Prompt for AI Agents
In src/components/layout/MemberList.tsx around lines 85 to 87, the useEffect
currently resets setAvatarLoadFailed only once (empty deps) causing the avatar
fallback to persist when a new avatarUrl arrives; update the effect to depend on
avatarUrl (e.g., include avatarUrl in the dependency array) so that
setAvatarLoadFailed(false) runs whenever the avatar URL changes and clears the
failure state for new images.
| useStore.setState((state) => { | ||
| const server = state.servers.find((s) => s.id === serverId); | ||
| if (!server) return {}; | ||
|
|
||
| // Create private chat objects for pinned users | ||
| const restoredPrivateChats: PrivateChat[] = sortedPinnedChats.map( | ||
| ({ username, order }) => ({ | ||
| id: uuidv4(), | ||
| username, | ||
| serverId, | ||
| unreadCount: 0, | ||
| isMentioned: false, | ||
| lastActivity: new Date(), | ||
| isPinned: true, | ||
| order, | ||
| isOnline: false, // Will be updated by MONITOR | ||
| isAway: false, | ||
| }), | ||
| ); | ||
|
|
||
| const updatedServers = state.servers.map((s) => { | ||
| if (s.id === serverId) { | ||
| return { | ||
| ...s, | ||
| privateChats: [...(s.privateChats || []), ...restoredPrivateChats], | ||
| }; | ||
| } | ||
| return s; | ||
| }); | ||
|
|
||
| return { servers: updatedServers }; | ||
| }); |
There was a problem hiding this comment.
Avoid duplicating pinned PM tabs on reconnect.
On each ready event we append freshly restored pinned PM objects to server.privateChats without first checking whether a chat for that username already exists. Because we keep the previous privateChats array when a server reconnects, the next ready call produces duplicate tabs (same user, different IDs) for every pinned contact. Please update the existing entry in place (set isPinned, order, etc.) instead of blindly concatenating, or filter the restoration payload to skip usernames that are already present (case-insensitive).
🤖 Prompt for AI Agents
In src/store/index.ts around lines 3751 to 3782, the restore logic appends
restoredPrivateChats to server.privateChats unconditionally, causing duplicate
pinned PMs on reconnect; instead, for each restoredPrivateChat perform a
case-insensitive check against existing s.privateChats usernames and if a match
exists update that existing chat in-place (set isPinned=true, order,
lastActivity if newer, reset unreadCount/isMentioned/isOnline/isAway as
appropriate) otherwise add the new chat; alternatively filter
restoredPrivateChats before concatenation to exclude usernames already present
(compare toLowerCase()) so you never create duplicate entries.
There was a problem hiding this comment.
slop, ai_docs?
I mean maybe is worth to start having it, but maybe it is also too much unecessary context even for ais.
Applies to everythin in ths directory.
| {/* User avatar with status indicator */} | ||
| <div className="relative flex-shrink-0"> | ||
| {(() => { | ||
| const userMetadata = getUserMetadata( | ||
| privateChat.username, | ||
| ); | ||
| const avatarUrl = userMetadata?.avatar?.value; | ||
|
|
||
| return avatarUrl ? ( | ||
| <img | ||
| src={avatarUrl} | ||
| alt={privateChat.username} | ||
| className={`rounded-full object-cover ${ | ||
| selectedPrivateChatId === privateChat.id | ||
| ? "w-8 h-8" | ||
| : "w-6 h-6" | ||
| }`} | ||
| onError={(e) => { | ||
| // Fallback to FaUser icon on error | ||
| e.currentTarget.style.display = "none"; | ||
| const parent = | ||
| e.currentTarget.parentElement; | ||
| const fallbackIcon = parent?.querySelector( | ||
| ".fallback-user-icon", | ||
| ); | ||
| if (fallbackIcon) { | ||
| ( | ||
| fallbackIcon as HTMLElement | ||
| ).style.display = "block"; | ||
| } | ||
| }} | ||
| /> | ||
| ) : ( | ||
| <FaUser | ||
| className={`shrink-0 fallback-user-icon ${ | ||
| selectedPrivateChatId === privateChat.id | ||
| ? "text-2xl" | ||
| : "" | ||
| }`} | ||
| /> | ||
| ); | ||
| })()} | ||
| {/* Fallback icon (hidden by default if avatar exists) */} | ||
| {getUserMetadata(privateChat.username)?.avatar | ||
| ?.value && ( | ||
| <FaUser | ||
| className={`shrink-0 fallback-user-icon ${ | ||
| selectedPrivateChatId === privateChat.id | ||
| ? "text-2xl" | ||
| : "" | ||
| }`} | ||
| style={{ display: "none" }} | ||
| /> | ||
| )} | ||
| {/* Status indicator */} | ||
| <span | ||
| className={`absolute -bottom-0.5 -right-0.5 w-3 h-3 rounded-full border-2 border-discord-dark-200 ${ | ||
| privateChat.isOnline | ||
| ? privateChat.isAway | ||
| ? "bg-yellow-500" | ||
| : "bg-green-500" | ||
| : "bg-gray-500" | ||
| }`} | ||
| title={ | ||
| privateChat.isOnline | ||
| ? privateChat.isAway | ||
| ? "Away" | ||
| : "Online" | ||
| : "Offline" | ||
| } | ||
| /> | ||
| </div> | ||
| <div className="flex flex-col truncate min-w-0"> | ||
| {/* Display name or username */} | ||
| <span className="truncate font-medium max-w-full"> | ||
| {(() => { | ||
| const userMetadata = getUserMetadata( | ||
| privateChat.username, | ||
| ); | ||
| const displayName = | ||
| userMetadata?.["display-name"]?.value; | ||
| const user = getUserFromChannels( | ||
| privateChat.username, | ||
| ); | ||
| return ( | ||
| <> | ||
| {displayName || privateChat.username} | ||
| {/* Only show verified badge if NO display-name (showing username directly) */} | ||
| {renderUserBadges( | ||
| privateChat.username, | ||
| privateChat, | ||
| user, | ||
| !displayName, | ||
| )} | ||
| </> | ||
| ); | ||
| })()} | ||
| </span> | ||
| {/* Badge with nick/realname and status/away message */} | ||
| <div className="flex items-center gap-1.5 text-xs truncate"> | ||
| {(() => { | ||
| const userMetadata = getUserMetadata( | ||
| privateChat.username, | ||
| ); | ||
| const displayName = | ||
| userMetadata?.["display-name"]?.value; | ||
| const user = getUserFromChannels( | ||
| privateChat.username, | ||
| ); | ||
|
|
||
| // Show username in green badge if display-name exists | ||
| const showUsernameBadge = !!displayName; | ||
|
|
||
| // Determine what to show after the username badge | ||
| let secondPart: React.ReactNode = null; | ||
| if (!displayName) { | ||
| // If no display-name (nick is shown as main text), show realname | ||
| const realname = | ||
| privateChat.realname || user?.realname; | ||
| if (realname) { | ||
| // Parse IRC colors/formatting in realname | ||
| secondPart = mircToHtml(realname); | ||
| } | ||
| } | ||
|
|
||
| // Away message or status (always check for this) | ||
| const awayMsg = privateChat.awayMessage; | ||
| const statusText = userMetadata?.status?.value; | ||
| const statusOrAway = awayMsg || statusText; | ||
| const isAway = !!awayMsg; | ||
|
|
||
| // If we have both secondPart and status, append status | ||
| if (secondPart && statusOrAway) { | ||
| secondPart = ( | ||
| <> | ||
| {secondPart} | ||
| <span className="text-discord-text-muted opacity-50 mx-1.5"> | ||
| • | ||
| </span> | ||
| <span | ||
| className={`text-discord-text-muted truncate ${isAway ? "italic" : ""}`} | ||
| > | ||
| {statusOrAway} | ||
| </span> | ||
| </> | ||
| ); | ||
| } else if (!secondPart && statusOrAway) { | ||
| // Only status/away, no realname | ||
| secondPart = ( | ||
| <span | ||
| className={`text-discord-text-muted truncate ${isAway ? "italic" : ""}`} | ||
| > | ||
| {statusOrAway} | ||
| </span> | ||
| ); | ||
| } | ||
|
|
||
| // Render the badge | ||
| if (showUsernameBadge && secondPart) { | ||
| return ( | ||
| <> | ||
| <span | ||
| className={`bg-gray-300 text-black px-0.5 py-0 rounded font-bold whitespace-nowrap ${ | ||
| selectedPrivateChatId === | ||
| privateChat.id | ||
| ? "text-[11px]" | ||
| : "text-[9px]" | ||
| }`} | ||
| > | ||
| {privateChat.username} | ||
| {renderUserBadges( | ||
| privateChat.username, | ||
| privateChat, | ||
| user, | ||
| )} | ||
| </span> | ||
| <span className="text-discord-text-muted opacity-50"> | ||
| • | ||
| </span> | ||
| {secondPart} | ||
| </> | ||
| ); | ||
| } | ||
| if (showUsernameBadge) { | ||
| return ( | ||
| <span | ||
| className={`bg-gray-300 text-black px-0.5 py-0 rounded font-bold whitespace-nowrap ${ | ||
| selectedPrivateChatId === privateChat.id | ||
| ? "text-[11px]" | ||
| : "text-[9px]" | ||
| }`} | ||
| > | ||
| {privateChat.username} | ||
| {renderUserBadges( | ||
| privateChat.username, | ||
| privateChat, | ||
| user, | ||
| )} | ||
| </span> | ||
| ); | ||
| } | ||
| if (secondPart) { | ||
| return secondPart; | ||
| } | ||
|
|
||
| return null; | ||
| })()} | ||
| </div> | ||
| </div> |
There was a problem hiding this comment.
Could be a new component together with getUserMetatadat. This file is so huge and I think it makes even worse for LLMs.
There was a problem hiding this comment.
true, a refactoring for next minor release
| @@ -26,13 +26,20 @@ const LOCAL_STORAGE_SERVERS_KEY = "savedServers"; | |||
| const LOCAL_STORAGE_METADATA_KEY = "serverMetadata"; | |||
| const LOCAL_STORAGE_SETTINGS_KEY = "globalSettings"; | |||
| const LOCAL_STORAGE_CHANNEL_ORDER_KEY = "channelOrder"; | |||
There was a problem hiding this comment.
Oh I thought it was not new object and used the order of the channels in the array itself 🤔 was this relaly needed?
There was a problem hiding this comment.
This is retaining the order of your pinned PMs
There was a problem hiding this comment.
LOCAL_STORAGE_CHANNEL_ORDER_KEY = "channelOrder"; ?
| ); | ||
| } | ||
|
|
||
| // Load pinned private chats from localStorage |
There was a problem hiding this comment.
We have hundreds of these load and save from storage functions, pretty sure the ycould be just the same and we pass in the key, or if you want, an abstract parent class or fabric or whatever you will
There was a problem hiding this comment.
you mean just use localStorage.setItem? I like the wrappers.
| // Request chathistory for this new PM (if server supports it) | ||
| setTimeout(() => { | ||
| ircClient.sendRaw(serverId, `CHATHISTORY LATEST ${username} * 100`); | ||
| }, 50); | ||
|
|
||
| // Only request WHO if we don't already have complete user info | ||
| if (!hasUserInfo) { | ||
| // Request WHO to get their current status (H=here/green, G=gone/yellow) using WHOX to also get account | ||
| // Fields: u=username, h=hostname, n=nickname, f=flags, a=account, r=realname | ||
| setTimeout(() => { | ||
| ircClient.sendRaw(serverId, `WHO ${username} %uhnafr`); | ||
| }, 100); | ||
| } |
There was a problem hiding this comment.
Why is there timeouts one after the other here 🤔 not easy to make this more event driven or just one after the other?
There was a problem hiding this comment.
Not sure I follow, the timeouts are simply so we don't fill up a RecvQ too quickly
There was a problem hiding this comment.
also a supermassive file that needs some future refactoring 🤔
Removed comment about throttling typing notifications.
…o feature/pinned-pms-and-monitor
…bsidianirc/obsidianirc into feature/pinned-pms-and-monitor
Co-authored-by: Matheus Fillipe <matheusfillipeag@gmail.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/lib/ircClient.ts (2)
821-842: Consider adding input validation for empty target arrays.The MONITOR command implementations are correct per IRC specification. However, calling
monitorAddormonitorRemovewith an empty array would send invalid commands likeMONITOR +(with trailing space).Consider adding guards:
monitorAdd(serverId: string, targets: string[]): void { + if (targets.length === 0) return; const targetsStr = targets.join(","); this.sendRaw(serverId, `MONITOR + ${targetsStr}`); } monitorRemove(serverId: string, targets: string[]): void { + if (targets.length === 0) return; const targetsStr = targets.join(","); this.sendRaw(serverId, `MONITOR - ${targetsStr}`); }
1657-1702: Remove unusedopLevelFieldvariable.The extracted
opLevelField(parv[7]) is never used; remove its declaration to avoid confusion:- const opLevelField = parv[7] || "";
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/lib/ircClient.ts(7 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/lib/ircClient.ts (1)
src/types/index.ts (1)
BaseIRCEvent(187-189)
🔇 Additional comments (4)
src/lib/ircClient.ts (4)
169-200: LGTM! Well-structured event interfaces.The new event type definitions for WHOX and MONITOR are comprehensive and properly typed. The use of optional fields in MONONLINE targets correctly handles extended-monitor format variations.
395-396: LGTM! Correct capability names.The MONITOR capabilities are properly added to the client's capability list.
1625-1656: LGTM! Improved WHO reply parsing.The updated RPL_WHOREPLY handler correctly parses the trailing parameter containing both hopcount and realname, and gracefully handles the edge case where only hopcount is present.
1719-1728: LGTM! Correct RPL_AWAY handling.The RPL_AWAY handler correctly parses the away message and triggers the appropriate event.
| } else if (command === "730") { | ||
| // RPL_MONONLINE | ||
| // Format: 730 <nick> :target[!user@host][,target[!user@host]]* | ||
| const targetList = parv.slice(1).join(" "); | ||
| const cleanTargetList = targetList.startsWith(":") | ||
| ? targetList.substring(1) | ||
| : targetList; | ||
| const targets = cleanTargetList.split(",").map((target) => { | ||
| const parts = target.split("!"); | ||
| if (parts.length === 2) { | ||
| const [nick, userhost] = parts; | ||
| const [user, host] = userhost.split("@"); | ||
| return { nick, user, host }; | ||
| } | ||
| return { nick: target }; | ||
| }); | ||
| this.triggerEvent("MONONLINE", { serverId, targets }); | ||
| } else if (command === "731") { | ||
| // RPL_MONOFFLINE | ||
| // Format: 731 <nick> :target[,target2]* | ||
| const targetList = parv.slice(1).join(" "); | ||
| const cleanTargetList = targetList.startsWith(":") | ||
| ? targetList.substring(1) | ||
| : targetList; | ||
| const targets = cleanTargetList.split(","); | ||
| this.triggerEvent("MONOFFLINE", { serverId, targets }); | ||
| } else if (command === "732") { | ||
| // RPL_MONLIST | ||
| // Format: 732 <nick> :target[,target2]* | ||
| const targetList = parv.slice(1).join(" "); | ||
| const cleanTargetList = targetList.startsWith(":") | ||
| ? targetList.substring(1) | ||
| : targetList; | ||
| const targets = cleanTargetList.split(","); | ||
| this.triggerEvent("MONLIST", { serverId, targets }); | ||
| } else if (command === "733") { | ||
| // RPL_ENDOFMONLIST | ||
| this.triggerEvent("ENDOFMONLIST", { serverId }); | ||
| } else if (command === "734") { | ||
| // ERR_MONLISTFULL | ||
| // Format: 734 <nick> <limit> <targets> :Monitor list is full. | ||
| const limit = Number.parseInt(parv[1], 10); | ||
| const targetList = parv[2]; | ||
| const targets = targetList.split(","); | ||
| this.triggerEvent("MONLISTFULL", { serverId, limit, targets }); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Consider refactoring repeated parsing logic.
The MONITOR numeric handlers contain duplicated code for parsing comma-separated target lists. The same pattern appears in handlers 730, 731, and 732.
Consider extracting a helper function:
private parseTargetList(parv: string[]): string[] {
const targetList = parv.slice(1).join(" ");
const cleanTargetList = targetList.startsWith(":")
? targetList.substring(1)
: targetList;
return cleanTargetList.split(",").filter(t => t.length > 0);
}Then use it in handlers:
} else if (command === "730") {
// RPL_MONONLINE
- const targetList = parv.slice(1).join(" ");
- const cleanTargetList = targetList.startsWith(":")
- ? targetList.substring(1)
- : targetList;
- const targets = cleanTargetList.split(",").map((target) => {
+ const targets = this.parseTargetList(parv).map((target) => {
const parts = target.split("!");
if (parts.length === 2) {
const [nick, userhost] = parts;
const [user, host] = userhost.split("@");
return { nick, user, host };
}
return { nick: target };
});
this.triggerEvent("MONONLINE", { serverId, targets });Potential parsing issue in RPL_MONONLINE.
Line 1536-1540: If a target has the format "nick!" (exclamation but no userhost), the code will split by "@" on an undefined value. Consider adding a check:
const parts = target.split("!");
if (parts.length === 2) {
const [nick, userhost] = parts;
- const [user, host] = userhost.split("@");
- return { nick, user, host };
+ if (userhost) {
+ const [user, host] = userhost.split("@");
+ return { nick, user, host };
+ }
}
return { nick: target };…mprovements (obbyworld#81) * feat: implement pinned private messages with MONITOR and extended-monitor support Features: - Add pinned private messages with localStorage persistence - Implement drag-and-drop reordering for pinned PMs - Add full MONITOR protocol support (IRCv3) - Implement extended-monitor for away-notify integration - Add status indicators (online/away/offline) to PM tabs - Auto-MONITOR users when PM tabs are opened - Auto-cleanup MONITOR entries when unpinned PMs are closed - Restore pinned PMs and MONITOR subscriptions on reconnect - Server-specific MONITOR lists (not global) Bug Fixes: - Fix PM persistence through page refreshes - Add user avatar display in private message chat header - Fix status indicator colors based on WHO response (H=green, G=yellow) - Remove incorrect global METADATA UNSUB when closing PMs - Update private chat status from WHO_REPLY events - Handle WHO_END for individual users (not just channels) - Send WHO request when opening existing PM tabs - Update METADATA handler to refresh private chat state Implements IRCv3 MONITOR (730-734 numerics) and extended-monitor specs * feat: improve UI consistency and typing notifications - Fix typing notification cleanup on user disconnect (PART/KICK/QUIT) - Add 6-second auto-clear timer for typing notifications with reset on new activity - Update typing notification rate limit to 3 seconds - Make channel and PM tab avatars consistent size (24px when deselected) - Add darker gray hover background to chat messages - Improve member list styling to match PM tabs - Add status prefix badges with distinct blue background - Fix WHOX parsing to extract op levels from flags field - Update message spacing for better readability * fix: resolve all linting and code quality issues - Remove useless React fragment in ChannelList.tsx - Replace explicit 'any' types with proper TypeScript types - Fix optional chain usage in store/index.ts - Replace non-null assertion with type assertion - Ensure all biome linting rules pass * feat: optimize channel list and member list spacing - Remove left margins from channel tabs for tighter layout - Adjust channel list default width to compensate for removed margins - Apply consistent horizontal padding to both channel and member lists - Fine-tune member list spacing for optimal visual balance * Fix duplicate channel bug with case-insensitive handling - Make all IRC channel operations case-insensitive to prevent duplicates - Auto-rename channels to match server canonical case - Optimize UI spacing for channel and member lists * Fix mobile channel list width to span available screen space - Change mobile channel list width from fixed w-64 to calculated w-[calc(100vw-72px)] - This ensures the channel list spans the full available width minus the server list width on mobile - Prevents overflow while maximizing usable space for channel navigation * Fix mobile channel list width and prevent text overflow - Set mobile channel list width to w-[calc(100vw-72px)] to span available screen space - Add max-w-full classes to prevent text overflow in channel names and UI elements - Ensure proper responsive behavior on mobile devices * Remove doc refs lol * Remove throttling comment from notifyTyping function Removed comment about throttling typing notifications. * Remove slop * Update src/lib/ircClient.ts Co-authored-by: Matheus Fillipe <matheusfillipeag@gmail.com> --------- Co-authored-by: Matheus Fillipe <matheusfillipeag@gmail.com>
🚀 Features
Pinned Private Messages with MONITOR Support
UI Consistency and Typing Notifications
🐛 Bug Fixes
Duplicate Channel Handling
Mobile Layout Fixes
Code Quality
Spacing Optimizations
📋 Implementation Details
MONITOR Protocol Support
UI Improvements
Performance Optimizations
🧪 Testing
📚 Documentation
Summary by CodeRabbit