Skip to content

Commit fa7fca8

Browse files
committed
feat: IRC chat improve (Room State, Message Parse)
1 parent e0e261a commit fa7fca8

File tree

2 files changed

+91
-29
lines changed

2 files changed

+91
-29
lines changed

src/components/features/social/InlineIRCChat.vue

Lines changed: 64 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
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"
@@ -36,17 +36,45 @@
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';
90118
import { useI18n } from 'vue-i18n';
91119
92120
const attrs = useAttrs();
93-
const { messages, connected, status, sendIrcMessage, forceReconnect, ensureIrcConnection } = useIrcChat();
121+
const { messages, connected, status, sendIrcMessage, forceReconnect, ensureIrcConnection, onlineUsers, onlineGuests } = useIrcChat();
94122
const { t } = useI18n();
95123
const inputMessage = ref('');
96124
const ircInput = ref<HTMLInputElement | null>(null);
@@ -101,6 +129,15 @@ const isExpanded = ref(false);
101129
const messagesContainer = ref<HTMLElement | null>(null);
102130
const { 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+
104141
const 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;

src/composables/useIrcChat.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,32 @@ import { invoke } from '@tauri-apps/api/core';
33
import { listen } from '@tauri-apps/api/event';
44
import { useToast } from '../services/toastService';
55

6+
interface SenderInfo {
7+
username: string;
8+
role: string;
9+
}
10+
11+
interface RoomState {
12+
online_users: number;
13+
online_guests: number;
14+
}
15+
616
interface IrcMessage {
717
time: string;
818
content: string;
919
type?: string;
1020
isHistory?: boolean;
21+
sender?: SenderInfo;
22+
roomState?: RoomState;
1123
}
1224

1325
interface IncomingIrcPayload {
1426
type: string;
1527
time?: string;
1628
content?: string;
1729
history?: boolean;
30+
sender?: SenderInfo;
31+
room_state?: RoomState;
1832
}
1933

2034
type IrcStatus = 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'error';
@@ -25,6 +39,8 @@ const messages = ref<IrcMessage[]>([]);
2539
const connected = ref(false);
2640
const isConnecting = ref(false);
2741
const status = ref<IrcStatus>('disconnected');
42+
const onlineUsers = ref(0);
43+
const onlineGuests = ref(0);
2844

2945
let connectionPromise: Promise<void> | null = null;
3046
let listenersRegistered = false;
@@ -61,6 +77,8 @@ const parseIrcPayload = (payload: unknown): IrcMessage | null => {
6177
content: parsed.content || '',
6278
type: parsed.type,
6379
isHistory: Boolean(parsed.history),
80+
sender: parsed.sender,
81+
roomState: parsed.room_state,
6482
};
6583
} catch {
6684
return { time: fallbackTime, content: payload, type: 'system' };
@@ -88,7 +106,12 @@ const registerListeners = async (): Promise<void> => {
88106
await listen<string>('irc-message', (event) => {
89107
const msg = parseIrcPayload(event.payload);
90108
if (msg) {
91-
messages.value.push(msg);
109+
if (msg.type === 'room_state' && msg.roomState) {
110+
onlineUsers.value = msg.roomState.online_users;
111+
onlineGuests.value = msg.roomState.online_guests;
112+
} else {
113+
messages.value.push(msg);
114+
}
92115
}
93116
});
94117

@@ -192,8 +215,10 @@ export function useIrcChat() {
192215
connected,
193216
isConnecting,
194217
status,
218+
onlineUsers,
219+
onlineGuests,
195220
ensureIrcConnection,
196221
forceReconnect,
197222
sendIrcMessage
198223
};
199-
}
224+
}

0 commit comments

Comments
 (0)