Skip to content

Commit 1699aa5

Browse files
committed
settings [nfc]: Centralize logic for checking device animation settings
This is only used with image emojis, for now, but it'll also be useful for #1936, suppressing image animations in image previews. Related: #1936
1 parent 9ee8ccc commit 1699aa5

File tree

10 files changed

+84
-42
lines changed

10 files changed

+84
-42
lines changed

lib/widgets/emoji.dart

Lines changed: 16 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class EmojiWidget extends StatelessWidget {
1313
required this.squareDimension,
1414
this.squareDimensionScaler = TextScaler.noScaling,
1515
this.imagePlaceholderStyle = EmojiImagePlaceholderStyle.square,
16-
this.neverAnimateImage = false,
16+
this.imageAnimationMode = ImageAnimationMode.animateConditionally,
1717
this.buildCustomTextEmoji,
1818
});
1919

@@ -35,11 +35,12 @@ class EmojiWidget extends StatelessWidget {
3535

3636
final EmojiImagePlaceholderStyle imagePlaceholderStyle;
3737

38-
/// Whether to show an animated emoji in its still (non-animated) variant
39-
/// only, even if device settings permit animation.
38+
/// Whether to show an animated emoji in its still or animated version.
4039
///
41-
/// Defaults to false.
42-
final bool neverAnimateImage;
40+
/// Ignored except for animated image emoji.
41+
///
42+
/// Defaults to [ImageAnimationMode.animateConditionally].
43+
final ImageAnimationMode imageAnimationMode;
4344

4445
/// An optional callback to specify a custom plain-text emoji style.
4546
///
@@ -66,7 +67,7 @@ class EmojiWidget extends StatelessWidget {
6667
EmojiImagePlaceholderStyle.nothing => SizedBox.shrink(),
6768
EmojiImagePlaceholderStyle.text => _buildTextEmoji(),
6869
},
69-
neverAnimate: neverAnimateImage),
70+
animationMode: imageAnimationMode),
7071
UnicodeEmojiDisplay() => UnicodeEmojiWidget(
7172
emojiDisplay: emojiDisplay,
7273
size: squareDimension,
@@ -185,7 +186,7 @@ class ImageEmojiWidget extends StatelessWidget {
185186
required this.size,
186187
this.textScaler = TextScaler.noScaling,
187188
this.errorBuilder,
188-
this.neverAnimate = false,
189+
this.animationMode = ImageAnimationMode.animateConditionally,
189190
});
190191

