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:
co-authored by
Claude Opus 5.5
parent
a30d57ba69
commit
a6461fe23c
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user