- 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>
116 lines
3.2 KiB
Dart
116 lines
3.2 KiB
Dart
// Version: 1.0.0 | Created: 2026-04-01
|
|
// Message input bar. Text field + send button.
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
class MessageInput extends StatefulWidget {
|
|
const MessageInput({
|
|
super.key,
|
|
required this.onSend,
|
|
required this.isSending,
|
|
});
|
|
|
|
final Future<void> Function(String text) onSend;
|
|
final bool isSending;
|
|
|
|
@override
|
|
State<MessageInput> createState() => _MessageInputState();
|
|
}
|
|
|
|
class _MessageInputState extends State<MessageInput> {
|
|
final _controller = TextEditingController();
|
|
bool _hasText = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_controller.addListener(() {
|
|
final hasText = _controller.text.trim().isNotEmpty;
|
|
if (hasText != _hasText) {
|
|
setState(() => _hasText = hasText);
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _submit() async {
|
|
final text = _controller.text.trim();
|
|
if (text.isEmpty) return;
|
|
_controller.clear();
|
|
await widget.onSend(text);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.fromLTRB(8, 8, 8, 12),
|
|
decoration: BoxDecoration(
|
|
color: theme.colorScheme.surface,
|
|
border: Border(
|
|
top: BorderSide(color: theme.colorScheme.outline.withAlpha(51)),
|
|
),
|
|
),
|
|
child: SafeArea(
|
|
top: false,
|
|
child: Row(
|
|
children: [
|
|
IconButton(
|
|
icon: const Icon(Icons.add),
|
|
tooltip: 'Attach file (Phase 2)',
|
|
onPressed: null, // Phase 2
|
|
color: theme.colorScheme.onSurface.withAlpha(153),
|
|
),
|
|
Expanded(
|
|
child: TextField(
|
|
controller: _controller,
|
|
decoration: const InputDecoration(
|
|
hintText: 'Message',
|
|
contentPadding: EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 10,
|
|
),
|
|
),
|
|
textInputAction: TextInputAction.send,
|
|
onSubmitted: (_) => _submit(),
|
|
maxLines: null,
|
|
keyboardType: TextInputType.multiline,
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
AnimatedSwitcher(
|
|
duration: const Duration(milliseconds: 150),
|
|
child: widget.isSending
|
|
? const SizedBox(
|
|
width: 40,
|
|
height: 40,
|
|
child: Center(
|
|
child: SizedBox(
|
|
width: 20,
|
|
height: 20,
|
|
child: CircularProgressIndicator(strokeWidth: 2.5),
|
|
),
|
|
),
|
|
)
|
|
: IconButton(
|
|
icon: const Icon(Icons.send_rounded),
|
|
onPressed: _hasText ? _submit : null,
|
|
color: _hasText
|
|
? theme.colorScheme.primary
|
|
: theme.colorScheme.onSurface.withAlpha(77),
|
|
tooltip: 'Send message',
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|