191192
final ImageEmojiDisplay emojiDisplay;
@@ -202,30 +203,20 @@ class ImageEmojiWidget extends StatelessWidget {
202203

203204
final ImageErrorWidgetBuilder? errorBuilder;
204205

205-
/// Whether to show an animated emoji in its still (non-animated) variant
206-
/// only, even if device settings permit animation.
206+
/// Whether to show an animated emoji in its still or animated version.
207+
///
208+
/// Ignored for non-animated emoji.
207209
///
208-
/// Defaults to false.
209-
final bool neverAnimate;
210+
/// Defaults to [ImageAnimationMode.animateConditionally].
211+
final ImageAnimationMode animationMode;
210212

211213
@override
212214
Widget build(BuildContext context) {
213-
final doNotAnimate =
214-
neverAnimate
215-
// From reading code, this doesn't actually get set on iOS:
216-
// https://github.com/zulip/zulip-flutter/pull/410#discussion_r1408522293
217-
|| MediaQuery.disableAnimationsOf(context)
218-
|| (defaultTargetPlatform == TargetPlatform.iOS
219-
// TODO(#1924) On iOS 17+ (new in 2023), there's a more closely
220-
// relevant setting than "reduce motion". It's called "auto-play
221-
// animated images"; we should use that once Flutter exposes it.
222-
&& WidgetsBinding.instance.platformDispatcher.accessibilityFeatures.reduceMotion);
223-
224215
final size = textScaler.scale(this.size);
225216

226-
final resolvedUrl = doNotAnimate
227-
? (emojiDisplay.resolvedStillUrl ?? emojiDisplay.resolvedUrl)
228-
: emojiDisplay.resolvedUrl;
217+
final resolvedUrl = animationMode.resolve(context)
218+
? emojiDisplay.resolvedUrl
219+
: (emojiDisplay.resolvedStillUrl ?? emojiDisplay.resolvedUrl);
229220

230221
return RealmContentNetworkImage(
231222
width: size, height: size,

lib/widgets/image.dart

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'package:flutter/foundation.dart';
12
import 'package:flutter/widgets.dart';
23

34
import '../api/core.dart';
@@ -107,3 +108,44 @@ class RealmContentNetworkImage extends StatelessWidget {
107108
);
108109
}
109110
}
111+
112+
/// Whether to show an animated image in its still or animated version.
113+
///
114+
/// Use [resolve] to evaluate this for the given [BuildContext],
115+
/// which reads device-setting data for [animateConditionally].
116+
enum ImageAnimationMode {
117+
/// Always show the animated version.
118+
animateAlways,
119+
120+
/// Always show the still version.
121+
animateNever,
122+
123+
/// Show the animated version
124+
/// just if animations aren't disabled in device settings.
125+
animateConditionally,
126+
;
127+
128+
/// True if the image should be animated, false if it should be still.
129+
bool resolve(BuildContext context) {
130+
switch (this) {
131+
case animateAlways: return true;
132+
case animateNever: return false;
133+
case animateConditionally:
134+
// From reading code, this doesn't actually get set on iOS:
135+
// https://github.com/zulip/zulip-flutter/pull/410#discussion_r1408522293
136+
if (MediaQuery.disableAnimationsOf(context)) return false;
137+
138+
if (
139+
defaultTargetPlatform == TargetPlatform.iOS
140+
// TODO(#1924) On iOS 17+ (new in 2023), there's a more closely
141+
// relevant setting than "reduce motion". It's called "auto-play
142+
// animated images"; we should use that once Flutter exposes it.
143+
&& WidgetsBinding.instance.platformDispatcher.accessibilityFeatures.reduceMotion
144+
) {
145+
return false;
146+
}
147+
148+
return true;
149+
}
150+
}
151+
}

lib/widgets/profile.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import 'app_bar.dart';
1515
import 'button.dart';
1616
import 'content.dart';
1717
import 'icons.dart';
18+
import 'image.dart';
1819
import 'message_list.dart';
1920
import 'page.dart';
2021
import 'remote_settings.dart';
@@ -87,7 +88,7 @@ class ProfilePage extends StatelessWidget {
8788
userId: userId,
8889
fontSize: nameStyle.fontSize!,
8990
textScaler: MediaQuery.textScalerOf(context),
90-
neverAnimate: false,
91+
animationMode: ImageAnimationMode.animateConditionally,
9192
),
9293
]),
9394
textAlign: TextAlign.center,
@@ -267,7 +268,7 @@ class _SetStatusButton extends StatelessWidget {
267268
fontSize: 16,
268269
textScaler: MediaQuery.textScalerOf(context),
269270
position: StatusEmojiPosition.before,
270-
neverAnimate: false,
271+
animationMode: ImageAnimationMode.animateConditionally,
271272
),
272273
userStatus.text == null
273274
? TextSpan(text: zulipLocalizations.noStatusText,

lib/widgets/set_status.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import '../log.dart';
1010
import 'app_bar.dart';
1111
import 'emoji_reaction.dart';
1212
import 'icons.dart';
13+
import 'image.dart';
1314
import 'inset_shadow.dart';
1415
import 'page.dart';
1516
import 'store.dart';
@@ -214,7 +215,11 @@ class _SetStatusPageState extends State<SetStatusPage> {
214215
final emoji = change.emoji.or(oldStatus.emoji);
215216
return emoji == null
216217
? const Icon(ZulipIcons.smile, size: 24)
217-
: UserStatusEmoji(emoji: emoji, size: 24, neverAnimate: false);
218+
: UserStatusEmoji(
219+
emoji: emoji,
220+
size: 24,
221+
animationMode: ImageAnimationMode.animateConditionally,
222+
);
218223
}),
219224
Icon(ZulipIcons.chevron_down, size: 16),
220225
]),

