Skip to content

Commit

Permalink
emoji_reaction: Add EmojiReactionTheme, including dark variant
Browse files Browse the repository at this point in the history
Related: zulip#95
  • Loading branch information
chrisbobbe authored and gnprice committed Jul 26, 2024
1 parent 0bae1dc commit 6324bf3
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 25 deletions.
126 changes: 103 additions & 23 deletions lib/widgets/emoji_reaction.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,102 @@ import 'content.dart';
import 'store.dart';
import 'text.dart';

/// Emoji-reaction styles that differ between light and dark themes.
class EmojiReactionTheme extends ThemeExtension<EmojiReactionTheme> {
EmojiReactionTheme.light() :
this._(
bgSelected: Colors.white,

// TODO shadow effect, following web, which uses `box-shadow: inset`:
// https://developer.mozilla.org/en-US/docs/Web/CSS/box-shadow#inset
// Needs Flutter support for something like that:
// https://github.com/flutter/flutter/issues/18636
// https://github.com/flutter/flutter/issues/52999
// Until then use a solid color; a much-lightened version of the shadow color.
// Also adapt by making [borderUnselected] more transparent, so we'll
// want to check that against web when implementing the shadow.
bgUnselected: const HSLColor.fromAHSL(0.08, 210, 0.50, 0.875).toColor(),

borderSelected: Colors.black.withOpacity(0.45),

// TODO see TODO on [bgUnselected] about shadow effect
borderUnselected: Colors.black.withOpacity(0.05),

textSelected: const HSLColor.fromAHSL(1, 210, 0.20, 0.20).toColor(),
textUnselected: const HSLColor.fromAHSL(1, 210, 0.20, 0.25).toColor(),
);

EmojiReactionTheme.dark() :
this._(
bgSelected: Colors.black.withOpacity(0.8),
bgUnselected: Colors.black.withOpacity(0.3),
borderSelected: Colors.white.withOpacity(0.75),
borderUnselected: Colors.white.withOpacity(0.15),
textSelected: Colors.white.withOpacity(0.85),
textUnselected: Colors.white.withOpacity(0.75),
);

EmojiReactionTheme._({
required this.bgSelected,
required this.bgUnselected,
required this.borderSelected,
required this.borderUnselected,
required this.textSelected,
required this.textUnselected,
});

/// The [EmojiReactionTheme] from the context's active theme.
///
/// The [ThemeData] must include [EmojiReactionTheme] in [ThemeData.extensions].
static EmojiReactionTheme of(BuildContext context) {
final theme = Theme.of(context);
final extension = theme.extension<EmojiReactionTheme>();
assert(extension != null);
return extension!;
}

final Color bgSelected;
final Color bgUnselected;
final Color borderSelected;
final Color borderUnselected;
final Color textSelected;
final Color textUnselected;

@override
EmojiReactionTheme copyWith({
Color? bgSelected,
Color? bgUnselected,
Color? borderSelected,
Color? borderUnselected,
Color? textSelected,
Color? textUnselected,
}) {
return EmojiReactionTheme._(
bgSelected: bgSelected ?? this.bgSelected,
bgUnselected: bgUnselected ?? this.bgUnselected,
borderSelected: borderSelected ?? this.borderSelected,
borderUnselected: borderUnselected ?? this.borderUnselected,
textSelected: textSelected ?? this.textSelected,
textUnselected: textUnselected ?? this.textUnselected,
);
}

@override
EmojiReactionTheme lerp(EmojiReactionTheme other, double t) {
if (identical(this, other)) {
return this;
}
return EmojiReactionTheme._(
bgSelected: Color.lerp(bgSelected, other.bgSelected, t)!,
bgUnselected: Color.lerp(bgUnselected, other.bgUnselected, t)!,
borderSelected: Color.lerp(borderSelected, other.borderSelected, t)!,
borderUnselected: Color.lerp(borderUnselected, other.borderUnselected, t)!,
textSelected: Color.lerp(textSelected, other.textSelected, t)!,
textUnselected: Color.lerp(textUnselected, other.textUnselected, t)!,
);
}
}

