import 'package:flutter/material.dart'; import '../api.dart'; import '../chat_socket.dart'; import '../main.dart'; /// Chat: channel list (REST) + a per-channel view that loads history over /// REST and then streams live messages over the `/chat` WebSocket gateway /// (`chat:join` / `chat:send` / `chat:message`). class ChatScreen extends StatefulWidget { const ChatScreen({super.key, required this.kcId}); final String kcId; @override State createState() => _ChatScreenState(); } class _ChatScreenState extends State { Future>? _future; @override void didChangeDependencies() { super.didChangeDependencies(); _future ??= AppScope.of(context).api.channels(widget.kcId); } static const _typeLabels = { 'GEMEINDE_GRUPPE': 'Gemeinde-Gruppe', 'DIREKT': 'Direktnachricht', 'LT_UEBERGREIFEND': 'Leitungsteam', 'BROADCAST': 'Ankündigungen', }; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Chat')), body: FutureBuilder>( future: _future, builder: (context, snap) { if (snap.connectionState != ConnectionState.done) { return const Center(child: CircularProgressIndicator()); } if (snap.hasError) { return Center( child: Padding( padding: const EdgeInsets.all(24), child: Text('${snap.error}', textAlign: TextAlign.center), ), ); } final channels = snap.data!; if (channels.isEmpty) { return const Center(child: Text('Keine Kanäle sichtbar.')); } return ListView( children: [ for (final c in channels) ListTile( leading: const Icon(Icons.tag), title: Text(_typeLabels[c.type] ?? c.type), subtitle: Text(c.id), onTap: () => Navigator.of(context).push( MaterialPageRoute( builder: (_) => _ChannelMessages( channelId: c.id, title: _typeLabels[c.type] ?? c.type, ), ), ), ), ], ); }, ), ); } } class _ChannelMessages extends StatefulWidget { const _ChannelMessages({required this.channelId, required this.title}); final String channelId; final String title; @override State<_ChannelMessages> createState() => _ChannelMessagesState(); } class _ChannelMessagesState extends State<_ChannelMessages> { final List _messages = []; final _composer = TextEditingController(); final _scroll = ScrollController(); ChatSocket? _socket; bool _loading = true; String? _loadError; String _wsStatus = 'verbinde…'; @override void didChangeDependencies() { super.didChangeDependencies(); if (_socket != null) return; final api = AppScope.of(context).api; _load(api); _socket = ChatSocket(api.chatWsUri()) ..connect(widget.channelId) ..messages.listen(_onIncoming) ..status.listen((s) => mounted ? setState(() => _wsStatus = s) : null); } Future _load(Api api) async { try { final history = await api.messages(widget.channelId); if (!mounted) return; setState(() { _messages ..clear() ..addAll(history); _loading = false; }); _jump(); } catch (e) { if (mounted) setState(() { _loadError = '$e'; _loading = false; }); } } void _onIncoming(ChatMessage m) { if (!mounted) return; setState(() => _messages.add(m)); _jump(); } void _jump() { WidgetsBinding.instance.addPostFrameCallback((_) { if (_scroll.hasClients) { _scroll.jumpTo(_scroll.position.maxScrollExtent); } }); } void _send() { final text = _composer.text.trim(); if (text.isEmpty) return; _socket?.sendMessage(widget.channelId, text); _composer.clear(); } @override void dispose() { _socket?.dispose(); _composer.dispose(); _scroll.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), bottom: PreferredSize( preferredSize: const Size.fromHeight(18), child: Text('WebSocket: $_wsStatus', style: const TextStyle(fontSize: 11)), ), ), body: Column( children: [ Expanded(child: _body(context)), SafeArea( top: false, child: Padding( padding: const EdgeInsets.all(8), child: Row( children: [ Expanded( child: TextField( controller: _composer, onSubmitted: (_) => _send(), decoration: const InputDecoration( hintText: 'Nachricht…', border: OutlineInputBorder(), isDense: true, ), ), ), IconButton(icon: const Icon(Icons.send), onPressed: _send), ], ), ), ), ], ), ); } Widget _body(BuildContext context) { if (_loading) return const Center(child: CircularProgressIndicator()); if (_loadError != null) { return Center( child: Padding( padding: const EdgeInsets.all(24), child: Text(_loadError!, textAlign: TextAlign.center), ), ); } if (_messages.isEmpty) { return const Center(child: Text('Noch keine Nachrichten.')); } return ListView.builder( controller: _scroll, padding: const EdgeInsets.all(12), itemCount: _messages.length, itemBuilder: (context, i) { final m = _messages[i]; return Align( alignment: Alignment.centerLeft, child: Container( margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(10), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(m.body), const SizedBox(height: 2), Text(m.createdAt, style: Theme.of(context).textTheme.labelSmall), ], ), ), ); }, ); } }