Files
pyramid/lib/features/chat/chat_input.dart
T
Bernd Steckmeister cdf48c4d45 feat: in-app update downloader with progress, Gitea auto-push hook
- update_checker: Gitea at git.steggi-matrix.work, Windows + Android support, asset URL detection
- update_download_dialog: animated Pyramid logo, progress bar, platform install (exe/apk)
- update_banner: Herunterladen button triggers dialog, dismiss option
- Android: REQUEST_INSTALL_PACKAGES permission + FileProvider for APK install
- post-commit hook: auto-push to Gitea on every commit
- chat_input: larger send button (60px desktop), more padding, bigger icon
- rooms_panel: space menu visible on mobile (leave space fix)
- docs: feature-checklist.md added

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 15:35:59 +02:00

202 lines
6.0 KiB
Dart

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:matrix/matrix.dart';
class ChatInput extends StatefulWidget {
final Room room;
final Event? replyTo;
final VoidCallback? onClearReply;
const ChatInput({
super.key,
required this.room,
this.replyTo,
this.onClearReply,
});
@override
State<ChatInput> createState() => _ChatInputState();
}
class _ChatInputState extends State<ChatInput> {
final _ctrl = TextEditingController();
bool _canSend = false;
@override
void initState() {
super.initState();
_ctrl.addListener(() {
final hasText = _ctrl.text.trim().isNotEmpty;
if (hasText != _canSend) setState(() => _canSend = hasText);
});
}
@override
void dispose() {
_ctrl.dispose();
super.dispose();
}
Future<void> _send() async {
final text = _ctrl.text.trim();
if (text.isEmpty) return;
_ctrl.clear();
setState(() => _canSend = false);
await widget.room.sendTextEvent(
text,
inReplyTo: widget.replyTo,
);
widget.onClearReply?.call();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDesktop = !kIsWeb &&
(defaultTargetPlatform == TargetPlatform.windows ||
defaultTargetPlatform == TargetPlatform.linux ||
defaultTargetPlatform == TargetPlatform.macOS);
return SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (widget.replyTo != null) _ReplyBar(
event: widget.replyTo!,
onClear: widget.onClearReply ?? () {},
),
Padding(
padding: EdgeInsets.fromLTRB(
isDesktop ? 12 : 8,
isDesktop ? 10 : 4,
isDesktop ? 12 : 8,
isDesktop ? 14 : 8,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: TextField(
controller: _ctrl,
minLines: 1,
maxLines: 6,
textCapitalization: TextCapitalization.sentences,
style: TextStyle(fontSize: isDesktop ? 16 : 14),
decoration: InputDecoration(
hintText: 'Nachricht...',
hintStyle: TextStyle(
fontSize: isDesktop ? 16 : 14,
color: theme.colorScheme.onSurface.withAlpha(100),
),
contentPadding: EdgeInsets.symmetric(
horizontal: isDesktop ? 22 : 16,
vertical: isDesktop ? 20 : 10,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(isDesktop ? 16 : 24),
borderSide: BorderSide.none,
),
filled: true,
fillColor: theme.colorScheme.surfaceContainerHigh,
),
onSubmitted: (_) => _send(),
keyboardType: TextInputType.multiline,
inputFormatters: [
_SendOnEnterFormatter(onSend: _send),
],
),
),
SizedBox(width: isDesktop ? 10 : 8),
GestureDetector(
onTap: _canSend ? _send : null,
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
width: isDesktop ? 60 : 44,
height: isDesktop ? 60 : 44,
decoration: BoxDecoration(
color: _canSend
? theme.colorScheme.primary
: theme.colorScheme.primary.withAlpha(70),
borderRadius: BorderRadius.circular(isDesktop ? 16 : 14),
),
child: Center(
child: Icon(
Icons.send_rounded,
size: isDesktop ? 26 : 20,
color: Colors.white,
),
),
),
),
],
),
),
],
),
);
}
}
class _ReplyBar extends StatelessWidget {
final Event event;
final VoidCallback onClear;
const _ReplyBar({required this.event, required this.onClear});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
padding: const EdgeInsets.fromLTRB(16, 8, 8, 4),
decoration: BoxDecoration(
border: Border(
top: BorderSide(color: theme.dividerColor),
left: BorderSide(color: theme.colorScheme.primary, width: 3),
),
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
event.senderFromMemoryOrFallback.calcDisplayname(),
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
Text(
event.body,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall,
),
],
),
),
IconButton(
icon: const Icon(Icons.close, size: 18),
onPressed: onClear,
),
],
),
);
}
}
class _SendOnEnterFormatter extends TextInputFormatter {
final VoidCallback onSend;
_SendOnEnterFormatter({required this.onSend});
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
return newValue;
}
}