Skip to content

Commit 9e82157

Browse files
rajveermalviyagnprice
authored andcommitted
content: Add support for displaying unicode emojis
- Parse the zulip emoji code class names to unicode codepoints - Generate a TextSpan for each unicode emoji - Remove fallback emoji name container Fixes: zulip#58
1 parent e724963 commit 9e82157

File tree

4 files changed

+71
-29
lines changed

4 files changed

+71
-29
lines changed

lib/model/content.dart

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -452,22 +452,22 @@ abstract class EmojiNode extends InlineContentNode {
452452
}
453453

454454
class UnicodeEmojiNode extends EmojiNode {
455-
const UnicodeEmojiNode({super.debugHtmlNode, required this.text});
455+
const UnicodeEmojiNode({super.debugHtmlNode, required this.emojiUnicode});
456456

457-
final String text;
457+
final String emojiUnicode;
458458

459459
@override
460460
bool operator ==(Object other) {
461-
return other is UnicodeEmojiNode && other.text == text;
461+
return other is UnicodeEmojiNode && other.emojiUnicode == emojiUnicode;
462462
}
463463

464464
@override
465-
int get hashCode => Object.hash('UnicodeEmojiNode', text);
465+
int get hashCode => Object.hash('UnicodeEmojiNode', emojiUnicode);
466466

467467
@override
468468
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
469469
super.debugFillProperties(properties);
470-
properties.add(StringProperty('text', text));
470+
properties.add(StringProperty('emojiUnicode', emojiUnicode));
471471
}
472472
}
473473

