Files
m8chat-app2/lib/features/chat/presentation/chat_screen.dart
help4bis 8f13c725a4 feat: Phase 1 complete — Matrix login, rooms, chat, profile
- Direct m.login.password auth against matrix.m8chat.au
- Room list with unread badges, last message, timestamps
- Chat timeline (text, images, files, replies, reactions)
- Profile screen with expandable Notifications and Security sections
- Olm E2EE initialisation (web WASM bootstrap)
- Global error handler preventing Matrix SDK crashes
- GoRouter with refreshListenable (no recreation on auth change)
- Feature-first clean architecture: Riverpod + GoRouter + Drift
- Deployed to https://app2.m8chat.au

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 06:26:57 +10:00

162 lines
4.3 KiB
Dart

// Version: 1.0.0 | Created: 2026-04-01
// Full chat screen — timeline + message input.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/network/matrix_client.dart';
import 'chat_controller.dart';
import 'message_bubble.dart';
import 'message_input.dart';
class ChatScreen extends ConsumerWidget {
const ChatScreen({super.key, required this.roomId});
final String roomId;
@override
Widget build(BuildContext context, WidgetRef ref) {
// Decode the roomId — GoRouter encodes ! as %21 etc.
final decodedRoomId = Uri.decodeComponent(roomId);
final client = ref.watch(matrixClientProvider);
final room = client.getRoomById(decodedRoomId);
final roomName = room?.getLocalizedDisplayname() ?? 'Chat';
final roomAvatar = room?.avatar?.toString();
return Scaffold(
appBar: AppBar(
titleSpacing: 0,
title: Row(
children: [
_RoomAvatarSmall(name: roomName, avatarUrl: roomAvatar),
const SizedBox(width: 10),
Flexible(child: Text(roomName, overflow: TextOverflow.ellipsis)),
],
),
actions: [
IconButton(
icon: const Icon(Icons.call),
tooltip: 'Start call (Phase 2)',
onPressed: null, // Phase 2
),
IconButton(
icon: const Icon(Icons.more_vert),
tooltip: 'Room options',
onPressed: () {
// Phase 2: room settings sheet
},
),
],
),
body: Column(
children: [
Expanded(child: _Timeline(roomId: decodedRoomId)),
_Input(roomId: decodedRoomId),
],
),
);
}
}
class _RoomAvatarSmall extends StatelessWidget {
const _RoomAvatarSmall({required this.name, this.avatarUrl});
final String name;
final String? avatarUrl;
@override
Widget build(BuildContext context) {
final initials = name.isNotEmpty ? name[0].toUpperCase() : '?';
if (avatarUrl != null) {
return CircleAvatar(
radius: 18,
backgroundImage: NetworkImage(avatarUrl!),
);
}
return CircleAvatar(
radius: 18,
backgroundColor: Theme.of(context).colorScheme.primary.withAlpha(51),
child: Text(
initials,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
),
),
);
}
}
class _Timeline extends ConsumerWidget {
const _Timeline({required this.roomId});
final String roomId;
@override
Widget build(BuildContext context, WidgetRef ref) {
final timelineAsync = ref.watch(chatTimelineProvider(roomId));
return timelineAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, _) => Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Text('Could not load messages: $error'),
),
),
data: (messages) {
if (messages.isEmpty) {
return Center(
child: Text(
'No messages yet. Say hello!',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withAlpha(102),
),
),
);
}
return ListView.builder(
reverse: true,
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: messages.length,
itemBuilder: (context, index) {
return MessageBubble(message: messages[index]);
},
);
},
);
}
}
class _Input extends ConsumerWidget {
const _Input({required this.roomId});
final String roomId;
@override
Widget build(BuildContext context, WidgetRef ref) {
final isSending = ref.watch(sendMessageProvider);
return MessageInput(
isSending: isSending,
onSend: (text) async {
final error = await ref
.read(sendMessageProvider.notifier)
.send(roomId, text);
if (error != null && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to send message: $error'),
backgroundColor: Theme.of(context).colorScheme.error,
),
);
}
},
);
}
}