feat: chat view with timeline, message bubbles, reply and send
This commit is contained in:
@@ -1,14 +1,156 @@
|
||||
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 StatelessWidget {
|
||||
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(title: Text(roomId)),
|
||||
body: const Center(child: Text('Chat')),
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user