From be11d579c863244790e0bb35fd4a1cdcec50abaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Ignacio=20Torres?= Date: Sun, 5 Jan 2025 19:16:40 -0800 Subject: [PATCH] feat: show status publish time on feed --- lib/data/status.dart | 13 +++++++++++++ lib/widgets/status_card.dart | 4 ++++ pubspec.lock | 21 +++++++++++++++++++++ pubspec.yaml | 1 + test/data_test/status_test.dart | 5 +++++ test/services_test/api_test.dart | 12 ++++++------ test/widgets_test/status_card_test.dart | 1 + 7 files changed, 51 insertions(+), 6 deletions(-) diff --git a/lib/data/status.dart b/lib/data/status.dart index 44512a3..bc038d0 100644 --- a/lib/data/status.dart +++ b/lib/data/status.dart @@ -1,4 +1,7 @@ +import 'dart:ui'; + import 'package:feathr/data/account.dart'; +import 'package:relative_time/relative_time.dart'; /// The [Status] class represents information for a given status (toot) /// made in a Mastodon instance. @@ -9,6 +12,9 @@ class Status { /// ID of the status in the Mastodon instance it belongs to final String id; + /// Datetime when the status was created + final DateTime createdAt; + /// Main content of the status, in HTML format final String content; @@ -29,6 +35,7 @@ class Status { Status({ required this.id, + required this.createdAt, required this.content, required this.account, required this.favorited, @@ -42,6 +49,7 @@ class Status { factory Status.fromJson(Map data) { return Status( id: data["id"]!, + createdAt: DateTime.parse(data["created_at"]!), content: data["content"]!, account: Account.fromJson(data["account"]!), favorited: data["favourited"]!, @@ -58,4 +66,9 @@ class Status { } return content; } + + String getRelativeDate() { + // TODO: set up localization for the app + return createdAt.relativeTimeLocale(const Locale('en')); + } } diff --git a/lib/widgets/status_card.dart b/lib/widgets/status_card.dart index ee8b2c8..4356aae 100644 --- a/lib/widgets/status_card.dart +++ b/lib/widgets/status_card.dart @@ -138,6 +138,10 @@ class _StatusCardState extends State { status.account.acct, style: TextStyle(color: Colors.white.withValues(alpha: 0.6)), ), + trailing: Text( + status.getRelativeDate(), + style: TextStyle(color: Colors.white.withValues(alpha: 0.6)), + ), ), Padding( padding: const EdgeInsets.all(8.0), diff --git a/pubspec.lock b/pubspec.lock index 7c764a8..ffadf30 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -275,6 +275,11 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_localizations: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" flutter_secure_storage: dependency: "direct main" description: @@ -429,6 +434,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" + intl: + dependency: transitive + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" io: dependency: transitive description: @@ -701,6 +714,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" + relative_time: + dependency: "direct main" + description: + name: relative_time + sha256: "4e6c3b27d98ff6af5061b6dbaca178b5aa2607b7a7c2a77e6cae1d32b2759893" + url: "https://pub.dev" + source: hosted + version: "5.0.0" shelf: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 9d642b2..061ce96 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -55,6 +55,7 @@ dependencies: # Form validations validators: ^3.0.0 + relative_time: ^5.0.0 dev_dependencies: flutter_test: diff --git a/test/data_test/status_test.dart b/test/data_test/status_test.dart index d470ff7..ef8052f 100644 --- a/test/data_test/status_test.dart +++ b/test/data_test/status_test.dart @@ -6,6 +6,7 @@ void main() { const Map testStatusNoReblog = { "id": "11223344", "content": "

I am a toot!

", + "created_at": "2025-01-01T00:00:00Z", "favourited": true, "bookmarked": false, "reblogged": true, @@ -25,6 +26,7 @@ void main() { const Map testStatusWithReblog = { "id": "11223344", "content": "

I am a toot!

", + "created_at": "2025-01-01T00:00:00Z", "favourited": true, "bookmarked": false, "reblogged": true, @@ -41,6 +43,7 @@ void main() { "reblog": { "id": "55667788", "content": "

I am an internal toot!

", + "created_at": "2025-01-01T00:00:00Z", "favourited": false, "bookmarked": true, "reblogged": false, @@ -64,6 +67,7 @@ void main() { expect(status.id, equals("11223344")); expect(status.content, equals("

I am a toot!

")); + expect(status.createdAt, equals(DateTime.parse("2025-01-01T00:00:00Z"))); expect(status.favorited, isTrue); expect(status.bookmarked, isFalse); expect(status.reblogged, isTrue); @@ -84,6 +88,7 @@ void main() { final status = Status.fromJson(testStatusWithReblog); expect(status.id, equals("11223344")); expect(status.content, equals("

I am a toot!

")); + expect(status.createdAt, equals(DateTime.parse("2025-01-01T00:00:00Z"))); expect(status.favorited, isTrue); expect(status.bookmarked, isFalse); expect(status.reblogged, isTrue); diff --git a/test/services_test/api_test.dart b/test/services_test/api_test.dart index fce8323..72ebbc8 100644 --- a/test/services_test/api_test.dart +++ b/test/services_test/api_test.dart @@ -114,7 +114,7 @@ void main() { httpClient: mockClient)) .thenAnswer( (_) async => http.Response( - '{"id":"$testStatusId","content":"

