mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-06-14 22:55:12 +10:00
fix(chat): fix jump-to-oldest-unread scroll not reaching target message
- Pass initialUnreadCount to chat screens before markRead clears it - Use two-phase scroll: jumpTo estimated offset to build lazy items, then ensureVisible for precise positioning - Await ensureVisible before clearing scroll guard to prevent scrollToBottomIfAtBottom from overriding the animation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -49,6 +49,25 @@ class ChatScrollController extends ScrollController {
|
||||
}
|
||||
}
|
||||
|
||||
/// Jumps toward an off-screen message so that lazy ListView.builder builds
|
||||
/// items near it. Only visible + cacheExtent items have real heights, so we
|
||||
/// use proportion of maxScrollExtent (itself an estimate from built items'
|
||||
/// avg height). Call [onJumped] on the next frame to ensureVisible/scroll
|
||||
/// to the exact target.
|
||||
void jumpToEstimatedOffset({
|
||||
required int unreadCount,
|
||||
required int totalMessages,
|
||||
required VoidCallback onJumped,
|
||||
}) {
|
||||
if (!hasClients || totalMessages == 0) return;
|
||||
final maxExtent = position.maxScrollExtent;
|
||||
final jumpOffset = maxExtent * (unreadCount / totalMessages);
|
||||
if (jumpOffset > 100) {
|
||||
jumpTo(jumpOffset);
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => onJumped());
|
||||
}
|
||||
|
||||
void scrollToBottomIfAtBottom() {
|
||||
// Only scroll if jump button is NOT showing (i.e., already at bottom)
|
||||
if (!showJumpToBottom.value && hasClients && position.maxScrollExtent > 0) {
|
||||
|
||||
@@ -37,8 +37,13 @@ import 'map_screen.dart';
|
||||
|
||||
class ChannelChatScreen extends StatefulWidget {
|
||||
final Channel channel;
|
||||
final int initialUnreadCount;
|
||||
|
||||
const ChannelChatScreen({super.key, required this.channel});
|
||||
const ChannelChatScreen({
|
||||
super.key,
|
||||
required this.channel,
|
||||
this.initialUnreadCount = 0,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ChannelChatScreen> createState() => _ChannelChatScreenState();
|
||||
@@ -66,13 +71,11 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final settings = context.read<AppSettingsService>().settings;
|
||||
final idx = widget.channel.index;
|
||||
final unread = connector.getUnreadCountForChannelIndex(idx);
|
||||
final unread = widget.initialUnreadCount;
|
||||
final messages = connector.getChannelMessages(widget.channel);
|
||||
ChannelMessage? anchor;
|
||||
if (settings.jumpToOldestUnread && unread > 0) {
|
||||
anchor = _findOldestUnreadChannelAnchor(
|
||||
connector.getChannelMessages(widget.channel),
|
||||
unread,
|
||||
);
|
||||
anchor = _findOldestUnreadChannelAnchor(messages, unread);
|
||||
}
|
||||
connector.setActiveChannel(idx);
|
||||
_connector = connector;
|
||||
@@ -80,7 +83,14 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
_channelSkipNextBottomSnap = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
_scrollToMessage(anchor!.messageId);
|
||||
_scrollController.jumpToEstimatedOffset(
|
||||
unreadCount: unread,
|
||||
totalMessages: messages.length,
|
||||
onJumped: () {
|
||||
if (!mounted) return;
|
||||
_scrollToMessage(anchor!.messageId);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -492,13 +492,18 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
],
|
||||
),
|
||||
onTap: () async {
|
||||
final unread =
|
||||
connector.getUnreadCountForChannelIndex(channel.index);
|
||||
connector.markChannelRead(channel.index);
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
if (context.mounted) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ChannelChatScreen(channel: channel),
|
||||
builder: (context) => ChannelChatScreen(
|
||||
channel: channel,
|
||||
initialUnreadCount: unread,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -48,8 +48,13 @@ import 'telemetry_screen.dart';
|
||||
|
||||
class ChatScreen extends StatefulWidget {
|
||||
final Contact contact;
|
||||
final int initialUnreadCount;
|
||||
|
||||
const ChatScreen({super.key, required this.contact});
|
||||
const ChatScreen({
|
||||
super.key,
|
||||
required this.contact,
|
||||
this.initialUnreadCount = 0,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ChatScreen> createState() => _ChatScreenState();
|
||||
@@ -75,13 +80,11 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final settings = context.read<AppSettingsService>().settings;
|
||||
final keyHex = widget.contact.publicKeyHex;
|
||||
final unread = connector.getUnreadCountForContactKey(keyHex);
|
||||
final unread = widget.initialUnreadCount;
|
||||
final messages = connector.getMessages(widget.contact);
|
||||
Message? anchor;
|
||||
if (settings.jumpToOldestUnread && unread > 0) {
|
||||
anchor = _findOldestUnreadAnchor(
|
||||
connector.getMessages(widget.contact),
|
||||
unread,
|
||||
);
|
||||
anchor = _findOldestUnreadAnchor(messages, unread);
|
||||
}
|
||||
connector.setActiveContact(keyHex);
|
||||
_connector = connector;
|
||||
@@ -89,15 +92,24 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
setState(() => _pendingUnreadScrollTarget = anchor);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
final ctx = _unreadScrollKey.currentContext;
|
||||
if (ctx != null) {
|
||||
Scrollable.ensureVisible(
|
||||
ctx,
|
||||
duration: const Duration(milliseconds: 350),
|
||||
alignment: 0.15,
|
||||
);
|
||||
}
|
||||
setState(() => _pendingUnreadScrollTarget = null);
|
||||
_scrollController.jumpToEstimatedOffset(
|
||||
unreadCount: unread,
|
||||
totalMessages: messages.length,
|
||||
onJumped: () async {
|
||||
if (!mounted) return;
|
||||
final ctx = _unreadScrollKey.currentContext;
|
||||
if (ctx != null) {
|
||||
await Scrollable.ensureVisible(
|
||||
ctx,
|
||||
duration: const Duration(milliseconds: 350),
|
||||
alignment: 0.15,
|
||||
);
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() => _pendingUnreadScrollTarget = null);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1305,11 +1317,18 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
}
|
||||
|
||||
void _openChat(BuildContext context, Contact contact) {
|
||||
// Check if this is a repeater
|
||||
context.read<MeshCoreConnector>().markContactRead(contact.publicKeyHex);
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final unread =
|
||||
connector.getUnreadCountForContactKey(contact.publicKeyHex);
|
||||
connector.markContactRead(contact.publicKeyHex);
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => ChatScreen(contact: contact)),
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ChatScreen(
|
||||
contact: contact,
|
||||
initialUnreadCount: unread,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -930,10 +930,18 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
} else if (contact.type == advTypeRoom) {
|
||||
_showRoomLogin(context, contact, RoomLoginDestination.chat);
|
||||
} else {
|
||||
context.read<MeshCoreConnector>().markContactRead(contact.publicKeyHex);
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final unread =
|
||||
connector.getUnreadCountForContactKey(contact.publicKeyHex);
|
||||
connector.markContactRead(contact.publicKeyHex);
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => ChatScreen(contact: contact)),
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ChatScreen(
|
||||
contact: contact,
|
||||
initialUnreadCount: unread,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -988,7 +996,10 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
builder: (context) => RoomLoginDialog(
|
||||
room: room,
|
||||
onLogin: (password, isAdmin) {
|
||||
context.read<MeshCoreConnector>().markContactRead(room.publicKeyHex);
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final unread =
|
||||
connector.getUnreadCountForContactKey(room.publicKeyHex);
|
||||
connector.markContactRead(room.publicKeyHex);
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
@@ -999,7 +1010,10 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
password: password,
|
||||
isAdmin: isAdmin,
|
||||
)
|
||||
: ChatScreen(contact: room),
|
||||
: ChatScreen(
|
||||
contact: room,
|
||||
initialUnreadCount: unread,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1391,11 +1391,18 @@ class _MapScreenState extends State<MapScreen> {
|
||||
room: room,
|
||||
// onLogin(password, isAdmin) isAdmin not used for room caht screen
|
||||
onLogin: (password, _) {
|
||||
// Navigate to chat screen after successful login
|
||||
context.read<MeshCoreConnector>().markContactRead(room.publicKeyHex);
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final unread =
|
||||
connector.getUnreadCountForContactKey(room.publicKeyHex);
|
||||
connector.markContactRead(room.publicKeyHex);
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => ChatScreen(contact: room)),
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ChatScreen(
|
||||
contact: room,
|
||||
initialUnreadCount: unread,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user