Refactor how reshares are handled, interactions with posts to take into account that, and refreshing it

codemagic-setup
Hank Grabowski 2023-06-26 17:40:47 -04:00
rodzic dda42c899a
commit df8ec9009f
14 zmienionych plików z 131 dodań i 154 usunięć

Wyświetl plik

@ -2,7 +2,6 @@ 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';
@ -112,18 +111,6 @@ class _InteractionsBarControlState extends State<InteractionsBarControl> {
}
Future<void> addComment() async {
final needingReshareIdFix = getIt<FriendicaVersionChecker>()
.canUseFeature(RelaticaFeatures.reshareIdFix);
final myId = getIt<AccountsService>().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}');
}

Wyświetl plik

@ -7,7 +7,9 @@ import '../../models/connection.dart';
import '../../models/timeline_entry.dart';
import '../../models/visibility.dart';
import '../../routes.dart';
import '../../services/auth_service.dart';
import '../../services/connections_manager.dart';
import '../../services/reshared_via_service.dart';
import '../../utils/active_profile_selector.dart';
import '../../utils/dateutils.dart';
import '../image_control.dart';
@ -32,14 +34,21 @@ class StatusHeaderControl extends StatelessWidget {
Widget build(BuildContext context) {
late final Connection author;
late final Connection reshareAuthor;
final activeProfile = getIt<AccountsService>().currentProfile;
final reshareId = getIt<ActiveProfileSelector<ReshareViaService>>()
.getForProfile(activeProfile)
.transform((s) => s.getForPost(entry.id)?.resharers.firstOrNull)
.getValueOrElse(() => null);
getIt<ActiveProfileSelector<ConnectionsManager>>().activeEntry.match(
getIt<ActiveProfileSelector<ConnectionsManager>>()
.getForProfile(activeProfile)
.match(
onSuccess: (manager) {
author =
manager.getById(entry.authorId).getValueOrElse(() => Connection());
reshareAuthor = manager
.getById(entry.reshareAuthorId)
.getValueOrElse(() => Connection());
reshareAuthor = reshareId == null
? Connection()
: manager.getById(reshareId).getValueOrElse(() => Connection());
},
onError: (error) {
_logger.severe('Error getting connections manageR: $error');
@ -75,9 +84,10 @@ class StatusHeaderControl extends StatelessWidget {
),
],
),
if (reshareAuthor.isNotEmpty) ...[
if (reshareAuthor.isNotEmpty &&
author.id != activeProfile.userId) ...[
const HorizontalPadding(width: 3.0),
const Text('reshared post by: '),
const Text('reshared by '),
const HorizontalPadding(width: 3.0),
Row(
mainAxisSize: MainAxisSize.min,

Wyświetl plik

@ -29,6 +29,7 @@ import 'services/interactions_manager.dart';
import 'services/network_status_service.dart';
import 'services/notifications_manager.dart';
import 'services/persistent_info_service.dart';
import 'services/reshared_via_service.dart';
import 'services/secrets_service.dart';
import 'services/setting_service.dart';
import 'services/timeline_entry_filter_service.dart';
@ -91,6 +92,9 @@ Future<void> dependencyInjectionInitialization() async {
getIt.registerSingleton<AccountsService>(accountsService);
getIt<ActiveProfileSelector<IConnectionsRepo>>().subscribeToProfileSwaps();
getIt<ActiveProfileSelector<InstanceInfo>>().subscribeToProfileSwaps();
getIt.registerSingleton<ActiveProfileSelector<ReshareViaService>>(
ActiveProfileSelector((p) => ReshareViaService())
..subscribeToProfileSwaps());
getIt.registerSingleton<ActiveProfileSelector<IGroupsRepo>>(
ActiveProfileSelector((p) => MemoryGroupsRepo())

Wyświetl plik

@ -154,6 +154,8 @@ final FriendicaVersion v2023_04_01 = FriendicaVersion(
DateTime(2023, 04),
extra: '1',
);
final FriendicaVersion v2023_05 = FriendicaVersion(DateTime(2023, 05));
final FriendicaVersion v2023_09 = FriendicaVersion(DateTime(2023, 09));
final knownFriendicaVersions = [
// 2018 Versions
@ -192,6 +194,8 @@ final knownFriendicaVersions = [
v2023_01,
v2023_04,
v2023_04_01,
v2023_05,
v2023_09,
];
FriendicaVersion latestVersion() => knownFriendicaVersions.last;

Wyświetl plik

@ -16,12 +16,6 @@ class TimelineEntry {
final String parentAuthorId;
final String reshareOriginalPostId;
final String reshareAuthor;
final String reshareAuthorId;
final int creationTimestamp;
final int backdatedTimestamp;
@ -65,7 +59,6 @@ class TimelineEntry {
TimelineEntry(
{this.id = '',
this.parentId = '',
this.reshareOriginalPostId = '',
this.creationTimestamp = 0,
this.backdatedTimestamp = 0,
this.modificationTimestamp = 0,
@ -78,8 +71,6 @@ class TimelineEntry {
this.authorId = '',
this.parentAuthor = '',
this.parentAuthorId = '',
this.reshareAuthor = '',
this.reshareAuthorId = '',
this.externalLink = '',
this.locationData = const LocationData(),
this.isFavorited = false,
@ -97,7 +88,6 @@ 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()
@ -111,8 +101,6 @@ class TimelineEntry {
authorId = 'Random authorId ${randomId()}',
parentAuthor = 'Random parent author ${randomId()}',
parentAuthorId = 'Random parent author id ${randomId()}',
reshareAuthor = 'Random parent author ${randomId()}',
reshareAuthorId = 'Random parent author id ${randomId()}',
locationData = LocationData.randomBuilt(),
isFavorited = DateTime.now().second ~/ 2 == 0 ? true : false,
tags = [],
@ -158,8 +146,6 @@ 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,
@ -171,8 +157,6 @@ class TimelineEntry {
authorId: authorId ?? this.authorId,
parentAuthor: parentAuthor ?? this.parentAuthor,
parentAuthorId: parentAuthorId ?? this.parentAuthorId,
reshareAuthor: parentAuthor ?? this.reshareAuthor,
reshareAuthorId: parentAuthorId ?? this.reshareAuthorId,
locationData: locationData ?? this.locationData,
isFavorited: isFavorited ?? this.isFavorited,
tags: tags ?? this.tags,
@ -200,12 +184,9 @@ class TimelineEntry {
other is TimelineEntry &&
runtimeType == other.runtimeType &&
id == other.id &&
reshareOriginalPostId == other.reshareOriginalPostId &&
parentId == other.parentId &&
parentAuthor == other.parentAuthor &&
parentAuthorId == other.parentAuthorId &&
reshareAuthor == other.reshareAuthor &&
reshareAuthorId == other.reshareAuthorId &&
creationTimestamp == other.creationTimestamp &&
backdatedTimestamp == other.backdatedTimestamp &&
modificationTimestamp == other.modificationTimestamp &&
@ -229,12 +210,9 @@ class TimelineEntry {
@override
int get hashCode =>
id.hashCode ^
reshareOriginalPostId.hashCode ^
parentId.hashCode ^
parentAuthor.hashCode ^
parentAuthorId.hashCode ^
reshareAuthor.hashCode ^
reshareAuthorId.hashCode ^
creationTimestamp.hashCode ^
backdatedTimestamp.hashCode ^
modificationTimestamp.hashCode ^

Wyświetl plik

@ -42,10 +42,10 @@ extension NotificationMastodonExtension on UserNotification {
case NotificationType.unknown:
content = '${from.name} has unknown interaction notification';
break;
case NotificationType.favourite:
case NotificationType.mention:
case NotificationType.reshare:
case NotificationType.reblog:
case NotificationType.favourite:
case NotificationType.mention:
case NotificationType.status:
final status = TimelineEntryMastodonExtensions.fromJson(json['status']);
statusId = status.id;
@ -58,11 +58,8 @@ extension NotificationMastodonExtension on UserNotification {
final baseContent = type == NotificationType.mention
? "${from.name} ${type.toVerb()}"
: "${from.name} ${type.toVerb()} ${status.author}'s";
final shareInfo = status.reshareAuthorId.isNotEmpty
? "reshare of ${status.reshareAuthor}'s"
: '';
final bodyText = htmlToSimpleText(status.body).truncate(length: 100);
content = "$baseContent $shareInfo $referenceType: $bodyText";
content = "$baseContent $referenceType: $bodyText";
break;
case NotificationType.direct_message:
// this is a Relatica internal type so nothing to do here

Wyświetl plik

@ -1,4 +1,6 @@
import 'package:logging/logging.dart';
import 'package:relatica/services/auth_service.dart';
import 'package:relatica/services/reshared_via_service.dart';
import '../../globals.dart';
import '../../models/engagement_summary.dart';
@ -19,6 +21,45 @@ final _logger = Logger('TimelineEntryMastodonExtensions');
extension TimelineEntryMastodonExtensions on TimelineEntry {
static TimelineEntry fromJson(Map<String, dynamic> json) {
final activeProfile = getIt<AccountsService>().currentProfile;
final resharedViaService = getIt<ActiveProfileSelector<ReshareViaService>>()
.getForProfile(activeProfile)
.fold(
onSuccess: (s) => s,
onError: (error) {
_logger.severe('Error getting reshared via service: $error');
return null;
});
final connectionManager = getIt<ActiveProfileSelector<ConnectionsManager>>()
.getForProfile(activeProfile)
.fold(
onSuccess: (m) => m,
onError: (error) {
_logger.severe('Error getting connection manager: $error');
return null;
});
final id = json['id'] ?? '';
final parentId = json['in_reply_to_id'] ?? '';
final parentAuthor = json['in_reply_to_account_id'] ?? '';
final parentAuthorId = json['in_reply_to_account_id'] ?? '';
final author = json['account']['display_name'];
final authorId = json['account']['id'];
final resharePostData = json['reblog'];
if (resharePostData != null) {
final rebloggedUser =
ConnectionMastodonExtensions.fromJson(resharePostData['account']);
connectionManager?.upsertConnection(rebloggedUser);
final resharedPost = fromJson(resharePostData);
resharedViaService?.upsertResharedVia(
postId: resharedPost.id,
resharerId: authorId,
);
return resharedPost;
}
final int timestamp = json.containsKey('created_at')
? OffsetDateTimeUtils.epochSecTimeFromTimeZoneString(json['created_at'])
.fold(
@ -28,21 +69,18 @@ extension TimelineEntryMastodonExtensions on TimelineEntry {
return 0;
})
: 0;
final id = json['id'] ?? '';
final youReshared = json['reblogged'] ?? false;
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'] ?? '';
final body = json['content'] ?? '';
final author = json['account']['display_name'];
final authorId = json['account']['id'];
const title = '';
final body = json['content'] ?? '';
final spoilerText = json['spoiler_text'] ?? '';
final externalLink = json['uri'] ?? '';
final actualLocationData = LocationData();
const actualLocationData = LocationData();
final modificationTimestamp = timestamp;
final backdatedTimestamp = timestamp;
final isFavorited = json['favourited'] ?? false;
@ -61,35 +99,9 @@ extension TimelineEntryMastodonExtensions on TimelineEntry {
rebloggedCount: rebloggedCount,
repliesCount: repliesCount,
);
final linkPreviewData =
LinkPreviewMastodonExtensions.fromJson(json['card']);
final connectionManager =
getIt<ActiveProfileSelector<ConnectionsManager>>().activeEntry.fold(
onSuccess: (m) => m,
onError: (error) {
_logger.severe('Error getting connection manager: $error');
return null;
});
final connection = ConnectionMastodonExtensions.fromJson(json['account']);
connectionManager?.upsertConnection(connection);
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 linkPreviewData = LinkPreviewMastodonExtensions.fromJson(
json['card'],
);
final List<dynamic>? tagsJson = json['tags'];
final tags = <String>[];
if (tagsJson?.isNotEmpty ?? false) {
@ -101,6 +113,9 @@ extension TimelineEntryMastodonExtensions on TimelineEntry {
}
}
final connection = ConnectionMastodonExtensions.fromJson(json['account']);
connectionManager?.upsertConnection(connection);
return TimelineEntry(
creationTimestamp: timestamp,
modificationTimestamp: modificationTimestamp,
@ -111,11 +126,8 @@ extension TimelineEntryMastodonExtensions on TimelineEntry {
youReshared: youReshared,
visibility: visibility,
id: id,
reshareOriginalPostId: reshareOriginalPostId,
parentId: parentId,
parentAuthorId: parentAuthorId,
reshareAuthor: reshareAuthor,
reshareAuthorId: reshareAuthorId,
isFavorited: isFavorited,
externalLink: externalLink,
author: author,

Wyświetl plik

@ -5,7 +5,6 @@ import 'package:result_monad/result_monad.dart';
import '../friendica_client/friendica_client.dart';
import '../friendica_client/paging_data.dart';
import '../globals.dart';
import '../models/TimelineIdentifiers.dart';
import '../models/auth/profile.dart';
import '../models/entry_tree_item.dart';
@ -14,7 +13,6 @@ import '../models/image_entry.dart';
import '../models/media_attachment_uploads/new_entry_media_items.dart';
import '../models/timeline_entry.dart';
import '../models/visibility.dart';
import 'feature_version_checker.dart';
import 'media_upload_attachment_helper.dart';
class EntryManagerService extends ChangeNotifier {
@ -48,7 +46,7 @@ class EntryManagerService extends ChangeNotifier {
Result<EntryTreeItem, ExecError> getPostTreeEntryBy(String id) {
_logger.finest('Getting post: $id');
final idForCall = mapInteractionId(id);
final idForCall = id;
final postNode = _getPostRootNode(idForCall);
if (postNode == null) {
return Result.error(ExecError(
@ -180,7 +178,7 @@ class EntryManagerService extends ChangeNotifier {
required Visibility newMediaItemVisibility,
}) async {
_logger.finest('Editing post: $text');
final idForCall = mapInteractionId(id);
final idForCall = id;
final mediaIds = existingMediaItems
.map((m) => m.scales.isEmpty ? m.id : m.scales.first.id)
.toList();
@ -397,54 +395,20 @@ class EntryManagerService extends ChangeNotifier {
return updatedPosts;
}
String mapInteractionId(String id) {
return getEntryById(id).transform((e) {
if (e.reshareOriginalPostId.isEmpty) {
return id;
}
final fvc = getIt<FriendicaVersionChecker>();
if (fvc.canUseFeature(RelaticaFeatures.reshareIdFix)) {
return e.reshareOriginalPostId;
}
return id;
}).getValueOrElse(() => id);
}
FutureResult<EntryTreeItem, ExecError> refreshStatusChain(String id) async {
_logger.finest('Refreshing post: $id');
final client = StatusesClient(profile);
final idForCall = mapInteractionId(id);
var parentId = '';
final result = await client
.getPostOrComment(idForCall, fullContext: false)
.withResult((entries) =>
parentId = entries.isEmpty ? '' : entries.first.parentId)
.getPostOrComment(id, fullContext: false)
.andThenAsync((rootItems) async => await client
.getPostOrComment(idForCall, fullContext: true)
.getPostOrComment(id, fullContext: true)
.andThenSuccessAsync(
(contextItems) async => [...rootItems, ...contextItems]))
.withResult((items) async {
_cleanupEntriesForId(id);
await processNewItems(items, client.profile.username, null);
});
if (parentId.isNotEmpty &&
getIt<FriendicaVersionChecker>()
.canUseFeature(RelaticaFeatures.reshareIdFix)) {
final parentIdForCall = mapInteractionId(parentId);
await client
.getPostOrComment(parentIdForCall, fullContext: false)
.withResult((entries) =>
parentId = entries.isEmpty ? '' : entries.first.parentId)
.andThenAsync((rootItems) async => await client
.getPostOrComment(idForCall, fullContext: true)
.transformAsync(
(contextItems) async => [...rootItems, ...contextItems]))
.withResult((items) async {
await processNewItems(items, client.profile.username, null);
});
}
return result.mapValue((_) {
_logger.finest('$id post updated');
return _nodeToTreeItem(_getPostRootNode(id)!, client.profile.userId);
@ -462,7 +426,7 @@ class EntryManagerService extends ChangeNotifier {
FutureResult<EntryTreeItem, ExecError> resharePost(String id) async {
_logger.finest('Resharing post: $id');
final client = StatusesClient(profile);
final idForCall = mapInteractionId(id);
final idForCall = id;
final result =
await client.resharePost(idForCall).andThenSuccessAsync((item) async {
await processNewItems([item], client.profile.username, null);
@ -485,7 +449,7 @@ class EntryManagerService extends ChangeNotifier {
FutureResult<bool, ExecError> unResharePost(String id) async {
_logger.finest('Unresharing post: $id');
final client = StatusesClient(profile);
final idForCall = mapInteractionId(id);
final idForCall = id;
final result =
await client.unResharePost(idForCall).andThenSuccessAsync((item) async {
await processNewItems([item], client.profile.username, null);
@ -505,7 +469,7 @@ class EntryManagerService extends ChangeNotifier {
String id, bool newStatus) async {
final interactionClient = InteractionsClient(profile);
final postsClient = StatusesClient(profile);
final idForCall = mapInteractionId(id);
final idForCall = id;
final result =
await interactionClient.changeFavoriteStatus(idForCall, newStatus);
if (result.isFailure) {
@ -533,8 +497,7 @@ class EntryManagerService extends ChangeNotifier {
childenEntries[c.id] = _nodeToTreeItem(c, currentId);
}
final entry = _entries[node.id]!;
final isMine =
entry.authorId == currentId || entry.reshareAuthorId == currentId;
final isMine = entry.authorId == currentId;
return EntryTreeItem(
_entries[node.id]!,
isMine: isMine,
@ -544,7 +507,9 @@ class EntryManagerService extends ChangeNotifier {
void _cleanupEntriesForId(String id) {
if (_parentPostIds.containsKey(id)) {
_parentPostIds.remove(id);
final parentPostId = _parentPostIds.remove(id);
final parentPostNode = _postNodes[parentPostId];
parentPostNode?.removeChildById(id);
}
if (_entries.containsKey(id)) {
@ -573,7 +538,7 @@ class _Node {
_children[node.id] = node;
}
_Node? removeChildById(String id) {
void removeChildById(String id) {
if (_children.containsKey(id)) {
_children.remove(id);
}

Wyświetl plik

@ -78,9 +78,6 @@ class FriendicaVersionChecker {
v2023_04,
),
RelaticaFeatures.postSpoilerText: FriendicaVersionRequirement(v2023_04),
RelaticaFeatures.reshareIdFix: FriendicaVersionRequirement(
v2023_04,
),
RelaticaFeatures.statusEditing: FriendicaVersionRequirement(v2023_04),
RelaticaFeatures.usingActualFollowRequests: FriendicaVersionRequirement(
v2023_04,

Wyświetl plik

@ -6,8 +6,7 @@ import '../globals.dart';
import '../models/auth/profile.dart';
import '../models/connection.dart';
import '../models/exec_error.dart';
import '../utils/active_profile_selector.dart';
import 'entry_manager_service.dart';
import 'auth_service.dart';
class InteractionsManager extends ChangeNotifier {
final _likesByStatusId = <String, List<Connection>>{};
@ -43,8 +42,9 @@ class InteractionsManager extends ChangeNotifier {
FutureResult<List<Connection>, ExecError> updateLikesForStatus(
String statusId) async {
final idForCall = _mapStatusId(statusId);
final likesResult = await InteractionsClient(profile).getLikes(idForCall);
final likesResult =
await InteractionsClient(getIt<AccountsService>().currentProfile)
.getLikes(statusId);
if (likesResult.isSuccess) {
_likesByStatusId[statusId] = likesResult.value;
notifyListeners();
@ -54,20 +54,13 @@ class InteractionsManager extends ChangeNotifier {
FutureResult<List<Connection>, ExecError> updateResharesForStatus(
String statusId) async {
final idForCall = _mapStatusId(statusId);
final resharesResult =
await InteractionsClient(profile).getReshares(idForCall);
await InteractionsClient(getIt<AccountsService>().currentProfile)
.getReshares(statusId);
if (resharesResult.isSuccess) {
_resharesByStatusId[statusId] = resharesResult.value;
notifyListeners();
}
return resharesResult;
}
String _mapStatusId(String statusId) {
return getIt<ActiveProfileSelector<EntryManagerService>>()
.getForProfile(profile)
.transform((m) => m.mapInteractionId(statusId))
.getValueOrElse(() => statusId);
}
}

Wyświetl plik

@ -0,0 +1,31 @@
class ReshareViaService {
final _postsVia = <String, ResharedViaData>{};
ResharedViaData? getForPost(String postId) => _postsVia[postId];
void upsertResharedVia({required String postId, required String resharerId}) {
final resharedData = _postsVia.putIfAbsent(
postId,
() => ResharedViaData(postId: postId),
);
_postsVia[postId] = resharedData.withUpsertedResharer(resharerId);
}
}
class ResharedViaData {
final String postId;
final Set<String> resharers;
bool get hasResharedVia => resharers.isNotEmpty;
const ResharedViaData({
required this.postId,
this.resharers = const {},
});
ResharedViaData withUpsertedResharer(String resharerId) => ResharedViaData(
postId: postId,
resharers: {resharerId, ...resharers},
);
}

Wyświetl plik

@ -95,7 +95,6 @@ extension TimelineEntryFilterOps on TimelineEntryFilter {
var authorFiltered = authorFilters.isEmpty ? true : false;
for (final filter in authorFilters) {
if (filter.isFiltered(entry.authorId) ||
filter.isFiltered(entry.reshareAuthorId) ||
filter.isFiltered(entry.parentAuthorId)) {
authorFiltered = true;
break;

Wyświetl plik

@ -1452,5 +1452,5 @@ packages:
source: hosted
version: "3.1.1"
sdks:
dart: ">=3.0.0-0 <4.0.0"
dart: ">=3.0.0 <4.0.0"
flutter: ">=3.7.0"

Wyświetl plik

@ -5,7 +5,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 0.6.0+1
environment:
sdk: '>=2.18.2 <3.0.0'
sdk: '>=3.0.0 <4.0.0'
dependencies:
flutter: