diff --git a/lib/features/chat/chat_composer.dart b/lib/features/chat/chat_composer.dart index 77c2c99..5c46b54 100644 --- a/lib/features/chat/chat_composer.dart +++ b/lib/features/chat/chat_composer.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'dart:ui' as ui; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -331,8 +332,10 @@ class _ChatComposerState extends ConsumerState { // block in the rooms list (~89px on the user's display). Grows for // multiline. constraints: const BoxConstraints(minHeight: 53), + // Rahmen + Fokus-Ring außen (würden vom Zuschnitt sonst + // abgeschnitten), innen Milchglas: der Chat läuft unter dem + // schwebenden Feld durch und scheint unscharf durch. decoration: BoxDecoration( - color: pt.bg2, borderRadius: BorderRadius.circular(pt.rBase + 4), border: Border.all( color: _focus.hasFocus ? pt.accent : pt.border, @@ -341,7 +344,13 @@ class _ChatComposerState extends ConsumerState { ? [BoxShadow(color: pt.accentSoft, blurRadius: 0, spreadRadius: 3)] : null, ), - child: Focus( + child: ClipRRect( + borderRadius: BorderRadius.circular(pt.rBase + 3), + child: BackdropFilter( + filter: ui.ImageFilter.blur(sigmaX: 12, sigmaY: 12), + child: ColoredBox( + color: pt.bg2.withValues(alpha: 0.78), + child: Focus( onFocusChange: (_) => setState(() {}), child: Row( crossAxisAlignment: CrossAxisAlignment.center, @@ -433,6 +442,9 @@ class _ChatComposerState extends ConsumerState { ), ], ), + ), + ), + ), ), ), ), diff --git a/lib/features/chat/chat_view.dart b/lib/features/chat/chat_view.dart index 0d2cc04..8b75088 100644 --- a/lib/features/chat/chat_view.dart +++ b/lib/features/chat/chat_view.dart @@ -19,6 +19,7 @@ import 'package:pyramid/features/chat/chat_provider.dart'; import 'package:pyramid/features/chat/message_group.dart'; import 'package:pyramid/features/chat/pinned_messages_panel.dart'; import 'package:pyramid/widgets/hover_region.dart'; +import 'package:pyramid/widgets/measure_size.dart'; import 'package:pyramid/widgets/mxc_image.dart'; import 'package:pyramid/core/notification_service.dart'; diff --git a/lib/features/chat/view/chat_message_list.dart b/lib/features/chat/view/chat_message_list.dart index 13bb75a..87c1ebc 100644 --- a/lib/features/chat/view/chat_message_list.dart +++ b/lib/features/chat/view/chat_message_list.dart @@ -19,6 +19,10 @@ class _MessageList extends StatelessWidget { final String? highlightEventId; final bool highlightOn; + /// Höhe der darüber schwebenden Eingabeleiste: so viel Platz bleibt unten + /// frei, damit die neueste Nachricht ganz über die Leiste scrollen kann. + final double bottomInset; + const _MessageList({ required this.timeline, required this.scrollCtrl, @@ -35,6 +39,7 @@ class _MessageList extends StatelessWidget { this.jumpKey, this.highlightEventId, this.highlightOn = false, + this.bottomInset = 0, }); @override @@ -241,7 +246,7 @@ class _MessageList extends StatelessWidget { controller: scrollCtrl, reverse: true, physics: const ClampingScrollPhysics(), - padding: const EdgeInsets.only(bottom: 8), + padding: EdgeInsets.only(bottom: 8 + bottomInset), scrollCacheExtent: const ScrollCacheExtent.pixels(2500), itemCount: itemKeys.length, findChildIndexCallback: (Key key) { diff --git a/lib/features/chat/view/chat_misc.dart b/lib/features/chat/view/chat_misc.dart index e7a9244..ce923bd 100644 --- a/lib/features/chat/view/chat_misc.dart +++ b/lib/features/chat/view/chat_misc.dart @@ -129,3 +129,25 @@ class NoChatSelected extends StatelessWidget { ); } } + +/// Fläche der schwebenden Eingabeleiste: um das Eingabefeld herum völlig +/// klar – der Chat ist dort normal zu sehen –, fängt aber alle Berührungen +/// ab (ColoredBox ist auch transparent „opak“ für Treffer), damit dort nur +/// die Eingabe reagiert und nicht der Chat dahinter. Die Unschärfe sitzt nur +/// hinter dem Eingabefeld selbst (chat_composer.dart). +class _FloatingComposerArea extends StatelessWidget { + final PyramidTheme pt; + final Widget child; + const _FloatingComposerArea({required this.pt, required this.child}); + + @override + Widget build(BuildContext context) { + return ColoredBox( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.only(top: 6), + child: child, + ), + ); + } +} diff --git a/lib/features/chat/view/chat_view_core.dart b/lib/features/chat/view/chat_view_core.dart index 2f88beb..fd504d0 100644 --- a/lib/features/chat/view/chat_view_core.dart +++ b/lib/features/chat/view/chat_view_core.dart @@ -31,6 +31,10 @@ class _ChatViewState extends ConsumerState { // Jump-to-message (from search): scroll to + briefly highlight a target event. final _jumpKey = GlobalKey(); + + // Höhe der schwebenden Eingabeleiste (gemessen, wächst mit Antwort-Leiste + // oder mehrzeiligem Text). + double _composerHeight = 72; String? _jumpEventId; String? _highlightEventId; bool _highlightOn = false; @@ -445,13 +449,14 @@ class _ChatViewState extends ConsumerState { jumpKey: _jumpKey, highlightEventId: _highlightEventId, highlightOn: _highlightOn, + bottomInset: room != null ? _composerHeight : 0, ); }, ), if (_showScrollDown) Positioned( right: 16, - bottom: 12, + bottom: (room != null ? _composerHeight : 0) + 12, child: AnimatedOpacity( opacity: _showScrollDown ? 1 : 0, duration: const Duration(milliseconds: 150), @@ -481,16 +486,45 @@ class _ChatViewState extends ConsumerState { ), ), ), + // Schwebende Eingabeleiste (Bernds Wunsch 2026-10-07): + // der Chat läuft optisch darunter weiter + // (durchscheinend + Unschärfe), die ganze Fläche + // fängt aber Berührungen ab – dort reagiert nur + // die Eingabe, nicht der Chat dahinter. + if (room != null) + Positioned( + left: 0, + right: 0, + bottom: 0, + child: MeasureSize( + onChange: (size) { + if (mounted && + (size.height - _composerHeight).abs() > + 0.5) { + setState( + () => _composerHeight = size.height); + } + }, + child: _FloatingComposerArea( + pt: pt, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _TypingIndicator(room: room, pt: pt), + ChatComposer( + room: room, + replyTo: _replyTo, + onClearReply: () => + setState(() => _replyTo = null), + ), + ], + ), + ), + ), + ), ], ), ), - if (room != null) _TypingIndicator(room: room, pt: pt), - if (room != null) - ChatComposer( - room: room, - replyTo: _replyTo, - onClearReply: () => setState(() => _replyTo = null), - ), ], ), ), diff --git a/lib/widgets/measure_size.dart b/lib/widgets/measure_size.dart new file mode 100644 index 0000000..11efcd4 --- /dev/null +++ b/lib/widgets/measure_size.dart @@ -0,0 +1,37 @@ +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; + +/// Meldet die gezeichnete Größe des Kindes, sobald sie sich ändert (z. B. +/// die Höhe der schwebenden Eingabeleiste, die mit Antwort-Leiste oder +/// mehrzeiligem Text wächst). Die Meldung kommt nach dem Layout, damit der +/// Empfänger gefahrlos setState aufrufen kann. +class MeasureSize extends SingleChildRenderObjectWidget { + const MeasureSize({super.key, required this.onChange, super.child}); + + final ValueChanged onChange; + + @override + RenderObject createRenderObject(BuildContext context) => + _MeasureSizeRender(onChange); + + @override + void updateRenderObject(BuildContext context, RenderObject renderObject) { + (renderObject as _MeasureSizeRender).onChange = onChange; + } +} + +class _MeasureSizeRender extends RenderProxyBox { + _MeasureSizeRender(this.onChange); + + ValueChanged onChange; + Size? _last; + + @override + void performLayout() { + super.performLayout(); + final size = this.size; + if (size == _last) return; + _last = size; + WidgetsBinding.instance.addPostFrameCallback((_) => onChange(size)); + } +}