Skip to content

Commit bf1d607

Browse files
committed
lightbox: Prevent hero animation between message lists
Fixes: #930
1 parent 44df81f commit bf1d607

File tree

3 files changed

+71
-10
lines changed

3 files changed

+71
-10
lines changed

Diff for: lib/widgets/content.dart

+5-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import 'dialog.dart';
2020
import 'icons.dart';
2121
import 'lightbox.dart';
2222
import 'message_list.dart';
23+
import 'page.dart';
2324
import 'poll.dart';
2425
import 'store.dart';
2526
import 'text.dart';
@@ -638,6 +639,7 @@ class MessageImage extends StatelessWidget {
638639
// TODO image hover animation
639640
final srcUrl = node.srcUrl;
640641
final thumbnailUrl = node.thumbnailUrl;
642+
final pageContext = PageRoot.contextOf(context);
641643
final store = PerAccountStoreWidget.of(context);
642644
final resolvedSrcUrl = store.tryResolveUrl(srcUrl);
643645
final resolvedThumbnailUrl = thumbnailUrl == null
@@ -653,12 +655,14 @@ class MessageImage extends StatelessWidget {
653655
src: resolvedSrcUrl,
654656
thumbnailUrl: resolvedThumbnailUrl,
655657
originalWidth: node.originalWidth,
656-
originalHeight: node.originalHeight));
658+
originalHeight: node.originalHeight,
659+
pageContext: pageContext));
657660
},
658661
child: node.loading
659662
? const CupertinoActivityIndicator()
660663
: resolvedSrcUrl == null ? null : LightboxHero(
661664
message: message,
665+
pageContext: pageContext,
662666
src: resolvedSrcUrl,
663667
child: RealmContentNetworkImage(
664668
resolvedThumbnailUrl ?? resolvedSrcUrl,

Diff for: lib/widgets/lightbox.dart

+14-5
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,22 @@ import 'store.dart';
2121
// fly to an image preview with a different URL, following a message edit
2222
// while the lightbox was open.
2323
class _LightboxHeroTag {
24-
_LightboxHeroTag({required this.messageId, required this.src});
24+
_LightboxHeroTag({required this.messageId, required this.src, required this.pageContext});
2525

2626
final int messageId;
2727
final Uri src;
28+
final BuildContext pageContext;
2829

2930
@override
3031
bool operator ==(Object other) {
3132
return other is _LightboxHeroTag &&
3233
other.messageId == messageId &&
33-
other.src == src;
34+
other.src == src &&
35+
other.pageContext == pageContext;
3436
}
3537

3638
@override
37-
int get hashCode => Object.hash('_LightboxHeroTag', messageId, src);
39+
int get hashCode => Object.hash('_LightboxHeroTag', messageId, src, pageContext);
3840
}
3941

4042
/// Builds a [Hero] from an image in the message list to the lightbox page.
@@ -44,16 +46,18 @@ class LightboxHero extends StatelessWidget {
4446
required this.message,
4547
required this.src,
4648
required this.child,
49+
required this.pageContext,
4750
});
4851

4952
final Message message;
5053
final Uri src;
5154
final Widget child;
55+
final BuildContext pageContext;
5256

5357
@override
5458
Widget build(BuildContext context) {
5559
return Hero(
56-
tag: _LightboxHeroTag(messageId: message.id, src: src),
60+
tag: _LightboxHeroTag(messageId: message.id, src: src, pageContext: pageContext),
5761
flightShuttleBuilder: (
5862
BuildContext flightContext,
5963
Animation<double> animation,
@@ -230,6 +234,7 @@ class _ImageLightboxPage extends StatefulWidget {
230234
required this.thumbnailUrl,
231235
required this.originalWidth,
232236
required this.originalHeight,
237+
required this.pageContext,
233238
});
234239

235240
final Animation<double> routeEntranceAnimation;
@@ -238,6 +243,7 @@ class _ImageLightboxPage extends StatefulWidget {
238243
final Uri? thumbnailUrl;
239244
final double? originalWidth;
240245
final double? originalHeight;
246+
final BuildContext pageContext;
241247

242248
@override
243249
State<_ImageLightboxPage> createState() => _ImageLightboxPageState();
@@ -319,6 +325,7 @@ class _ImageLightboxPageState extends State<_ImageLightboxPage> {
319325
child: LightboxHero(
320326
message: widget.message,
321327
src: widget.src,
328+
pageContext: widget.pageContext,
322329
child: RealmContentNetworkImage(widget.src,
323330
filterQuality: FilterQuality.medium,
324331
frameBuilder: _frameBuilder,
@@ -603,6 +610,7 @@ Route<void> getImageLightboxRoute({
603610
required Uri? thumbnailUrl,
604611
required double? originalWidth,
605612
required double? originalHeight,
613+
required BuildContext pageContext,
606614
}) {
607615
return _getLightboxRoute(
608616
accountId: accountId,
@@ -614,7 +622,8 @@ Route<void> getImageLightboxRoute({
614622
src: src,
615623
thumbnailUrl: thumbnailUrl,
616624
originalWidth: originalWidth,
617-
originalHeight: originalHeight);
625+
originalHeight: originalHeight,
626+
pageContext: pageContext);
618627
});
619628
}
620629

Diff for: test/widgets/lightbox_test.dart

+52-4
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import 'package:zulip/model/localizations.dart';
1313
import 'package:zulip/widgets/app.dart';
1414
import 'package:zulip/widgets/content.dart';
1515
import 'package:zulip/widgets/lightbox.dart';
16+
import 'package:zulip/widgets/page.dart';
1617

1718
import '../example_data.dart' as eg;
1819
import '../model/binding.dart';
@@ -207,14 +208,19 @@ void main() {
207208
addTearDown(testBinding.reset);
208209
await testBinding.globalStore.add(eg.selfAccount, eg.initialSnapshot());
209210

210-
// ZulipApp instead of TestZulipApp because we need the navigator to push
211-
// the lightbox route. The lightbox page works together with the route;
212-
// it takes the route's entrance animation.
213-
await tester.pumpWidget(const ZulipApp());
211+
// ZulipApp instead of TestZulipApp because we need:
212+
// 1. The navigator to push the lightbox route. The lightbox page works
213+
// together with the route; it takes the route's entrance animation.
214+
// 2. The PageRoot widget to provide context for Hero animations between
215+
// the message list and lightbox.
216+
await tester.pumpWidget(PageRoot(
217+
child: const ZulipApp()
218+
));
214219
await tester.pump();
215220
final navigator = await ZulipApp.navigator;
216221
unawaited(navigator.push(getImageLightboxRoute(
217222
accountId: eg.selfAccount.id,
223+
pageContext: PageRoot.contextOf(navigator.context),
218224
message: message ?? eg.streamMessage(),
219225
src: src,
220226
thumbnailUrl: thumbnailUrl,
@@ -286,6 +292,48 @@ void main() {
286292
debugNetworkImageHttpClientProvider = null;
287293
});
288294

295+
testWidgets('LightboxHero with different pageContext does not share hero animation', (tester) async {
296+
final message = eg.streamMessage();
297+
final src = Uri.parse('https://chat.example/image.png');
298+
await tester.pumpWidget(MaterialApp(
299+
home: Scaffold(
300+
body: Row(
301+
children: [
302+
Builder(builder: (context1) {
303+
return LightboxHero(
304+
message: message,
305+
src: src,
306+
pageContext: context1,
307+
child: const SizedBox(width: 50, height: 50),
308+
);
309+
}),
310+
Builder(builder: (context2) {
311+
return LightboxHero(
312+
message: message,
313+
src: src,
314+
pageContext: context2,
315+
child: const SizedBox(width: 50, height: 50),
316+
);
317+
}),
318+
],
319+
),
320+
),
321+
));
322+
323+
final heroes = tester.widgetList<Hero>(find.byType(Hero)).toList();
324+
expect(heroes.length, 2);
325+
326+
final heroTag1 = heroes[0].tag;
327+
final heroTag2 = heroes[1].tag;
328+
expect(heroTag1, isNot(equals(heroTag2)));
329+
330+
final tag1 = heroTag1 as dynamic;
331+
final tag2 = heroTag2 as dynamic;
332+
expect(tag1.messageId, equals(tag2.messageId));
333+
expect(tag1.src, equals(tag2.src));
334+
expect(tag1.pageContext, isNot(equals(tag2.pageContext)));
335+
});
336+
289337
// TODO test _CopyLinkButton
290338
// TODO test thumbnail gets shown, then gets replaced when main image loads
291339
// TODO test image is scaled down to fit, but not up

0 commit comments

Comments
 (0)