create-fluxstack 1.12.0 → 1.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/client/src/live/RoomChatDemo.tsx +24 -105
- package/app/server/live/LiveRoomChat.ts +45 -203
- package/package.json +1 -1
|
@@ -1,16 +1,9 @@
|
|
|
1
|
-
// 🔥 RoomChatDemo -
|
|
2
|
-
//
|
|
3
|
-
// Demonstra o uso do sistema $room para:
|
|
4
|
-
// - Entrar/sair de múltiplas salas
|
|
5
|
-
// - Enviar mensagens para sala ativa
|
|
6
|
-
// - Ver quem está digitando
|
|
7
|
-
// - Trocar entre salas
|
|
1
|
+
// 🔥 RoomChatDemo - Chat multi-salas simplificado
|
|
8
2
|
|
|
9
3
|
import { useState, useEffect, useRef, useMemo } from 'react'
|
|
10
4
|
import { Live } from '@/core/client'
|
|
11
5
|
import { LiveRoomChat } from '@server/live/LiveRoomChat'
|
|
12
6
|
|
|
13
|
-
// Salas disponíveis
|
|
14
7
|
const AVAILABLE_ROOMS = [
|
|
15
8
|
{ id: 'geral', name: '💬 Geral' },
|
|
16
9
|
{ id: 'tech', name: '💻 Tecnologia' },
|
|
@@ -22,32 +15,23 @@ export function RoomChatDemo() {
|
|
|
22
15
|
const [text, setText] = useState('')
|
|
23
16
|
const messagesEndRef = useRef<HTMLDivElement>(null)
|
|
24
17
|
|
|
25
|
-
// Username aleatório
|
|
26
18
|
const defaultUsername = useMemo(() => {
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
const noun = nouns[Math.floor(Math.random() * nouns.length)]
|
|
31
|
-
const num = Math.floor(Math.random() * 100)
|
|
32
|
-
return `${adj}${noun}${num}`
|
|
19
|
+
const adj = ['Happy', 'Cool', 'Fast', 'Smart', 'Brave'][Math.floor(Math.random() * 5)]
|
|
20
|
+
const noun = ['Panda', 'Tiger', 'Eagle', 'Wolf', 'Bear'][Math.floor(Math.random() * 5)]
|
|
21
|
+
return `${adj}${noun}${Math.floor(Math.random() * 100)}`
|
|
33
22
|
}, [])
|
|
34
23
|
|
|
35
|
-
// Live component - estado sincronizado automaticamente
|
|
36
24
|
const chat = Live.use(LiveRoomChat, {
|
|
37
25
|
initialState: { ...LiveRoomChat.defaultState, username: defaultUsername }
|
|
38
26
|
})
|
|
39
27
|
|
|
40
|
-
// Mensagens e typing vêm diretamente do estado sincronizado
|
|
41
28
|
const activeRoom = chat.$state.activeRoom
|
|
42
29
|
const activeMessages = activeRoom ? (chat.$state.messages[activeRoom] || []) : []
|
|
43
|
-
const activeTyping = activeRoom ? (chat.$state.typingUsers[activeRoom] || []) : []
|
|
44
30
|
|
|
45
|
-
// Auto scroll quando mensagens mudam
|
|
46
31
|
useEffect(() => {
|
|
47
32
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
|
48
33
|
}, [activeMessages.length])
|
|
49
34
|
|
|
50
|
-
// Handlers
|
|
51
35
|
const handleJoinRoom = async (roomId: string, roomName: string) => {
|
|
52
36
|
if (chat.$rooms.includes(roomId)) {
|
|
53
37
|
await chat.switchRoom({ roomId })
|
|
@@ -56,25 +40,16 @@ export function RoomChatDemo() {
|
|
|
56
40
|
}
|
|
57
41
|
}
|
|
58
42
|
|
|
59
|
-
const handleLeaveRoom = async (roomId: string) => {
|
|
60
|
-
await chat.leaveRoom({ roomId })
|
|
61
|
-
}
|
|
62
|
-
|
|
63
43
|
const handleSendMessage = async () => {
|
|
64
|
-
if (!text.trim() || !
|
|
44
|
+
if (!text.trim() || !activeRoom) return
|
|
65
45
|
await chat.sendMessage({ text })
|
|
66
46
|
setText('')
|
|
67
47
|
}
|
|
68
48
|
|
|
69
|
-
const handleTyping = () => {
|
|
70
|
-
chat.startTyping({})
|
|
71
|
-
}
|
|
72
|
-
|
|
73
49
|
return (
|
|
74
50
|
<div className="flex h-[600px] bg-gray-900 rounded-2xl overflow-hidden border border-white/10">
|
|
75
|
-
{/* Sidebar
|
|
51
|
+
{/* Sidebar */}
|
|
76
52
|
<div className="w-64 bg-gray-800/50 border-r border-white/10 flex flex-col">
|
|
77
|
-
{/* Header */}
|
|
78
53
|
<div className="p-4 border-b border-white/10">
|
|
79
54
|
<h2 className="text-lg font-bold text-white mb-2">💬 Room Chat</h2>
|
|
80
55
|
<div className="flex items-center gap-2">
|
|
@@ -83,57 +58,38 @@ export function RoomChatDemo() {
|
|
|
83
58
|
</div>
|
|
84
59
|
</div>
|
|
85
60
|
|
|
86
|
-
{/* Salas Disponíveis */}
|
|
87
61
|
<div className="flex-1 overflow-auto p-2">
|
|
88
62
|
<p className="text-xs text-gray-500 px-2 py-1">SALAS</p>
|
|
89
63
|
{AVAILABLE_ROOMS.map(room => {
|
|
90
64
|
const isJoined = chat.$rooms.includes(room.id)
|
|
91
|
-
const isActive =
|
|
92
|
-
const unreadCount = 0 // TODO: implementar contagem
|
|
65
|
+
const isActive = activeRoom === room.id
|
|
93
66
|
|
|
94
67
|
return (
|
|
95
68
|
<div
|
|
96
69
|
key={room.id}
|
|
97
70
|
onClick={() => handleJoinRoom(room.id, room.name)}
|
|
98
71
|
className={`
|
|
99
|
-
flex items-center justify-between px-3 py-2 rounded-lg cursor-pointer mb-1
|
|
100
|
-
|
|
101
|
-
${isActive
|
|
102
|
-
? 'bg-purple-500/20 text-purple-300'
|
|
103
|
-
: isJoined
|
|
104
|
-
? 'bg-white/5 text-gray-300 hover:bg-white/10'
|
|
105
|
-
: 'text-gray-500 hover:bg-white/5 hover:text-gray-400'
|
|
106
|
-
}
|
|
72
|
+
flex items-center justify-between px-3 py-2 rounded-lg cursor-pointer mb-1 transition-all group
|
|
73
|
+
${isActive ? 'bg-purple-500/20 text-purple-300' : isJoined ? 'bg-white/5 text-gray-300 hover:bg-white/10' : 'text-gray-500 hover:bg-white/5'}
|
|
107
74
|
`}
|
|
108
75
|
>
|
|
109
76
|
<span className="flex items-center gap-2">
|
|
110
77
|
{room.name}
|
|
111
|
-
{isJoined &&
|
|
112
|
-
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400" />
|
|
113
|
-
)}
|
|
78
|
+
{isJoined && <span className="w-1.5 h-1.5 rounded-full bg-emerald-400" />}
|
|
114
79
|
</span>
|
|
115
|
-
|
|
116
80
|
{isJoined && !isActive && (
|
|
117
81
|
<button
|
|
118
|
-
onClick={(e) => {
|
|
119
|
-
e.stopPropagation()
|
|
120
|
-
handleLeaveRoom(room.id)
|
|
121
|
-
}}
|
|
82
|
+
onClick={(e) => { e.stopPropagation(); chat.leaveRoom({ roomId: room.id }) }}
|
|
122
83
|
className="opacity-0 group-hover:opacity-100 text-red-400 hover:text-red-300 text-xs"
|
|
123
|
-
>
|
|
124
|
-
✕
|
|
125
|
-
</button>
|
|
84
|
+
>✕</button>
|
|
126
85
|
)}
|
|
127
86
|
</div>
|
|
128
87
|
)
|
|
129
88
|
})}
|
|
130
89
|
</div>
|
|
131
90
|
|
|
132
|
-
{/* Salas ativas */}
|
|
133
91
|
<div className="p-3 border-t border-white/10">
|
|
134
|
-
<p className="text-xs text-gray-500
|
|
135
|
-
Em {chat.$rooms.length} sala(s)
|
|
136
|
-
</p>
|
|
92
|
+
<p className="text-xs text-gray-500">Em {chat.$rooms.length} sala(s)</p>
|
|
137
93
|
</div>
|
|
138
94
|
</div>
|
|
139
95
|
|
|
@@ -141,25 +97,19 @@ export function RoomChatDemo() {
|
|
|
141
97
|
<div className="flex-1 flex flex-col">
|
|
142
98
|
{activeRoom ? (
|
|
143
99
|
<>
|
|
144
|
-
{/* Header da sala */}
|
|
145
100
|
<div className="px-4 py-3 border-b border-white/10 flex items-center justify-between">
|
|
146
101
|
<div>
|
|
147
102
|
<h3 className="text-white font-semibold">
|
|
148
103
|
{chat.$state.rooms.find(r => r.id === activeRoom)?.name || activeRoom}
|
|
149
104
|
</h3>
|
|
150
|
-
<p className="text-xs text-gray-500">
|
|
151
|
-
{activeMessages.length} mensagens
|
|
152
|
-
</p>
|
|
105
|
+
<p className="text-xs text-gray-500">{activeMessages.length} mensagens</p>
|
|
153
106
|
</div>
|
|
154
107
|
<button
|
|
155
|
-
onClick={() =>
|
|
156
|
-
className="px-3 py-1 text-sm bg-red-500/20 text-red-300 rounded-lg hover:bg-red-500/30
|
|
157
|
-
>
|
|
158
|
-
Sair
|
|
159
|
-
</button>
|
|
108
|
+
onClick={() => chat.leaveRoom({ roomId: activeRoom })}
|
|
109
|
+
className="px-3 py-1 text-sm bg-red-500/20 text-red-300 rounded-lg hover:bg-red-500/30"
|
|
110
|
+
>Sair</button>
|
|
160
111
|
</div>
|
|
161
112
|
|
|
162
|
-
{/* Mensagens */}
|
|
163
113
|
<div className="flex-1 overflow-auto p-4 space-y-3">
|
|
164
114
|
{activeMessages.length === 0 ? (
|
|
165
115
|
<div className="text-center text-gray-500 py-8">
|
|
@@ -168,63 +118,32 @@ export function RoomChatDemo() {
|
|
|
168
118
|
</div>
|
|
169
119
|
) : (
|
|
170
120
|
activeMessages.map(msg => (
|
|
171
|
-
<div
|
|
172
|
-
|
|
173
|
-
className={`flex flex-col ${msg.user === chat.$state.username ? 'items-end' : 'items-start'}`}
|
|
174
|
-
>
|
|
175
|
-
<div
|
|
176
|
-
className={`
|
|
177
|
-
max-w-[80%] rounded-2xl px-4 py-2
|
|
178
|
-
${msg.user === chat.$state.username
|
|
179
|
-
? 'bg-purple-500/30 text-purple-100'
|
|
180
|
-
: 'bg-white/10 text-gray-200'
|
|
181
|
-
}
|
|
182
|
-
`}
|
|
183
|
-
>
|
|
121
|
+
<div key={msg.id} className={`flex flex-col ${msg.user === chat.$state.username ? 'items-end' : 'items-start'}`}>
|
|
122
|
+
<div className={`max-w-[80%] rounded-2xl px-4 py-2 ${msg.user === chat.$state.username ? 'bg-purple-500/30 text-purple-100' : 'bg-white/10 text-gray-200'}`}>
|
|
184
123
|
<p className="text-xs text-gray-400 mb-1">{msg.user}</p>
|
|
185
124
|
<p>{msg.text}</p>
|
|
186
125
|
</div>
|
|
187
|
-
<span className="text-xs text-gray-600 mt-1">
|
|
188
|
-
{new Date(msg.timestamp).toLocaleTimeString()}
|
|
189
|
-
</span>
|
|
126
|
+
<span className="text-xs text-gray-600 mt-1">{new Date(msg.timestamp).toLocaleTimeString()}</span>
|
|
190
127
|
</div>
|
|
191
128
|
))
|
|
192
129
|
)}
|
|
193
130
|
<div ref={messagesEndRef} />
|
|
194
131
|
</div>
|
|
195
132
|
|
|
196
|
-
{/* Typing indicator */}
|
|
197
|
-
{activeTyping.length > 0 && (
|
|
198
|
-
<div className="px-4 py-1 text-xs text-gray-500">
|
|
199
|
-
{activeTyping.filter(u => u !== chat.$state.username).join(', ')} está digitando...
|
|
200
|
-
</div>
|
|
201
|
-
)}
|
|
202
|
-
|
|
203
|
-
{/* Input */}
|
|
204
133
|
<div className="p-4 border-t border-white/10">
|
|
205
134
|
<div className="flex gap-2">
|
|
206
135
|
<input
|
|
207
136
|
value={text}
|
|
208
|
-
onChange={(e) =>
|
|
209
|
-
|
|
210
|
-
handleTyping()
|
|
211
|
-
}}
|
|
212
|
-
onKeyDown={(e) => {
|
|
213
|
-
if (e.key === 'Enter' && !e.shiftKey) {
|
|
214
|
-
e.preventDefault()
|
|
215
|
-
handleSendMessage()
|
|
216
|
-
}
|
|
217
|
-
}}
|
|
137
|
+
onChange={(e) => setText(e.target.value)}
|
|
138
|
+
onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSendMessage() } }}
|
|
218
139
|
placeholder="Digite uma mensagem..."
|
|
219
140
|
className="flex-1 px-4 py-2 rounded-xl bg-white/10 border border-white/20 text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-500/50"
|
|
220
141
|
/>
|
|
221
142
|
<button
|
|
222
143
|
onClick={handleSendMessage}
|
|
223
144
|
disabled={!text.trim()}
|
|
224
|
-
className="px-6 py-2 rounded-xl bg-purple-500/30 text-purple-200 hover:bg-purple-500/40
|
|
225
|
-
>
|
|
226
|
-
Enviar
|
|
227
|
-
</button>
|
|
145
|
+
className="px-6 py-2 rounded-xl bg-purple-500/30 text-purple-200 hover:bg-purple-500/40 disabled:opacity-50"
|
|
146
|
+
>Enviar</button>
|
|
228
147
|
</div>
|
|
229
148
|
</div>
|
|
230
149
|
</>
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
// LiveRoomChat - Chat
|
|
1
|
+
// LiveRoomChat - Chat multi-salas simplificado
|
|
2
2
|
|
|
3
3
|
import { LiveComponent, type FluxStackWebSocket } from '@core/types/types'
|
|
4
4
|
|
|
5
5
|
// Componente Cliente (Ctrl+Click para navegar)
|
|
6
6
|
import type { RoomChatDemo as _Client } from '@client/src/live/RoomChatDemo'
|
|
7
7
|
|
|
8
|
-
// Tipos
|
|
9
8
|
export interface ChatMessage {
|
|
10
9
|
id: string
|
|
11
10
|
user: string
|
|
@@ -13,23 +12,17 @@ export interface ChatMessage {
|
|
|
13
12
|
timestamp: number
|
|
14
13
|
}
|
|
15
14
|
|
|
16
|
-
export interface RoomInfo {
|
|
17
|
-
id: string
|
|
18
|
-
name: string
|
|
19
|
-
}
|
|
20
|
-
|
|
21
15
|
export class LiveRoomChat extends LiveComponent<typeof LiveRoomChat.defaultState> {
|
|
22
16
|
static componentName = 'LiveRoomChat'
|
|
23
17
|
static defaultState = {
|
|
24
18
|
username: '',
|
|
25
19
|
activeRoom: null as string | null,
|
|
26
|
-
rooms: [] as
|
|
27
|
-
messages: {} as Record<string, ChatMessage[]
|
|
28
|
-
typingUsers: {} as Record<string, string[]>
|
|
20
|
+
rooms: [] as { id: string; name: string }[],
|
|
21
|
+
messages: {} as Record<string, ChatMessage[]>
|
|
29
22
|
}
|
|
30
|
-
protected roomType = 'room-chat'
|
|
31
23
|
|
|
32
|
-
|
|
24
|
+
// Listeners por sala para evitar duplicação
|
|
25
|
+
private roomListeners = new Map<string, (() => void)[]>()
|
|
33
26
|
|
|
34
27
|
constructor(
|
|
35
28
|
initialState: Partial<typeof LiveRoomChat.defaultState> = {},
|
|
@@ -39,141 +32,68 @@ export class LiveRoomChat extends LiveComponent<typeof LiveRoomChat.defaultState
|
|
|
39
32
|
super(initialState, ws, options)
|
|
40
33
|
}
|
|
41
34
|
|
|
42
|
-
// ===== Gerenciamento de Salas =====
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Entrar em uma sala de chat
|
|
46
|
-
*/
|
|
47
35
|
async joinRoom(payload: { roomId: string; roomName?: string }) {
|
|
48
36
|
const { roomId, roomName } = payload
|
|
49
37
|
|
|
50
|
-
//
|
|
51
|
-
this
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
this.addMessageToState(roomId, msg)
|
|
56
|
-
})
|
|
38
|
+
// Já está na sala? Apenas ativar
|
|
39
|
+
if (this.roomListeners.has(roomId)) {
|
|
40
|
+
this.state.activeRoom = roomId
|
|
41
|
+
return { success: true, roomId }
|
|
42
|
+
}
|
|
57
43
|
|
|
58
|
-
//
|
|
59
|
-
this.$room(roomId).
|
|
60
|
-
|
|
44
|
+
// Entrar e escutar mensagens
|
|
45
|
+
this.$room(roomId).join()
|
|
46
|
+
const unsub = this.$room(roomId).on('message:new', (msg: ChatMessage) => {
|
|
47
|
+
const msgs = this.state.messages[roomId] || []
|
|
48
|
+
this.setState({
|
|
49
|
+
messages: { ...this.state.messages, [roomId]: [...msgs, msg].slice(-100) }
|
|
50
|
+
})
|
|
61
51
|
})
|
|
52
|
+
this.roomListeners.set(roomId, [unsub])
|
|
62
53
|
|
|
63
|
-
// Atualizar
|
|
64
|
-
const rooms = [
|
|
65
|
-
...this.state.rooms.filter(r => r.id !== roomId),
|
|
66
|
-
{ id: roomId, name: roomName || roomId }
|
|
67
|
-
]
|
|
68
|
-
|
|
54
|
+
// Atualizar estado
|
|
69
55
|
this.setState({
|
|
70
|
-
rooms,
|
|
71
56
|
activeRoom: roomId,
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
[roomId]: this.state.messages[roomId] || []
|
|
75
|
-
},
|
|
76
|
-
typingUsers: {
|
|
77
|
-
...this.state.typingUsers,
|
|
78
|
-
[roomId]: []
|
|
79
|
-
}
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
return {
|
|
83
|
-
success: true,
|
|
84
|
-
roomId,
|
|
85
|
-
rooms: this.$rooms
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
private addMessageToState(roomId: string, msg: ChatMessage) {
|
|
90
|
-
const currentMessages = this.state.messages[roomId] || []
|
|
91
|
-
const newMessages = [...currentMessages, msg].slice(-100)
|
|
92
|
-
|
|
93
|
-
this.setState({
|
|
94
|
-
messages: {
|
|
95
|
-
...this.state.messages,
|
|
96
|
-
[roomId]: newMessages
|
|
97
|
-
}
|
|
57
|
+
rooms: [...this.state.rooms.filter(r => r.id !== roomId), { id: roomId, name: roomName || roomId }],
|
|
58
|
+
messages: { ...this.state.messages, [roomId]: this.state.messages[roomId] || [] }
|
|
98
59
|
})
|
|
99
|
-
}
|
|
100
60
|
|
|
101
|
-
|
|
102
|
-
const current = this.state.typingUsers[roomId] || []
|
|
103
|
-
let updated: string[]
|
|
104
|
-
|
|
105
|
-
if (typing && !current.includes(user)) {
|
|
106
|
-
updated = [...current, user]
|
|
107
|
-
} else if (!typing) {
|
|
108
|
-
updated = current.filter(u => u !== user)
|
|
109
|
-
} else {
|
|
110
|
-
return // Sem mudança
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
this.setState({
|
|
114
|
-
typingUsers: {
|
|
115
|
-
...this.state.typingUsers,
|
|
116
|
-
[roomId]: updated
|
|
117
|
-
}
|
|
118
|
-
})
|
|
61
|
+
return { success: true, roomId }
|
|
119
62
|
}
|
|
120
63
|
|
|
121
|
-
/**
|
|
122
|
-
* Sair de uma sala
|
|
123
|
-
*/
|
|
124
64
|
async leaveRoom(payload: { roomId: string }) {
|
|
125
65
|
const { roomId } = payload
|
|
126
66
|
|
|
127
|
-
//
|
|
67
|
+
// Limpar listeners
|
|
68
|
+
this.roomListeners.get(roomId)?.forEach(fn => fn())
|
|
69
|
+
this.roomListeners.delete(roomId)
|
|
128
70
|
this.$room(roomId).leave()
|
|
129
71
|
|
|
130
|
-
// Atualizar
|
|
72
|
+
// Atualizar estado
|
|
131
73
|
const rooms = this.state.rooms.filter(r => r.id !== roomId)
|
|
132
|
-
const
|
|
133
|
-
? (rooms[0]?.id || null)
|
|
134
|
-
: this.state.activeRoom
|
|
135
|
-
|
|
136
|
-
// Remover mensagens e typing da sala
|
|
137
|
-
const { [roomId]: _msgs, ...restMessages } = this.state.messages
|
|
138
|
-
const { [roomId]: _typing, ...restTyping } = this.state.typingUsers
|
|
74
|
+
const { [roomId]: _, ...restMessages } = this.state.messages
|
|
139
75
|
|
|
140
76
|
this.setState({
|
|
141
77
|
rooms,
|
|
142
|
-
activeRoom,
|
|
143
|
-
messages: restMessages
|
|
144
|
-
typingUsers: restTyping
|
|
78
|
+
activeRoom: this.state.activeRoom === roomId ? (rooms[0]?.id || null) : this.state.activeRoom,
|
|
79
|
+
messages: restMessages
|
|
145
80
|
})
|
|
146
81
|
|
|
147
|
-
return {
|
|
148
|
-
success: true,
|
|
149
|
-
rooms: this.$rooms
|
|
150
|
-
}
|
|
82
|
+
return { success: true }
|
|
151
83
|
}
|
|
152
84
|
|
|
153
|
-
/**
|
|
154
|
-
* Trocar sala ativa (para UI)
|
|
155
|
-
*/
|
|
156
85
|
async switchRoom(payload: { roomId: string }) {
|
|
157
|
-
if (!this.$rooms.includes(payload.roomId))
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
this.setState({ activeRoom: payload.roomId })
|
|
86
|
+
if (!this.$rooms.includes(payload.roomId)) throw new Error('Not in this room')
|
|
87
|
+
this.state.activeRoom = payload.roomId
|
|
162
88
|
return { success: true }
|
|
163
89
|
}
|
|
164
90
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Enviar mensagem para sala ativa
|
|
169
|
-
*/
|
|
170
|
-
async sendMessage(payload: { text: string; roomId?: string }) {
|
|
171
|
-
const roomId = payload.roomId || this.state.activeRoom
|
|
91
|
+
async sendMessage(payload: { text: string }) {
|
|
92
|
+
const roomId = this.state.activeRoom
|
|
172
93
|
if (!roomId) throw new Error('No active room')
|
|
173
94
|
|
|
174
95
|
const text = payload.text?.trim()
|
|
175
96
|
if (!text) throw new Error('Message cannot be empty')
|
|
176
|
-
if (text.length > 1000) throw new Error('Message too long')
|
|
177
97
|
|
|
178
98
|
const message: ChatMessage = {
|
|
179
99
|
id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
@@ -182,104 +102,26 @@ export class LiveRoomChat extends LiveComponent<typeof LiveRoomChat.defaultState
|
|
|
182
102
|
timestamp: Date.now()
|
|
183
103
|
}
|
|
184
104
|
|
|
185
|
-
// Adicionar
|
|
186
|
-
this.
|
|
187
|
-
|
|
188
|
-
|
|
105
|
+
// Adicionar localmente e emitir para outros
|
|
106
|
+
const msgs = this.state.messages[roomId] || []
|
|
107
|
+
this.setState({
|
|
108
|
+
messages: { ...this.state.messages, [roomId]: [...msgs, message].slice(-100) }
|
|
109
|
+
})
|
|
189
110
|
this.$room(roomId).emit('message:new', message)
|
|
190
111
|
|
|
191
|
-
// Parar de digitar
|
|
192
|
-
this.stopTyping({ roomId })
|
|
193
|
-
|
|
194
112
|
return { success: true, message }
|
|
195
113
|
}
|
|
196
114
|
|
|
197
|
-
/**
|
|
198
|
-
* Indicar que está digitando
|
|
199
|
-
*/
|
|
200
|
-
async startTyping(payload: { roomId?: string }) {
|
|
201
|
-
const roomId = payload.roomId || this.state.activeRoom
|
|
202
|
-
if (!roomId) return { success: false }
|
|
203
|
-
|
|
204
|
-
// Emitir evento
|
|
205
|
-
this.$room(roomId).emit('user:typing', {
|
|
206
|
-
user: this.state.username,
|
|
207
|
-
typing: true
|
|
208
|
-
})
|
|
209
|
-
|
|
210
|
-
// Limpar timer anterior
|
|
211
|
-
const existingTimer = this.typingTimers.get(roomId)
|
|
212
|
-
if (existingTimer) clearTimeout(existingTimer)
|
|
213
|
-
|
|
214
|
-
// Auto-parar após 3 segundos
|
|
215
|
-
const timer = setTimeout(() => {
|
|
216
|
-
this.stopTyping({ roomId })
|
|
217
|
-
}, 3000)
|
|
218
|
-
this.typingTimers.set(roomId, timer)
|
|
219
|
-
|
|
220
|
-
return { success: true }
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Parar de digitar
|
|
225
|
-
*/
|
|
226
|
-
async stopTyping(payload: { roomId?: string }) {
|
|
227
|
-
const roomId = payload.roomId || this.state.activeRoom
|
|
228
|
-
if (!roomId) return { success: false }
|
|
229
|
-
|
|
230
|
-
// Limpar timer
|
|
231
|
-
const timer = this.typingTimers.get(roomId)
|
|
232
|
-
if (timer) {
|
|
233
|
-
clearTimeout(timer)
|
|
234
|
-
this.typingTimers.delete(roomId)
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// Emitir evento
|
|
238
|
-
this.$room(roomId).emit('user:typing', {
|
|
239
|
-
user: this.state.username,
|
|
240
|
-
typing: false
|
|
241
|
-
})
|
|
242
|
-
|
|
243
|
-
return { success: true }
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// ===== Configuração =====
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Definir nome do usuário
|
|
250
|
-
*/
|
|
251
115
|
async setUsername(payload: { username: string }) {
|
|
252
116
|
const username = payload.username?.trim()
|
|
253
|
-
if (!username) throw new Error('
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
this.setState({ username })
|
|
257
|
-
return { success: true, username }
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Obter estado de uma sala
|
|
262
|
-
*/
|
|
263
|
-
async getRoomState(payload: { roomId: string }) {
|
|
264
|
-
const state = this.$room(payload.roomId).state
|
|
265
|
-
return {
|
|
266
|
-
success: true,
|
|
267
|
-
state: {
|
|
268
|
-
messages: state.messages || [],
|
|
269
|
-
typingUsers: state.typingUsers || []
|
|
270
|
-
}
|
|
271
|
-
}
|
|
117
|
+
if (!username || username.length > 30) throw new Error('Invalid username')
|
|
118
|
+
this.state.username = username
|
|
119
|
+
return { success: true }
|
|
272
120
|
}
|
|
273
121
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
// Limpar timers
|
|
278
|
-
for (const timer of this.typingTimers.values()) {
|
|
279
|
-
clearTimeout(timer)
|
|
280
|
-
}
|
|
281
|
-
this.typingTimers.clear()
|
|
282
|
-
|
|
122
|
+
destroy() {
|
|
123
|
+
for (const fns of this.roomListeners.values()) fns.forEach(fn => fn())
|
|
124
|
+
this.roomListeners.clear()
|
|
283
125
|
super.destroy()
|
|
284
126
|
}
|
|
285
127
|
}
|