2727 <div class =" overflow-hidden transition-all duration-300 ease-out"
2828 :class =" isExpanded ? 'max-h-80 opacity-100' : 'max-h-0 opacity-0'" >
2929 <div class =" flex flex-col h-[280px] bg-base-100/40" >
30- <div class =" flex justify-between " >
30+ <div class =" flex justify-between items-center " >
3131 <transition name =" irc-status" >
3232 <div v-if =" isExpanded"
3333 class =" p-4 flex items-center gap-2 text-xs font-semibold pointer-events-none select-none"
3636 <span >{{ statusMeta.label }}</span >
3737 </div >
3838 </transition >
39+
40+ <transition name =" irc-status" >
41+ <div v-if =" isExpanded && connected"
42+ class =" p-4 flex items-center gap-3 text-xs text-base-content/70 select-none" >
43+ <div class =" flex items-center gap-1" title =" Online Users" >
44+ <span class =" w-2 h-2 rounded-full bg-success mr-1" ></span >
45+ <span >{{ onlineUsers }}</span >
46+ </div >
47+ <div class =" flex items-center gap-1" title =" Online Guests" >
48+ <span class =" w-2 h-2 rounded-full bg-blue-500/30 mr-1" ></span >
49+ <span >{{ onlineGuests }}</span >
50+ </div >
51+ </div >
52+ </transition >
3953 </div >
4054
4155 <div class =" flex-1 overflow-y-auto px-4 pb-4 space-y-2" ref =" messagesContainer" >
4256 <div v-for =" (msg, index) in visibleMessages" :key =" index"
4357 class =" text-sm wrap-break-word whitespace-pre-wrap" >
4458 <span class =" opacity-70 mr-2" >[{{ msg.time }}]</span >
45- <span v-for =" (part, pIndex) in parseMessage(msg.content, msg.type)" :key =" pIndex"
46- :style =" { color: part.color }"
47- :class =" [{ 'cursor-pointer underline': part.isName, 'font-bold': part.bold }]"
48- @contextmenu.prevent =" part.isName && openContextMenu($event, part.text, extractDisplayName(msg.content))" >{{
49- part.text }}</span >
59+
60+ <template v-if =" msg .sender " >
61+ <span class =" cursor-pointer hover:underline"
62+ :style =" { color: getRoleColor(msg.sender.role) }"
63+ @contextmenu.prevent =" openContextMenu($event, msg.sender.username, msg.sender.username)" >
64+ {{ msg.sender.username }}
65+ </span >
66+ <span v-for =" (part, pIndex) in parseMessage(msg.content)" :key =" pIndex"
67+ :style =" { color: part.color }" :class =" { 'font-bold': part.bold }" >
68+ {{ part.text }}
69+ </span >
70+ </template >
71+ <template v-else >
72+ <span v-for =" (part, pIndex) in parseMessage(msg.content, msg.type)" :key =" pIndex"
73+ :style =" { color: part.color }"
74+ :class =" [{ 'cursor-pointer underline': part.isName, 'font-bold': part.bold }]"
75+ @contextmenu.prevent =" part.isName && openContextMenu($event, part.text, extractDisplayName(msg.content))" >{{
76+ part.text }}</span >
77+ </template >
5078 </div >
5179 </div >
5280
@@ -90,7 +118,7 @@ import { useIrcChat } from '../../../composables/useIrcChat';
90118import { useI18n } from ' vue-i18n' ;
91119
92120const attrs = useAttrs ();
93- const { messages, connected, status, sendIrcMessage, forceReconnect, ensureIrcConnection } = useIrcChat ();
121+ const { messages, connected, status, sendIrcMessage, forceReconnect, ensureIrcConnection, onlineUsers, onlineGuests } = useIrcChat ();
94122const { t } = useI18n ();
95123const inputMessage = ref (' ' );
96124const ircInput = ref <HTMLInputElement | null >(null );
@@ -101,6 +129,15 @@ const isExpanded = ref(false);
101129const messagesContainer = ref <HTMLElement | null >(null );
102130const { addToast } = useToast ();
103131
132+ const getRoleColor = (role : string ) => {
133+ switch (role .toLowerCase ()) {
134+ case ' admin' : return ' #AA0000' ;
135+ case ' developer' : return ' #AA00AA' ;
136+ case ' moderator' : return ' #00AA00' ;
137+ default : return undefined ;
138+ }
139+ };
140+
104141const parseMessage = (msg : string , type ? : string ) => {
105142 const colorMap: Record <string , string > = {
106143 ' 0' : ' #000000' , ' 1' : ' #0000AA' , ' 2' : ' #00AA00' , ' 3' : ' #00AAAA' ,
@@ -109,6 +146,21 @@ const parseMessage = (msg: string, type?: string) => {
109146 ' c' : ' #FF5555' , ' d' : ' #FF55FF' , ' e' : ' #FFFF55' , ' f' : ' #FFFFFF'
110147 };
111148
149+ let contentToParse = msg ;
150+
151+ if (type !== ' system' ) {
152+ const nameStripRegex = / ^ . *? (?= §7\( | \[ §)/ ;
153+
154+ if (nameStripRegex .test (msg )) {
155+ contentToParse = msg .replace (nameStripRegex , ' ' );
156+ } else {
157+ const colonIndex = msg .indexOf (' : ' );
158+ if (colonIndex !== - 1 && colonIndex < 50 && ! msg .toLowerCase ().startsWith (' system' )) {
159+ contentToParse = msg .substring (colonIndex + 2 );
160+ }
161+ }
162+ }
163+
112164 const parts: { text: string ; color? : string ; isName? : boolean ; bold? : boolean }[] = [];
113165 let currentColor: string | undefined = undefined ;
114166 let currentBold = false ;
@@ -117,23 +169,10 @@ const parseMessage = (msg: string, type?: string) => {
117169 let lastIndex = 0 ;
118170 let match;
119171
120- const colonIndex = msg .indexOf (' :' );
121- const headerEnd = colonIndex === - 1 ? - 1 : colonIndex ;
122- const headerRaw = colonIndex === - 1 ? ' ' : msg .substring (0 , colonIndex );
123- const headerStripped = stripColorCodes (headerRaw ).trim ();
124- const headerStrippedLower = headerStripped .toLowerCase ();
125- const headerFirstToken = headerStripped .split (/ \s + / )[0 ] || ' ' ;
126- const headerLooksLikeNick = (headerStripped .includes (' (' ) || headerStripped .includes (' [' ) || / ^ [A-Za-z0-9 _\- ] + $ / .test (headerFirstToken ))
127- && ! headerStrippedLower .startsWith (' @' ) && ! headerStrippedLower .includes (' profile' ) && ! / \b (id| ip| name):? / i .test (headerStripped );
128- let namePartMarked = false ;
129-
130- while ((match = regex .exec (msg )) !== null ) {
131- const text = msg .substring (lastIndex , match .index );
172+ while ((match = regex .exec (contentToParse )) !== null ) {
173+ const text = contentToParse .substring (lastIndex , match .index );
132174 if (text ) {
133- const isInHeader = headerEnd !== - 1 && match .index <= headerEnd && lastIndex < headerEnd ;
134- const isName = isInHeader && ! namePartMarked && text .trim ().length > 0 && type !== ' system' && headerStrippedLower !== ' system' && headerLooksLikeNick ;
135- if (isName ) namePartMarked = true ;
136- parts .push ({ text , color: currentColor , isName: !! isName , bold: currentBold });
175+ parts .push ({ text , color: currentColor , isName: false , bold: currentBold });
137176 }
138177
139178 const code = match [1 ].toLowerCase ();
@@ -149,11 +188,9 @@ const parseMessage = (msg: string, type?: string) => {
149188 lastIndex = regex .lastIndex ;
150189 }
151190
152- const remaining = msg .substring (lastIndex );
191+ const remaining = contentToParse .substring (lastIndex );
153192 if (remaining ) {
154- const isInHeader = headerEnd !== - 1 && lastIndex < headerEnd ;
155- const isName = isInHeader && ! namePartMarked && remaining .trim ().length > 0 && type !== ' system' && headerStrippedLower !== ' system' && headerLooksLikeNick ;
156- parts .push ({ text: remaining , color: currentColor , isName: !! isName , bold: currentBold });
193+ parts .push ({ text: remaining , color: currentColor , isName: false , bold: currentBold });
157194 }
158195
159196 return parts ;
0 commit comments