From a6461fe23cdecc75cf12ea689c566a05201fa521 Mon Sep 17 00:00:00 2001 From: Bernd Steckmeister Date: Wed, 7 Oct 2026 23:06:20 +0200 Subject: [PATCH] feat: Trenner bleibt 6 s stehen, Testprofil im Fenstertitel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "Neue Nachrichten"-Trenner verschwand beim Öffnen aus der Benachrichtigung sofort (Bernd). Jetzt: bei den neuesten Nachrichten angekommen noch 6 s stehen, dann weich ausblenden; wer hochscrollt, behält ihn. - Windows: mit PYRAMID_PROFILE_DIR heißt das Fenster "Pyramid - TESTINSTANZ" (Testinstanz mit Testkonto nicht mit der echten Pyramid verwechseln). Co-Authored-By: Claude Opus 5.5 --- lib/features/chat/view/chat_view_core.dart | 51 ++++++++++++++++++---- windows/runner/main.cpp | 9 +++- 2 files changed, 50 insertions(+), 10 deletions(-) diff --git a/lib/features/chat/view/chat_view_core.dart b/lib/features/chat/view/chat_view_core.dart index 71fb408..e8bb42f 100644 --- a/lib/features/chat/view/chat_view_core.dart +++ b/lib/features/chat/view/chat_view_core.dart @@ -28,6 +28,11 @@ class _ChatViewState extends ConsumerState { // Fade-out of the "Neue Nachrichten" divider once the user reaches the bottom. bool _unreadFading = false; Timer? _unreadFadeTimer; + // So lange bleibt der Trenner stehen, nachdem man bei den neuesten + // Nachrichten angekommen ist (Bernd 2026-10-07: verschwand beim Öffnen aus + // der Benachrichtigung sofort). + static const _unreadDwell = Duration(seconds: 6); + Timer? _unreadDwellTimer; // Zuletzt von hier als gelesen gemeldetes Event. Verhindert, dass jeder // Neuaufbau erneut eine Lesebestätigung schickt, und markiert nach dem // Zurückkehren in die App, ab wo „Neue Nachrichten“ beginnen. @@ -162,6 +167,7 @@ class _ChatViewState extends ConsumerState { _historyExhausted = false; _unreadFading = false; _unreadFadeTimer?.cancel(); + _unreadDwellTimer?.cancel(); _lastMarkedEventId = null; _popInAfter = null; _jumpEventId = null; @@ -177,6 +183,7 @@ class _ChatViewState extends ConsumerState { @override void dispose() { _unreadFadeTimer?.cancel(); + _unreadDwellTimer?.cancel(); _highlightTimer?.cancel(); _scrollCtrl.dispose(); super.dispose(); @@ -247,10 +254,12 @@ class _ChatViewState extends ConsumerState { duration: const Duration(milliseconds: 250), curve: Curves.easeOut, ); + _scheduleUnreadFadeIfAtNewest(); return; } if (attempt >= 24) { _didScrollToUnread = true; // give up — stay at the newest messages + _scheduleUnreadFadeIfAtNewest(); return; } if (attempt > 0 && attempt % 4 == 0 && _scrollCtrl.hasClients) { @@ -311,16 +320,46 @@ class _ChatViewState extends ConsumerState { .any((e) => _MessageList._isVisible(e) && e.senderId != me); if (hasNew) { _unreadFadeTimer?.cancel(); + _unreadDwellTimer?.cancel(); setState(() { _sessionFullyReadId = since; _sessionHasUnread = true; _unreadFading = false; }); + _scheduleUnreadFadeIfAtNewest(); } } _markRead(timeline); } + bool get _atNewest => + !_scrollCtrl.hasClients || _scrollCtrl.position.pixels < 60; + + /// „Neue Nachrichten“-Trenner nach [_unreadDwell] weich ausblenden – nur + /// wenn man dann noch bei den neuesten Nachrichten ist. Wer zwischendurch + /// hochscrollt, behält ihn; unten angekommen beginnt die Wartezeit neu. + void _scheduleUnreadFade() { + if (!_sessionHasUnread || _unreadFading) return; + if (_unreadDwellTimer?.isActive ?? false) return; + _unreadDwellTimer = Timer(_unreadDwell, () { + if (!mounted || !_sessionHasUnread || !_atNewest) return; + setState(() => _unreadFading = true); + _unreadFadeTimer?.cancel(); + _unreadFadeTimer = Timer(const Duration(milliseconds: 900), () { + if (mounted) setState(() { _sessionHasUnread = false; _unreadFading = false; }); + }); + }); + } + + // Nach dem Sprung zum Trenner (Animation ~250 ms) prüfen: Passt alles + // Ungelesene auf den Bildschirm, scrollt niemand mehr – dann läuft die + // Verweildauer trotzdem an. + void _scheduleUnreadFadeIfAtNewest() { + Future.delayed(const Duration(milliseconds: 400), () { + if (mounted && _atNewest) _scheduleUnreadFade(); + }); + } + Future _onScroll() async { if (!_scrollCtrl.hasClients) return; final pixels = _scrollCtrl.position.pixels; @@ -328,15 +367,9 @@ class _ChatViewState extends ConsumerState { if (pixels < 60) { _markRead(null); - // Reached the newest messages → the "Neue Nachrichten" divider is no - // longer useful. Fade it out, then remove it. - if (_sessionHasUnread && !_unreadFading) { - setState(() => _unreadFading = true); - _unreadFadeTimer?.cancel(); - _unreadFadeTimer = Timer(const Duration(milliseconds: 900), () { - if (mounted) setState(() { _sessionHasUnread = false; _unreadFading = false; }); - }); - } + // Bei den neuesten Nachrichten angekommen → Trenner nach einer + // Verweildauer ausblenden. + _scheduleUnreadFade(); } final shouldShow = pixels > 400; if (shouldShow != _showScrollDown) { diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp index 75a5497..f6a36af 100644 --- a/windows/runner/main.cpp +++ b/windows/runner/main.cpp @@ -27,7 +27,14 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(1280, 720); - if (!window.Create(L"Pyramid", origin, size)) { + // Testprofil (PYRAMID_PROFILE_DIR, z. B. die Testinstanz mit einem + // Testkonto) im Fenstertitel kenntlich machen, damit sie nicht mit der + // echten Pyramid verwechselt wird. Ohne die Variable bleibt alles wie immer. + wchar_t profile_probe[2] = {0}; + const bool test_profile = + ::GetEnvironmentVariableW(L"PYRAMID_PROFILE_DIR", profile_probe, 2) > 0; + if (!window.Create(test_profile ? L"Pyramid - TESTINSTANZ" : L"Pyramid", + origin, size)) { return EXIT_FAILURE; } window.SetQuitOnClose(true);