Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions lib/model/emoji.dart
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,14 @@ final class EmojiCandidate {

/// The portion of [PerAccountStore] describing what emoji exist.
mixin EmojiStore {
/// An [EmojiDisplay] for the specified emoji.
///
/// Use [EmojiDisplay.resolve] on the result to apply the user's [Emojiset]
/// setting.
///
/// May be a [TextEmojiDisplay] even if the emojiset is not [Emojiset.text];
/// this happens when we can't understand the data that describes the emoji
/// (e.g. when an image emoji's URL doesn't parse)..
EmojiDisplay emojiDisplayFor({
required ReactionType emojiType,
required String emojiCode,
Expand Down
10 changes: 5 additions & 5 deletions lib/widgets/autocomplete.dart
Original file line number Diff line number Diff line change
Expand Up @@ -370,13 +370,13 @@ class _EmojiAutocompleteItem extends StatelessWidget {
final designVariables = DesignVariables.of(context);
final candidate = option.candidate;

// TODO deduplicate this logic with [EmojiPickerListEntry]
final emojiDisplay = candidate.emojiDisplay.resolve(store.userSettings);
final Widget? glyph = switch (emojiDisplay) {
ImageEmojiDisplay() =>
ImageEmojiWidget(size: _size, emojiDisplay: emojiDisplay),
UnicodeEmojiDisplay() =>
UnicodeEmojiWidget(size: _size, emojiDisplay: emojiDisplay),
ImageEmojiDisplay() || UnicodeEmojiDisplay() => EmojiWidget(
emojiDisplay: emojiDisplay,
squareDimension: _size,
imagePlaceholderStyle: EmojiImagePlaceholderStyle.square,
),
TextEmojiDisplay() => null, // The text is already shown separately.
};

Expand Down
99 changes: 98 additions & 1 deletion lib/widgets/emoji.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,97 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';

import '../api/model/model.dart';
import '../model/emoji.dart';
import 'content.dart';

/// A widget showing an emoji.
class EmojiWidget extends StatelessWidget {
const EmojiWidget({
super.key,
required this.emojiDisplay,
required this.squareDimension,
this.squareDimensionScaler = TextScaler.noScaling,
this.imagePlaceholderStyle = EmojiImagePlaceholderStyle.square,
this.neverAnimateImage = false,
this.buildCustomTextEmoji,
});

final EmojiDisplay emojiDisplay;

/// The base width and height to use for the emoji square.
///
/// This will be scaled by [squareDimensionScaler].
///
/// This is ignored when using the plain-text emoji style.
final double squareDimension;

/// A [TextScaler] to apply to [squareDimension].
///
/// Defaults to [TextScaler.noScaling].
///
/// This is ignored when using the plain-text emoji style.
final TextScaler squareDimensionScaler;

final EmojiImagePlaceholderStyle imagePlaceholderStyle;

/// Whether to show an animated emoji in its still (non-animated) variant
/// only, even if device settings permit animation.
///
/// Defaults to false.
final bool neverAnimateImage;

/// An optional callback to specify a custom plain-text emoji style.
///
/// If this is not passed, a simple [Text] widget with no added styling
/// is used.
final Widget Function()? buildCustomTextEmoji;

Widget _buildTextEmoji() {
return buildCustomTextEmoji?.call()
?? Text(textEmojiForEmojiName(emojiDisplay.emojiName));
}

@override
Widget build(BuildContext context) {
final emojiDisplay = this.emojiDisplay;
return switch (emojiDisplay) {
ImageEmojiDisplay() => ImageEmojiWidget(
emojiDisplay: emojiDisplay,
size: squareDimension,
textScaler: squareDimensionScaler,
errorBuilder: (_, _, _) => switch (imagePlaceholderStyle) {
EmojiImagePlaceholderStyle.square =>
SizedBox.square(dimension: squareDimensionScaler.scale(squareDimension)),
EmojiImagePlaceholderStyle.nothing => SizedBox.shrink(),
EmojiImagePlaceholderStyle.text => _buildTextEmoji(),
},
neverAnimate: neverAnimateImage),
UnicodeEmojiDisplay() => UnicodeEmojiWidget(
emojiDisplay: emojiDisplay,
size: squareDimension,
textScaler: squareDimensionScaler),
TextEmojiDisplay() => _buildTextEmoji(),
};
}
}

/// In [EmojiWidget], how to present an image emoji when we don't have the image.
enum EmojiImagePlaceholderStyle {
/// A square of [EmojiWidget.squareDimension]
/// scaled by [EmojiWidget.squareDimensionScaler].
square,

/// A [SizedBox.shrink].
nothing,

/// A plain-text emoji.
///
/// See [EmojiWidget.buildCustomTextEmoji] for how plain-text emojis are
/// styled.
text,
}

class UnicodeEmojiWidget extends StatelessWidget {
const UnicodeEmojiWidget({
super.key,
Expand Down Expand Up @@ -90,7 +178,6 @@ class UnicodeEmojiWidget extends StatelessWidget {
}
}


class ImageEmojiWidget extends StatelessWidget {
const ImageEmojiWidget({
super.key,
Expand Down Expand Up @@ -146,3 +233,13 @@ class ImageEmojiWidget extends StatelessWidget {
resolvedUrl);
}
}

/// The text to display for an emoji in the "Plain text" emoji theme.
///
/// See [Emojiset.text].
String textEmojiForEmojiName(String emojiName) {
// Encourage line breaks before "_" (common in these), but try not
// to leave a colon alone on a line. See:
// <https://github.com/flutter/flutter/issues/61081#issuecomment-1103330522>
return ':\ufeff${emojiName.replaceAll('_', '\u200b_')}\ufeff:';
}
104 changes: 28 additions & 76 deletions lib/widgets/emoji_reaction.dart
Original file line number Diff line number Diff line change
Expand Up @@ -224,14 +224,14 @@ class ReactionChip extends StatelessWidget {
emojiName: emojiName,
).resolve(store.userSettings);

final emoji = switch (emojiDisplay) {
UnicodeEmojiDisplay() => _UnicodeEmoji(
emojiDisplay: emojiDisplay),
ImageEmojiDisplay() => _ImageEmoji(
emojiDisplay: emojiDisplay, emojiName: emojiName, selected: selfVoted),
TextEmojiDisplay() => _TextEmoji(
emojiDisplay: emojiDisplay, selected: selfVoted),
};
final emoji = EmojiWidget(
emojiDisplay: emojiDisplay,
squareDimension: _squareEmojiSize,
squareDimensionScaler: _squareEmojiScalerClamped(context),
imagePlaceholderStyle: EmojiImagePlaceholderStyle.text,
buildCustomTextEmoji: () => _TextEmoji(
emojiName: emojiName, selected: selfVoted),
);

Widget result = Material(
color: backgroundColor,
Expand Down Expand Up @@ -336,59 +336,14 @@ TextScaler _textEmojiScalerClamped(BuildContext context) =>
TextScaler _labelTextScalerClamped(BuildContext context) =>
MediaQuery.textScalerOf(context).clamp(maxScaleFactor: 2);

class _UnicodeEmoji extends StatelessWidget {
const _UnicodeEmoji({required this.emojiDisplay});

final UnicodeEmojiDisplay emojiDisplay;

@override
Widget build(BuildContext context) {
return UnicodeEmojiWidget(
size: _squareEmojiSize,
textScaler: _squareEmojiScalerClamped(context),
emojiDisplay: emojiDisplay);
}
}

class _ImageEmoji extends StatelessWidget {
const _ImageEmoji({
required this.emojiDisplay,
required this.emojiName,
required this.selected,
});

final ImageEmojiDisplay emojiDisplay;
final String emojiName;
final bool selected;

@override
Widget build(BuildContext context) {
return ImageEmojiWidget(
size: _squareEmojiSize,
// Unicode and text emoji get scaled; it would look weird if image emoji didn't.
textScaler: _squareEmojiScalerClamped(context),
emojiDisplay: emojiDisplay,
errorBuilder: (context, _, _) => _TextEmoji(
emojiDisplay: TextEmojiDisplay(emojiName: emojiName), selected: selected),
);
}
}

class _TextEmoji extends StatelessWidget {
const _TextEmoji({required this.emojiDisplay, required this.selected});
const _TextEmoji({required this.emojiName, required this.selected});

final TextEmojiDisplay emojiDisplay;
final String emojiName;
final bool selected;

@override
Widget build(BuildContext context) {
final emojiName = emojiDisplay.emojiName;

// Encourage line breaks before "_" (common in these), but try not
// to leave a colon alone on a line. See:
// <https://github.com/flutter/flutter/issues/61081#issuecomment-1103330522>
final text = ':\ufeff${emojiName.replaceAll('_', '\u200b_')}\ufeff:';

final reactionTheme = EmojiReactionTheme.of(context);
return Text(
textAlign: TextAlign.end,
Expand All @@ -400,7 +355,7 @@ class _TextEmoji extends StatelessWidget {
color: selected ? reactionTheme.textSelected : reactionTheme.textUnselected,
).merge(weightVariableTextStyle(context,
wght: selected ? 600 : null)),
text);
textEmojiForEmojiName(emojiName));
}
}

Expand Down Expand Up @@ -600,13 +555,13 @@ class EmojiPickerListEntry extends StatelessWidget {
final store = PerAccountStoreWidget.of(context);
final designVariables = DesignVariables.of(context);

// TODO deduplicate this logic with [_EmojiAutocompleteItem]
final emojiDisplay = emoji.emojiDisplay.resolve(store.userSettings);
final Widget? glyph = switch (emojiDisplay) {
ImageEmojiDisplay() =>
ImageEmojiWidget(size: _emojiSize, emojiDisplay: emojiDisplay),
UnicodeEmojiDisplay() =>
UnicodeEmojiWidget(size: _emojiSize, emojiDisplay: emojiDisplay),
ImageEmojiDisplay() || UnicodeEmojiDisplay() => EmojiWidget(
emojiDisplay: emojiDisplay,
squareDimension: _emojiSize,
imagePlaceholderStyle: EmojiImagePlaceholderStyle.square,
),
TextEmojiDisplay() => null, // The text is already shown separately.
};

Expand Down Expand Up @@ -888,22 +843,19 @@ class _ViewReactionsEmojiItem extends StatelessWidget {
emojiType: reactionWithVotes.reactionType,
emojiCode: reactionWithVotes.emojiCode,
emojiName: emojiName);
// (Not calling EmojiDisplay.resolve. For expediency, rather than design a
// reasonable layout for [Emojiset.text], in this case we just override that
// setting and show the emoji anyway.)

// Don't use a :text_emoji:-style display here.
final placeholder = SizedBox.square(dimension: emojiSize);

// TODO make a helper widget for this
final emoji = switch (emojiDisplay) {
UnicodeEmojiDisplay() => UnicodeEmojiWidget(
size: emojiSize,
emojiDisplay: emojiDisplay),
ImageEmojiDisplay() => ImageEmojiWidget(
size: emojiSize,
emojiDisplay: emojiDisplay,
// If image emoji fails to load, show nothing.
errorBuilder: (_, _, _) => placeholder),
TextEmojiDisplay() => placeholder,
};
final emoji = EmojiWidget(
emojiDisplay: emojiDisplay,
squareDimension: emojiSize,
buildCustomTextEmoji: () =>
// Invoked when an image emoji's URL didn't parse; see
// EmojiStore.emojiDisplayFor. Don't show text, just an empty square.
// TODO(design) refine?; offer a visible touch target with tooltip?
SizedBox.square(dimension: emojiSize),
);

Widget result = Tooltip(
message: emojiName,
Expand Down
36 changes: 15 additions & 21 deletions lib/widgets/user.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
import '../api/model/model.dart';
import '../model/avatar_url.dart';
import '../model/binding.dart';
import '../model/emoji.dart';
import '../model/presence.dart';
import 'content.dart';
import 'emoji.dart';
Expand Down Expand Up @@ -349,33 +348,28 @@ class UserStatusEmoji extends StatelessWidget {
final store = PerAccountStoreWidget.of(context);
final effectiveEmoji = emoji ?? store.getUserStatus(userId!).emoji;

final placeholder = SizedBox.shrink();
if (effectiveEmoji == null) return placeholder;
if (effectiveEmoji == null) return SizedBox.shrink();

final emojiDisplay = store.emojiDisplayFor(
emojiType: effectiveEmoji.reactionType,
emojiCode: effectiveEmoji.emojiCode,
emojiName: effectiveEmoji.emojiName)
// Web doesn't seem to respect the emojiset user settings for user status.
// The user-status feature doesn't support a :text_emoji:-style display.
// .resolve(store.userSettings)
;
return switch (emojiDisplay) {
UnicodeEmojiDisplay() => Padding(
padding: padding,
child: UnicodeEmojiWidget(size: size, emojiDisplay: emojiDisplay)),
ImageEmojiDisplay() => Padding(
padding: padding,
child: ImageEmojiWidget(
size: size,
emojiDisplay: emojiDisplay,
neverAnimate: neverAnimate,
// If image emoji fails to load, show nothing.
errorBuilder: (_, _, _) => placeholder)),
// The user-status feature doesn't support a :text_emoji:-style display.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this line would be good as a replacement for the "Web doesn't seem to respect …" comment above. This one is stating a product/design choice we've made, which is what I think has effectively happened here.

// Also, if an image emoji's URL string doesn't parse, it'll fall back to
// a :text_emoji:-style display. We show nothing for this case.
TextEmojiDisplay() => placeholder,
};

return Padding(
padding: padding,
child: EmojiWidget(
emojiDisplay: emojiDisplay,
Comment on lines +363 to +364
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: over-indented as of the penultimate commit:

+      child: switch (emojiDisplay) {
+          UnicodeEmojiDisplay() => UnicodeEmojiWidget(size: size, emojiDisplay:
 emojiDisplay),
+          ImageEmojiDisplay() => ImageEmojiWidget(

squareDimension: size,
neverAnimateImage: neverAnimate,
buildCustomTextEmoji: () =>
// Invoked when an image emoji's URL didn't parse; see
// EmojiStore.emojiDisplayFor. Don't show text, just an empty square.
// TODO(design) refine?; offer a visible touch target with tooltip?
SizedBox.square(dimension: size),
));
}
}

Expand Down