mirror of
https://github.com/kc1awv/rrc-web.git
synced 2026-08-18 09:48:50 -04:00
add in join/part messages, user list management, and reformat display (remove chat bubbles...)
This commit is contained in:
parent
8a27a3f518
commit
d4d7ff28e5
5 changed files with 260 additions and 108 deletions
|
|
@ -896,25 +896,17 @@ class BackendService:
|
|||
self.ping_task = self.loop.create_task(self._ping_loop())
|
||||
|
||||
async def _on_joined(self, room: str, env: dict) -> None:
|
||||
"""Handle JOINED confirmation from RRC."""
|
||||
if room not in self.rooms:
|
||||
if len(self.rooms) >= self.max_rooms:
|
||||
logger.error(f"Room limit reached ({self.max_rooms}), cannot join room: {room}")
|
||||
if self.broadcast:
|
||||
await self.broadcast(
|
||||
{
|
||||
"type": "error",
|
||||
"error": f"Cannot join room: server room limit reached ({self.max_rooms})",
|
||||
}
|
||||
)
|
||||
return
|
||||
self.rooms[room] = {"messages": [], "users": set()}
|
||||
"""Handle JOINED confirmation from RRC.
|
||||
|
||||
This handles two scenarios:
|
||||
1. Self-join: Multiple hashes (full member list) - we just joined the room
|
||||
2. Member-join: Single hash - another user joined the room we're in
|
||||
"""
|
||||
body = env.get(K_BODY)
|
||||
users = []
|
||||
|
||||
logger.debug(f"JOINED room={room}, body type={type(body)}, body={body}")
|
||||
|
||||
# Extract user list from body (could be dict or list)
|
||||
user_list = None
|
||||
if isinstance(body, dict):
|
||||
user_list = body.get(B_JOINED_USERS)
|
||||
|
|
@ -923,7 +915,28 @@ class BackendService:
|
|||
user_list = body
|
||||
logger.debug(f"Body is list directly, user_list={user_list}")
|
||||
|
||||
if isinstance(user_list, list):
|
||||
if not isinstance(user_list, list):
|
||||
user_list = []
|
||||
|
||||
# Determine if this is a self-join (multiple users) or member-join (single user)
|
||||
is_self_join = len(user_list) != 1
|
||||
|
||||
if is_self_join:
|
||||
# We're joining the room - create/reset room with full member list
|
||||
if room not in self.rooms:
|
||||
if len(self.rooms) >= self.max_rooms:
|
||||
logger.error(f"Room limit reached ({self.max_rooms}), cannot join room: {room}")
|
||||
if self.broadcast:
|
||||
await self.broadcast(
|
||||
{
|
||||
"type": "error",
|
||||
"error": f"Cannot join room: server room limit reached ({self.max_rooms})",
|
||||
}
|
||||
)
|
||||
return
|
||||
self.rooms[room] = {"messages": [], "users": set()}
|
||||
|
||||
users = []
|
||||
for user_hash in user_list:
|
||||
if isinstance(user_hash, (bytes, bytearray)):
|
||||
user_hex = user_hash.hex()
|
||||
|
|
@ -931,44 +944,138 @@ class BackendService:
|
|||
users.append(self._format_user(user_hash))
|
||||
logger.debug(f"Added user: {self._format_user(user_hash)}")
|
||||
|
||||
message = {
|
||||
"type": "system",
|
||||
"room": room,
|
||||
"text": f"Joined room: {room}",
|
||||
"timestamp": self._get_timestamp(),
|
||||
}
|
||||
self.rooms[room]["messages"].append(message)
|
||||
|
||||
if self.broadcast:
|
||||
await self.broadcast(message)
|
||||
await self.broadcast(
|
||||
{
|
||||
"type": "room_joined",
|
||||
"room": room,
|
||||
"users": users,
|
||||
}
|
||||
)
|
||||
|
||||
async def _on_parted(self, room: str, _env: dict) -> None:
|
||||
"""Handle PARTED confirmation from RRC."""
|
||||
message = {
|
||||
"type": "system",
|
||||
"room": room,
|
||||
"text": f"Left room: {room}",
|
||||
"timestamp": self._get_timestamp(),
|
||||
}
|
||||
|
||||
if room in self.rooms:
|
||||
message = {
|
||||
"type": "system",
|
||||
"room": room,
|
||||
"text": f"Joined room: {room}",
|
||||
"timestamp": self._get_timestamp(),
|
||||
}
|
||||
self.rooms[room]["messages"].append(message)
|
||||
|
||||
if self.broadcast:
|
||||
await self.broadcast(message)
|
||||
await self.broadcast(
|
||||
{
|
||||
"type": "room_parted",
|
||||
if self.broadcast:
|
||||
await self.broadcast(message)
|
||||
await self.broadcast(
|
||||
{
|
||||
"type": "room_joined",
|
||||
"room": room,
|
||||
"users": users,
|
||||
}
|
||||
)
|
||||
else:
|
||||
# Another user joined the room we're already in
|
||||
if room not in self.rooms:
|
||||
logger.warning(f"Received JOINED for unknown room: {room}")
|
||||
return
|
||||
|
||||
user_hash = user_list[0]
|
||||
if isinstance(user_hash, (bytes, bytearray)):
|
||||
user_hex = user_hash.hex()
|
||||
|
||||
# Add user to room member list
|
||||
self.rooms[room]["users"].add(user_hex)
|
||||
user_formatted = self._format_user(user_hash)
|
||||
|
||||
# Create join notification message
|
||||
message = {
|
||||
"type": "join",
|
||||
"room": room,
|
||||
"user": user_formatted,
|
||||
"timestamp": self._get_timestamp(),
|
||||
}
|
||||
)
|
||||
self.rooms[room]["messages"].append(message)
|
||||
|
||||
if self.broadcast:
|
||||
await self.broadcast(message)
|
||||
# Also send user list update
|
||||
users = [self._format_user(bytes.fromhex(u)) for u in self.rooms[room]["users"]]
|
||||
await self.broadcast(
|
||||
{
|
||||
"type": "user_list_update",
|
||||
"room": room,
|
||||
"users": users,
|
||||
}
|
||||
)
|
||||
|
||||
async def _on_parted(self, room: str, env: dict) -> None:
|
||||
"""Handle PARTED confirmation from RRC.
|
||||
|
||||
This handles two scenarios:
|
||||
1. Self-part: Multiple hashes (remaining members) - we left the room
|
||||
2. Member-part: Single hash - another user left the room we're in
|
||||
"""
|
||||
body = env.get(K_BODY)
|
||||
|
||||
logger.debug(f"PARTED room={room}, body type={type(body)}, body={body}")
|
||||
|
||||
# Extract user list from body (could be dict or list)
|
||||
user_list = None
|
||||
if isinstance(body, dict):
|
||||
user_list = body.get(B_JOINED_USERS) # Reuse same key for remaining members
|
||||
logger.debug(f"Body is dict, user_list={user_list}")
|
||||
elif isinstance(body, list):
|
||||
user_list = body
|
||||
logger.debug(f"Body is list directly, user_list={user_list}")
|
||||
|
||||
if not isinstance(user_list, list):
|
||||
user_list = []
|
||||
|
||||
# Determine if this is a self-part (we left) or member-part (single user left)
|
||||
is_self_part = len(user_list) != 1
|
||||
|
||||
if is_self_part:
|
||||
# We left the room
|
||||
message = {
|
||||
"type": "system",
|
||||
"room": room,
|
||||
"text": f"Left room: {room}",
|
||||
"timestamp": self._get_timestamp(),
|
||||
}
|
||||
|
||||
if room in self.rooms:
|
||||
self.rooms[room]["messages"].append(message)
|
||||
|
||||
if self.broadcast:
|
||||
await self.broadcast(message)
|
||||
await self.broadcast(
|
||||
{
|
||||
"type": "room_parted",
|
||||
"room": room,
|
||||
}
|
||||
)
|
||||
else:
|
||||
# Another user left the room we're in
|
||||
if room not in self.rooms:
|
||||
logger.warning(f"Received PARTED for unknown room: {room}")
|
||||
return
|
||||
|
||||
user_hash = user_list[0]
|
||||
if isinstance(user_hash, (bytes, bytearray)):
|
||||
user_hex = user_hash.hex()
|
||||
user_formatted = self._format_user(user_hash)
|
||||
|
||||
# Remove user from room member list
|
||||
self.rooms[room]["users"].discard(user_hex)
|
||||
|
||||
# Create part notification message
|
||||
message = {
|
||||
"type": "part",
|
||||
"room": room,
|
||||
"user": user_formatted,
|
||||
"timestamp": self._get_timestamp(),
|
||||
}
|
||||
self.rooms[room]["messages"].append(message)
|
||||
|
||||
if self.broadcast:
|
||||
await self.broadcast(message)
|
||||
# Also send user list update
|
||||
users = [self._format_user(bytes.fromhex(u)) for u in self.rooms[room]["users"]]
|
||||
await self.broadcast(
|
||||
{
|
||||
"type": "user_list_update",
|
||||
"room": room,
|
||||
"users": users,
|
||||
}
|
||||
)
|
||||
|
||||
async def _on_close(self) -> None:
|
||||
"""Handle connection close from RRC."""
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1,9 +1,10 @@
|
|||
<script>
|
||||
import { currentRoomData, identityHash, sentMessageIds } from "../stores.js";
|
||||
import { currentRoom, currentRoomData, identityHash, sentMessageIds } from "../stores.js";
|
||||
|
||||
let messageContainer;
|
||||
let shouldAutoScroll = $state(true);
|
||||
let showScrollButton = $state(false);
|
||||
let showTimestamps = $state(true);
|
||||
|
||||
// Check if user is scrolled near the bottom
|
||||
function isNearBottom() {
|
||||
|
|
@ -69,64 +70,65 @@
|
|||
return false;
|
||||
}
|
||||
|
||||
function getMessageClass(msg) {
|
||||
if (isOwnMessage(msg)) {
|
||||
return "chat chat-end";
|
||||
}
|
||||
|
||||
function getMessageText(msg) {
|
||||
switch (msg.type) {
|
||||
case "message":
|
||||
return "chat chat-start";
|
||||
case "system":
|
||||
return "chat chat-end";
|
||||
case "join":
|
||||
return `→ ${msg.user} joined`;
|
||||
case "part":
|
||||
return `← ${msg.user} left`;
|
||||
case "notice":
|
||||
return "chat chat-end";
|
||||
case "error":
|
||||
return "chat chat-end";
|
||||
return formatNotice(msg.text);
|
||||
default:
|
||||
return "chat";
|
||||
return msg.text;
|
||||
}
|
||||
}
|
||||
|
||||
function getBubbleClass(msg) {
|
||||
if (isOwnMessage(msg)) {
|
||||
return "chat-bubble chat-bubble-accent";
|
||||
|
||||
function formatNotice(text) {
|
||||
if (!text) return text;
|
||||
|
||||
// Parse IRC-style room notice: "room test: registered; mode=+nrt; topic=this is a test room"
|
||||
const match = text.match(/^room\s+(\S+):\s*registered;\s*mode=([^;]+);\s*topic=(.+)$/);
|
||||
if (match) {
|
||||
const [, roomName, mode, topic] = match;
|
||||
return topic ? `Topic: ${topic}` : `Room ${roomName} registered`;
|
||||
}
|
||||
|
||||
switch (msg.type) {
|
||||
case "message":
|
||||
return "chat-bubble chat-bubble-primary";
|
||||
case "system":
|
||||
return "chat-bubble chat-bubble-info";
|
||||
case "notice":
|
||||
return "chat-bubble chat-bubble-warning";
|
||||
case "error":
|
||||
return "chat-bubble chat-bubble-error";
|
||||
default:
|
||||
return "chat-bubble";
|
||||
}
|
||||
// If it doesn't match the expected format, return as-is
|
||||
return text;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="message-area-wrapper">
|
||||
{#if $currentRoom !== '[Hub]'}
|
||||
<div class="flex justify-end p-2 border-b border-base-300">
|
||||
<label class="label cursor-pointer gap-2">
|
||||
<span class="label-text text-xs">Show timestamps</span>
|
||||
<input type="checkbox" class="toggle toggle-sm" bind:checked={showTimestamps} />
|
||||
</label>
|
||||
</div>
|
||||
{/if}
|
||||
<div bind:this={messageContainer} class="message-area p-4" onscroll={handleScroll}>
|
||||
{#each ($currentRoomData?.messages || []) as msg, index (msg.timestamp + msg.text + index)}
|
||||
<div class={getMessageClass(msg)}>
|
||||
{#if msg.user}
|
||||
<div class="chat-header">
|
||||
{msg.user}
|
||||
<time class="text-xs opacity-50">{msg.timestamp}</time>
|
||||
</div>
|
||||
{/if}
|
||||
<div class={getBubbleClass(msg)} style="white-space: pre-line;">
|
||||
{msg.text}
|
||||
{#each ($currentRoomData?.messages || []) as msg, index (msg.timestamp + (msg.text || msg.type || '') + (msg.user || '') + index)}
|
||||
{#if msg.type === 'join' || msg.type === 'part' || msg.type === 'notice' || msg.type === 'system' || msg.type === 'error'}
|
||||
<!-- Join/Part/Notice/System/Error informational messages -->
|
||||
<div class="my-2">
|
||||
{#if showTimestamps || $currentRoom === '[Hub]'}
|
||||
<span class="text-xs opacity-40 mr-2">{msg.timestamp}</span>
|
||||
{/if}
|
||||
<span class="text-sm opacity-60 {msg.type === 'join' ? 'text-success' : msg.type === 'part' ? 'text-warning' : msg.type === 'error' ? 'text-error' : 'text-info'}">
|
||||
{getMessageText(msg)}
|
||||
</span>
|
||||
</div>
|
||||
{#if !msg.user && msg.timestamp}
|
||||
<div class="chat-footer opacity-50">
|
||||
<time class="text-xs">{msg.timestamp}</time>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if msg.type === 'message'}
|
||||
<!-- Regular chat messages -->
|
||||
<div class="my-1">
|
||||
{#if showTimestamps || $currentRoom === '[Hub]'}
|
||||
<span class="text-xs opacity-40 mr-2">{msg.timestamp}</span>
|
||||
{/if}
|
||||
<span class="font-semibold {isOwnMessage(msg) ? 'text-accent' : 'text-primary'}">{msg.user}</span>
|
||||
<span class="ml-2" style="white-space: pre-line;">{msg.text}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
|
|
@ -164,13 +166,4 @@
|
|||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 8px -1px rgba(0, 0, 0, 0.15), 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* --TEST-- Fix text wrapping for long messages */
|
||||
:global(.chat-bubble) {
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
max-width: 100%;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -96,6 +96,12 @@ function handleMessage(data) {
|
|||
case 'system':
|
||||
addSystemMessage(data);
|
||||
break;
|
||||
case 'join':
|
||||
addJoinMessage(data);
|
||||
break;
|
||||
case 'part':
|
||||
addPartMessage(data);
|
||||
break;
|
||||
case 'message_sent':
|
||||
handleMessageSent(data);
|
||||
break;
|
||||
|
|
@ -236,6 +242,52 @@ function addSystemMessage(data) {
|
|||
});
|
||||
}
|
||||
|
||||
function addJoinMessage(data) {
|
||||
const room = data.room || get(currentRoom);
|
||||
|
||||
rooms.update(r => {
|
||||
if (!r.has(room)) {
|
||||
r.set(room, { messages: [], users: new Set(), unread: 0 });
|
||||
}
|
||||
|
||||
const roomData = r.get(room);
|
||||
roomData.messages.push({
|
||||
type: 'join',
|
||||
user: data.user,
|
||||
timestamp: data.timestamp || new Date().toLocaleTimeString()
|
||||
});
|
||||
|
||||
if (room !== get(currentRoom)) {
|
||||
roomData.unread++;
|
||||
}
|
||||
|
||||
return new Map(r);
|
||||
});
|
||||
}
|
||||
|
||||
function addPartMessage(data) {
|
||||
const room = data.room || get(currentRoom);
|
||||
|
||||
rooms.update(r => {
|
||||
if (!r.has(room)) {
|
||||
r.set(room, { messages: [], users: new Set(), unread: 0 });
|
||||
}
|
||||
|
||||
const roomData = r.get(room);
|
||||
roomData.messages.push({
|
||||
type: 'part',
|
||||
user: data.user,
|
||||
timestamp: data.timestamp || new Date().toLocaleTimeString()
|
||||
});
|
||||
|
||||
if (room !== get(currentRoom)) {
|
||||
roomData.unread++;
|
||||
}
|
||||
|
||||
return new Map(r);
|
||||
});
|
||||
}
|
||||
|
||||
function handleMessageSent(data) {
|
||||
// Track the message ID of messages we sent
|
||||
if (data.message_id) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue