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>
This commit is contained in:
115
lib/features/chat/presentation/message_input.dart
Normal file
115
lib/features/chat/presentation/message_input.dart
Normal file
@@ -0,0 +1,115 @@
|
||||
// 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',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user