class ReactionChipsList extends StatelessWidget {
const ReactionChipsList({
super.key,
Expand All @@ -32,24 +128,6 @@ class ReactionChipsList extends StatelessWidget {
}
}

final _textColorSelected = const HSLColor.fromAHSL(1, 210, 0.20, 0.20).toColor();
final _textColorUnselected = const HSLColor.fromAHSL(1, 210, 0.20, 0.25).toColor();

const _backgroundColorSelected = Colors.white;
// TODO shadow effect, following web, which uses `box-shadow: inset`:
// https://developer.mozilla.org/en-US/docs/Web/CSS/box-shadow#inset
// Needs Flutter support for something like that:
// https://github.com/flutter/flutter/issues/18636
// https://github.com/flutter/flutter/issues/52999
// Until then use a solid color; a much-lightened version of the shadow color.
// Also adapt by making [_borderColorUnselected] more transparent, so we'll
// want to check that against web when implementing the shadow.
final _backgroundColorUnselected = const HSLColor.fromAHSL(0.08, 210, 0.50, 0.875).toColor();

final _borderColorSelected = Colors.black.withOpacity(0.45);
// TODO see TODO on [_backgroundColorUnselected] about shadow effect
final _borderColorUnselected = Colors.black.withOpacity(0.05);

class ReactionChip extends StatelessWidget {
final bool showName;
final int messageId;
Expand Down Expand Up @@ -85,10 +163,11 @@ class ReactionChip extends StatelessWidget {
}).join(', ')
: userIds.length.toString();

final borderColor = selfVoted ? _borderColorSelected : _borderColorUnselected;
final labelColor = selfVoted ? _textColorSelected : _textColorUnselected;
final backgroundColor = selfVoted ? _backgroundColorSelected : _backgroundColorUnselected;
final splashColor = selfVoted ? _backgroundColorUnselected : _backgroundColorSelected;
final reactionTheme = EmojiReactionTheme.of(context);
final borderColor = selfVoted ? reactionTheme.borderSelected : reactionTheme.borderUnselected;
final labelColor = selfVoted ? reactionTheme.textSelected : reactionTheme.textUnselected;
final backgroundColor = selfVoted ? reactionTheme.bgSelected : reactionTheme.bgUnselected;
final splashColor = selfVoted ? reactionTheme.bgUnselected : reactionTheme.bgSelected;
final highlightColor = splashColor.withOpacity(0.5);

final borderSide = BorderSide(
Expand Down Expand Up @@ -349,14 +428,15 @@ class _TextEmoji extends StatelessWidget {

@override
Widget build(BuildContext context) {
final reactionTheme = EmojiReactionTheme.of(context);
return Text(
textAlign: TextAlign.end,
textScaler: _textEmojiScalerClamped(context),
textWidthBasis: TextWidthBasis.longestLine,
style: TextStyle(
fontSize: 14 * 0.8,
height: 1, // to be denser when we have to wrap
color: selected ? _textColorSelected : _textColorUnselected,
color: selected ? reactionTheme.textSelected : reactionTheme.textUnselected,
).merge(weightVariableTextStyle(context,
wght: selected ? 600 : null)),
// Encourage line breaks before "_" (common in these), but try not
Expand Down
7 changes: 5 additions & 2 deletions lib/widgets/theme.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:flutter/material.dart';

import '../api/model/model.dart';
import 'content.dart';
import 'emoji_reaction.dart';
import 'stream_colors.dart';
import 'text.dart';

Expand Down Expand Up @@ -37,11 +38,13 @@ ThemeData zulipThemeData(BuildContext context) {
switch (brightness) {
case Brightness.light: {
designVariables = DesignVariables.light();
themeExtensions = [ContentTheme.light(context), designVariables];
themeExtensions =
[ContentTheme.light(context), designVariables, EmojiReactionTheme.light()];
}
case Brightness.dark: {
designVariables = DesignVariables.dark();
themeExtensions = [ContentTheme.dark(context), designVariables];
themeExtensions =
[ContentTheme.dark(context), designVariables, EmojiReactionTheme.dark()];
}
}

Expand Down
5 changes: 5 additions & 0 deletions test/flutter_checks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,8 @@ extension MediaQueryDataChecks on Subject<MediaQueryData> {
Subject<TextScaler> get textScaler => has((x) => x.textScaler, 'textScaler');
// TODO more
}

extension MaterialChecks on Subject<Material> {
Subject<Color?> get color => has((x) => x.color, 'color');
// TODO more
}
42 changes: 42 additions & 0 deletions test/widgets/emoji_reaction_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'package:zulip/api/model/events.dart';
import 'package:zulip/api/model/model.dart';
import 'package:zulip/model/store.dart';
import 'package:zulip/widgets/emoji_reaction.dart';
import 'package:zulip/widgets/theme.dart';

import '../example_data.dart' as eg;
import '../flutter_checks.dart';
Expand Down Expand Up @@ -216,6 +217,47 @@ void main() {
}
});

testWidgets('Smoke test for light/dark/lerped', (tester) async {
await prepare();
await store.addUsers([eg.selfUser, eg.otherUser]);

assert(!debugFollowPlatformBrightness); // to be removed with #95
debugFollowPlatformBrightness = true;
addTearDown(() { debugFollowPlatformBrightness = false; });
tester.platformDispatcher.platformBrightnessTestValue = Brightness.light;
addTearDown(tester.platformDispatcher.clearPlatformBrightnessTestValue);

await setupChipsInBox(tester, reactions: [
Reaction.fromJson({
'user_id': eg.selfUser.userId,
'emoji_name': 'smile', 'emoji_code': '1f642', 'reaction_type': 'unicode_emoji'}),
Reaction.fromJson({
'user_id': eg.otherUser.userId,
'emoji_name': 'tada', 'emoji_code': '1f389', 'reaction_type': 'unicode_emoji'}),
]);

Color? backgroundColor(String emojiName) {
final material = tester.widget<Material>(find.descendant(
of: find.byTooltip(emojiName), matching: find.byType(Material)));
return material.color;
}

check(backgroundColor('smile')).equals(EmojiReactionTheme.light().bgSelected);
check(backgroundColor('tada')).equals(EmojiReactionTheme.light().bgUnselected);

tester.platformDispatcher.platformBrightnessTestValue = Brightness.dark;
await tester.pump();

await tester.pump(kThemeAnimationDuration * 0.4);
final expectedLerped = EmojiReactionTheme.light().lerp(EmojiReactionTheme.dark(), 0.4);
check(backgroundColor('smile')).equals(expectedLerped.bgSelected);
check(backgroundColor('tada')).equals(expectedLerped.bgUnselected);

await tester.pump(kThemeAnimationDuration * 0.6);
check(backgroundColor('smile')).equals(EmojiReactionTheme.dark().bgSelected);
check(backgroundColor('tada')).equals(EmojiReactionTheme.dark().bgUnselected);
});

// TODO more tests:
// - Tapping a chip does the right thing
// - When an image emoji fails to load, falls back to :text_emoji:
Expand Down

0 comments on commit 6324bf3

Please sign in to comment.