From 1ec9939f9b59e6c488122af305cb822d640c96e1 Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Tue, 15 Mar 2022 20:42:30 -0400 Subject: [PATCH] Add reaction and comment processing for Diaspora data. --- .../components/media_timeline_component.dart | 2 +- .../lib/src/components/tree_entry_card.dart | 10 +-- .../diaspora/models/diaspora_reaction.dart | 55 +++++++++++++ .../diaspora/models/diaspora_relayables.dart | 13 ++++ .../diaspora_contact_serializer.dart | 6 ++ .../diaspora_posts_serializer.dart | 41 +++++----- .../diaspora_relayables_serializer.dart | 78 +++++++++++++++++++ .../services/diaspora_archive_service.dart | 78 ++++++++++++++++++- .../diaspora_profile_json_reader.dart | 25 ++++++ .../friendica_timeline_entry_serializer.dart | 2 +- .../lib/src/models/timeline_entry.dart | 51 +++++++----- .../lib/src/screens/stats_screen.dart | 2 +- .../lib/src/services/connections_manager.dart | 9 +++ 13 files changed, 322 insertions(+), 50 deletions(-) create mode 100644 friendica_archive_browser/lib/src/diaspora/models/diaspora_reaction.dart create mode 100644 friendica_archive_browser/lib/src/diaspora/models/diaspora_relayables.dart create mode 100644 friendica_archive_browser/lib/src/diaspora/serializers/diaspora_relayables_serializer.dart diff --git a/friendica_archive_browser/lib/src/components/media_timeline_component.dart b/friendica_archive_browser/lib/src/components/media_timeline_component.dart index 66c188b..51096b5 100644 --- a/friendica_archive_browser/lib/src/components/media_timeline_component.dart +++ b/friendica_archive_browser/lib/src/components/media_timeline_component.dart @@ -1,8 +1,8 @@ import 'dart:math'; import 'package:flutter/material.dart'; -import 'package:friendica_archive_browser/src/screens/media_slideshow_screen.dart'; import 'package:friendica_archive_browser/src/models/media_attachment.dart'; +import 'package:friendica_archive_browser/src/screens/media_slideshow_screen.dart'; import 'package:friendica_archive_browser/src/services/archive_service_provider.dart'; import 'package:friendica_archive_browser/src/settings/settings_controller.dart'; import 'package:provider/provider.dart'; diff --git a/friendica_archive_browser/lib/src/components/tree_entry_card.dart b/friendica_archive_browser/lib/src/components/tree_entry_card.dart index 3059bb0..6e50c47 100644 --- a/friendica_archive_browser/lib/src/components/tree_entry_card.dart +++ b/friendica_archive_browser/lib/src/components/tree_entry_card.dart @@ -40,7 +40,7 @@ class TreeEntryCard extends StatelessWidget { ? entry.title : entry.parentId.isEmpty ? (entry.isReshare ? 'Reshare' : 'Post') - : 'Comment on post by ${entry.parentAuthor}'; + : 'Comment on post by ${entry.author}'; final dateStamp = ' At ' + formatter.format( DateTime.fromMillisecondsSinceEpoch(entry.creationTimestamp * 1000) @@ -81,7 +81,8 @@ class TreeEntryCard extends StatelessWidget { icon: const Icon(Icons.copy)), ), Tooltip( - message: 'Open link to original item', + message: + 'Open link to original item (${entry.externalLink})', child: IconButton( onPressed: () async { await canLaunch(entry.externalLink) @@ -130,10 +131,9 @@ class TreeEntryCard extends StatelessWidget { ), if (entry.locationData.hasData()) entry.locationData.toWidget(spacingHeight), - if (treeEntry.entry.externalLink.isNotEmpty) ...[ + if (treeEntry.entry.links.isNotEmpty) ...[ const SizedBox(height: spacingHeight), - LinkElementsComponent( - links: [Uri.parse(treeEntry.entry.externalLink)]) + LinkElementsComponent(links: treeEntry.entry.links) ], if (entry.mediaAttachments.isNotEmpty) ...[ const SizedBox(height: spacingHeight), diff --git a/friendica_archive_browser/lib/src/diaspora/models/diaspora_reaction.dart b/friendica_archive_browser/lib/src/diaspora/models/diaspora_reaction.dart new file mode 100644 index 0000000..15029e2 --- /dev/null +++ b/friendica_archive_browser/lib/src/diaspora/models/diaspora_reaction.dart @@ -0,0 +1,55 @@ +class DiasporaReaction { + final String authorString; + final String guid; + final String parentGuid; + final ParentType parentType; + final ReactionType reactionType; + + DiasporaReaction( + {required this.authorString, + this.guid = '', + required this.parentGuid, + this.parentType = ParentType.post, + required this.reactionType}); + + static DiasporaReaction fromJson(Map json) => + DiasporaReaction( + authorString: json['author'] ?? '', + guid: json['guid'] ?? '', + parentGuid: json['parent_guid'] ?? '', + parentType: _parentTypeFromString(json['parent_type'] ?? ''), + reactionType: _reactionTypeFromBool(json['positive'] ?? true)); +} + +enum ParentType { + unknown, + comment, + post, +} + +ParentType _parentTypeFromString(String string) { + final stringLower = string.toLowerCase(); + if (stringLower == 'post') { + return ParentType.post; + } + + if (stringLower == 'comment') { + return ParentType.comment; + } + + return ParentType.unknown; +} + +enum ReactionType { + unknown, + dislike, + like, +} + +ReactionType _reactionTypeFromBool(bool value) { + if (value) { + return ReactionType.like; + } + + return ReactionType.dislike; +} diff --git a/friendica_archive_browser/lib/src/diaspora/models/diaspora_relayables.dart b/friendica_archive_browser/lib/src/diaspora/models/diaspora_relayables.dart new file mode 100644 index 0000000..abf1319 --- /dev/null +++ b/friendica_archive_browser/lib/src/diaspora/models/diaspora_relayables.dart @@ -0,0 +1,13 @@ +import 'package:friendica_archive_browser/src/diaspora/models/diaspora_reaction.dart'; +import 'package:friendica_archive_browser/src/models/timeline_entry.dart'; + +class DiasporaRelayable { + final DiasporaReaction? reaction; + final TimelineEntry? comment; + + bool get isReaction => reaction != null; + + bool get isComment => comment != null; + + DiasporaRelayable({this.reaction, this.comment}); +} diff --git a/friendica_archive_browser/lib/src/diaspora/serializers/diaspora_contact_serializer.dart b/friendica_archive_browser/lib/src/diaspora/serializers/diaspora_contact_serializer.dart index 6f54bd2..f602163 100644 --- a/friendica_archive_browser/lib/src/diaspora/serializers/diaspora_contact_serializer.dart +++ b/friendica_archive_browser/lib/src/diaspora/serializers/diaspora_contact_serializer.dart @@ -1,4 +1,5 @@ import 'package:friendica_archive_browser/src/models/connection.dart'; +import 'package:uuid/uuid.dart'; Connection contactFromDiasporaJson(Map json) { const network = "Diaspora"; @@ -25,6 +26,11 @@ Connection contactFromDiasporaJson(Map json) { network: network); } +Connection contactFromDiasporaId(String accountId) => Connection( + id: 'generated_${const Uuid().v4}', + status: ConnectionStatus.none, + profileUrl: _profileUrlFromAccountId(accountId)); + Uri _profileUrlFromAccountId(String accountId) { if (accountId.isEmpty) { return Uri(); diff --git a/friendica_archive_browser/lib/src/diaspora/serializers/diaspora_posts_serializer.dart b/friendica_archive_browser/lib/src/diaspora/serializers/diaspora_posts_serializer.dart index 94af926..93d8820 100644 --- a/friendica_archive_browser/lib/src/diaspora/serializers/diaspora_posts_serializer.dart +++ b/friendica_archive_browser/lib/src/diaspora/serializers/diaspora_posts_serializer.dart @@ -46,7 +46,9 @@ Result _buildReshareMessageType( final String parentGuid = entityData['root_guid'] ?? ''; final String parentName = entityData['root_author'] ?? ''; final deletedPost = parentGuid.isEmpty || parentName.isEmpty; - final externalLink = deletedPost ? '' : _buildReshareUrl(authorName, parentName, parentGuid); + final reshareLink = deletedPost + ? '' + : _buildPostOrReshareUrl(authorName, parentName, parentGuid); final text = deletedPost ? 'Original post deleted by author' : ''; final author = connections.getByName(authorName).getValueOrElse(() => Connection()); @@ -54,16 +56,16 @@ Result _buildReshareMessageType( .getByName(parentName) .getValueOrElse(() => Connection(name: parentName)); final timelineEntry = TimelineEntry( - id: postId, - creationTimestamp: epochTime, - author: author.name, - authorId: author.id, - isReshare: true, - parentAuthor: parentAuthor.name, - parentAuthorId: parentAuthor.id, - externalLink: externalLink, - body: text, - ); + id: postId, + creationTimestamp: epochTime, + author: author.name, + authorId: author.id, + externalLink: _buildPostOrReshareUrl(authorName, '', postId), + isReshare: true, + parentAuthor: parentAuthor.name, + parentAuthorId: parentAuthor.id, + body: text, + links: [Uri.parse(reshareLink)]); return Result.ok(timelineEntry); } @@ -83,16 +85,19 @@ Result _buildStatusMessageType( .map((e) => mediaAttachmentfromDiasporaJson(e)); final timelineEntry = TimelineEntry( - id: postId, - creationTimestamp: epochTime, - body: postHtml, - author: author.name, - mediaAttachments: photos.toList(), - authorId: author.id); + id: postId, + creationTimestamp: epochTime, + body: postHtml, + author: author.name, + externalLink: _buildPostOrReshareUrl(authorName, '', postId), + mediaAttachments: photos.toList(), + authorId: author.id, + ); return Result.ok(timelineEntry); } -String _buildReshareUrl(String author, String rootAuthor, String rootGuid) { +String _buildPostOrReshareUrl( + String author, String rootAuthor, String rootGuid) { final accountId = rootAuthor.isNotEmpty ? rootAuthor : author; final accountIdPieces = accountId.split('@'); if (accountIdPieces.length != 2) { diff --git a/friendica_archive_browser/lib/src/diaspora/serializers/diaspora_relayables_serializer.dart b/friendica_archive_browser/lib/src/diaspora/serializers/diaspora_relayables_serializer.dart new file mode 100644 index 0000000..a9a944f --- /dev/null +++ b/friendica_archive_browser/lib/src/diaspora/serializers/diaspora_relayables_serializer.dart @@ -0,0 +1,78 @@ +import 'package:friendica_archive_browser/src/diaspora/models/diaspora_relayables.dart'; +import 'package:friendica_archive_browser/src/models/timeline_entry.dart'; +import 'package:friendica_archive_browser/src/utils/exec_error.dart'; +import 'package:friendica_archive_browser/src/utils/offsetdatetime_utils.dart'; +import 'package:logging/logging.dart'; +import 'package:markdown/markdown.dart'; +import 'package:result_monad/result_monad.dart'; + +import '../models/diaspora_reaction.dart'; + +final _logger = Logger('DiasporaPostsSerializer'); +const _commentType = 'comment'; +const _likeType = 'like'; +const _knownRelayableTypes = [_commentType, _likeType]; + +Result relayableFromDiasporaPostJson( + Map json) { + if (!json.containsKey('entity_data')) { + return Result.error(ExecError.message('Relayable item has no entity data')); + } + final entityType = json['entity_type'] ?? ''; + final entityData = json['entity_data'] ?? {}; + if (!_knownRelayableTypes.contains(entityType)) { + final guid = entityData['guid']; + final error = 'Unknown entity type $entityType for Relayable ID: $guid'; + _logger.severe(error); + return Result.error(ExecError.message(error)); + } + + if (entityType == _commentType) { + return _buildCommentRelayable(entityData); + } + + if (entityType == _likeType) { + return _buildReactionRelayable(entityData); + } + + return Result.error(ExecError.message('Unknown type: $entityType')); +} + +Result _buildCommentRelayable( + Map entityData) { + final author = entityData['author'] ?? ''; + final guid = entityData['guid'] ?? ''; + final parentGuid = entityData['parent_guid'] ?? ''; + final commentMarkdown = entityData['text'] ?? ''; + final commentHtml = markdownToHtml(commentMarkdown); + final epochTime = OffsetDateTimeUtils.epochSecTimeFromTimeZoneString( + entityData['created_at'] ?? '') + .getValueOrElse(() => -1); + + final timelineEntry = TimelineEntry( + id: guid, + creationTimestamp: epochTime, + body: commentHtml, + author: author, + parentId: parentGuid, + externalLink: _buildCommentUrl(author, parentGuid, guid), + ); + return Result.ok(DiasporaRelayable(comment: timelineEntry)); +} + +Result _buildReactionRelayable( + Map entityData) { + final reaction = DiasporaReaction.fromJson(entityData); + return Result.ok(DiasporaRelayable(reaction: reaction)); +} + +String _buildCommentUrl(String author, String parentGuid, String commentGuid) { + final accountIdPieces = author.split('@'); + if (accountIdPieces.length != 2) { + return commentGuid; + } + + final server = accountIdPieces[1]; + + return 'https://$server/p/$parentGuid#$commentGuid'; +} diff --git a/friendica_archive_browser/lib/src/diaspora/services/diaspora_archive_service.dart b/friendica_archive_browser/lib/src/diaspora/services/diaspora_archive_service.dart index 6dccaa9..1b465d5 100644 --- a/friendica_archive_browser/lib/src/diaspora/services/diaspora_archive_service.dart +++ b/friendica_archive_browser/lib/src/diaspora/services/diaspora_archive_service.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:friendica_archive_browser/src/diaspora/services/diaspora_path_mapping_service.dart'; import 'package:friendica_archive_browser/src/diaspora/services/diaspora_profile_json_reader.dart'; +import 'package:friendica_archive_browser/src/models/connection.dart'; import 'package:friendica_archive_browser/src/services/archive_service_interface.dart'; import 'package:path/path.dart' as p; import 'package:result_monad/result_monad.dart'; @@ -10,6 +11,8 @@ import '../../models/entry_tree_item.dart'; import '../../models/local_image_archive_entry.dart'; import '../../services/connections_manager.dart'; import '../../utils/exec_error.dart'; +import '../models/diaspora_reaction.dart'; +import '../serializers/diaspora_contact_serializer.dart'; class DiasporaArchiveService implements ArchiveService { @override @@ -94,15 +97,84 @@ class DiasporaArchiveService implements ArchiveService { if (_ownersName.isEmpty) { _ownersName = reader.readOwnersName(); reader.readContacts(); - final newPosts = reader.readPosts().map((e) => EntryTreeItem(e, false)); + final entryTree = {}; + final newPosts = + reader.readPosts().map((e) => EntryTreeItem(e, false)).toList(); + + for (final post in newPosts) { + entryTree[post.id] = post; + } + + final userComments = reader + .readUserRelayables() + .where((r) => r.isComment) + .map((r) => r.comment!); + final othersRelayables = reader.readOthersRelayables(); + final othersComments = + othersRelayables.where((r) => r.isComment).map((r) => r.comment!); + final othersReactions = + othersRelayables.where((r) => r.isReaction).map((r) => r.reaction!); + + final allComments = [...userComments, ...othersComments]; + allComments.sort( + (c1, c2) => c1.creationTimestamp.compareTo(c2.creationTimestamp)); + + for (final comment in allComments) { + final parentId = comment.parentId; + final parent = entryTree[parentId]; + if (parent == null) { + final newEntry = EntryTreeItem(comment, true); + entryTree[comment.id] = newEntry; + if (userComments.contains(comment)) { + _orphanedCommentEntries.add(newEntry); + } + } else { + parent.addChild(EntryTreeItem(comment, false)); + } + } + + for (final reaction in othersReactions) { + final treeEntry = entryTree[reaction.parentGuid]; + if (treeEntry == null) { + continue; + } + + final builtConnections = {}; + final builtConnection = builtConnections[reaction.authorString] ?? + contactFromDiasporaId(reaction.authorString); + builtConnections[reaction.authorString] = builtConnection; + final result = + connectionsManager.getByProfileUrl(builtConnection.profileUrl); + final connection = result.fold( + onSuccess: (c) => c, + onError: (error) { + connectionsManager.addConnection(builtConnection); + return builtConnection; + }); + + switch (reaction.reactionType) { + case ReactionType.unknown: + break; + case ReactionType.dislike: + treeEntry.entry.dislikes.add(connection); + break; + case ReactionType.like: + treeEntry.entry.likes.add(connection); + break; + } + } + _postEntries.addAll(newPosts); } } - _postEntries.sort((p1, p2) => - p2.entry.creationTimestamp.compareTo(p1.entry.creationTimestamp)); + _postEntries.sort((p1, p2) => _reverseChronologicalSort(p1, p2)); + _orphanedCommentEntries.sort((p1, p2) => _reverseChronologicalSort(p1, p2)); } + int _reverseChronologicalSort(EntryTreeItem p1, EntryTreeItem p2) => + p2.entry.creationTimestamp.compareTo(p1.entry.creationTimestamp); + void _populateTopLevelSubDirectory() { final topLevelDirectories = Directory(_baseArchiveFolder) .listSync(recursive: false) diff --git a/friendica_archive_browser/lib/src/diaspora/services/diaspora_profile_json_reader.dart b/friendica_archive_browser/lib/src/diaspora/services/diaspora_profile_json_reader.dart index 6520207..b36c9d0 100644 --- a/friendica_archive_browser/lib/src/diaspora/services/diaspora_profile_json_reader.dart +++ b/friendica_archive_browser/lib/src/diaspora/services/diaspora_profile_json_reader.dart @@ -1,7 +1,9 @@ import 'dart:convert'; import 'dart:io'; +import 'package:friendica_archive_browser/src/diaspora/models/diaspora_relayables.dart'; import 'package:friendica_archive_browser/src/diaspora/serializers/diaspora_contact_serializer.dart'; +import 'package:friendica_archive_browser/src/diaspora/serializers/diaspora_relayables_serializer.dart'; import 'package:friendica_archive_browser/src/models/connection.dart'; import 'package:friendica_archive_browser/src/models/timeline_entry.dart'; import 'package:friendica_archive_browser/src/services/connections_manager.dart'; @@ -61,4 +63,27 @@ class DiasporaProfileJsonReader { .sort((p1, p2) => p2.creationTimestamp.compareTo(p1.creationTimestamp)); return posts; } + + List readUserRelayables() { + if (connectionsManager.length == 0) { + readContacts(); + } + + return _readRelayableJson(jsonData['user']?['relayables'] ?? []); + } + + List readOthersRelayables() { + if (connectionsManager.length == 0) { + readContacts(); + } + + return _readRelayableJson(jsonData['others_data']?['relayables'] ?? []); + } + + List _readRelayableJson(List relayableJsonList) => + relayableJsonList + .map((e) => relayableFromDiasporaPostJson(e)) + .where((r) => r.isSuccess) + .map((r) => r.value) + .toList(); } diff --git a/friendica_archive_browser/lib/src/friendica/serializers/friendica_timeline_entry_serializer.dart b/friendica_archive_browser/lib/src/friendica/serializers/friendica_timeline_entry_serializer.dart index 636c876..dd40f96 100644 --- a/friendica_archive_browser/lib/src/friendica/serializers/friendica_timeline_entry_serializer.dart +++ b/friendica_archive_browser/lib/src/friendica/serializers/friendica_timeline_entry_serializer.dart @@ -55,12 +55,12 @@ TimelineEntry timelineEntryFromFriendicaJson( modificationTimestamp: modificationTimestamp, backdatedTimestamp: backdatedTimestamp, locationData: actualLocationData, - externalLink: externalLink, body: body, isReshare: isReshare, id: id, parentId: parentId, parentAuthorId: parentAuthorId, + externalLink: externalLink, author: author, authorId: authorId, parentAuthor: parentAuthor, diff --git a/friendica_archive_browser/lib/src/models/timeline_entry.dart b/friendica_archive_browser/lib/src/models/timeline_entry.dart index cad73df..36f63c8 100644 --- a/friendica_archive_browser/lib/src/models/timeline_entry.dart +++ b/friendica_archive_browser/lib/src/models/timeline_entry.dart @@ -41,6 +41,8 @@ class TimelineEntry { final List dislikes; + final List links; + TimelineEntry({ this.id = '', this.parentId = '', @@ -56,10 +58,13 @@ class TimelineEntry { this.parentAuthorId = '', this.externalLink = '', this.locationData = const LocationData(), - this.likes = const [], - this.dislikes = const [], + this.links = const [], + List? likes, + List? dislikes, List? mediaAttachments, - }) : mediaAttachments = mediaAttachments ?? []; + }) : mediaAttachments = mediaAttachments ?? [], + likes = likes ?? [], + dislikes = dislikes ?? []; TimelineEntry.randomBuilt() : creationTimestamp = DateTime.now().millisecondsSinceEpoch, @@ -78,6 +83,7 @@ class TimelineEntry { locationData = LocationData.randomBuilt(), likes = const [], dislikes = const [], + links = [], mediaAttachments = [ MediaAttachment.randomBuilt(), MediaAttachment.randomBuilt() @@ -103,24 +109,26 @@ class TimelineEntry { List? dislikes, List? links}) { return TimelineEntry( - creationTimestamp: creationTimestamp ?? this.creationTimestamp, - backdatedTimestamp: backdatedTimestamp ?? this.backdatedTimestamp, - modificationTimestamp: - modificationTimestamp ?? this.modificationTimestamp, - id: id ?? this.id, - isReshare: isReshare ?? this.isReshare, - parentId: parentId ?? this.parentId, - externalLink: externalLink ?? this.externalLink, - body: body ?? this.body, - title: title ?? this.title, - author: author ?? this.author, - authorId: authorId ?? this.authorId, - parentAuthor: parentAuthor ?? this.parentAuthor, - parentAuthorId: parentAuthorId ?? this.parentAuthorId, - locationData: locationData ?? this.locationData, - mediaAttachments: mediaAttachments ?? this.mediaAttachments, - likes: likes ?? this.likes, - dislikes: dislikes ?? this.dislikes); + creationTimestamp: creationTimestamp ?? this.creationTimestamp, + backdatedTimestamp: backdatedTimestamp ?? this.backdatedTimestamp, + modificationTimestamp: + modificationTimestamp ?? this.modificationTimestamp, + id: id ?? this.id, + isReshare: isReshare ?? this.isReshare, + parentId: parentId ?? this.parentId, + externalLink: externalLink ?? this.externalLink, + body: body ?? this.body, + title: title ?? this.title, + author: author ?? this.author, + authorId: authorId ?? this.authorId, + parentAuthor: parentAuthor ?? this.parentAuthor, + parentAuthorId: parentAuthorId ?? this.parentAuthorId, + locationData: locationData ?? this.locationData, + mediaAttachments: mediaAttachments ?? this.mediaAttachments, + likes: likes ?? this.likes, + dislikes: dislikes ?? this.dislikes, + links: links ?? this.links, + ); } @override @@ -147,6 +155,7 @@ class TimelineEntry { if (mediaAttachments.isNotEmpty) 'Photos and Videos:', ...mediaAttachments.map((e) => e.toHumanString(mapper)), if (locationData.hasPosition) locationData.toHumanString(), + if (links.isNotEmpty) ...['Links:', ...links.map((e) => e.toString())] ].join('\n'); } diff --git a/friendica_archive_browser/lib/src/screens/stats_screen.dart b/friendica_archive_browser/lib/src/screens/stats_screen.dart index 4407ed1..3358922 100644 --- a/friendica_archive_browser/lib/src/screens/stats_screen.dart +++ b/friendica_archive_browser/lib/src/screens/stats_screen.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:friendica_archive_browser/src/components/filter_control_component.dart'; import 'package:friendica_archive_browser/src/components/heatmap_widget.dart'; import 'package:friendica_archive_browser/src/components/timechart_widget.dart'; import 'package:friendica_archive_browser/src/components/top_interactactors_widget.dart'; import 'package:friendica_archive_browser/src/components/word_frequency_widget.dart'; -import 'package:friendica_archive_browser/src/components/filter_control_component.dart'; import 'package:friendica_archive_browser/src/models/model_utils.dart'; import 'package:friendica_archive_browser/src/models/time_element.dart'; import 'package:friendica_archive_browser/src/screens/standin_status_screen.dart'; diff --git a/friendica_archive_browser/lib/src/services/connections_manager.dart b/friendica_archive_browser/lib/src/services/connections_manager.dart index 137fccd..c71401f 100644 --- a/friendica_archive_browser/lib/src/services/connections_manager.dart +++ b/friendica_archive_browser/lib/src/services/connections_manager.dart @@ -4,12 +4,14 @@ import 'package:result_monad/result_monad.dart'; class ConnectionsManager { final _connectionsById = {}; final _connectionsByName = {}; + final _connectionsByProfileUrl = {}; int get length => _connectionsById.length; void clearCaches() { _connectionsById.clear(); _connectionsByName.clear(); + _connectionsByProfileUrl.clear(); } bool addConnection(Connection connection) { @@ -18,6 +20,7 @@ class ConnectionsManager { } _connectionsById[connection.id] = connection; _connectionsByName[connection.name] = connection; + _connectionsByProfileUrl[connection.profileUrl] = connection; return true; } @@ -43,4 +46,10 @@ class ConnectionsManager { return result != null ? Result.ok(result) : Result.error('$name not found'); } + + Result getByProfileUrl(Uri url) { + final result = _connectionsByProfileUrl[url]; + + return result != null ? Result.ok(result) : Result.error('$url not found'); + } }