Add reaction and comment processing for Diaspora data.

This commit is contained in:
Hank Grabowski 2022-03-15 20:42:30 -04:00
parent 4a9d57f4c7
commit 1ec9939f9b
13 changed files with 322 additions and 50 deletions

View file

@ -1,8 +1,8 @@
import 'dart:math'; import 'dart:math';
import 'package:flutter/material.dart'; 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/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/services/archive_service_provider.dart';
import 'package:friendica_archive_browser/src/settings/settings_controller.dart'; import 'package:friendica_archive_browser/src/settings/settings_controller.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';

View file

@ -40,7 +40,7 @@ class TreeEntryCard extends StatelessWidget {
? entry.title ? entry.title
: entry.parentId.isEmpty : entry.parentId.isEmpty
? (entry.isReshare ? 'Reshare' : 'Post') ? (entry.isReshare ? 'Reshare' : 'Post')
: 'Comment on post by ${entry.parentAuthor}'; : 'Comment on post by ${entry.author}';
final dateStamp = ' At ' + final dateStamp = ' At ' +
formatter.format( formatter.format(
DateTime.fromMillisecondsSinceEpoch(entry.creationTimestamp * 1000) DateTime.fromMillisecondsSinceEpoch(entry.creationTimestamp * 1000)
@ -81,7 +81,8 @@ class TreeEntryCard extends StatelessWidget {
icon: const Icon(Icons.copy)), icon: const Icon(Icons.copy)),
), ),
Tooltip( Tooltip(
message: 'Open link to original item', message:
'Open link to original item (${entry.externalLink})',
child: IconButton( child: IconButton(
onPressed: () async { onPressed: () async {
await canLaunch(entry.externalLink) await canLaunch(entry.externalLink)
@ -130,10 +131,9 @@ class TreeEntryCard extends StatelessWidget {
), ),
if (entry.locationData.hasData()) if (entry.locationData.hasData())
entry.locationData.toWidget(spacingHeight), entry.locationData.toWidget(spacingHeight),
if (treeEntry.entry.externalLink.isNotEmpty) ...[ if (treeEntry.entry.links.isNotEmpty) ...[
const SizedBox(height: spacingHeight), const SizedBox(height: spacingHeight),
LinkElementsComponent( LinkElementsComponent(links: treeEntry.entry.links)
links: [Uri.parse(treeEntry.entry.externalLink)])
], ],
if (entry.mediaAttachments.isNotEmpty) ...[ if (entry.mediaAttachments.isNotEmpty) ...[
const SizedBox(height: spacingHeight), const SizedBox(height: spacingHeight),

View file

@ -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<String, dynamic> 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;
}

View file

@ -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});
}

View file

@ -1,4 +1,5 @@
import 'package:friendica_archive_browser/src/models/connection.dart'; import 'package:friendica_archive_browser/src/models/connection.dart';
import 'package:uuid/uuid.dart';
Connection contactFromDiasporaJson(Map<String, dynamic> json) { Connection contactFromDiasporaJson(Map<String, dynamic> json) {
const network = "Diaspora"; const network = "Diaspora";
@ -25,6 +26,11 @@ Connection contactFromDiasporaJson(Map<String, dynamic> json) {
network: network); network: network);
} }
Connection contactFromDiasporaId(String accountId) => Connection(
id: 'generated_${const Uuid().v4}',
status: ConnectionStatus.none,
profileUrl: _profileUrlFromAccountId(accountId));
Uri _profileUrlFromAccountId(String accountId) { Uri _profileUrlFromAccountId(String accountId) {
if (accountId.isEmpty) { if (accountId.isEmpty) {
return Uri(); return Uri();

View file

@ -46,7 +46,9 @@ Result<TimelineEntry, ExecError> _buildReshareMessageType(
final String parentGuid = entityData['root_guid'] ?? ''; final String parentGuid = entityData['root_guid'] ?? '';
final String parentName = entityData['root_author'] ?? ''; final String parentName = entityData['root_author'] ?? '';
final deletedPost = parentGuid.isEmpty || parentName.isEmpty; 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 text = deletedPost ? 'Original post deleted by author' : '';
final author = final author =
connections.getByName(authorName).getValueOrElse(() => Connection()); connections.getByName(authorName).getValueOrElse(() => Connection());
@ -54,16 +56,16 @@ Result<TimelineEntry, ExecError> _buildReshareMessageType(
.getByName(parentName) .getByName(parentName)
.getValueOrElse(() => Connection(name: parentName)); .getValueOrElse(() => Connection(name: parentName));
final timelineEntry = TimelineEntry( final timelineEntry = TimelineEntry(
id: postId, id: postId,
creationTimestamp: epochTime, creationTimestamp: epochTime,
author: author.name, author: author.name,
authorId: author.id, authorId: author.id,
isReshare: true, externalLink: _buildPostOrReshareUrl(authorName, '', postId),
parentAuthor: parentAuthor.name, isReshare: true,
parentAuthorId: parentAuthor.id, parentAuthor: parentAuthor.name,
externalLink: externalLink, parentAuthorId: parentAuthor.id,
body: text, body: text,
); links: [Uri.parse(reshareLink)]);
return Result.ok(timelineEntry); return Result.ok(timelineEntry);
} }
@ -83,16 +85,19 @@ Result<TimelineEntry, ExecError> _buildStatusMessageType(
.map((e) => mediaAttachmentfromDiasporaJson(e)); .map((e) => mediaAttachmentfromDiasporaJson(e));
final timelineEntry = TimelineEntry( final timelineEntry = TimelineEntry(
id: postId, id: postId,
creationTimestamp: epochTime, creationTimestamp: epochTime,
body: postHtml, body: postHtml,
author: author.name, author: author.name,
mediaAttachments: photos.toList(), externalLink: _buildPostOrReshareUrl(authorName, '', postId),
authorId: author.id); mediaAttachments: photos.toList(),
authorId: author.id,
);
return Result.ok(timelineEntry); 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 accountId = rootAuthor.isNotEmpty ? rootAuthor : author;
final accountIdPieces = accountId.split('@'); final accountIdPieces = accountId.split('@');
if (accountIdPieces.length != 2) { if (accountIdPieces.length != 2) {

View file

@ -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<DiasporaRelayable, ExecError> relayableFromDiasporaPostJson(
Map<String, dynamic> 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<DiasporaRelayable, ExecError> _buildCommentRelayable(
Map<String, dynamic> 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<DiasporaRelayable, ExecError> _buildReactionRelayable(
Map<String, dynamic> 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';
}

View file

@ -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_path_mapping_service.dart';
import 'package:friendica_archive_browser/src/diaspora/services/diaspora_profile_json_reader.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:friendica_archive_browser/src/services/archive_service_interface.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:result_monad/result_monad.dart'; 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 '../../models/local_image_archive_entry.dart';
import '../../services/connections_manager.dart'; import '../../services/connections_manager.dart';
import '../../utils/exec_error.dart'; import '../../utils/exec_error.dart';
import '../models/diaspora_reaction.dart';
import '../serializers/diaspora_contact_serializer.dart';
class DiasporaArchiveService implements ArchiveService { class DiasporaArchiveService implements ArchiveService {
@override @override
@ -94,15 +97,84 @@ class DiasporaArchiveService implements ArchiveService {
if (_ownersName.isEmpty) { if (_ownersName.isEmpty) {
_ownersName = reader.readOwnersName(); _ownersName = reader.readOwnersName();
reader.readContacts(); reader.readContacts();
final newPosts = reader.readPosts().map((e) => EntryTreeItem(e, false)); final entryTree = <String, EntryTreeItem>{};
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 = <String, Connection>{};
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.addAll(newPosts);
} }
} }
_postEntries.sort((p1, p2) => _postEntries.sort((p1, p2) => _reverseChronologicalSort(p1, p2));
p2.entry.creationTimestamp.compareTo(p1.entry.creationTimestamp)); _orphanedCommentEntries.sort((p1, p2) => _reverseChronologicalSort(p1, p2));
} }
int _reverseChronologicalSort(EntryTreeItem p1, EntryTreeItem p2) =>
p2.entry.creationTimestamp.compareTo(p1.entry.creationTimestamp);
void _populateTopLevelSubDirectory() { void _populateTopLevelSubDirectory() {
final topLevelDirectories = Directory(_baseArchiveFolder) final topLevelDirectories = Directory(_baseArchiveFolder)
.listSync(recursive: false) .listSync(recursive: false)

View file

@ -1,7 +1,9 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; 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_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/connection.dart';
import 'package:friendica_archive_browser/src/models/timeline_entry.dart'; import 'package:friendica_archive_browser/src/models/timeline_entry.dart';
import 'package:friendica_archive_browser/src/services/connections_manager.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)); .sort((p1, p2) => p2.creationTimestamp.compareTo(p1.creationTimestamp));
return posts; return posts;
} }
List<DiasporaRelayable> readUserRelayables() {
if (connectionsManager.length == 0) {
readContacts();
}
return _readRelayableJson(jsonData['user']?['relayables'] ?? []);
}
List<DiasporaRelayable> readOthersRelayables() {
if (connectionsManager.length == 0) {
readContacts();
}
return _readRelayableJson(jsonData['others_data']?['relayables'] ?? []);
}
List<DiasporaRelayable> _readRelayableJson(List<dynamic> relayableJsonList) =>
relayableJsonList
.map((e) => relayableFromDiasporaPostJson(e))
.where((r) => r.isSuccess)
.map((r) => r.value)
.toList();
} }

View file

@ -55,12 +55,12 @@ TimelineEntry timelineEntryFromFriendicaJson(
modificationTimestamp: modificationTimestamp, modificationTimestamp: modificationTimestamp,
backdatedTimestamp: backdatedTimestamp, backdatedTimestamp: backdatedTimestamp,
locationData: actualLocationData, locationData: actualLocationData,
externalLink: externalLink,
body: body, body: body,
isReshare: isReshare, isReshare: isReshare,
id: id, id: id,
parentId: parentId, parentId: parentId,
parentAuthorId: parentAuthorId, parentAuthorId: parentAuthorId,
externalLink: externalLink,
author: author, author: author,
authorId: authorId, authorId: authorId,
parentAuthor: parentAuthor, parentAuthor: parentAuthor,

View file

@ -41,6 +41,8 @@ class TimelineEntry {
final List<Connection> dislikes; final List<Connection> dislikes;
final List<Uri> links;
TimelineEntry({ TimelineEntry({
this.id = '', this.id = '',
this.parentId = '', this.parentId = '',
@ -56,10 +58,13 @@ class TimelineEntry {
this.parentAuthorId = '', this.parentAuthorId = '',
this.externalLink = '', this.externalLink = '',
this.locationData = const LocationData(), this.locationData = const LocationData(),
this.likes = const <Connection>[], this.links = const [],
this.dislikes = const <Connection>[], List<Connection>? likes,
List<Connection>? dislikes,
List<MediaAttachment>? mediaAttachments, List<MediaAttachment>? mediaAttachments,
}) : mediaAttachments = mediaAttachments ?? <MediaAttachment>[]; }) : mediaAttachments = mediaAttachments ?? <MediaAttachment>[],
likes = likes ?? <Connection>[],
dislikes = dislikes ?? <Connection>[];
TimelineEntry.randomBuilt() TimelineEntry.randomBuilt()
: creationTimestamp = DateTime.now().millisecondsSinceEpoch, : creationTimestamp = DateTime.now().millisecondsSinceEpoch,
@ -78,6 +83,7 @@ class TimelineEntry {
locationData = LocationData.randomBuilt(), locationData = LocationData.randomBuilt(),
likes = const <Connection>[], likes = const <Connection>[],
dislikes = const <Connection>[], dislikes = const <Connection>[],
links = [],
mediaAttachments = [ mediaAttachments = [
MediaAttachment.randomBuilt(), MediaAttachment.randomBuilt(),
MediaAttachment.randomBuilt() MediaAttachment.randomBuilt()
@ -103,24 +109,26 @@ class TimelineEntry {
List<Connection>? dislikes, List<Connection>? dislikes,
List<Uri>? links}) { List<Uri>? links}) {
return TimelineEntry( return TimelineEntry(
creationTimestamp: creationTimestamp ?? this.creationTimestamp, creationTimestamp: creationTimestamp ?? this.creationTimestamp,
backdatedTimestamp: backdatedTimestamp ?? this.backdatedTimestamp, backdatedTimestamp: backdatedTimestamp ?? this.backdatedTimestamp,
modificationTimestamp: modificationTimestamp:
modificationTimestamp ?? this.modificationTimestamp, modificationTimestamp ?? this.modificationTimestamp,
id: id ?? this.id, id: id ?? this.id,
isReshare: isReshare ?? this.isReshare, isReshare: isReshare ?? this.isReshare,
parentId: parentId ?? this.parentId, parentId: parentId ?? this.parentId,
externalLink: externalLink ?? this.externalLink, externalLink: externalLink ?? this.externalLink,
body: body ?? this.body, body: body ?? this.body,
title: title ?? this.title, title: title ?? this.title,
author: author ?? this.author, author: author ?? this.author,
authorId: authorId ?? this.authorId, authorId: authorId ?? this.authorId,
parentAuthor: parentAuthor ?? this.parentAuthor, parentAuthor: parentAuthor ?? this.parentAuthor,
parentAuthorId: parentAuthorId ?? this.parentAuthorId, parentAuthorId: parentAuthorId ?? this.parentAuthorId,
locationData: locationData ?? this.locationData, locationData: locationData ?? this.locationData,
mediaAttachments: mediaAttachments ?? this.mediaAttachments, mediaAttachments: mediaAttachments ?? this.mediaAttachments,
likes: likes ?? this.likes, likes: likes ?? this.likes,
dislikes: dislikes ?? this.dislikes); dislikes: dislikes ?? this.dislikes,
links: links ?? this.links,
);
} }
@override @override
@ -147,6 +155,7 @@ class TimelineEntry {
if (mediaAttachments.isNotEmpty) 'Photos and Videos:', if (mediaAttachments.isNotEmpty) 'Photos and Videos:',
...mediaAttachments.map((e) => e.toHumanString(mapper)), ...mediaAttachments.map((e) => e.toHumanString(mapper)),
if (locationData.hasPosition) locationData.toHumanString(), if (locationData.hasPosition) locationData.toHumanString(),
if (links.isNotEmpty) ...['Links:', ...links.map((e) => e.toString())]
].join('\n'); ].join('\n');
} }

View file

@ -1,9 +1,9 @@
import 'package:flutter/material.dart'; 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/heatmap_widget.dart';
import 'package:friendica_archive_browser/src/components/timechart_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/top_interactactors_widget.dart';
import 'package:friendica_archive_browser/src/components/word_frequency_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/model_utils.dart';
import 'package:friendica_archive_browser/src/models/time_element.dart'; import 'package:friendica_archive_browser/src/models/time_element.dart';
import 'package:friendica_archive_browser/src/screens/standin_status_screen.dart'; import 'package:friendica_archive_browser/src/screens/standin_status_screen.dart';

View file

@ -4,12 +4,14 @@ import 'package:result_monad/result_monad.dart';
class ConnectionsManager { class ConnectionsManager {
final _connectionsById = <String, Connection>{}; final _connectionsById = <String, Connection>{};
final _connectionsByName = <String, Connection>{}; final _connectionsByName = <String, Connection>{};
final _connectionsByProfileUrl = <Uri, Connection>{};
int get length => _connectionsById.length; int get length => _connectionsById.length;
void clearCaches() { void clearCaches() {
_connectionsById.clear(); _connectionsById.clear();
_connectionsByName.clear(); _connectionsByName.clear();
_connectionsByProfileUrl.clear();
} }
bool addConnection(Connection connection) { bool addConnection(Connection connection) {
@ -18,6 +20,7 @@ class ConnectionsManager {
} }
_connectionsById[connection.id] = connection; _connectionsById[connection.id] = connection;
_connectionsByName[connection.name] = connection; _connectionsByName[connection.name] = connection;
_connectionsByProfileUrl[connection.profileUrl] = connection;
return true; return true;
} }
@ -43,4 +46,10 @@ class ConnectionsManager {
return result != null ? Result.ok(result) : Result.error('$name not found'); return result != null ? Result.ok(result) : Result.error('$name not found');
} }
Result<Connection, String> getByProfileUrl(Uri url) {
final result = _connectionsByProfileUrl[url];
return result != null ? Result.ok(result) : Result.error('$url not found');
}
} }