feat: federnde Übergänge und Animationen, Entwürfe je Raum
- Handy: Chat gleitet federnd über die Chatliste (Liste bleibt aufgebaut, Scrollposition erhalten), beim Zurück wieder hinaus; Liste schon während des Hinausgleitens bedienbar. Mitgliederleiste gleitet herein. - Neue/gesendete Nachrichten ploppen weich auf (nur live, nicht beim Öffnen oder Nachladen), Reaktionen ploppen auf und hüpfen beim Hochzählen, Ungelesen-Zähler hüpft, Scroll-nach-unten-Knopf federt (Handy größer), Antwort-Leiste klappt federnd auf/zu, Senden-Pfeil ploppt, Vollansicht zoomt auf. Alles skaliert mit Einstellung „Animationen“ (0 % = aus). - Fix: Beim Raumwechsel blieben Antwort-Ziel, Auswahl und getippter Text am neuen Raum hängen (Antwort auf Raum A konnte in Raum B rausgehen). Jetzt eigenes Eingabefeld je Raum mit Entwurf im Speicher. - Antwort-Leiste: größeres Schließen-X auf dem Handy. - Tests: test/spring_motion_test.dart, UI-Durchlauf mit Übergängen. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
1d0ccfde8b
commit
ae6f651f4c
@@ -23,6 +23,9 @@ class _MessageList extends StatelessWidget {
|
||||
/// frei, damit die neueste Nachricht ganz über die Leiste scrollen kann.
|
||||
final double bottomInset;
|
||||
|
||||
/// Siehe MessageGroup.popInAfter.
|
||||
final DateTime? popInAfter;
|
||||
|
||||
const _MessageList({
|
||||
required this.timeline,
|
||||
required this.scrollCtrl,
|
||||
@@ -40,6 +43,7 @@ class _MessageList extends StatelessWidget {
|
||||
this.highlightEventId,
|
||||
this.highlightOn = false,
|
||||
this.bottomInset = 0,
|
||||
this.popInAfter,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -292,6 +296,7 @@ class _MessageList extends StatelessWidget {
|
||||
jumpKey: jumpKey,
|
||||
highlightEventId: highlightEventId,
|
||||
highlightOn: highlightOn,
|
||||
popInAfter: popInAfter,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -32,6 +32,9 @@ class _ChatViewState extends ConsumerState<ChatView> {
|
||||
// Neuaufbau erneut eine Lesebestätigung schickt, und markiert nach dem
|
||||
// Zurückkehren in die App, ab wo „Neue Nachrichten“ beginnen.
|
||||
String? _lastMarkedEventId;
|
||||
// Was danach eintrifft, ploppt federnd auf (beim Öffnen gesetzt, damit die
|
||||
// schon vorhandenen Nachrichten ruhig erscheinen).
|
||||
DateTime? _popInAfter;
|
||||
|
||||
// Jump-to-message (from search): scroll to + briefly highlight a target event.
|
||||
final _jumpKey = GlobalKey();
|
||||
@@ -115,6 +118,9 @@ class _ChatViewState extends ConsumerState<ChatView> {
|
||||
|
||||
bool get _inSelectionMode => _selectedIds.isNotEmpty;
|
||||
|
||||
double get _scrollBtnSize =>
|
||||
isMobileLayout(MediaQuery.sizeOf(context)) ? 44 : 36;
|
||||
|
||||
void _toggleSelect(String eventId) {
|
||||
setState(() {
|
||||
if (_selectedIds.contains(eventId)) {
|
||||
@@ -142,6 +148,11 @@ class _ChatViewState extends ConsumerState<ChatView> {
|
||||
void didUpdateWidget(ChatView oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.roomId != widget.roomId) {
|
||||
// Antwort/Auswahl gehören zum alten Raum – sonst ginge eine Antwort
|
||||
// auf eine Nachricht aus Raum A in Raum B raus.
|
||||
_replyTo = null;
|
||||
_selectedIds.clear();
|
||||
_showScrollDown = false;
|
||||
_profileExpanded = false;
|
||||
_sessionFullyReadId = null;
|
||||
_sessionHasUnread = false;
|
||||
@@ -152,6 +163,7 @@ class _ChatViewState extends ConsumerState<ChatView> {
|
||||
_unreadFading = false;
|
||||
_unreadFadeTimer?.cancel();
|
||||
_lastMarkedEventId = null;
|
||||
_popInAfter = null;
|
||||
_jumpEventId = null;
|
||||
_highlightEventId = null;
|
||||
_highlightOn = false;
|
||||
@@ -202,6 +214,7 @@ class _ChatViewState extends ConsumerState<ChatView> {
|
||||
setState(() {
|
||||
_sessionFullyReadId = newFullyReadId;
|
||||
_sessionHasUnread = newHasUnread;
|
||||
_popInAfter = events.firstOrNull?.originServerTs ?? DateTime.now();
|
||||
});
|
||||
if (newHasUnread) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToUnread(timeline));
|
||||
@@ -499,28 +512,35 @@ class _ChatViewState extends ConsumerState<ChatView> {
|
||||
highlightEventId: _highlightEventId,
|
||||
highlightOn: _highlightOn,
|
||||
bottomInset: room != null ? _composerHeight : 0,
|
||||
popInAfter: _popInAfter,
|
||||
);
|
||||
},
|
||||
),
|
||||
if (_showScrollDown)
|
||||
Positioned(
|
||||
Positioned(
|
||||
right: 16,
|
||||
bottom: (room != null ? _composerHeight : 0) + 12,
|
||||
child: AnimatedOpacity(
|
||||
opacity: _showScrollDown ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 150),
|
||||
child: IgnorePointer(
|
||||
ignoring: !_showScrollDown,
|
||||
child: AnimatedScale(
|
||||
scale: _showScrollDown ? 1 : 0,
|
||||
duration: motionDuration(
|
||||
context, _showScrollDown ? 520 : 180),
|
||||
curve: _showScrollDown
|
||||
? Curves.elasticOut
|
||||
: Curves.easeInBack,
|
||||
child: GestureDetector(
|
||||
onTap: () => _scrollCtrl.animateTo(
|
||||
0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOut,
|
||||
),
|
||||
child: SpringPress(
|
||||
child: Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
width: _scrollBtnSize,
|
||||
height: _scrollBtnSize,
|
||||
decoration: BoxDecoration(
|
||||
color: pt.bg0,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
borderRadius: BorderRadius.circular(_scrollBtnSize / 2),
|
||||
border: Border.all(color: pt.border),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
@@ -530,10 +550,13 @@ class _ChatViewState extends ConsumerState<ChatView> {
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(Icons.keyboard_arrow_down_rounded, size: 20, color: pt.fgMuted),
|
||||
child: Icon(Icons.keyboard_arrow_down_rounded,
|
||||
size: _scrollBtnSize * 0.56, color: pt.fgMuted),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Schwebende Eingabeleiste (Bernds Wunsch 2026-10-07):
|
||||
// der Chat läuft optisch darunter weiter
|
||||
@@ -561,6 +584,9 @@ class _ChatViewState extends ConsumerState<ChatView> {
|
||||
children: [
|
||||
_TypingIndicator(room: room, pt: pt),
|
||||
ChatComposer(
|
||||
// Je Raum ein eigenes Eingabefeld
|
||||
// (Text, „tippt…“, Entwurf).
|
||||
key: ValueKey('composer:${room.id}'),
|
||||
room: room,
|
||||
replyTo: _replyTo,
|
||||
onClearReply: () =>
|
||||
|
||||
Reference in New Issue
Block a user