From aafe6dea7c1f1c9bf774dbe90b0f3667b885aca6 Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Mon, 20 Mar 2023 14:30:51 -0400 Subject: [PATCH] Add Visibility object concept throughout, including image displays --- .../flattened_tree_entry_control.dart | 2 - lib/controls/timeline/post_control.dart | 2 - .../timeline/status_header_control.dart | 5 +- lib/friendica_client/friendica_client.dart | 1 + lib/models/image_entry.dart | 25 ++++---- lib/models/media_attachment.dart | 49 ++++++++-------- lib/models/timeline_entry.dart | 20 ++++--- lib/models/visibility.dart | 57 +++++++++++++++++++ lib/screens/gallery_screen.dart | 22 +++++-- .../image_entry_friendica_extensions.dart | 22 ++++--- ...media_attachment_friendica_extensions.dart | 26 +++++---- .../timeline_entry_friendica_extensions.dart | 8 ++- .../visibility_friendica_extensions.dart | 33 +++++++++++ .../media_attachment_mastodon_extension.dart | 20 +++++++ .../timeline_entry_mastodon_extensions.dart | 12 ++-- 15 files changed, 226 insertions(+), 78 deletions(-) create mode 100644 lib/models/visibility.dart create mode 100644 lib/serializers/friendica/visibility_friendica_extensions.dart create mode 100644 lib/serializers/mastodon/media_attachment_mastodon_extension.dart diff --git a/lib/controls/timeline/flattened_tree_entry_control.dart b/lib/controls/timeline/flattened_tree_entry_control.dart index 191291e..0bf9b05 100644 --- a/lib/controls/timeline/flattened_tree_entry_control.dart +++ b/lib/controls/timeline/flattened_tree_entry_control.dart @@ -42,8 +42,6 @@ class _StatusControlState extends State { TimelineEntry get entry => item.timelineEntry; - bool get isPublic => entry.isPublic; - bool get isPost => entry.parentId.isEmpty; bool get hasComments => entry.engagementSummary.repliesCount > 0; diff --git a/lib/controls/timeline/post_control.dart b/lib/controls/timeline/post_control.dart index a331ee2..5eef249 100644 --- a/lib/controls/timeline/post_control.dart +++ b/lib/controls/timeline/post_control.dart @@ -44,8 +44,6 @@ class _PostControlState extends State { TimelineEntry get entry => item.entry; - bool get isPublic => item.entry.isPublic; - @override void initState() { super.initState(); diff --git a/lib/controls/timeline/status_header_control.dart b/lib/controls/timeline/status_header_control.dart index e0e3216..e72b4cc 100644 --- a/lib/controls/timeline/status_header_control.dart +++ b/lib/controls/timeline/status_header_control.dart @@ -5,6 +5,7 @@ import 'package:logging/logging.dart'; import '../../globals.dart'; import '../../models/connection.dart'; import '../../models/timeline_entry.dart'; +import '../../models/visibility.dart'; import '../../routes.dart'; import '../../services/connections_manager.dart'; import '../../utils/active_profile_selector.dart'; @@ -106,7 +107,9 @@ class StatusHeaderControl extends StatelessWidget { ), const HorizontalPadding(), Icon( - entry.isPublic ? Icons.public : Icons.lock, + entry.visibility.type == VisibilityType.public + ? Icons.public + : Icons.lock, color: Theme.of(context).hintColor, size: Theme.of(context).textTheme.caption?.fontSize, ), diff --git a/lib/friendica_client/friendica_client.dart b/lib/friendica_client/friendica_client.dart index 8c5d920..e5afe6d 100644 --- a/lib/friendica_client/friendica_client.dart +++ b/lib/friendica_client/friendica_client.dart @@ -651,6 +651,7 @@ class StatusesClient extends FriendicaClient { if (spoilerText.isNotEmpty) 'spoiler_text': spoilerText, if (inReplyToId.isNotEmpty) 'in_reply_to_id': inReplyToId, if (mediaIds.isNotEmpty) 'media_ids': mediaIds, + 'visibility': 'unlisted', 'friendica': { 'title': '', }, diff --git a/lib/models/image_entry.dart b/lib/models/image_entry.dart index 844a4cb..3c58a8f 100644 --- a/lib/models/image_entry.dart +++ b/lib/models/image_entry.dart @@ -1,3 +1,5 @@ +import 'visibility.dart'; + class ImageEntry { final String id; final String album; @@ -7,18 +9,21 @@ class ImageEntry { final DateTime created; final int height; final int width; + final Visibility visibility; final List scales; - ImageEntry( - {required this.id, - required this.album, - required this.filename, - required this.description, - required this.thumbnailUrl, - required this.created, - required this.height, - required this.width, - required this.scales}); + ImageEntry({ + required this.id, + required this.album, + required this.filename, + required this.description, + required this.thumbnailUrl, + required this.created, + required this.height, + required this.width, + required this.visibility, + required this.scales, + }); @override bool operator ==(Object other) => diff --git a/lib/models/media_attachment.dart b/lib/models/media_attachment.dart index f249a45..fd3f493 100644 --- a/lib/models/media_attachment.dart +++ b/lib/models/media_attachment.dart @@ -1,8 +1,9 @@ import 'package:path/path.dart' as p; -import 'package:relatica/models/image_entry.dart'; import '../globals.dart'; import 'attachment_media_type_enum.dart'; +import 'image_entry.dart'; +import 'visibility.dart'; class MediaAttachment { static final _graphicsExtensions = ['jpg', 'png', 'gif', 'tif']; @@ -26,16 +27,20 @@ class MediaAttachment { final String description; - MediaAttachment( - {required this.id, - required this.uri, - required this.creationTimestamp, - required this.metadata, - required this.thumbnailUri, - required this.fullFileUri, - required this.title, - required this.explicitType, - required this.description}); + final Visibility visibility; + + MediaAttachment({ + required this.id, + required this.uri, + required this.creationTimestamp, + required this.metadata, + required this.thumbnailUri, + required this.fullFileUri, + required this.title, + required this.explicitType, + required this.description, + required this.visibility, + }); MediaAttachment.randomBuilt() : id = randomId(), @@ -46,7 +51,11 @@ class MediaAttachment { thumbnailUri = Uri.parse('${randomId()}.jpg'), description = 'Random description ${randomId()}', explicitType = AttachmentMediaType.image, - metadata = {'value1': randomId(), 'value2': randomId()}; + metadata = { + 'value1': randomId(), + 'value2': randomId(), + }, + visibility = Visibility.public(); MediaAttachment.blank() : id = '', @@ -57,19 +66,8 @@ class MediaAttachment { title = '', fullFileUri = Uri(), description = '', - metadata = {}; - - factory MediaAttachment.fromMastodonJson(Map json) => - MediaAttachment( - id: json['id'] ?? '', - uri: Uri.parse(json['url'] ?? 'http://localhost'), - creationTimestamp: 0, - metadata: {}, - thumbnailUri: Uri.parse(json['url'] ?? 'http://localhost'), - title: '', - fullFileUri: Uri.parse(json['remote_url'] ?? 'http://localhost'), - explicitType: AttachmentMediaType.parse(json['type']), - description: json['description'] ?? ''); + metadata = {}, + visibility = Visibility.public(); @override String toString() { @@ -86,6 +84,7 @@ class MediaAttachment { created: DateTime.fromMillisecondsSinceEpoch(creationTimestamp), height: 0, width: 0, + visibility: visibility, scales: []); } diff --git a/lib/models/timeline_entry.dart b/lib/models/timeline_entry.dart index 0d4e98d..33b6d18 100644 --- a/lib/models/timeline_entry.dart +++ b/lib/models/timeline_entry.dart @@ -5,6 +5,7 @@ import 'link_data.dart'; import 'link_preview_data.dart'; import 'location_data.dart'; import 'media_attachment.dart'; +import 'visibility.dart'; class TimelineEntry { final String id; @@ -33,7 +34,7 @@ class TimelineEntry { final bool youReshared; - final bool isPublic; + final Visibility visibility; final String author; @@ -64,7 +65,7 @@ class TimelineEntry { this.backdatedTimestamp = 0, this.modificationTimestamp = 0, this.youReshared = false, - this.isPublic = true, + Visibility? visibility, this.body = '', this.title = '', this.spoilerText = '', @@ -82,7 +83,8 @@ class TimelineEntry { this.dislikes = const [], this.mediaAttachments = const [], this.engagementSummary = const EngagementSummary(), - this.linkPreviewData}); + this.linkPreviewData}) + : visibility = visibility ?? Visibility.public(); TimelineEntry.randomBuilt() : creationTimestamp = DateTime.now().millisecondsSinceEpoch, @@ -90,7 +92,9 @@ class TimelineEntry { modificationTimestamp = DateTime.now().millisecondsSinceEpoch, id = randomId(), youReshared = DateTime.now().second ~/ 2 == 0 ? true : false, - isPublic = DateTime.now().second ~/ 2 == 0 ? true : false, + visibility = DateTime.now().second ~/ 2 == 0 + ? Visibility.public() + : Visibility.private(), parentId = randomId(), externalLink = 'Random external link ${randomId()}', body = 'Random post text ${randomId()}', @@ -116,7 +120,7 @@ class TimelineEntry { int? backdatedTimestamp, int? modificationTimestamp, bool? isReshare, - bool? isPublic, + Visibility? visibility, String? id, String? parentId, String? externalLink, @@ -145,7 +149,7 @@ class TimelineEntry { modificationTimestamp ?? this.modificationTimestamp, id: id ?? this.id, youReshared: isReshare ?? this.youReshared, - isPublic: isPublic ?? this.isPublic, + visibility: visibility ?? this.visibility, parentId: parentId ?? this.parentId, externalLink: externalLink ?? this.externalLink, body: body ?? this.body, @@ -195,7 +199,7 @@ class TimelineEntry { title == other.title && spoilerText == other.spoilerText && youReshared == other.youReshared && - isPublic == other.isPublic && + visibility == other.visibility && author == other.author && authorId == other.authorId && externalLink == other.externalLink && @@ -222,7 +226,7 @@ class TimelineEntry { title.hashCode ^ spoilerText.hashCode ^ youReshared.hashCode ^ - isPublic.hashCode ^ + visibility.hashCode ^ author.hashCode ^ authorId.hashCode ^ externalLink.hashCode ^ diff --git a/lib/models/visibility.dart b/lib/models/visibility.dart new file mode 100644 index 0000000..7218eff --- /dev/null +++ b/lib/models/visibility.dart @@ -0,0 +1,57 @@ +enum VisibilityType { + public, + private, +} + +class Visibility { + final VisibilityType type; + + final List allowedUserIds; + + final List excludedUserIds; + + final List allowedGroupIds; + + final List excludedGroupIds; + + bool get hasDetails => + allowedUserIds.isNotEmpty || + excludedUserIds.isNotEmpty || + allowedGroupIds.isNotEmpty || + excludedGroupIds.isNotEmpty; + + const Visibility({ + required this.type, + this.allowedUserIds = const [], + this.excludedUserIds = const [], + this.allowedGroupIds = const [], + this.excludedGroupIds = const [], + }); + + factory Visibility.public() => const Visibility( + type: VisibilityType.public, + ); + + factory Visibility.private() => const Visibility( + type: VisibilityType.private, + ); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Visibility && + runtimeType == other.runtimeType && + type == other.type && + allowedUserIds == other.allowedUserIds && + excludedUserIds == other.excludedUserIds && + allowedGroupIds == other.allowedGroupIds && + excludedGroupIds == other.excludedGroupIds; + + @override + int get hashCode => + type.hashCode ^ + allowedUserIds.hashCode ^ + excludedUserIds.hashCode ^ + allowedGroupIds.hashCode ^ + excludedGroupIds.hashCode; +} diff --git a/lib/screens/gallery_screen.dart b/lib/screens/gallery_screen.dart index 2444a41..3b9b6d7 100644 --- a/lib/screens/gallery_screen.dart +++ b/lib/screens/gallery_screen.dart @@ -6,6 +6,7 @@ import '../controls/login_aware_cached_network_image.dart'; import '../controls/standard_appbar.dart'; import '../controls/status_and_refresh_button.dart'; import '../globals.dart'; +import '../models/visibility.dart'; import '../serializers/friendica/image_entry_friendica_extensions.dart'; import '../services/gallery_service.dart'; import '../services/network_status_service.dart'; @@ -122,10 +123,23 @@ class GalleryScreen extends StatelessWidget { ); })); }, - child: LoginAwareCachedNetworkImage( - width: thumbnailDimension, - height: thumbnailDimension, - imageUrl: image.thumbnailUrl, + child: Card( + child: Stack( + children: [ + LoginAwareCachedNetworkImage( + width: thumbnailDimension, + height: thumbnailDimension, + imageUrl: image.thumbnailUrl, + ), + Positioned( + bottom: 5.0, + right: 5.0, + child: Icon(image.visibility.type == VisibilityType.public + ? Icons.public + : Icons.lock), + ), + ], + ), ), ), ); diff --git a/lib/serializers/friendica/image_entry_friendica_extensions.dart b/lib/serializers/friendica/image_entry_friendica_extensions.dart index 97864ba..fd86ec9 100644 --- a/lib/serializers/friendica/image_entry_friendica_extensions.dart +++ b/lib/serializers/friendica/image_entry_friendica_extensions.dart @@ -1,6 +1,7 @@ import '../../models/attachment_media_type_enum.dart'; import '../../models/image_entry.dart'; import '../../models/media_attachment.dart'; +import 'visibility_friendica_extensions.dart'; extension ImageEntryFriendicaExtension on ImageEntry { static ImageEntry fromJson(Map json) => ImageEntry( @@ -12,6 +13,7 @@ extension ImageEntryFriendicaExtension on ImageEntry { created: DateTime.tryParse(json['created']) ?? DateTime(0), height: json['height'], width: json['width'], + visibility: VisibilityFriendicaExtensions.fromJson(json), scales: (json['scales'] as List? ?? []) .map((scaleJson) => _scaleFromJson(scaleJson)) .toList()); @@ -30,14 +32,16 @@ extension ImageEntryFriendicaExtension on ImageEntry { final thumbUri = Uri.parse(thumbnailUrl); final fullFileUri = scales.first.link; return MediaAttachment( - id: id, - uri: fullFileUri, - fullFileUri: fullFileUri, - creationTimestamp: created.millisecondsSinceEpoch, - metadata: {}, - thumbnailUri: thumbUri, - title: filename, - explicitType: AttachmentMediaType.image, - description: description); + id: id, + uri: fullFileUri, + fullFileUri: fullFileUri, + creationTimestamp: created.millisecondsSinceEpoch, + metadata: {}, + thumbnailUri: thumbUri, + title: filename, + explicitType: AttachmentMediaType.image, + description: description, + visibility: visibility, + ); } } diff --git a/lib/serializers/friendica/media_attachment_friendica_extensions.dart b/lib/serializers/friendica/media_attachment_friendica_extensions.dart index c213877..6ed27ca 100644 --- a/lib/serializers/friendica/media_attachment_friendica_extensions.dart +++ b/lib/serializers/friendica/media_attachment_friendica_extensions.dart @@ -1,8 +1,12 @@ import '../../models/attachment_media_type_enum.dart'; import '../../models/media_attachment.dart'; +import '../../models/visibility.dart'; extension MediaAttachmentFriendicaExtensions on MediaAttachment { - static MediaAttachment fromJson(Map json) { + static MediaAttachment fromJson( + Map json, + Visibility visibility, + ) { final id = json['id']; final uri = Uri.parse(json['url']); const creationTimestamp = 0; @@ -18,14 +22,16 @@ extension MediaAttachmentFriendicaExtensions on MediaAttachment { const description = ''; return MediaAttachment( - id: id, - uri: uri, - fullFileUri: uri, - creationTimestamp: creationTimestamp, - metadata: metadata, - thumbnailUri: thumbnailUri, - title: title, - explicitType: explicitType, - description: description); + id: id, + uri: uri, + fullFileUri: uri, + creationTimestamp: creationTimestamp, + metadata: metadata, + thumbnailUri: thumbnailUri, + title: title, + explicitType: explicitType, + description: description, + visibility: visibility, + ); } } diff --git a/lib/serializers/friendica/timeline_entry_friendica_extensions.dart b/lib/serializers/friendica/timeline_entry_friendica_extensions.dart index 75acf1e..81e9097 100644 --- a/lib/serializers/friendica/timeline_entry_friendica_extensions.dart +++ b/lib/serializers/friendica/timeline_entry_friendica_extensions.dart @@ -2,6 +2,7 @@ import 'package:logging/logging.dart'; import '../../models/location_data.dart'; import '../../models/timeline_entry.dart'; +import '../../models/visibility.dart'; import '../../utils/dateutils.dart'; import 'connection_friendica_extensions.dart'; import 'media_attachment_friendica_extensions.dart'; @@ -22,7 +23,8 @@ extension TimelineEntryFriendicaExtensions on TimelineEntry { : 0; final id = json['id_str'] ?? ''; final isReshare = json.containsKey('retweeted_status'); - final isPublic = json['friendica_private'] == 'false'; + final isPublic = !(json['friendica_private'] ?? false); + final visibility = isPublic ? Visibility.public() : Visibility.private(); final parentId = json['in_reply_to_status_id_str'] ?? ''; final parentAuthor = json['in_reply_to_screen_name'] ?? ''; final parentAuthorId = json['in_reply_to_user_id_str'] ?? ''; @@ -35,7 +37,7 @@ extension TimelineEntryFriendicaExtensions on TimelineEntry { final modificationTimestamp = timestamp; final backdatedTimestamp = timestamp; final mediaAttachments = (json['attachments'] as List? ?? []) - .map((j) => MediaAttachmentFriendicaExtensions.fromJson(j)) + .map((j) => MediaAttachmentFriendicaExtensions.fromJson(j, visibility)) .toList(); final likes = (json['friendica_activities']?['like'] as List? ?? []) @@ -53,7 +55,7 @@ extension TimelineEntryFriendicaExtensions on TimelineEntry { locationData: actualLocationData, body: body, youReshared: isReshare, - isPublic: isPublic, + visibility: visibility, id: id, parentId: parentId, parentAuthorId: parentAuthorId, diff --git a/lib/serializers/friendica/visibility_friendica_extensions.dart b/lib/serializers/friendica/visibility_friendica_extensions.dart new file mode 100644 index 0000000..09380b6 --- /dev/null +++ b/lib/serializers/friendica/visibility_friendica_extensions.dart @@ -0,0 +1,33 @@ +import '../../models/visibility.dart'; + +extension VisibilityFriendicaExtensions on Visibility { + static Visibility fromJson(Map json) { + final allowedUserIds = _parseAcl(json['allow_cid']); + final excludedGroupIds = _parseAcl(json['deny_cid']); + final allowedGroupIds = _parseAcl(json['allow_gid']); + final excludedUserIds = _parseAcl(json['deny_cid']); + final topLevelPrivate = json['friendica_private']; + late final VisibilityType type; + if (topLevelPrivate == null) { + type = allowedUserIds.isEmpty && + excludedUserIds.isEmpty && + allowedGroupIds.isEmpty && + excludedGroupIds.isEmpty + ? VisibilityType.public + : VisibilityType.private; + } else { + type = topLevelPrivate ? VisibilityType.public : VisibilityType.private; + } + + return Visibility( + type: type, + allowedUserIds: allowedUserIds, + excludedUserIds: excludedUserIds, + allowedGroupIds: allowedGroupIds, + excludedGroupIds: excludedGroupIds, + ); + } + + static List _parseAcl(String? acl) => + acl?.split(RegExp('[><]')).where((e) => e.isNotEmpty).toList() ?? []; +} diff --git a/lib/serializers/mastodon/media_attachment_mastodon_extension.dart b/lib/serializers/mastodon/media_attachment_mastodon_extension.dart new file mode 100644 index 0000000..e3b791d --- /dev/null +++ b/lib/serializers/mastodon/media_attachment_mastodon_extension.dart @@ -0,0 +1,20 @@ +import '../../models/attachment_media_type_enum.dart'; +import '../../models/media_attachment.dart'; +import '../../models/visibility.dart'; + +extension MediaAttachmentMastodonExtension on MediaAttachment { + static MediaAttachment fromJson( + Map json, Visibility visibility) { + return MediaAttachment( + id: json['id'] ?? '', + uri: Uri.parse(json['url'] ?? 'http://localhost'), + creationTimestamp: 0, + metadata: {}, + thumbnailUri: Uri.parse(json['url'] ?? 'http://localhost'), + title: '', + fullFileUri: Uri.parse(json['remote_url'] ?? 'http://localhost'), + explicitType: AttachmentMediaType.parse(json['type']), + description: json['description'] ?? '', + visibility: visibility); + } +} diff --git a/lib/serializers/mastodon/timeline_entry_mastodon_extensions.dart b/lib/serializers/mastodon/timeline_entry_mastodon_extensions.dart index 904be33..7111a08 100644 --- a/lib/serializers/mastodon/timeline_entry_mastodon_extensions.dart +++ b/lib/serializers/mastodon/timeline_entry_mastodon_extensions.dart @@ -4,8 +4,8 @@ import '../../globals.dart'; import '../../models/engagement_summary.dart'; import '../../models/link_data.dart'; import '../../models/location_data.dart'; -import '../../models/media_attachment.dart'; import '../../models/timeline_entry.dart'; +import '../../models/visibility.dart'; import '../../services/connections_manager.dart'; import '../../services/hashtag_service.dart'; import '../../utils/active_profile_selector.dart'; @@ -13,6 +13,7 @@ import '../../utils/dateutils.dart'; import 'connection_mastodon_extensions.dart'; import 'hashtag_mastodon_extensions.dart'; import 'link_preview_mastodon_extensions.dart'; +import 'media_attachment_mastodon_extension.dart'; final _logger = Logger('TimelineEntryMastodonExtensions'); @@ -29,7 +30,9 @@ extension TimelineEntryMastodonExtensions on TimelineEntry { : 0; final id = json['id'] ?? ''; final youReshared = json['reblogged'] ?? false; - final isPublic = json['visibility'] == 'public'; + final visibility = ['public', 'unlisted'].contains(json['visibility']) + ? Visibility.public() + : Visibility.private(); final parentId = json['in_reply_to_id'] ?? ''; final parentAuthor = json['in_reply_to_account_id'] ?? ''; final parentAuthorId = json['in_reply_to_account_id'] ?? ''; @@ -47,7 +50,8 @@ extension TimelineEntryMastodonExtensions on TimelineEntry { ? [] : [LinkData.fromMastodonJson(json['card'])]; final mediaAttachments = (json['media_attachments'] as List? ?? []) - .map((json) => MediaAttachment.fromMastodonJson(json)) + .map((json) => + MediaAttachmentMastodonExtension.fromJson(json, visibility)) .toList(); final favoritesCount = json['favourites_count'] ?? 0; final repliesCount = json['replies_count'] ?? 0; @@ -100,7 +104,7 @@ extension TimelineEntryMastodonExtensions on TimelineEntry { spoilerText: spoilerText, body: body, youReshared: youReshared, - isPublic: isPublic, + visibility: visibility, id: id, parentId: parentId, parentAuthorId: parentAuthorId,