Skip to content

emoji_reaction: Add EmojiReactionTheme, including dark variant #805

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jul 26, 2024
3 changes: 3 additions & 0 deletions assets/l10n/app_ar.arb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{

}
14 changes: 3 additions & 11 deletions integration_test/unreadmarker_test.dart
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:zulip/api/model/events.dart';
import 'package:zulip/api/model/model.dart';
import 'package:zulip/model/narrow.dart';
import 'package:zulip/model/store.dart';
import 'package:zulip/widgets/message_list.dart';
import 'package:zulip/widgets/page.dart';
import 'package:zulip/widgets/store.dart';

import '../test/api/fake_api.dart';
import '../test/example_data.dart' as eg;
import '../test/model/binding.dart';
import '../test/model/message_list_test.dart';
import '../test/widgets/test_app.dart';

void main() {
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
Expand All @@ -34,13 +31,8 @@ void main() {
connection.prepare(json:
newestResult(foundOldest: true, messages: messages).toJson());

await tester.pumpWidget(
MaterialApp(
home: GlobalStoreWidget(
child: PerAccountStoreWidget(
accountId: eg.selfAccount.id,
placeholder: const LoadingPlaceholderPage(),
child: const MessageListPage(narrow: CombinedFeedNarrow())))));
await tester.pumpWidget(TestZulipApp(accountId: eg.selfAccount.id,
child: const MessageListPage(narrow: CombinedFeedNarrow())));
await tester.pumpAndSettle();
return messages;
}
Expand Down
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
20 changes: 20 additions & 0 deletions test/flutter_checks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,23 @@ extension TypographyChecks on Subject<Typography> {
extension InlineSpanChecks on Subject<InlineSpan> {
Subject<TextStyle?> get style => has((x) => x.style, 'style');
}

extension SizeChecks on Subject<Size> {
Subject<double> get width => has((x) => x.width, 'width');
Subject<double> get height => has((x) => x.height, 'height');
}

extension ElementChecks on Subject<Element> {
Subject<Size?> get size => has((t) => t.size, 'size');
// TODO more
}

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
}
2 changes: 2 additions & 0 deletions test/notifications/display_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,8 @@ void main() {
pushedRoutes = [];
final testNavObserver = TestNavigatorObserver()
..onPushed = (route, prevRoute) => pushedRoutes.add(route);
// This uses [ZulipApp] instead of [TestZulipApp] because notification
// logic uses `await ZulipApp.navigator`.
await tester.pumpWidget(ZulipApp(navigatorObservers: [testNavObserver]));
if (early) {
check(pushedRoutes).isEmpty();
Expand Down
15 changes: 3 additions & 12 deletions test/widgets/action_sheet_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import 'dart:convert';
import 'package:checks/checks.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:zulip/api/model/model.dart';
Expand All @@ -18,9 +17,7 @@ import 'package:zulip/widgets/compose_box.dart';
import 'package:zulip/widgets/content.dart';
import 'package:zulip/widgets/icons.dart';
import 'package:zulip/widgets/message_list.dart';
import 'package:zulip/widgets/store.dart';
import 'package:share_plus_platform_interface/method_channel/method_channel_share.dart';
import 'package:zulip/widgets/theme.dart';
import '../api/fake_api.dart';

import '../example_data.dart' as eg;
Expand All @@ -32,6 +29,7 @@ import '../test_clipboard.dart';
import '../test_share_plus.dart';
import 'compose_box_checks.dart';
import 'dialog_checks.dart';
import 'test_app.dart';

/// Simulates loading a [MessageListPage] and long-pressing on [message].
Future<void> setupToMessageActionSheet(WidgetTester tester, {
Expand Down Expand Up @@ -60,15 +58,8 @@ Future<void> setupToMessageActionSheet(WidgetTester tester, {
messages: [message],
).toJson());

await tester.pumpWidget(Builder(builder: (context) =>
MaterialApp(
theme: zulipThemeData(context),
localizationsDelegates: ZulipLocalizations.localizationsDelegates,
supportedLocales: ZulipLocalizations.supportedLocales,
home: GlobalStoreWidget(
child: PerAccountStoreWidget(
accountId: eg.selfAccount.id,
child: MessageListPage(narrow: narrow))))));
await tester.pumpWidget(TestZulipApp(accountId: eg.selfAccount.id,
child: MessageListPage(narrow: narrow)));

// global store, per-account store, and message list get loaded
await tester.pumpAndSettle();
Expand Down
17 changes: 4 additions & 13 deletions test/widgets/actions_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import 'dart:convert';

import 'package:checks/checks.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:zulip/api/model/initial_snapshot.dart';
Expand All @@ -12,15 +11,14 @@ import 'package:zulip/model/localizations.dart';
import 'package:zulip/model/narrow.dart';
import 'package:zulip/model/store.dart';
import 'package:zulip/widgets/actions.dart';
import 'package:zulip/widgets/store.dart';
import 'package:zulip/widgets/theme.dart';

import '../api/fake_api.dart';
import '../example_data.dart' as eg;
import '../model/binding.dart';
import '../model/unreads_checks.dart';
import '../stdlib_checks.dart';
import 'dialog_checks.dart';
import 'test_app.dart';

void main() {
TestZulipBinding.ensureInitialized();
Expand All @@ -39,16 +37,9 @@ void main() {
store = await testBinding.globalStore.perAccount(eg.selfAccount.id);
connection = store.connection as FakeApiConnection;

await tester.pumpWidget(Builder(builder: (context) =>
MaterialApp(
theme: zulipThemeData(context),
localizationsDelegates: ZulipLocalizations.localizationsDelegates,
supportedLocales: ZulipLocalizations.supportedLocales,
home: GlobalStoreWidget(
child: PerAccountStoreWidget(
accountId: eg.selfAccount.id,
child: const Scaffold(
body: Placeholder()))))));
await tester.pumpWidget(TestZulipApp(accountId: eg.selfAccount.id,
child: const Scaffold(body: Placeholder())));
// global store, per-account store get loaded
await tester.pumpAndSettle();
context = tester.element(find.byType(Placeholder));
}
Expand Down
11 changes: 3 additions & 8 deletions test/widgets/app_test.dart
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import 'package:checks/checks.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:zulip/model/database.dart';
import 'package:zulip/widgets/app.dart';
import 'package:zulip/widgets/inbox.dart';
import 'package:zulip/widgets/page.dart';
import 'package:zulip/widgets/store.dart';

import '../example_data.dart' as eg;
import '../flutter_checks.dart';
import '../model/binding.dart';
import '../test_navigation.dart';
import 'page_checks.dart';
import 'test_app.dart';

void main() {
TestZulipBinding.ensureInitialized();
Expand Down Expand Up @@ -67,12 +66,8 @@ void main() {
.insertAccount(account.toCompanion(false));
}

await tester.pumpWidget(
const MaterialApp(
localizationsDelegates: ZulipLocalizations.localizationsDelegates,
supportedLocales: ZulipLocalizations.supportedLocales,
home: GlobalStoreWidget(
child: ChooseAccountPage())));
await tester.pumpWidget(const TestZulipApp(
child: ChooseAccountPage()));

// global store gets loaded
await tester.pumpAndSettle();
Expand Down
Loading