Files
pyramid/lib/features/chat/chat_page.dart
T
2026-04-19 20:09:43 +02:00

157 lines
4.4 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import 'package:pyramid/features/chat/chat_input.dart';
import 'package:pyramid/features/chat/chat_provider.dart';
import 'package:pyramid/features/chat/message_bubble.dart';
class ChatPage extends ConsumerStatefulWidget {
final String roomId;
const ChatPage({super.key, required this.roomId});
@override
ConsumerState<ChatPage> createState() => _ChatPageState();
}
class _ChatPageState extends ConsumerState<ChatPage> {
final _scrollCtrl = ScrollController();
Event? _replyTo;
String get _roomId => Uri.decodeComponent(widget.roomId);
@override
void initState() {
super.initState();
_scrollCtrl.addListener(_onScroll);
}
@override
void dispose() {
_scrollCtrl.dispose();
super.dispose();
}
void _onScroll() {
if (_scrollCtrl.position.pixels >=
_scrollCtrl.position.maxScrollExtent - 300) {
final timeline = ref.read(timelineProvider(_roomId)).valueOrNull;
timeline?.requestHistory();
}
}
@override
Widget build(BuildContext context) {
final room = ref.watch(roomProvider(_roomId));
final timelineAsync = ref.watch(timelineProvider(_roomId));
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => context.go('/rooms'),
),
title: Text(room?.getLocalizedDisplayname() ?? _roomId),
actions: [
IconButton(
icon: const Icon(Icons.info_outline),
onPressed: () {},
),
],
),
body: Column(
children: [
Expanded(
child: timelineAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('Fehler: $e')),
data: (timeline) => _MessageList(
timeline: timeline,
scrollCtrl: _scrollCtrl,
currentUserId: room?.client.userID ?? '',
onReply: (event) => setState(() => _replyTo = event),
),
),
),
if (room != null)
ChatInput(
room: room,
replyTo: _replyTo,
onClearReply: () => setState(() => _replyTo = null),
),
],
),
);
}
}
class _MessageList extends StatelessWidget {
final Timeline timeline;
final ScrollController scrollCtrl;
final String currentUserId;
final ValueChanged<Event> onReply;
const _MessageList({
required this.timeline,
required this.scrollCtrl,
required this.currentUserId,
required this.onReply,
});
@override
Widget build(BuildContext context) {
final events = timeline.events
.where((e) =>
e.type == EventTypes.Message &&
e.status != EventStatus.error)
.toList();
if (events.isEmpty) {
return const Center(child: Text('Noch keine Nachrichten'));
}
return ListView.builder(
controller: scrollCtrl,
reverse: true,
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: events.length,
itemBuilder: (context, i) {
final event = events[i];
final isOwn = event.senderId == currentUserId;
final showDate = i == events.length - 1 ||
!_sameDay(event.originServerTs, events[i + 1].originServerTs);
final replyEventId =
event.content.tryGetMap<String, dynamic>('m.relates_to')
?['m.in_reply_to']?['event_id'] as String?;
final replyEvent = replyEventId != null
? timeline.events.firstWhere(
(e) => e.eventId == replyEventId,
orElse: () => event,
)
: null;
return Column(
children: [
if (showDate)
DateSeparator(date: event.originServerTs),
GestureDetector(
onLongPress: () => onReply(event),
child: MessageBubble(
event: event,
isOwn: isOwn,
replyEvent:
replyEvent?.eventId != event.eventId ? replyEvent : null,
),
),
],
);
},
);
}
bool _sameDay(DateTime a, DateTime b) =>
a.year == b.year && a.month == b.month && a.day == b.day;
}