Skip to content

Commit

Permalink
content: Implement GlobalTime
Browse files Browse the repository at this point in the history
Fixes: zulip#354
  • Loading branch information
sirpengi committed Jan 31, 2024
1 parent 6e29734 commit 4a424dd
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 0 deletions.
39 changes: 39 additions & 0 deletions lib/model/content.dart
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,28 @@ class MathInlineNode extends InlineContentNode {
}
}

class GlobalTimeNode extends InlineContentNode {
const GlobalTimeNode({super.debugHtmlNode, required this.datetime});

// `datetime` is always in UTC, this is enforced
// in [parseInlineContent].
final DateTime datetime;

@override
bool operator ==(Object other) {
return other is GlobalTimeNode && other.datetime == datetime;
}

@override
int get hashCode => Object.hash('GlobalTimeNode', datetime);

@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<DateTime>('datetime', datetime));
}
}

////////////////////////////////////////////////////////////////
// Ported from https://github.com/zulip/zulip-mobile/blob/c979530d6804db33310ed7d14a4ac62017432944/src/emoji/data.js#L108-L112
Expand Down Expand Up @@ -717,6 +739,23 @@ class _ZulipContentParser {
return ImageEmojiNode(src: src, alt: alt, debugHtmlNode: debugHtmlNode);
}

if (localName == 'time' && classes.isEmpty) {
final attr = element.attributes['datetime'];
if (attr == null) return unimplemented();

// This attribute is always in ISO 8601 format with a Z suffix;
// see `Timestamp` in zulip:zerver/lib/markdown/__init__.py .
final DateTime datetime;
try {
datetime = DateTime.parse(attr);
} on FormatException {
return unimplemented();
}
if (!datetime.isUtc) return unimplemented();

return GlobalTimeNode(datetime: datetime, debugHtmlNode: debugHtmlNode);
}

if (localName == 'span'
&& classes.length == 1
&& classes.contains('katex')) {
Expand Down
37 changes: 37 additions & 0 deletions lib/widgets/content.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:html/dom.dart' as dom;
import 'package:intl/intl.dart';

import '../api/core.dart';
import '../api/model/model.dart';
Expand All @@ -11,6 +12,7 @@ import '../model/internal_link.dart';
import '../model/store.dart';
import 'code_block.dart';
import 'dialog.dart';
import 'icons.dart';
import 'lightbox.dart';
import 'message_list.dart';
import 'store.dart';
Expand Down Expand Up @@ -523,6 +525,9 @@ class _InlineContentBuilder {
} else if (node is MathInlineNode) {
return TextSpan(style: _kInlineMathStyle,
children: [TextSpan(text: node.texSource)]);
} else if (node is GlobalTimeNode) {
return WidgetSpan(alignment: PlaceholderAlignment.middle,
child: GlobalTime(node: node));
} else if (node is UnimplementedInlineContentNode) {
return _errorUnimplemented(node);
} else {
Expand Down Expand Up @@ -681,6 +686,38 @@ class UserMention extends StatelessWidget {
// borderRadius: BorderRadius.all(Radius.circular(3))));
}

class GlobalTime extends StatelessWidget {
const GlobalTime({super.key, required this.node});

final GlobalTimeNode node;

static final _backgroundColor = const HSLColor.fromAHSL(1, 0, 0, 0.93).toColor();
static final _borderColor = const HSLColor.fromAHSL(1, 0, 0, 0.8).toColor();
static final _dateFormat = DateFormat('EEE, MMM d, y, h:mm a'); // TODO(intl): localize date

@override
Widget build(BuildContext context) {
// Design taken from css for `.rendered_markdown & time` in web,
// see zulip:web/styles/rendered_markdown.css .
final text = _dateFormat.format(node.datetime.toLocal());
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 2),
child: DecoratedBox(
decoration: BoxDecoration(
color: _backgroundColor,
border: Border.all(width: 1, color: _borderColor),
borderRadius: BorderRadius.circular(3)),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 0.2 * kBaseFontSize),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(ZulipIcons.clock, size: kBaseFontSize),
Text(text, style: Paragraph.getTextStyle(context)),
]))));
}
}

class MessageImageEmoji extends StatelessWidget {
const MessageImageEmoji({super.key, required this.node});

Expand Down
26 changes: 26 additions & 0 deletions test/model/content_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,32 @@ void main() {
'<span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal">λ</span></span></span></span></p>',
const MathInlineNode(texSource: r'\lambda'));

group('global times', () {
testParseInline('smoke',
// "<time:2024-01-30T17:33:00Z">"
'<p><time datetime="2024-01-30T17:33:00Z">2024-01-30T17:33:00Z</time></p>',
GlobalTimeNode(datetime: DateTime.parse('2024-01-30T17:33Z')),
);

testParseInline('handles missing attribute',
// No markdown, this is unexpected response
'<p><time>2024-01-30T17:33:00Z</time></p>',
inlineUnimplemented('<time>2024-01-30T17:33:00Z</time>'),
);

testParseInline('handles DateTime.parse failure',
// No markdown, this is unexpected response
'<p><time datetime="2024">2024-01-30T17:33:00Z</time></p>',
inlineUnimplemented('<time datetime="2024">2024-01-30T17:33:00Z</time>'),
);

testParseInline('handles unexpected timezone',
// No markdown, this is unexpected response
'<p><time datetime="2024-01-30T17:33:00">2024-01-30T17:33:00</time></p>',
inlineUnimplemented('<time datetime="2024-01-30T17:33:00">2024-01-30T17:33:00</time>'),
);
});

//
// Block content.
//
Expand Down
12 changes: 12 additions & 0 deletions test/widgets/content_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,18 @@ void main() {
tester.widget(find.text(r'\lambda'));
});

testWidgets('GlobalTime smoke', (tester) async {
// "<time:2024-01-30T17:33:00Z">"
await tester.pumpWidget(MaterialApp(home: BlockContentList(nodes: parseContent(
'<p><time datetime="2024-01-30T17:33:00Z">2024-01-30T17:33:00Z</time></p>'
).nodes)));
// The time is shown in the user's timezone and the result will depend on
// the timezone of the environment running this test. Accept here a wide
// range of times. See comments in "show dates" test in
// `test/widgets/message_list_test.dart`.
tester.widget(find.textContaining(RegExp(r'^(Tue, Jan 30|Wed, Jan 31), 2024, \d+:\d\d [AP]M$')));
});

Future<void> tapText(WidgetTester tester, Finder textFinder) async {
final height = tester.getSize(textFinder).height;
final target = tester.getTopLeft(textFinder)
Expand Down

0 comments on commit 4a424dd

Please sign in to comment.