@@ -525,6 +525,27 @@ class _ZulipContentParser {
525525

526526
static final _emojiClassRegexp = RegExp(r"^emoji(-[0-9a-f]+)*$");
527527

528+
// Ported from https://github.com/zulip/zulip-mobile/blob/c979530d6804db33310ed7d14a4ac62017432944/src/emoji/data.js#L108-L112
529+
//
530+
// Which was in turn ported from https://github.com/zulip/zulip/blob/63c9296d5339517450f79f176dc02d77b08020c8/zerver/models.py#L3235-L3242
531+
// and that describes the encoding as follows:
532+
//
533+
// > * For Unicode emoji, [emoji_code is] a dash-separated hex encoding of
534+
// > the sequence of Unicode codepoints that define this emoji in the
535+
// > Unicode specification. For examples, see "non_qualified" or
536+
// > "unified" in the following data, with "non_qualified" taking
537+
// > precedence when both present:
538+
// > https://raw.githubusercontent.com/iamcal/emoji-data/master/emoji_pretty.json
539+
String? tryParseEmojiCodeToUnicode(String code) {
540+
try {
541+
return String.fromCharCodes(code.split('-').map((hex) => int.parse(hex, radix: 16)));
542+
} on FormatException { // thrown by `int.parse`
543+
return null;
544+
} on ArgumentError { // thrown by `String.fromCharCodes`
545+
return null;
546+
}
547+
}
548+
528549
InlineContentNode parseInlineContent(dom.Node node) {
529550
assert(_debugParserContext == _ParserContext.inline);
530551
final debugHtmlNode = kDebugMode ? node : null;
@@ -582,7 +603,14 @@ class _ZulipContentParser {
582603
&& classes.length == 2
583604
&& classes.contains('emoji')
584605
&& classes.every(_emojiClassRegexp.hasMatch)) {
585-
return UnicodeEmojiNode(text: element.text, debugHtmlNode: debugHtmlNode);
606+
final emojiCode = classes
607+
.firstWhere((className) => className.startsWith('emoji-'))
608+
.replaceFirst('emoji-', '');
609+
assert(emojiCode.isNotEmpty);
610+
611+
final unicode = tryParseEmojiCodeToUnicode(emojiCode);
612+
if (unicode == null) return unimplemented();
613+
return UnicodeEmojiNode(emojiUnicode: unicode, debugHtmlNode: debugHtmlNode);
586614
}
587615

588616
if (localName == 'img'

lib/widgets/content.dart

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -460,8 +460,7 @@ class _InlineContentBuilder {
460460
return WidgetSpan(alignment: PlaceholderAlignment.middle,
461461
child: UserMention(node: node));
462462
} else if (node is UnicodeEmojiNode) {
463-
return WidgetSpan(alignment: PlaceholderAlignment.middle,
464-
child: MessageUnicodeEmoji(node: node));
463+
return TextSpan(text: node.emojiUnicode, recognizer: _recognizer);
465464
} else if (node is ImageEmojiNode) {
466465
return WidgetSpan(alignment: PlaceholderAlignment.middle,
467466
child: MessageImageEmoji(node: node));
@@ -620,23 +619,6 @@ class UserMention extends StatelessWidget {
620619
// borderRadius: BorderRadius.all(Radius.circular(3))));
621620
}
622621

623-
class MessageUnicodeEmoji extends StatelessWidget {
624-
const MessageUnicodeEmoji({super.key, required this.node});
625-
626-
final UnicodeEmojiNode node;
627-
628-
@override
629-
Widget build(BuildContext context) {
630-
// TODO(#58) get spritesheet and show actual emoji glyph
631-
final text = node.text;
632-
return Container(
633-
padding: const EdgeInsets.all(2),
634-
decoration: BoxDecoration(
635-
color: Colors.white, border: Border.all(color: Colors.purple)),
636-
child: Text(text));
637-
}
638-
}
639-
640622
class MessageImageEmoji extends StatelessWidget {
641623
const MessageImageEmoji({super.key, required this.node});
642624

test/model/content_test.dart

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,15 +141,20 @@ void main() {
141141
// TODO test group mentions and wildcard mentions
142142
});
143143

144-
testParseInline('parse Unicode emoji',
144+
testParseInline('parse Unicode emoji, encoded in span element',
145145
// ":thumbs_up:"
146146
'<p><span aria-label="thumbs up" class="emoji emoji-1f44d" role="img" title="thumbs up">:thumbs_up:</span></p>',
147-
const UnicodeEmojiNode(text: ':thumbs_up:'));
147+
const UnicodeEmojiNode(emojiUnicode: '\u{1f44d}')); // "👍"
148148

149-
testParseInline('parse Unicode emoji, multiple codepoints',
149+
testParseInline('parse Unicode emoji, encoded in span element, multiple codepoints',
150150
// ":transgender_flag:"
151151
'<p><span aria-label="transgender flag" class="emoji emoji-1f3f3-fe0f-200d-26a7-fe0f" role="img" title="transgender flag">:transgender_flag:</span></p>',
152-
const UnicodeEmojiNode(text: ':transgender_flag:'));
152+
const UnicodeEmojiNode(emojiUnicode: '\u{1f3f3}\u{fe0f}\u{200d}\u{26a7}\u{fe0f}')); // "🏳️‍⚧️"
153+
154+
testParseInline('parse Unicode emoji, not encoded in span element',
155+
// "\u{1fabf}"
156+
'<p>\u{1fabf}</p>',
157+
const TextNode('\u{1fabf}')); // "🪿"
153158

154159
testParseInline('parse custom emoji',
155160
// ":flutter:"

test/widgets/content_test.dart

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,33 @@ void main() {
155155
});
156156
});
157157

158+
group('UnicodeEmoji', () {
159+
Future<void> prepareContent(WidgetTester tester, String html) async {
160+
await tester.pumpWidget(MaterialApp(home: BlockContentList(nodes: parseContent(html).nodes)));
161+
}
162+
163+
testWidgets('encoded emoji span', (tester) async {
164+
await prepareContent(tester,
165+
// ":thumbs_up:"
166+
'<p><span aria-label="thumbs up" class="emoji emoji-1f44d" role="img" title="thumbs up">:thumbs_up:</span></p>');
167+
tester.widget(find.text('\u{1f44d}')); // "👍"
168+
});
169+
170+
testWidgets('encoded emoji span, with multiple codepoints', (tester) async {
171+
await prepareContent(tester,
172+
// ":transgender_flag:"
173+
'<p><span aria-label="transgender flag" class="emoji emoji-1f3f3-fe0f-200d-26a7-fe0f" role="img" title="transgender flag">:transgender_flag:</span></p>');
174+
tester.widget(find.text('\u{1f3f3}\u{fe0f}\u{200d}\u{26a7}\u{fe0f}')); // "🏳️‍⚧️"
175+
});
176+
177+
testWidgets('non encoded emoji', (tester) async {
178+
await prepareContent(tester,
179+
// "\u{1fabf}"
180+
'<p>\u{1fabf}</p>');
181+
tester.widget(find.text('\u{1fabf}')); // "🪿"
182+
});
183+
});
184+
158185
group('RealmContentNetworkImage', () {
159186
final authHeaders = authHeader(email: eg.selfAccount.email, apiKey: eg.selfAccount.apiKey);
160187

0 commit comments

Comments
 (0)