feat: Trenner bleibt 6 s stehen, Testprofil im Fenstertitel

- "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 <[email protected]>
This commit is contained in:
Bernd Steckmeister
2026-10-07 23:06:20 +02:00
co-authored by Claude Opus 5.5
parent a30d57ba69
commit a6461fe23c
2 changed files with 50 additions and 10 deletions
+42 -9
View File
@@ -28,6 +28,11 @@ class _ChatViewState extends ConsumerState<ChatView> {
// Fade-out of the "Neue Nachrichten" divider once the user reaches the bottom. // Fade-out of the "Neue Nachrichten" divider once the user reaches the bottom.
bool _unreadFading = false; bool _unreadFading = false;
Timer? _unreadFadeTimer; 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 // Zuletzt von hier als gelesen gemeldetes Event. Verhindert, dass jeder
// Neuaufbau erneut eine Lesebestätigung schickt, und markiert nach dem // Neuaufbau erneut eine Lesebestätigung schickt, und markiert nach dem
// Zurückkehren in die App, ab wo „Neue Nachrichten“ beginnen. // Zurückkehren in die App, ab wo „Neue Nachrichten“ beginnen.
@@ -162,6 +167,7 @@ class _ChatViewState extends ConsumerState<ChatView> {
_historyExhausted = false; _historyExhausted = false;
_unreadFading = false; _unreadFading = false;
_unreadFadeTimer?.cancel(); _unreadFadeTimer?.cancel();
_unreadDwellTimer?.cancel();
_lastMarkedEventId = null; _lastMarkedEventId = null;
_popInAfter = null; _popInAfter = null;
_jumpEventId = null; _jumpEventId = null;
@@ -177,6 +183,7 @@ class _ChatViewState extends ConsumerState<ChatView> {
@override @override
void dispose() { void dispose() {
_unreadFadeTimer?.cancel(); _unreadFadeTimer?.cancel();
_unreadDwellTimer?.cancel();
_highlightTimer?.cancel(); _highlightTimer?.cancel();
_scrollCtrl.dispose(); _scrollCtrl.dispose();
super.dispose(); super.dispose();
@@ -247,10 +254,12 @@ class _ChatViewState extends ConsumerState<ChatView> {
duration: const Duration(milliseconds: 250), duration: const Duration(milliseconds: 250),
curve: Curves.easeOut, curve: Curves.easeOut,
); );
_scheduleUnreadFadeIfAtNewest();
return; return;
} }
if (attempt >= 24) { if (attempt >= 24) {
_didScrollToUnread = true; // give up — stay at the newest messages _didScrollToUnread = true; // give up — stay at the newest messages
_scheduleUnreadFadeIfAtNewest();
return; return;
} }
if (attempt > 0 && attempt % 4 == 0 && _scrollCtrl.hasClients) { if (attempt > 0 && attempt % 4 == 0 && _scrollCtrl.hasClients) {
@@ -311,16 +320,46 @@ class _ChatViewState extends ConsumerState<ChatView> {
.any((e) => _MessageList._isVisible(e) && e.senderId != me); .any((e) => _MessageList._isVisible(e) && e.senderId != me);
if (hasNew) { if (hasNew) {
_unreadFadeTimer?.cancel(); _unreadFadeTimer?.cancel();
_unreadDwellTimer?.cancel();
setState(() { setState(() {
_sessionFullyReadId = since; _sessionFullyReadId = since;
_sessionHasUnread = true; _sessionHasUnread = true;
_unreadFading = false; _unreadFading = false;
}); });
_scheduleUnreadFadeIfAtNewest();
} }
} }
_markRead(timeline); _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<void> _onScroll() async { Future<void> _onScroll() async {
if (!_scrollCtrl.hasClients) return; if (!_scrollCtrl.hasClients) return;
final pixels = _scrollCtrl.position.pixels; final pixels = _scrollCtrl.position.pixels;
@@ -328,15 +367,9 @@ class _ChatViewState extends ConsumerState<ChatView> {
if (pixels < 60) { if (pixels < 60) {
_markRead(null); _markRead(null);
// Reached the newest messages → the "Neue Nachrichten" divider is no // Bei den neuesten Nachrichten angekommen → Trenner nach einer
// longer useful. Fade it out, then remove it. // Verweildauer ausblenden.
if (_sessionHasUnread && !_unreadFading) { _scheduleUnreadFade();
setState(() => _unreadFading = true);
_unreadFadeTimer?.cancel();
_unreadFadeTimer = Timer(const Duration(milliseconds: 900), () {
if (mounted) setState(() { _sessionHasUnread = false; _unreadFading = false; });
});
}
} }
final shouldShow = pixels > 400; final shouldShow = pixels > 400;
if (shouldShow != _showScrollDown) { if (shouldShow != _showScrollDown) {
+8 -1
View File
@@ -27,7 +27,14 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
FlutterWindow window(project); FlutterWindow window(project);
Win32Window::Point origin(10, 10); Win32Window::Point origin(10, 10);
Win32Window::Size size(1280, 720); 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; return EXIT_FAILURE;
} }
window.SetQuitOnClose(true); window.SetQuitOnClose(true);