Skip to content

Commit ef29c09

Browse files
committed
settings [nfc]: Centralize logic for checking device animation settings
1 parent 156d314 commit ef29c09

File tree

3 files changed

+70
-26
lines changed

3 files changed

+70
-26
lines changed

lib/model/settings.dart

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

34
import '../generated/l10n/zulip_localizations.dart';
@@ -473,3 +474,53 @@ class GlobalSettingsStore extends ChangeNotifier {
473474
notifyListeners();
474475
}
475476
}
477+
478+
/// Whether to show an animated image in its still or animated version.
479+
///
480+
/// Use [resolve] to evaluate this for the given [BuildContext],
481+
/// which reads device-setting data for [animateConditionally].
482+
///
483+
/// Callers should try to check whether an image is animated,
484+
/// i.e. whether it has separate still and animated versions.
485+
/// This can't be done perfectly (in 2025-10)
486+
/// because of animated emoji that were uploaded before Zulip Server 5
487+
/// and don't have `still_url` filled in:
488+
/// https://github.com/zulip/zulip/issues/36339 .
489+
// TODO(server-future) Remove comment once all supported servers have a fix for
490+
// zulip/zulip#36339, i.e. that have run a migration to fill in still_url.
491+
enum ImageAnimationMode {
492+
/// Always show the animated version, ignoring device settings.
493+
animateAlways,
494+
495+
/// Always show the still version, ignoring device settings.
496+
animateNever,
497+
498+
/// Show the animated version
499+
/// just if animations aren't disabled in device settings.
500+
animateConditionally,
501+
;
502+
503+
/// True if the image should be animated, false if it should be still.
504+
bool resolve(BuildContext context) {
505+
switch (this) {
506+
case animateAlways: return true;
507+
case animateNever: return false;
508+
case animateConditionally:
509+
// From reading code, this doesn't actually get set on iOS:
510+
// https://github.com/zulip/zulip-flutter/pull/410#discussion_r1408522293
511+
if (MediaQuery.disableAnimationsOf(context)) return false;
512+
513+
if (
514+
defaultTargetPlatform == TargetPlatform.iOS
515+
// TODO(#1924) On iOS 17+ (new in 2023), there's a more closely
516+
// relevant setting than "reduce motion". It's called "auto-play
517+
// animated images"; we should use that once Flutter exposes it.
518+
&& WidgetsBinding.instance.platformDispatcher.accessibilityFeatures.reduceMotion
519+
) {
520+
return false;
521+
}
522+
523+
return true;
524+
}
525+
}
526+
}

lib/widgets/emoji.dart

Lines changed: 17 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'package:flutter/widgets.dart';
33

44
import '../api/model/model.dart';
55
import '../model/emoji.dart';
6+
import '../model/settings.dart';
67
import 'content.dart';
78

89
/// A widget showing an emoji.
@@ -13,7 +14,7 @@ class EmojiWidget extends StatelessWidget {
1314
required this.squareDimension,
1415
this.squareDimensionScaler = TextScaler.noScaling,
1516
this.imagePlaceholderStyle = EmojiImagePlaceholderStyle.square,
16-
this.neverAnimateImage = false,
17+
this.imageAnimationMode = ImageAnimationMode.animateConditionally,
1718
this.buildCustomTextEmoji,
1819
});
1920

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

3637
final EmojiImagePlaceholderStyle imagePlaceholderStyle;
3738

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

4446
/// An optional callback to specify a custom plain-text emoji style.
4547
///
@@ -71,7 +73,7 @@ class EmojiWidget extends StatelessWidget {
7173
size: squareDimension,
7274
textScaler: squareDimensionScaler,
7375
errorBuilder: _getImageErrorBuilder(effectiveSquareDimension),
74-
neverAnimate: neverAnimateImage),
76+
animationMode: imageAnimationMode),
7577
UnicodeEmojiDisplay() => UnicodeEmojiWidget(
7678
size: squareDimension,
7779
emojiDisplay: emojiDisplay),
@@ -189,7 +191,7 @@ class ImageEmojiWidget extends StatelessWidget {
189191
required this.size,
190192
this.textScaler = TextScaler.noScaling,
191193
this.errorBuilder,
192-
this.neverAnimate = false,
194+
this.animationMode = ImageAnimationMode.animateConditionally,
193195
});
194196

195197
final ImageEmojiDisplay emojiDisplay;
@@ -206,30 +208,20 @@ class ImageEmojiWidget extends StatelessWidget {
206208

207209
final ImageErrorWidgetBuilder? errorBuilder;
208210

209-
/// Whether to show an animated emoji in its still (non-animated) variant
210-
/// only, even if device settings permit animation.
211+
/// Whether to show an animated emoji in its still or animated version.
212+
///
213+
/// Ignored for non-animated emoji.
211214
///
212-
/// Defaults to false.
213-
final bool neverAnimate;
215+
/// Defaults to [ImageAnimationMode.animateConditionally].
216+
final ImageAnimationMode animationMode;
214217

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

230-
final resolvedUrl = doNotAnimate
231-
? (emojiDisplay.resolvedStillUrl ?? emojiDisplay.resolvedUrl)
232-
: emojiDisplay.resolvedUrl;
222+
final resolvedUrl = animationMode.resolve(context)
223+
? emojiDisplay.resolvedUrl
224+
: (emojiDisplay.resolvedStillUrl ?? emojiDisplay.resolvedUrl);
233225

234226
return RealmContentNetworkImage(
235227
width: size, height: size,

lib/widgets/user.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import '../api/model/model.dart';
44
import '../model/avatar_url.dart';
55
import '../model/binding.dart';
66
import '../model/presence.dart';
7+
import '../model/settings.dart';
78
import 'content.dart';
89
import 'emoji.dart';
910
import 'icons.dart';
@@ -363,7 +364,7 @@ class UserStatusEmoji extends StatelessWidget {
363364
child: EmojiWidget(
364365
emojiDisplay: emojiDisplay,
365366
squareDimension: size,
366-
neverAnimateImage: neverAnimate,
367+
imageAnimationMode: ImageAnimationMode.animateNever,
367368
buildCustomTextEmoji: () =>
368369
// Invoked when an image emoji's URL didn't parse; see
369370
// EmojiStore.emojiDisplayFor. Don't show text, just an empty square.

0 commit comments

Comments
 (0)