diff --git a/CHANGELOG.md b/CHANGELOG.md index 9eac46e..4f09588 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ to make the zooming in of images more predictable. * Fixes * Seemingly disappearing contacts on Contacts screen have been corrected. + * Workaround for a regresssion in Friendica 2023.04 for reshared posts * New Features * Responsive design for allowing the image and video attachments to scale up for larger videos but limit timeline/list width on very large screens. diff --git a/lib/controls/timeline/interactions_bar_control.dart b/lib/controls/timeline/interactions_bar_control.dart index 831879d..f8728e3 100644 --- a/lib/controls/timeline/interactions_bar_control.dart +++ b/lib/controls/timeline/interactions_bar_control.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:logging/logging.dart'; import 'package:relatica/models/exec_error.dart'; +import 'package:relatica/services/auth_service.dart'; import 'package:result_monad/result_monad.dart'; import '../../globals.dart'; @@ -111,7 +112,21 @@ class _InteractionsBarControlState extends State { } Future addComment() async { - context.push('/comment/new?parent_id=${widget.entry.id}'); + final needingReshareIdFix = getIt() + .canUseFeature(RelaticaFeatures.reshareIdFix); + final myId = getIt().currentProfile.userId; + + if (needingReshareIdFix && + widget.entry.reshareOriginalPostId.isNotEmpty && + widget.entry.reshareAuthorId != myId) { + await showConfirmDialog(context, + 'Unable to comment on reshared posts with your current version of Friendica server.'); + return; + } + + if (mounted) { + context.push('/comment/new?parent_id=${widget.entry.id}'); + } } Future unResharePost() async { diff --git a/lib/models/friendica_version_requirement.dart b/lib/models/friendica_version_requirement.dart new file mode 100644 index 0000000..bb38983 --- /dev/null +++ b/lib/models/friendica_version_requirement.dart @@ -0,0 +1,32 @@ +import 'friendica_version.dart'; + +final FriendicaVersionRequirement unknownRequirement = + FriendicaVersionRequirement(unknown); + +class FriendicaVersionRequirement { + final FriendicaVersion minimumVersion; + final FriendicaVersion? maxVersion; + + const FriendicaVersionRequirement(this.minimumVersion, {this.maxVersion}); + + bool versionMeetsRequirement(FriendicaVersion version) { + if (version < minimumVersion) { + return false; + } + + if (maxVersion == null) { + return true; + } + + return version <= maxVersion!; + } + + @override + String toString() { + if (maxVersion == null) { + return 'requires at least Friendica $minimumVersion'; + } + + return 'works only on Friendica $minimumVersion to $maxVersion'; + } +} diff --git a/lib/models/timeline_entry.dart b/lib/models/timeline_entry.dart index 33b6d18..e11a14f 100644 --- a/lib/models/timeline_entry.dart +++ b/lib/models/timeline_entry.dart @@ -16,6 +16,8 @@ class TimelineEntry { final String parentAuthorId; + final String reshareOriginalPostId; + final String reshareAuthor; final String reshareAuthorId; @@ -61,6 +63,7 @@ class TimelineEntry { TimelineEntry( {this.id = '', this.parentId = '', + this.reshareOriginalPostId = '', this.creationTimestamp = 0, this.backdatedTimestamp = 0, this.modificationTimestamp = 0, @@ -91,6 +94,7 @@ class TimelineEntry { backdatedTimestamp = DateTime.now().millisecondsSinceEpoch, modificationTimestamp = DateTime.now().millisecondsSinceEpoch, id = randomId(), + reshareOriginalPostId = '', youReshared = DateTime.now().second ~/ 2 == 0 ? true : false, visibility = DateTime.now().second ~/ 2 == 0 ? Visibility.public() @@ -122,6 +126,7 @@ class TimelineEntry { bool? isReshare, Visibility? visibility, String? id, + String? reshareOriginalPostId, String? parentId, String? externalLink, String? body, @@ -148,6 +153,8 @@ class TimelineEntry { modificationTimestamp: modificationTimestamp ?? this.modificationTimestamp, id: id ?? this.id, + reshareOriginalPostId: + reshareOriginalPostId ?? this.reshareOriginalPostId, youReshared: isReshare ?? this.youReshared, visibility: visibility ?? this.visibility, parentId: parentId ?? this.parentId, @@ -187,6 +194,7 @@ class TimelineEntry { other is TimelineEntry && runtimeType == other.runtimeType && id == other.id && + reshareOriginalPostId == other.reshareOriginalPostId && parentId == other.parentId && parentAuthor == other.parentAuthor && parentAuthorId == other.parentAuthorId && @@ -214,6 +222,7 @@ class TimelineEntry { @override int get hashCode => id.hashCode ^ + reshareOriginalPostId.hashCode ^ parentId.hashCode ^ parentAuthor.hashCode ^ parentAuthorId.hashCode ^ diff --git a/lib/serializers/mastodon/timeline_entry_mastodon_extensions.dart b/lib/serializers/mastodon/timeline_entry_mastodon_extensions.dart index 0d6c235..f891eba 100644 --- a/lib/serializers/mastodon/timeline_entry_mastodon_extensions.dart +++ b/lib/serializers/mastodon/timeline_entry_mastodon_extensions.dart @@ -76,15 +76,18 @@ extension TimelineEntryMastodonExtensions on TimelineEntry { late final String reshareAuthor; late final String reshareAuthorId; + late final String reshareOriginalPostId; if (json['reblog'] != null) { final rebloggedUser = ConnectionMastodonExtensions.fromJson(json['reblog']['account']); connectionManager?.upsertConnection(rebloggedUser); reshareAuthor = rebloggedUser.name; reshareAuthorId = rebloggedUser.id; + reshareOriginalPostId = json['reblog']['id'] ?? id; } else { reshareAuthorId = ''; reshareAuthor = ''; + reshareOriginalPostId = ''; } final List? tags = json['tags']; @@ -106,6 +109,7 @@ extension TimelineEntryMastodonExtensions on TimelineEntry { youReshared: youReshared, visibility: visibility, id: id, + reshareOriginalPostId: reshareOriginalPostId, parentId: parentId, parentAuthorId: parentAuthorId, reshareAuthor: reshareAuthor, diff --git a/lib/services/connections_manager.dart b/lib/services/connections_manager.dart index 63940ee..899044f 100644 --- a/lib/services/connections_manager.dart +++ b/lib/services/connections_manager.dart @@ -19,6 +19,7 @@ class ConnectionsManager extends ChangeNotifier { static final _logger = Logger('$ConnectionsManager'); late final IConnectionsRepo conRepo; late final IGroupsRepo groupsRepo; + var groupsNotInitialized = true; ConnectionsManager(this.conRepo, this.groupsRepo); @@ -200,7 +201,8 @@ class ConnectionsManager extends ChangeNotifier { List getMyGroups() { final myGroups = groupsRepo.getMyGroups(); - if (myGroups.isEmpty) { + if (groupsNotInitialized) { + groupsNotInitialized = true; _updateMyGroups(true); } diff --git a/lib/services/entry_manager_service.dart b/lib/services/entry_manager_service.dart index e8b9ef4..26117ad 100644 --- a/lib/services/entry_manager_service.dart +++ b/lib/services/entry_manager_service.dart @@ -14,6 +14,7 @@ import '../models/media_attachment_uploads/new_entry_media_items.dart'; import '../models/timeline_entry.dart'; import '../models/visibility.dart'; import 'auth_service.dart'; +import 'feature_version_checker.dart'; import 'media_upload_attachment_helper.dart'; class EntryManagerService extends ChangeNotifier { @@ -44,8 +45,9 @@ class EntryManagerService extends ChangeNotifier { Result getPostTreeEntryBy(String id) { _logger.finest('Getting post: $id'); + final idForCall = mapInteractionId(id); final currentId = getIt().currentProfile.userId; - final postNode = _getPostRootNode(id); + final postNode = _getPostRootNode(idForCall); if (postNode == null) { return Result.error(ExecError( type: ErrorType.notFound, @@ -179,6 +181,7 @@ class EntryManagerService extends ChangeNotifier { required Visibility newMediaItemVisibility, }) async { _logger.finest('Editing post: $text'); + final idForCall = mapInteractionId(id); final mediaIds = existingMediaItems .map((m) => m.scales.isEmpty ? m.id : m.scales.first.id) .toList(); @@ -225,7 +228,10 @@ class EntryManagerService extends ChangeNotifier { final result = await StatusesClient(getIt().currentProfile) .editStatus( - id: id, text: text, spoilerText: spoilerText, mediaIds: mediaIds) + id: idForCall, + text: text, + spoilerText: spoilerText, + mediaIds: mediaIds) .andThenSuccessAsync((item) async { await processNewItems( [item], getIt().currentProfile.username, null); @@ -394,13 +400,29 @@ class EntryManagerService extends ChangeNotifier { return updatedPosts; } + String mapInteractionId(String id) { + return getEntryById(id).transform((e) { + if (e.reshareOriginalPostId.isEmpty) { + return id; + } + + final fvc = getIt(); + if (fvc.canUseFeature(RelaticaFeatures.reshareIdFix)) { + return e.reshareOriginalPostId; + } + + return id; + }).getValueOrElse(() => id); + } + FutureResult refreshStatusChain(String id) async { _logger.finest('Refreshing post: $id'); final client = StatusesClient(getIt().currentProfile); + final idForCall = mapInteractionId(id); final result = await client - .getPostOrComment(id, fullContext: false) + .getPostOrComment(idForCall, fullContext: false) .andThenAsync((rootItems) async => await client - .getPostOrComment(id, fullContext: true) + .getPostOrComment(idForCall, fullContext: true) .andThenSuccessAsync( (contextItems) async => [...rootItems, ...contextItems])) .andThenSuccessAsync((items) async { @@ -424,8 +446,9 @@ class EntryManagerService extends ChangeNotifier { FutureResult resharePost(String id) async { _logger.finest('Resharing post: $id'); final client = StatusesClient(getIt().currentProfile); + final idForCall = mapInteractionId(id); final result = - await client.resharePost(id).andThenSuccessAsync((item) async { + await client.resharePost(idForCall).andThenSuccessAsync((item) async { await processNewItems([item], client.profile.username, null); }); @@ -446,8 +469,9 @@ class EntryManagerService extends ChangeNotifier { FutureResult unResharePost(String id) async { _logger.finest('Unresharing post: $id'); final client = StatusesClient(getIt().currentProfile); + final idForCall = mapInteractionId(id); final result = - await client.unResharePost(id).andThenSuccessAsync((item) async { + await client.unResharePost(idForCall).andThenSuccessAsync((item) async { await processNewItems([item], client.profile.username, null); }); @@ -463,20 +487,29 @@ class EntryManagerService extends ChangeNotifier { FutureResult toggleFavorited( String id, bool newStatus) async { - final client = InteractionsClient(getIt().currentProfile); - final result = await client.changeFavoriteStatus(id, newStatus); + final profile = getIt().currentProfile; + final interactionClient = InteractionsClient(profile); + final postsClient = StatusesClient(profile); + final idForCall = mapInteractionId(id); + final result = + await interactionClient.changeFavoriteStatus(idForCall, newStatus); if (result.isFailure) { return result.errorCast(); } - final update = result.value; + final updateResult = + await postsClient.getPostOrComment(id, fullContext: false); + if (updateResult.isFailure) { + return updateResult.errorCast(); + } + final update = updateResult.value.first; _entries[update.id] = update; final node = update.parentId.isEmpty ? _postNodes[update.id]! : _postNodes[_parentPostIds[update.id]]!; notifyListeners(); - return Result.ok(_nodeToTreeItem(node, client.profile.userId)); + return Result.ok(_nodeToTreeItem(node, interactionClient.profile.userId)); } EntryTreeItem _nodeToTreeItem(_Node node, String currentId) { diff --git a/lib/services/feature_version_checker.dart b/lib/services/feature_version_checker.dart index a62af2b..919e329 100644 --- a/lib/services/feature_version_checker.dart +++ b/lib/services/feature_version_checker.dart @@ -6,11 +6,13 @@ import 'package:result_monad/result_monad.dart'; import '../globals.dart'; import '../models/exec_error.dart'; import '../models/friendica_version.dart'; +import '../models/friendica_version_requirement.dart'; enum RelaticaFeatures { diasporaReshare('Resharing Diaspora Posts'), directMessageCreation('Direct message creation with OAuth login'), postSpoilerText('Spoiler Text on Posts'), + reshareIdFix('Reshare ID fix'), statusEditing('Post/Comment Editing'), usingActualFollowRequests( 'Using Follow Request System not Friend Request Notifications'), @@ -29,8 +31,8 @@ class FriendicaVersionChecker { const FriendicaVersionChecker(); bool canUseFeature(RelaticaFeatures feature) { - final neededVersion = featureVersionRequirement[feature]; - if (neededVersion == null) { + final requirement = featureVersionRequirement[feature]; + if (requirement == null) { _logger.severe( 'Return false since no minimum version data in table for: $feature', ); @@ -39,7 +41,9 @@ class FriendicaVersionChecker { return getIt>() .activeEntry - .andThenSuccess((info) => info.friendicaVersion >= neededVersion) + .andThenSuccess( + (info) => requirement.versionMeetsRequirement(info.friendicaVersion), + ) .fold( onSuccess: (versionMet) => versionMet, onError: (error) { @@ -61,17 +65,26 @@ class FriendicaVersionChecker { ); } - FriendicaVersion getVersionRequirement(RelaticaFeatures feature) => - featureVersionRequirement[feature] ?? unknown; + FriendicaVersionRequirement getVersionRequirement(RelaticaFeatures feature) => + featureVersionRequirement[feature] ?? unknownRequirement; String versionErrorString(RelaticaFeatures feature) => - "${feature.label} requires at least Friendica ${getVersionRequirement(feature).toVersionString()}"; + "${feature.label} ${getVersionRequirement(feature)}"; - static final featureVersionRequirement = { - RelaticaFeatures.diasporaReshare: v2023_04, - RelaticaFeatures.directMessageCreation: v2023_04, - RelaticaFeatures.postSpoilerText: v2023_04, - RelaticaFeatures.statusEditing: v2023_04, - RelaticaFeatures.usingActualFollowRequests: v2023_04, + static final featureVersionRequirement = + { + RelaticaFeatures.diasporaReshare: FriendicaVersionRequirement(v2023_04), + RelaticaFeatures.directMessageCreation: FriendicaVersionRequirement( + v2023_04, + ), + RelaticaFeatures.postSpoilerText: FriendicaVersionRequirement(v2023_04), + RelaticaFeatures.reshareIdFix: FriendicaVersionRequirement( + v2023_04, + maxVersion: v2023_04, + ), + RelaticaFeatures.statusEditing: FriendicaVersionRequirement(v2023_04), + RelaticaFeatures.usingActualFollowRequests: FriendicaVersionRequirement( + v2023_04, + ), }; } diff --git a/lib/services/interactions_manager.dart b/lib/services/interactions_manager.dart index b4bc322..faa4759 100644 --- a/lib/services/interactions_manager.dart +++ b/lib/services/interactions_manager.dart @@ -5,7 +5,9 @@ import '../friendica_client/friendica_client.dart'; import '../globals.dart'; import '../models/connection.dart'; import '../models/exec_error.dart'; +import '../utils/active_profile_selector.dart'; import 'auth_service.dart'; +import 'entry_manager_service.dart'; class InteractionsManager extends ChangeNotifier { final _likesByStatusId = >{}; @@ -31,9 +33,10 @@ class InteractionsManager extends ChangeNotifier { FutureResult, ExecError> updateLikesForStatus( String statusId) async { + final idForCall = _mapStatusId(statusId); final likesResult = await InteractionsClient(getIt().currentProfile) - .getLikes(statusId); + .getLikes(idForCall); if (likesResult.isSuccess) { _likesByStatusId[statusId] = likesResult.value; notifyListeners(); @@ -43,13 +46,21 @@ class InteractionsManager extends ChangeNotifier { FutureResult, ExecError> updateResharesForStatus( String statusId) async { + final idForCall = _mapStatusId(statusId); final resharesResult = await InteractionsClient(getIt().currentProfile) - .getReshares(statusId); + .getReshares(idForCall); if (resharesResult.isSuccess) { _resharesByStatusId[statusId] = resharesResult.value; notifyListeners(); } return resharesResult; } + + String _mapStatusId(String statusId) { + return getIt>() + .activeEntry + .transform((m) => m.mapInteractionId(statusId)) + .getValueOrElse(() => statusId); + } } diff --git a/lib/services/timeline_manager.dart b/lib/services/timeline_manager.dart index 1f5bf69..a0efabb 100644 --- a/lib/services/timeline_manager.dart +++ b/lib/services/timeline_manager.dart @@ -28,6 +28,7 @@ class TimelineManager extends ChangeNotifier { final IGroupsRepo groupsRepo; final EntryManagerService entryManagerService; + var groupsNotInitialized = true; final cachedTimelines = {}; @@ -41,8 +42,9 @@ class TimelineManager extends ChangeNotifier { } Result, ExecError> getGroups() { - if (groupsRepo.getMyGroups().isEmpty) { + if (groupsNotInitialized) { _refreshGroupData(); + groupsNotInitialized = false; return Result.ok([]); }