lib/widgets/user.dart

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -297,24 +297,24 @@ class _PresenceCircleState extends State<PresenceCircle> with PerAccountStoreAwa
297297
/// widgets.
298298
/// When there is no status emoji to be shown, the padding will be omitted too.
299299
///
300-
/// Use [neverAnimate] to forcefully disable the animation for animated emojis.
301-
/// Defaults to true.
300+
/// Use [animationMode] to control whether an animated emoji is shown
301+
/// in its still or animated version.
302302
class UserStatusEmoji extends StatelessWidget {
303303
const UserStatusEmoji({
304304
super.key,
305305
this.userId,
306306
this.emoji,
307307
required this.size,
308308
this.padding = EdgeInsets.zero,
309-
this.neverAnimate = true,
309+
this.animationMode = ImageAnimationMode.animateNever,
310310
}) : assert((userId == null) != (emoji == null),
311311
'Only one of the userId or emoji should be provided.');
312312

313313
final int? userId;
314314
final StatusEmoji? emoji;
315315
final double size;
316316
final EdgeInsetsGeometry padding;
317-
final bool neverAnimate;
317+
final ImageAnimationMode animationMode;
318318

319319
static const double _spanPadding = 4;
320320

@@ -329,7 +329,7 @@ class UserStatusEmoji extends StatelessWidget {
329329
required double fontSize,
330330
required TextScaler textScaler,
331331
StatusEmojiPosition position = StatusEmojiPosition.after,
332-
bool neverAnimate = true,
332+
ImageAnimationMode animationMode = ImageAnimationMode.animateNever,
333333
}) {
334334
final (double paddingStart, double paddingEnd) = switch (position) {
335335
StatusEmojiPosition.before => (0, _spanPadding),
@@ -340,7 +340,7 @@ class UserStatusEmoji extends StatelessWidget {
340340
alignment: PlaceholderAlignment.middle,
341341
child: UserStatusEmoji(userId: userId, emoji: emoji, size: size,
342342
padding: EdgeInsetsDirectional.only(start: paddingStart, end: paddingEnd),
343-
neverAnimate: neverAnimate));
343+
animationMode: animationMode));
344344
}
345345

346346
@override
@@ -363,7 +363,7 @@ class UserStatusEmoji extends StatelessWidget {
363363
child: EmojiWidget(
364364
emojiDisplay: emojiDisplay,
365365
squareDimension: size,
366-
neverAnimateImage: neverAnimate,
366+
imageAnimationMode: animationMode,
367367
buildCustomTextEmoji: () =>
368368
// Invoked when an image emoji's URL didn't parse; see
369369
// EmojiStore.emojiDisplayFor. Don't show text, just an empty square.

test/widgets/autocomplete_test.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import 'package:zulip/model/store.dart';
1616
import 'package:zulip/model/typing_status.dart';
1717
import 'package:zulip/widgets/autocomplete.dart';
1818
import 'package:zulip/widgets/compose_box.dart';
19+
import 'package:zulip/widgets/image.dart';
1920
import 'package:zulip/widgets/message_list.dart';
2021
import 'package:zulip/widgets/user.dart';
2122

@@ -212,7 +213,7 @@ void main() {
212213
matching: find.byType(UserStatusEmoji));
213214
check(statusEmojiFinder).findsOne();
214215
check(tester.widget<UserStatusEmoji>(statusEmojiFinder)
215-
.neverAnimate).isTrue();
216+
.animationMode).equals(ImageAnimationMode.animateNever);
216217
check(find.ancestor(of: statusEmojiFinder,
217218
matching: find.byType(MentionAutocompleteItem))).findsOne();
218219
}

