feat: schwebende Eingabeleiste - Chat laeuft darunter weiter (Milchglas), Flaeche faengt Beruehrungen ab
Bernds Wunsch 2026-10-07: Die Nachrichtenliste reicht jetzt bis ganz unten; Tipp-Anzeige und Eingabefeld schweben darueber. Um das Feld herum ist die Flaeche klar (Chat sichtbar), aber beruehrungsdicht - dort reagiert nur die Eingabe. Hinter dem Feld selbst Milchglas (Unschaerfe + 78 % Flaeche), Rahmen und Fokus-Ring aussen. Die Liste bekommt unten genau die gemessene Hoehe der Leiste als Abstand (MeasureSize; waechst mit Antwort-Leiste/mehrzeiligem Text), damit die neueste Nachricht ganz ueber die Leiste scrollt; der Nach-unten-Knopf sitzt darueber. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
7063ebfceb
commit
223c9448f6
@@ -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<ChatComposer> {
|
||||
// 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<ChatComposer> {
|
||||
? [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<ChatComposer> {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,10 @@ class _ChatViewState extends ConsumerState<ChatView> {
|
||||
|
||||
// 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<ChatView> {
|
||||
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<ChatView> {
|
||||
),
|
||||
),
|
||||
),
|
||||
// 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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<Size> 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<Size> onChange;
|
||||
Size? _last;
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
super.performLayout();
|
||||
final size = this.size;
|
||||
if (size == _last) return;
|
||||
_last = size;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => onChange(size));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user