Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,13 @@ class _MessageList extends HookConsumerWidget {
: displayEntries.length - 1 - chronologicalIndex;
}

double latestAlignment() {
final viewportHeight = context.size?.height ?? 0;
return viewportHeight > 0
? (composerBottomInset / viewportHeight).clamp(0.0, 1.0).toDouble()
: 0.0;
}

Future<void> scrollToLatest() async {
if (!itemScrollController.isAttached || isAutoScrolling.value) return;
followsLatest.value = true;
Expand All @@ -163,6 +170,7 @@ class _MessageList extends HookConsumerWidget {
try {
await itemScrollController.scrollTo(
index: 0,
alignment: latestAlignment(),
duration: const Duration(milliseconds: 220),
curve: Curves.easeOutCubic,
);
Expand Down Expand Up @@ -199,12 +207,15 @@ class _MessageList extends HookConsumerWidget {
}

bool latestIsAtBoundary() {
// In this reversed list, item 0's leading edge is the bottom boundary.
// Being merely visible is not enough: a user who has pulled a tall
// newest row away from the boundary must not snap back on live updates.
// In this reversed list, item 0's leading edge is the visible bottom
// boundary above the composer. Being merely visible is not enough: a
// user who has pulled a tall newest row away from that boundary must not
// snap back on live updates.
final boundary = latestAlignment();
return itemPositionsListener.itemPositions.value.any(
(position) =>
position.index == 0 && position.itemLeadingEdge.abs() < 0.01,
position.index == 0 &&
(position.itemLeadingEdge - boundary).abs() < 0.01,
);
}

Expand Down
68 changes: 65 additions & 3 deletions mobile/test/features/channels/channel_detail_page_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1811,7 +1811,7 @@ void main() {
expect(latestMessage, findsOneWidget);
expect(
tester.getBottomLeft(latestMessage).dy,
lessThanOrEqualTo(tester.getTopLeft(composerDock).dy + 1),
closeTo(tester.getTopLeft(composerDock).dy, 1),
);

await tester.tap(find.text('Message #general'));
Expand All @@ -1823,7 +1823,7 @@ void main() {
);
expect(
tester.getBottomLeft(latestMessage).dy,
lessThanOrEqualTo(tester.getTopLeft(composerDock).dy + 1),
closeTo(tester.getTopLeft(composerDock).dy, 1),
);
expect(
find.byKey(const ValueKey('channel-jump-to-latest')),
Expand All @@ -1836,7 +1836,57 @@ void main() {
expect(latestMessage, findsOneWidget);
expect(
tester.getBottomLeft(latestMessage).dy,
lessThanOrEqualTo(tester.getTopLeft(composerDock).dy + 1),
closeTo(tester.getTopLeft(composerDock).dy, 1),
);
expect(
find.byKey(const ValueKey('channel-jump-to-latest')),
findsNothing,
);
},
);

testWidgets(
'keeps a short followed tail flush through composer resize',
(tester) async {
tester.view.physicalSize = const Size(400, 800);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
addTearDown(tester.view.reset);

final messages = [
for (var i = 0; i < 3; i++)
_textMsg(
id: 'msg$i',
pubkey: 'alice',
content: 'Message $i',
createdAt: 1000 + i,
),
];

await tester.pumpWidget(
_buildTestable(
messages: messages,
users: const {
'alice': UserProfile(pubkey: 'alice', displayName: 'Alice'),
},
),
);
await tester.pumpAndSettle();

final latestMessage = find.byKey(
const ValueKey('channel-message-group-msg2'),
);
final composerDock = find.byKey(
const ValueKey('channel-composer-dock'),
);

await tester.tap(find.text('Message #general'));
await tester.pumpAndSettle();

expect(
tester.getBottomLeft(latestMessage).dy,
closeTo(tester.getTopLeft(composerDock).dy, 1),
);
expect(
find.byKey(const ValueKey('channel-jump-to-latest')),
Expand Down Expand Up @@ -1990,6 +2040,18 @@ void main() {
await tester.pumpAndSettle();

expect(findRichText('Newest live update'), findsOneWidget);
final latestMessage = find.byKey(
const ValueKey('channel-message-group-newest'),
);
final composerDock = find.byKey(const ValueKey('channel-composer-dock'));
expect(
tester.getBottomLeft(latestMessage).dy,
closeTo(tester.getTopLeft(composerDock).dy, 1),
);
expect(
find.byKey(const ValueKey('channel-jump-to-latest')),
findsNothing,
);
});

testWidgets(
Expand Down
Loading