Skip to content

Commit 7dd90d1

Browse files
committed
emoji_reaction: Add EmojiReactionTheme, including dark variant
Related: #95
1 parent 309cff5 commit 7dd90d1

File tree

4 files changed

+155
-25
lines changed

4 files changed

+155
-25
lines changed

lib/widgets/emoji_reaction.dart

Lines changed: 105 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,104 @@ import 'content.dart';
88
import 'store.dart';
99
import 'text.dart';
1010

11+
/// Emoji-reaction styles that differ between light and dark themes.
12+
class EmojiReactionTheme extends ThemeExtension<EmojiReactionTheme> {
13+
factory EmojiReactionTheme.light() {
14+
return EmojiReactionTheme._(
15+
bgSelected: Colors.white,
16+
17+
// TODO shadow effect, following web, which uses `box-shadow: inset`:
18+
// https://developer.mozilla.org/en-US/docs/Web/CSS/box-shadow#inset
19+
// Needs Flutter support for something like that:
20+
// https://github.com/flutter/flutter/issues/18636
21+
// https://github.com/flutter/flutter/issues/52999
22+
// Until then use a solid color; a much-lightened version of the shadow color.
23+
// Also adapt by making [borderUnselected] more transparent, so we'll
24+
// want to check that against web when implementing the shadow.
25+
bgUnselected: const HSLColor.fromAHSL(0.08, 210, 0.50, 0.875).toColor(),
26+
27+
borderSelected: Colors.black.withOpacity(0.45),
28+
29+
// TODO see TODO on [bgUnselected] about shadow effect
30+
borderUnselected: Colors.black.withOpacity(0.05),
31+
32+
textSelected: const HSLColor.fromAHSL(1, 210, 0.20, 0.20).toColor(),
33+
textUnselected: const HSLColor.fromAHSL(1, 210, 0.20, 0.25).toColor(),
34+
);
35+
}
36+
37+
factory EmojiReactionTheme.dark() {
38+
return EmojiReactionTheme._(
39+
bgSelected: Colors.black.withOpacity(0.8),
40+
bgUnselected: Colors.black.withOpacity(0.3),
41+
borderSelected: Colors.white.withOpacity(0.75),
42+
borderUnselected: Colors.white.withOpacity(0.15),
43+
textSelected: Colors.white.withOpacity(0.85),
44+
textUnselected: Colors.white.withOpacity(0.75),
45+
);
46+
}
47+
48+
EmojiReactionTheme._({
49+
required this.bgSelected,
50+
required this.bgUnselected,
51+
required this.borderSelected,
52+
required this.borderUnselected,
53+
required this.textSelected,
54+
required this.textUnselected,
55+
});
56+
57+
/// The [EmojiReactionTheme] from the context's active theme.
58+
///
59+
/// The [ThemeData] must include [EmojiReactionTheme] in [ThemeData.extensions].
60+
static EmojiReactionTheme of(BuildContext context) {
61+
final theme = Theme.of(context);
62+
final extension = theme.extension<EmojiReactionTheme>();
63+
assert(extension != null);
64+
return extension!;
65+
}
66+
67+
final Color bgSelected;
68+
final Color bgUnselected;
69+
final Color borderSelected;
70+
final Color borderUnselected;
71+
final Color textSelected;
72+
final Color textUnselected;
73+
74+
@override
75+
EmojiReactionTheme copyWith({
76+
Color? bgSelected,
77+
Color? bgUnselected,
78+
Color? borderSelected,
79+
Color? borderUnselected,
80+
Color? textSelected,
81+
Color? textUnselected,
82+
}) {
83+
return EmojiReactionTheme._(
84+
bgSelected: bgSelected ?? this.bgSelected,
85+
bgUnselected: bgUnselected ?? this.bgUnselected,
86+
borderSelected: borderSelected ?? this.borderSelected,
87+
borderUnselected: borderUnselected ?? this.borderUnselected,
88+
textSelected: textSelected ?? this.textSelected,
89+
textUnselected: textUnselected ?? this.textUnselected,
90+
);
91+
}
92+
93+
@override
94+
EmojiReactionTheme lerp(EmojiReactionTheme other, double t) {
95+
if (identical(this, other)) {
96+
return this;
97+
}
98+
return EmojiReactionTheme._(
99+
bgSelected: Color.lerp(bgSelected, other.bgSelected, t)!,
100+
bgUnselected: Color.lerp(bgUnselected, other.bgUnselected, t)!,
101+
borderSelected: Color.lerp(borderSelected, other.borderSelected, t)!,
102+
borderUnselected: Color.lerp(borderUnselected, other.borderUnselected, t)!,
103+
textSelected: Color.lerp(textSelected, other.textSelected, t)!,
104+
textUnselected: Color.lerp(textUnselected, other.textUnselected, t)!,
105+
);
106+
}
107+
}
108+
11109
class ReactionChipsList extends StatelessWidget {
12110
const ReactionChipsList({
13111
super.key,
@@ -32,24 +130,6 @@ class ReactionChipsList extends StatelessWidget {
32130
}
33131
}
34132

35-
final _textColorSelected = const HSLColor.fromAHSL(1, 210, 0.20, 0.20).toColor();
36-
final _textColorUnselected = const HSLColor.fromAHSL(1, 210, 0.20, 0.25).toColor();
37-
38-
const _backgroundColorSelected = Colors.white;
39-
// TODO shadow effect, following web, which uses `box-shadow: inset`:
40-
// https://developer.mozilla.org/en-US/docs/Web/CSS/box-shadow#inset
41-
// Needs Flutter support for something like that:
42-
// https://github.com/flutter/flutter/issues/18636
43-
// https://github.com/flutter/flutter/issues/52999
44-
// Until then use a solid color; a much-lightened version of the shadow color.
45-
// Also adapt by making [_borderColorUnselected] more transparent, so we'll
46-
// want to check that against web when implementing the shadow.
47-
final _backgroundColorUnselected = const HSLColor.fromAHSL(0.08, 210, 0.50, 0.875).toColor();
48-
49-
final _borderColorSelected = Colors.black.withOpacity(0.45);
50-
// TODO see TODO on [_backgroundColorUnselected] about shadow effect
51-
final _borderColorUnselected = Colors.black.withOpacity(0.05);
52-
53133
class ReactionChip extends StatelessWidget {
54134
final bool showName;
55135
final int messageId;
@@ -85,10 +165,11 @@ class ReactionChip extends StatelessWidget {
85165
}).join(', ')
86166
: userIds.length.toString();
87167

88-
final borderColor = selfVoted ? _borderColorSelected : _borderColorUnselected;
89-
final labelColor = selfVoted ? _textColorSelected : _textColorUnselected;
90-
final backgroundColor = selfVoted ? _backgroundColorSelected : _backgroundColorUnselected;
91-
final splashColor = selfVoted ? _backgroundColorUnselected : _backgroundColorSelected;
168+
final reactionTheme = EmojiReactionTheme.of(context);
169+
final borderColor = selfVoted ? reactionTheme.borderSelected : reactionTheme.borderUnselected;
170+
final labelColor = selfVoted ? reactionTheme.textSelected : reactionTheme.textUnselected;
171+
final backgroundColor = selfVoted ? reactionTheme.bgSelected : reactionTheme.bgUnselected;
172+
final splashColor = selfVoted ? reactionTheme.bgUnselected : reactionTheme.bgSelected;
92173
final highlightColor = splashColor.withOpacity(0.5);
93174

94175
final borderSide = BorderSide(
@@ -349,14 +430,15 @@ class _TextEmoji extends StatelessWidget {
349430

350431
@override
351432
Widget build(BuildContext context) {
433+
final reactionTheme = EmojiReactionTheme.of(context);
352434
return Text(
353435
textAlign: TextAlign.end,
354436
textScaler: _textEmojiScalerClamped(context),
355437
textWidthBasis: TextWidthBasis.longestLine,
356438
style: TextStyle(
357439
fontSize: 14 * 0.8,
358440
height: 1, // to be denser when we have to wrap
359-
color: selected ? _textColorSelected : _textColorUnselected,
441+
color: selected ? reactionTheme.textSelected : reactionTheme.textUnselected,
360442
).merge(weightVariableTextStyle(context,
361443
wght: selected ? 600 : null)),
362444
// Encourage line breaks before "_" (common in these), but try not

lib/widgets/theme.dart

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
22

33
import '../api/model/model.dart';
44
import 'content.dart';
5+
import 'emoji_reaction.dart';
56
import 'stream_colors.dart';
67
import 'text.dart';
78

@@ -37,11 +38,13 @@ ThemeData zulipThemeData(BuildContext context) {
3738
switch (brightness) {
3839
case Brightness.light: {
3940
designVariables = DesignVariables.light();
40-
themeExtensions = [ContentTheme.light(context), designVariables];
41+
themeExtensions =
42+
[ContentTheme.light(context), designVariables, EmojiReactionTheme.light()];
4143
}
4244
case Brightness.dark: {
4345
designVariables = DesignVariables.dark();
44-
themeExtensions = [ContentTheme.dark(context), designVariables];
46+
themeExtensions =
47+
[ContentTheme.dark(context), designVariables, EmojiReactionTheme.dark()];
4548
}
4649
}
4750

test/flutter_checks.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,8 @@ extension MediaQueryDataChecks on Subject<MediaQueryData> {
126126
Subject<TextScaler> get textScaler => has((x) => x.textScaler, 'textScaler');
127127
// TODO more
128128
}
129+
130+
extension MaterialChecks on Subject<Material> {
131+
Subject<Color?> get color => has((x) => x.color, 'color');
132+
// TODO more
133+
}

test/widgets/emoji_reaction_test.dart

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import 'package:zulip/api/model/events.dart';
1111
import 'package:zulip/api/model/model.dart';
1212
import 'package:zulip/model/store.dart';
1313
import 'package:zulip/widgets/emoji_reaction.dart';
14+
import 'package:zulip/widgets/theme.dart';
1415

1516
import '../example_data.dart' as eg;
1617
import '../flutter_checks.dart';
@@ -214,6 +215,45 @@ void main() {
214215
}
215216
}
216217
}
218+
219+
testWidgets('Smoke test for light/dark/lerped', (tester) async {
220+
await prepare();
221+
await store.addUsers([eg.selfUser, eg.otherUser]);
222+
223+
debugFollowPlatformBrightness = true;
224+
tester.platformDispatcher.platformBrightnessTestValue = Brightness.light;
225+
addTearDown(tester.platformDispatcher.clearPlatformBrightnessTestValue);
226+
227+
await setupChipsInBox(tester, reactions: [
228+
Reaction.fromJson({
229+
'user_id': eg.selfUser.userId,
230+
'emoji_name': 'smile', 'emoji_code': '1f642', 'reaction_type': 'unicode_emoji'}),
231+
Reaction.fromJson({
232+
'user_id': eg.otherUser.userId,
233+
'emoji_name': 'tada', 'emoji_code': '1f389', 'reaction_type': 'unicode_emoji'}),
234+
]);
235+
236+
Color? backgroundColor(String emojiName) {
237+
final material = tester.widget<Material>(find.descendant(
238+
of: find.byTooltip(emojiName), matching: find.byType(Material)));
239+
return material.color;
240+
}
241+
242+
check(backgroundColor('smile')).equals(EmojiReactionTheme.light().bgSelected);
243+
check(backgroundColor('tada')).equals(EmojiReactionTheme.light().bgUnselected);
244+
245+
tester.platformDispatcher.platformBrightnessTestValue = Brightness.dark;
246+
await tester.pump();
247+
248+
await tester.pump(kThemeAnimationDuration * 0.4);
249+
final expectedLerped = EmojiReactionTheme.light().lerp(EmojiReactionTheme.dark(), 0.4);
250+
check(backgroundColor('smile')).equals(expectedLerped.bgSelected);
251+
check(backgroundColor('tada')).equals(expectedLerped.bgUnselected);
252+
253+
await tester.pump(kThemeAnimationDuration * 0.6);
254+
check(backgroundColor('smile')).equals(EmojiReactionTheme.dark().bgSelected);
255+
check(backgroundColor('tada')).equals(EmojiReactionTheme.dark().bgUnselected);
256+
});
217257
});
218258

219259
// TODO more tests:

0 commit comments

Comments
 (0)