I am a toot!

","favourited":true,"bookmarked":false,"reblogged":true,"account":{"id":"this is an id","username":"username123","acct":"username123","display_name":"user display name","locked":false,"bot":true,"avatar":"avatar-url","header":"header-url"}}', + '{"id":"$testStatusId","created_at": "2025-01-01T00:00:00Z","content":"

I am a toot!

","favourited":true,"bookmarked":false,"reblogged":true,"account":{"id":"this is an id","username":"username123","acct":"username123","display_name":"user display name","locked":false,"bot":true,"avatar":"avatar-url","header":"header-url"}}', 200, ), ); @@ -181,7 +181,7 @@ void main() { httpClient: mockClient)) .thenAnswer( (_) async => http.Response( - '{"id":"$testStatusId","content":"

I am a toot!

","favourited":false,"bookmarked":false,"reblogged":true,"account":{"id":"this is an id","username":"username123","acct":"username123","display_name":"user display name","locked":false,"bot":true,"avatar":"avatar-url","header":"header-url"}}', + '{"id":"$testStatusId","created_at": "2025-01-01T00:00:00Z","content":"

I am a toot!

","favourited":false,"bookmarked":false,"reblogged":true,"account":{"id":"this is an id","username":"username123","acct":"username123","display_name":"user display name","locked":false,"bot":true,"avatar":"avatar-url","header":"header-url"}}', 200, ), ); @@ -248,7 +248,7 @@ void main() { httpClient: mockClient)) .thenAnswer( (_) async => http.Response( - '{"id":"$testStatusId","content":"

I am a toot!

","favourited":true,"bookmarked":true,"reblogged":true,"account":{"id":"this is an id","username":"username123","acct":"username123","display_name":"user display name","locked":false,"bot":true,"avatar":"avatar-url","header":"header-url"}}', + '{"id":"$testStatusId","created_at": "2025-01-01T00:00:00Z","content":"

I am a toot!

","favourited":true,"bookmarked":true,"reblogged":true,"account":{"id":"this is an id","username":"username123","acct":"username123","display_name":"user display name","locked":false,"bot":true,"avatar":"avatar-url","header":"header-url"}}', 200, ), ); @@ -315,7 +315,7 @@ void main() { httpClient: mockClient)) .thenAnswer( (_) async => http.Response( - '{"id":"$testStatusId","content":"

I am a toot!

","favourited":false,"bookmarked":false,"reblogged":true,"account":{"id":"this is an id","username":"username123","acct":"username123","display_name":"user display name","locked":false,"bot":true,"avatar":"avatar-url","header":"header-url"}}', + '{"id":"$testStatusId","created_at": "2025-01-01T00:00:00Z","content":"

I am a toot!

","favourited":false,"bookmarked":false,"reblogged":true,"account":{"id":"this is an id","username":"username123","acct":"username123","display_name":"user display name","locked":false,"bot":true,"avatar":"avatar-url","header":"header-url"}}', 200, ), ); @@ -379,7 +379,7 @@ void main() { httpClient: mockClient)) .thenAnswer( (_) async => http.Response( - '{"reblog":{"id":"$testStatusId","content":"

I am a toot!

","favourited":true,"bookmarked":true,"reblogged":true,"account":{"id":"this is an id","username":"username123","acct":"username123","display_name":"user display name","locked":false,"bot":true,"avatar":"avatar-url","header":"header-url"}}}', + '{"reblog":{"id":"$testStatusId","created_at": "2025-01-01T00:00:00Z","content":"

I am a toot!

","favourited":true,"bookmarked":true,"reblogged":true,"account":{"id":"this is an id","username":"username123","acct":"username123","display_name":"user display name","locked":false,"bot":true,"avatar":"avatar-url","header":"header-url"}}}', 200, ), ); @@ -446,7 +446,7 @@ void main() { httpClient: mockClient)) .thenAnswer( (_) async => http.Response( - '{"reblog":{"id":"$testStatusId","content":"

I am a toot!

","favourited":true,"bookmarked":true,"reblogged":true,"account":{"id":"this is an id","username":"username123","acct":"username123","display_name":"user display name","locked":false,"bot":true,"avatar":"avatar-url","header":"header-url"}}}', + '{"reblog":{"id":"$testStatusId","created_at": "2025-01-01T00:00:00Z","content":"

I am a toot!

","favourited":true,"bookmarked":true,"reblogged":true,"account":{"id":"this is an id","username":"username123","acct":"username123","display_name":"user display name","locked":false,"bot":true,"avatar":"avatar-url","header":"header-url"}}}', 200, ), ); diff --git a/test/widgets_test/status_card_test.dart b/test/widgets_test/status_card_test.dart index 00f1b15..e51371f 100644 --- a/test/widgets_test/status_card_test.dart +++ b/test/widgets_test/status_card_test.dart @@ -15,6 +15,7 @@ void main() { ApiService apiService = getTestApiService(); Status status = Status( id: "12345678", + createdAt: DateTime(2025, 1, 5, 14, 30, 0), content: "

This is a toot!

", account: apiService.currentAccount!, favorited: true,