feat: Wahl result view + live WebSocket chat in the Flutter client
Backend: - fix(chat): ChatGateway stored the per-socket caller only after the async token check resolved, so a client that sent chat:join immediately on open raced ahead and got 4001. The caller is now stored as a promise that the message handlers await. Verified with a two-client send/receive E2E test against local Postgres. Client (client/app/): - Wahl screen gains a "Ergebnis" tab backed by GET /wahl/guest/results (PENDING / ASSIGNED with workshop + wish rank / UNASSIGNED). - Chat channel view loads history over REST, then connects the /chat WebSocket (chat_socket.dart): live chat:message stream + a compose bar that sends chat:send. Shows the socket status. - web_socket_channel dependency added. flutter analyze/test/build web all green; backend npm test 56. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
|
||||
import 'api.dart';
|
||||
|
||||
/// Thin wrapper over the raw `ws` chat gateway. The NestJS `WsAdapter`
|
||||
/// expects `{"event": ..., "data": ...}` frames in both directions.
|
||||
class ChatSocket {
|
||||
ChatSocket(this._uri);
|
||||
final Uri _uri;
|
||||
|
||||
WebSocketChannel? _channel;
|
||||
final _messages = StreamController<ChatMessage>.broadcast();
|
||||
final _status = StreamController<String>.broadcast();
|
||||
|
||||
/// Incoming `chat:message` frames.
|
||||
Stream<ChatMessage> get messages => _messages.stream;
|
||||
|
||||
/// "connected" / "closed" / "error: ..." for a small status line.
|
||||
Stream<String> get status => _status.stream;
|
||||
|
||||
void connect(String channelId) {
|
||||
_channel = WebSocketChannel.connect(_uri);
|
||||
_channel!.stream.listen(
|
||||
(raw) {
|
||||
_status.add('connected');
|
||||
try {
|
||||
final frame = jsonDecode(raw as String) as Map<String, dynamic>;
|
||||
if (frame['event'] == 'chat:message') {
|
||||
_messages.add(ChatMessage.fromJson(frame['data'] as Map<String, dynamic>));
|
||||
}
|
||||
} catch (_) {
|
||||
// ignore frames we don't model
|
||||
}
|
||||
},
|
||||
onError: (Object e) => _status.add('error: $e'),
|
||||
onDone: () => _status.add('closed'),
|
||||
);
|
||||
_send('chat:join', {'channelId': channelId});
|
||||
}
|
||||
|
||||
void sendMessage(String channelId, String body) {
|
||||
_send('chat:send', {'channelId': channelId, 'body': body});
|
||||
}
|
||||
|
||||
void _send(String event, Map<String, dynamic> data) {
|
||||
_channel?.sink.add(jsonEncode({'event': event, 'data': data}));
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_channel?.sink.close();
|
||||
_messages.close();
|
||||
_status.close();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user