test/widgets/message_list_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1870,7 +1870,7 @@ void main() {
18701870
matching: find.byType(UserStatusEmoji));
18711871
check(statusEmojiFinder).findsOne();
18721872
check(tester.widget<UserStatusEmoji>(statusEmojiFinder)
1873-
.neverAnimate).isTrue();
1873+
.animationMode).equals(ImageAnimationMode.animateNever);
18741874
check(find.ancestor(of: statusEmojiFinder,
18751875
matching: find.byType(SenderRow))).findsOne();
18761876
}

test/widgets/new_dm_sheet_test.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'package:zulip/widgets/app_bar.dart';
99
import 'package:zulip/widgets/compose_box.dart';
1010
import 'package:zulip/widgets/home.dart';
1111
import 'package:zulip/widgets/icons.dart';
12+
import 'package:zulip/widgets/image.dart';
1213
import 'package:zulip/widgets/new_dm_sheet.dart';
1314
import 'package:zulip/widgets/store.dart';
1415
import 'package:zulip/widgets/user.dart';
@@ -344,7 +345,7 @@ void main() {
344345
final tileStatusEmojiFinder = find.descendant(of: findUserTile(user),
345346
matching: statusEmojiFinder);
346347
check(tester.widget<UserStatusEmoji>(tileStatusEmojiFinder)
347-
.neverAnimate).isTrue();
348+
.animationMode).equals(ImageAnimationMode.animateNever);
348349
check(tileStatusEmojiFinder).findsOne();
349350
}
350351

@@ -354,7 +355,7 @@ void main() {
354355
final chipStatusEmojiFinder = find.descendant(of: findUserChip(user),
355356
matching: statusEmojiFinder);
356357
check(tester.widget<UserStatusEmoji>(chipStatusEmojiFinder)
357-
.neverAnimate).isTrue();
358+
.animationMode).equals(ImageAnimationMode.animateNever);
358359
check(chipStatusEmojiFinder).findsOne();
359360
}
360361

test/widgets/profile_test.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -469,7 +469,7 @@ void main() {
469469
matching: find.byType(UserStatusEmoji));
470470
check(statusEmojiFinder).findsOne();
471471
check(tester.widget<UserStatusEmoji>(statusEmojiFinder)
472-
.neverAnimate).isFalse();
472+
.animationMode).equals(ImageAnimationMode.animateConditionally);
473473
check(find.text('Busy')).findsOne();
474474
});
475475

@@ -495,7 +495,7 @@ void main() {
495495
check(statusButtonFinder).findsOne();
496496
check(statusEmojiFinder).findsOne();
497497
check(tester.widget<UserStatusEmoji>(statusEmojiFinder)
498-
.neverAnimate).isFalse();
498+
.animationMode).equals(ImageAnimationMode.animateConditionally);
499499
check(statusTextFinder).findsOne();
500500

501501
check(find.descendant(of: statusButtonFinder,

test/widgets/recent_dm_conversations_test.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import 'package:zulip/model/narrow.dart';
1010
import 'package:zulip/model/store.dart';
1111
import 'package:zulip/widgets/home.dart';
1212
import 'package:zulip/widgets/icons.dart';
13+
import 'package:zulip/widgets/image.dart';
1314
import 'package:zulip/widgets/message_list.dart';
1415
import 'package:zulip/widgets/new_dm_sheet.dart';
1516
import 'package:zulip/widgets/page.dart';
@@ -189,7 +190,7 @@ void main() {
189190
matching: find.byType(UserStatusEmoji));
190191
check(statusEmojiFinder).findsOne();
191192
check(tester.widget<UserStatusEmoji>(statusEmojiFinder)
192-
.neverAnimate).isTrue();
193+
.animationMode).equals(ImageAnimationMode.animateNever);
193194
check(find.ancestor(of: statusEmojiFinder,
194195
matching: find.byType(RecentDmConversationsItem))).findsOne();
195196
}

0 commit comments

Comments
 (0)