2023-03-21 01:55:47 +00:00
|
|
|
import 'package:flutter/material.dart' hide Visibility;
|
2022-11-17 16:04:14 +00:00
|
|
|
import 'package:logging/logging.dart';
|
2022-12-26 20:26:30 +00:00
|
|
|
import 'package:path/path.dart' as p;
|
2022-11-17 16:04:14 +00:00
|
|
|
import 'package:result_monad/result_monad.dart';
|
|
|
|
|
2023-01-24 00:56:04 +00:00
|
|
|
import '../friendica_client/friendica_client.dart';
|
2023-01-24 03:37:09 +00:00
|
|
|
import '../friendica_client/paging_data.dart';
|
2022-11-17 16:04:14 +00:00
|
|
|
import '../globals.dart';
|
|
|
|
import '../models/TimelineIdentifiers.dart';
|
2023-04-29 01:28:43 +00:00
|
|
|
import '../models/auth/profile.dart';
|
2022-11-17 16:04:14 +00:00
|
|
|
import '../models/entry_tree_item.dart';
|
|
|
|
import '../models/exec_error.dart';
|
2022-12-27 03:00:28 +00:00
|
|
|
import '../models/image_entry.dart';
|
|
|
|
import '../models/media_attachment_uploads/new_entry_media_items.dart';
|
2022-11-17 16:04:14 +00:00
|
|
|
import '../models/timeline_entry.dart';
|
2023-03-21 01:55:47 +00:00
|
|
|
import '../models/visibility.dart';
|
2023-04-27 13:13:13 +00:00
|
|
|
import 'feature_version_checker.dart';
|
2022-12-26 20:26:30 +00:00
|
|
|
import 'media_upload_attachment_helper.dart';
|
2022-11-17 16:04:14 +00:00
|
|
|
|
|
|
|
class EntryManagerService extends ChangeNotifier {
|
|
|
|
static final _logger = Logger('$EntryManagerService');
|
2022-11-21 21:21:45 +00:00
|
|
|
final _entries = <String, TimelineEntry>{};
|
|
|
|
final _parentPostIds = <String, String>{};
|
|
|
|
final _postNodes = <String, _Node>{};
|
2023-04-29 01:28:43 +00:00
|
|
|
final Profile profile;
|
|
|
|
|
|
|
|
EntryManagerService(this.profile);
|
2022-11-17 16:04:14 +00:00
|
|
|
|
2022-11-21 03:26:49 +00:00
|
|
|
void clear() {
|
2022-11-21 21:21:45 +00:00
|
|
|
_entries.clear();
|
|
|
|
_parentPostIds.clear();
|
2022-12-27 04:36:04 +00:00
|
|
|
_postNodes.clear();
|
2022-11-21 03:26:49 +00:00
|
|
|
}
|
|
|
|
|
2022-11-30 00:56:14 +00:00
|
|
|
_Node? _getPostRootNode(String id) {
|
|
|
|
final fromPosts = _postNodes[id];
|
|
|
|
if (fromPosts != null) {
|
|
|
|
return fromPosts;
|
|
|
|
}
|
|
|
|
|
|
|
|
final parentId = _parentPostIds[id];
|
|
|
|
if (parentId == null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return _postNodes[parentId];
|
|
|
|
}
|
|
|
|
|
2022-11-23 20:48:09 +00:00
|
|
|
Result<EntryTreeItem, ExecError> getPostTreeEntryBy(String id) {
|
|
|
|
_logger.finest('Getting post: $id');
|
2023-04-27 13:13:13 +00:00
|
|
|
final idForCall = mapInteractionId(id);
|
|
|
|
final postNode = _getPostRootNode(idForCall);
|
2022-11-23 20:48:09 +00:00
|
|
|
if (postNode == null) {
|
|
|
|
return Result.error(ExecError(
|
|
|
|
type: ErrorType.notFound,
|
|
|
|
message: 'Unknown post id: $id',
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
2023-04-29 01:28:43 +00:00
|
|
|
return Result.ok(_nodeToTreeItem(postNode, profile.userId));
|
2022-11-23 20:48:09 +00:00
|
|
|
}
|
|
|
|
|
2022-11-23 02:59:08 +00:00
|
|
|
Result<TimelineEntry, ExecError> getEntryById(String id) {
|
|
|
|
if (_entries.containsKey(id)) {
|
|
|
|
return Result.ok(_entries[id]!);
|
|
|
|
}
|
|
|
|
|
|
|
|
return Result.error(ExecError(
|
|
|
|
type: ErrorType.notFound,
|
|
|
|
message: 'Timeline entry not found: $id',
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
2022-12-27 04:36:04 +00:00
|
|
|
FutureResult<bool, ExecError> deleteEntryById(String id) async {
|
|
|
|
_logger.finest('Delete entry: $id');
|
2023-04-29 01:28:43 +00:00
|
|
|
final result = await StatusesClient(profile).deleteEntryById(id);
|
2022-12-27 04:36:04 +00:00
|
|
|
if (result.isFailure) {
|
|
|
|
return result.errorCast();
|
|
|
|
}
|
|
|
|
|
|
|
|
_cleanupEntriesForId(id);
|
|
|
|
notifyListeners();
|
|
|
|
return Result.ok(true);
|
|
|
|
}
|
|
|
|
|
2023-03-14 21:34:18 +00:00
|
|
|
FutureResult<bool, ExecError> createNewStatus(
|
|
|
|
String text, {
|
2022-12-26 20:26:30 +00:00
|
|
|
String spoilerText = '',
|
|
|
|
String inReplyToId = '',
|
2022-12-27 03:00:28 +00:00
|
|
|
required NewEntryMediaItems mediaItems,
|
|
|
|
required List<ImageEntry> existingMediaItems,
|
2023-03-21 01:55:47 +00:00
|
|
|
required Visibility visibility,
|
2022-12-26 20:26:30 +00:00
|
|
|
}) async {
|
2022-11-22 18:55:50 +00:00
|
|
|
_logger.finest('Creating new post: $text');
|
2022-12-27 03:00:28 +00:00
|
|
|
final mediaIds = existingMediaItems.map((m) => m.scales.first.id).toList();
|
2022-12-26 20:26:30 +00:00
|
|
|
for (final item in mediaItems.attachments) {
|
|
|
|
if (item.isExistingServerItem) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
final String extension = p.extension(item.localFilePath);
|
|
|
|
late final String filename;
|
|
|
|
if (item.remoteFilename.isEmpty) {
|
|
|
|
filename = p.basename(item.localFilePath);
|
|
|
|
} else {
|
|
|
|
if (item.remoteFilename
|
|
|
|
.toLowerCase()
|
|
|
|
.endsWith(extension.toLowerCase())) {
|
|
|
|
filename = item.remoteFilename;
|
|
|
|
} else {
|
|
|
|
filename = "${item.remoteFilename}$extension";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
final uploadResult =
|
2023-03-14 21:34:18 +00:00
|
|
|
await MediaUploadAttachmentHelper.getUploadableImageBytes(
|
2022-12-26 20:26:30 +00:00
|
|
|
item.localFilePath,
|
|
|
|
).andThenAsync(
|
2023-03-14 21:34:18 +00:00
|
|
|
(imageBytes) async =>
|
2023-04-29 01:28:43 +00:00
|
|
|
await RemoteFileClient(profile).uploadFileAsAttachment(
|
2022-12-26 20:26:30 +00:00
|
|
|
bytes: imageBytes,
|
|
|
|
album: mediaItems.albumName,
|
|
|
|
description: item.description,
|
|
|
|
fileName: filename,
|
2023-03-21 01:55:47 +00:00
|
|
|
visibility: visibility,
|
2022-12-26 20:26:30 +00:00
|
|
|
),
|
|
|
|
);
|
|
|
|
if (uploadResult.isSuccess) {
|
|
|
|
mediaIds.add(uploadResult.value.scales.first.id);
|
|
|
|
} else {
|
|
|
|
return Result.error(ExecError(
|
|
|
|
type: ErrorType.localError,
|
|
|
|
message: 'Error uploading image: ${uploadResult.error}'));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-29 01:28:43 +00:00
|
|
|
final result = await StatusesClient(profile)
|
2023-02-27 03:12:40 +00:00
|
|
|
.createNewStatus(
|
2023-03-14 21:34:18 +00:00
|
|
|
text: text,
|
|
|
|
spoilerText: spoilerText,
|
|
|
|
inReplyToId: inReplyToId,
|
2023-03-21 01:55:47 +00:00
|
|
|
mediaIds: mediaIds,
|
|
|
|
visibility: visibility)
|
2023-02-27 03:12:40 +00:00
|
|
|
.andThenSuccessAsync((item) async {
|
2023-04-29 01:28:43 +00:00
|
|
|
await processNewItems([item], profile.username, null);
|
2022-11-22 18:55:50 +00:00
|
|
|
return item;
|
2022-11-23 02:59:08 +00:00
|
|
|
}).andThenSuccessAsync((item) async {
|
|
|
|
if (inReplyToId.isNotEmpty) {
|
|
|
|
late final rootPostId;
|
|
|
|
if (_postNodes.containsKey(inReplyToId)) {
|
|
|
|
rootPostId = inReplyToId;
|
|
|
|
} else {
|
|
|
|
rootPostId = _parentPostIds[inReplyToId];
|
|
|
|
}
|
2022-11-30 00:56:14 +00:00
|
|
|
await refreshStatusChain(rootPostId);
|
2022-11-23 02:59:08 +00:00
|
|
|
}
|
|
|
|
return item;
|
2022-11-22 18:55:50 +00:00
|
|
|
});
|
|
|
|
|
2022-11-23 02:59:08 +00:00
|
|
|
return result.mapValue((status) {
|
|
|
|
_logger.finest('${status.id} status created');
|
|
|
|
return true;
|
2022-11-22 18:55:50 +00:00
|
|
|
}).mapError(
|
2023-03-14 21:34:18 +00:00
|
|
|
(error) {
|
2022-11-22 18:55:50 +00:00
|
|
|
_logger.finest('Error creating post: $error');
|
|
|
|
return ExecError(
|
|
|
|
type: ErrorType.localError,
|
|
|
|
message: error.toString(),
|
|
|
|
);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-03-16 15:37:46 +00:00
|
|
|
FutureResult<bool, ExecError> editStatus(
|
|
|
|
String id,
|
|
|
|
String text, {
|
|
|
|
String spoilerText = '',
|
|
|
|
required NewEntryMediaItems mediaItems,
|
|
|
|
required List<ImageEntry> existingMediaItems,
|
2023-03-21 01:55:47 +00:00
|
|
|
required Visibility newMediaItemVisibility,
|
2023-03-16 15:37:46 +00:00
|
|
|
}) async {
|
|
|
|
_logger.finest('Editing post: $text');
|
2023-04-27 13:13:13 +00:00
|
|
|
final idForCall = mapInteractionId(id);
|
2023-03-16 15:37:46 +00:00
|
|
|
final mediaIds = existingMediaItems
|
|
|
|
.map((m) => m.scales.isEmpty ? m.id : m.scales.first.id)
|
|
|
|
.toList();
|
|
|
|
for (final item in mediaItems.attachments) {
|
|
|
|
if (item.isExistingServerItem) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
final String extension = p.extension(item.localFilePath);
|
|
|
|
late final String filename;
|
|
|
|
if (item.remoteFilename.isEmpty) {
|
|
|
|
filename = p.basename(item.localFilePath);
|
|
|
|
} else {
|
|
|
|
if (item.remoteFilename
|
|
|
|
.toLowerCase()
|
|
|
|
.endsWith(extension.toLowerCase())) {
|
|
|
|
filename = item.remoteFilename;
|
|
|
|
} else {
|
|
|
|
filename = "${item.remoteFilename}$extension";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
final uploadResult =
|
|
|
|
await MediaUploadAttachmentHelper.getUploadableImageBytes(
|
|
|
|
item.localFilePath,
|
|
|
|
).andThenAsync(
|
2023-04-29 01:28:43 +00:00
|
|
|
(imageBytes) async => await RemoteFileClient(profile)
|
|
|
|
.uploadFileAsAttachment(
|
|
|
|
bytes: imageBytes,
|
|
|
|
album: mediaItems.albumName,
|
|
|
|
description: item.description,
|
|
|
|
fileName: filename,
|
|
|
|
visibility: newMediaItemVisibility),
|
2023-03-16 15:37:46 +00:00
|
|
|
);
|
|
|
|
if (uploadResult.isSuccess) {
|
|
|
|
mediaIds.add(uploadResult.value.scales.first.id);
|
|
|
|
} else {
|
|
|
|
return Result.error(ExecError(
|
|
|
|
type: ErrorType.localError,
|
|
|
|
message: 'Error uploading image: ${uploadResult.error}'));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-29 01:28:43 +00:00
|
|
|
final result = await StatusesClient(profile)
|
2023-03-16 15:37:46 +00:00
|
|
|
.editStatus(
|
2023-04-27 13:13:13 +00:00
|
|
|
id: idForCall,
|
|
|
|
text: text,
|
|
|
|
spoilerText: spoilerText,
|
|
|
|
mediaIds: mediaIds)
|
2023-03-16 15:37:46 +00:00
|
|
|
.andThenSuccessAsync((item) async {
|
2023-04-29 01:28:43 +00:00
|
|
|
await processNewItems([item], profile.username, null);
|
2023-03-16 15:37:46 +00:00
|
|
|
return item;
|
|
|
|
}).andThenSuccessAsync((item) async {
|
|
|
|
final inReplyToId = item.parentId;
|
|
|
|
if (inReplyToId.isNotEmpty) {
|
|
|
|
late final rootPostId;
|
|
|
|
if (_postNodes.containsKey(inReplyToId)) {
|
|
|
|
rootPostId = inReplyToId;
|
|
|
|
} else {
|
|
|
|
rootPostId = _parentPostIds[inReplyToId];
|
|
|
|
}
|
|
|
|
await refreshStatusChain(rootPostId);
|
|
|
|
}
|
|
|
|
return item;
|
|
|
|
});
|
|
|
|
|
|
|
|
return result.mapValue((status) {
|
|
|
|
_logger.finest('${status.id} status created');
|
|
|
|
return true;
|
|
|
|
}).mapError(
|
|
|
|
(error) {
|
|
|
|
_logger.finest('Error creating post: $error');
|
|
|
|
return ExecError(
|
|
|
|
type: ErrorType.localError,
|
|
|
|
message: error.toString(),
|
|
|
|
);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-11-17 16:04:14 +00:00
|
|
|
FutureResult<List<EntryTreeItem>, ExecError> updateTimeline(
|
2022-11-22 14:54:10 +00:00
|
|
|
TimelineIdentifiers type, int maxId, int sinceId) async {
|
2022-11-17 16:04:14 +00:00
|
|
|
_logger.fine(() => 'Updating timeline');
|
2023-04-29 01:28:43 +00:00
|
|
|
final client = TimelineClient(profile);
|
2023-01-24 03:37:09 +00:00
|
|
|
final itemsResult = await client.getTimeline(
|
|
|
|
type: type,
|
|
|
|
page: PagingData(
|
|
|
|
maxId: maxId > 0 ? maxId : null,
|
|
|
|
sinceId: sinceId > 0 ? sinceId : null,
|
|
|
|
),
|
|
|
|
);
|
2022-11-17 16:04:14 +00:00
|
|
|
if (itemsResult.isFailure) {
|
|
|
|
_logger.severe('Error getting timeline: ${itemsResult.error}');
|
|
|
|
return itemsResult.errorCast();
|
|
|
|
}
|
|
|
|
|
|
|
|
itemsResult.value.sort((t1, t2) => t1.id.compareTo(t2.id));
|
2023-03-14 21:34:18 +00:00
|
|
|
final updatedPosts =
|
2023-04-29 01:28:43 +00:00
|
|
|
await processNewItems(itemsResult.value, profile.userId, client);
|
2022-11-21 21:21:45 +00:00
|
|
|
_logger.finest(() {
|
2023-03-14 21:34:18 +00:00
|
|
|
final postCount = _entries.values.where((e) => e.parentId.isEmpty).length;
|
2022-11-21 21:21:45 +00:00
|
|
|
final commentCount = _entries.length - postCount;
|
|
|
|
final orphanCount = _entries.values
|
|
|
|
.where(
|
|
|
|
(e) => e.parentId.isNotEmpty && !_entries.containsKey(e.parentId))
|
|
|
|
.length;
|
|
|
|
return 'End of update # posts: $postCount, #comments: $commentCount, #orphans: $orphanCount';
|
|
|
|
});
|
2022-11-17 16:04:14 +00:00
|
|
|
return Result.ok(updatedPosts);
|
|
|
|
}
|
|
|
|
|
2023-03-14 21:34:18 +00:00
|
|
|
Future<List<EntryTreeItem>> processNewItems(
|
|
|
|
List<TimelineEntry> items,
|
|
|
|
String currentId,
|
|
|
|
FriendicaClient? client,
|
|
|
|
) async {
|
2022-11-30 00:56:14 +00:00
|
|
|
items.sort((i1, i2) => int.parse(i1.id).compareTo(int.parse(i2.id)));
|
2022-11-21 21:21:45 +00:00
|
|
|
final allSeenItems = [...items];
|
2022-11-18 23:31:28 +00:00
|
|
|
for (final item in items) {
|
2022-11-21 21:21:45 +00:00
|
|
|
_entries[item.id] = item;
|
2022-11-17 16:04:14 +00:00
|
|
|
}
|
|
|
|
|
2022-11-21 21:21:45 +00:00
|
|
|
final orphans = <TimelineEntry>[];
|
|
|
|
for (final item in items) {
|
2022-11-22 14:54:10 +00:00
|
|
|
if (item.parentId.isEmpty) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2022-11-21 21:21:45 +00:00
|
|
|
final parent = _entries[item.parentId];
|
|
|
|
if (parent == null) {
|
|
|
|
orphans.add(item);
|
|
|
|
} else {
|
|
|
|
if (parent.parentId.isEmpty) {
|
|
|
|
_parentPostIds[item.id] = parent.id;
|
|
|
|
}
|
2022-11-17 16:04:14 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-21 21:21:45 +00:00
|
|
|
for (final o in orphans) {
|
2023-04-29 01:28:43 +00:00
|
|
|
await StatusesClient(profile)
|
2023-02-24 22:53:48 +00:00
|
|
|
.getPostOrComment(o.id, fullContext: true)
|
2022-11-17 16:04:14 +00:00
|
|
|
.andThenSuccessAsync((items) async {
|
2023-03-14 21:34:18 +00:00
|
|
|
final parentPostId = items.firstWhere((e) => e.parentId.isEmpty).id;
|
2022-11-21 21:21:45 +00:00
|
|
|
_parentPostIds[o.id] = parentPostId;
|
|
|
|
allSeenItems.addAll(items);
|
|
|
|
for (final item in items) {
|
|
|
|
_entries[item.id] = item;
|
|
|
|
_parentPostIds[item.id] = parentPostId;
|
|
|
|
}
|
2022-11-17 16:04:14 +00:00
|
|
|
});
|
|
|
|
}
|
2022-11-21 21:21:45 +00:00
|
|
|
|
2022-12-29 05:05:13 +00:00
|
|
|
allSeenItems.sort((i1, i2) {
|
|
|
|
if (i1.parentId.isEmpty && i2.parentId.isNotEmpty) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (i2.parentId.isEmpty && i1.parentId.isNotEmpty) {
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
return int.parse(i1.id).compareTo(int.parse(i2.id));
|
|
|
|
});
|
2022-11-21 21:21:45 +00:00
|
|
|
final postNodesToReturn = <_Node>{};
|
2022-12-29 06:20:27 +00:00
|
|
|
var lastCount = 0;
|
2022-12-29 05:05:13 +00:00
|
|
|
while (allSeenItems.isNotEmpty) {
|
|
|
|
final seenItemsCopy = [...allSeenItems];
|
|
|
|
for (final item in seenItemsCopy) {
|
|
|
|
if (item.parentId.isEmpty) {
|
|
|
|
final postNode =
|
2023-03-14 21:34:18 +00:00
|
|
|
_postNodes.putIfAbsent(item.id, () => _Node(item.id));
|
2022-12-29 05:05:13 +00:00
|
|
|
postNodesToReturn.add(postNode);
|
|
|
|
allSeenItems.remove(item);
|
|
|
|
} else {
|
|
|
|
final parentParentPostId = _postNodes.containsKey(item.parentId)
|
|
|
|
? item.parentId
|
|
|
|
: _parentPostIds[item.parentId];
|
2023-02-08 16:24:04 +00:00
|
|
|
if (_postNodes[parentParentPostId] == null) {
|
|
|
|
_logger.severe(
|
|
|
|
'Error finding parent ${item.parentId} for entry ${item.id}');
|
|
|
|
continue;
|
|
|
|
}
|
2022-12-29 05:05:13 +00:00
|
|
|
final parentPostNode = _postNodes[parentParentPostId]!;
|
|
|
|
postNodesToReturn.add(parentPostNode);
|
|
|
|
_parentPostIds[item.id] = parentPostNode.id;
|
|
|
|
if (parentPostNode.getChildById(item.id) == null) {
|
|
|
|
final newNode = _Node(item.id);
|
|
|
|
final injectionNode = parentPostNode.id == item.parentId
|
|
|
|
? parentPostNode
|
|
|
|
: parentPostNode.getChildById(item.parentId);
|
|
|
|
if (injectionNode == null) {
|
|
|
|
continue;
|
|
|
|
} else {
|
|
|
|
injectionNode.addChild(newNode);
|
|
|
|
}
|
|
|
|
}
|
2022-12-29 06:20:27 +00:00
|
|
|
allSeenItems.remove(item);
|
2022-11-21 21:21:45 +00:00
|
|
|
}
|
|
|
|
}
|
2022-12-29 06:20:27 +00:00
|
|
|
if (allSeenItems.isNotEmpty && allSeenItems.length == lastCount) {
|
|
|
|
_logger.severe(
|
|
|
|
'Had ${allSeenItems.length} items left over after all iterations');
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
lastCount = allSeenItems.length;
|
2022-11-21 21:21:45 +00:00
|
|
|
}
|
|
|
|
|
2022-11-22 19:42:26 +00:00
|
|
|
final updatedPosts = postNodesToReturn
|
|
|
|
.map((node) => _nodeToTreeItem(node, currentId))
|
|
|
|
.toList();
|
2022-11-21 21:21:45 +00:00
|
|
|
|
2022-11-17 16:04:14 +00:00
|
|
|
_logger.finest(
|
2023-03-14 21:34:18 +00:00
|
|
|
'Completed processing new items ${client == null ? 'sub level' : 'top level'}');
|
2022-11-17 16:04:14 +00:00
|
|
|
return updatedPosts;
|
|
|
|
}
|
2022-11-18 21:50:15 +00:00
|
|
|
|
2023-04-27 13:13:13 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2022-11-30 00:56:14 +00:00
|
|
|
FutureResult<EntryTreeItem, ExecError> refreshStatusChain(String id) async {
|
2022-11-21 21:21:45 +00:00
|
|
|
_logger.finest('Refreshing post: $id');
|
2023-04-29 01:28:43 +00:00
|
|
|
final client = StatusesClient(profile);
|
2023-04-27 13:13:13 +00:00
|
|
|
final idForCall = mapInteractionId(id);
|
2023-04-28 00:35:46 +00:00
|
|
|
var parentId = '';
|
2022-11-18 23:31:28 +00:00
|
|
|
final result = await client
|
2023-04-27 13:13:13 +00:00
|
|
|
.getPostOrComment(idForCall, fullContext: false)
|
2023-04-28 00:35:46 +00:00
|
|
|
.withResult((entries) =>
|
2023-04-29 01:28:43 +00:00
|
|
|
parentId = entries.isEmpty ? '' : entries.first.parentId)
|
2023-03-14 21:34:18 +00:00
|
|
|
.andThenAsync((rootItems) async => await client
|
2023-04-27 13:13:13 +00:00
|
|
|
.getPostOrComment(idForCall, fullContext: true)
|
2023-03-14 21:34:18 +00:00
|
|
|
.andThenSuccessAsync(
|
|
|
|
(contextItems) async => [...rootItems, ...contextItems]))
|
2023-04-28 00:35:46 +00:00
|
|
|
.withResult((items) async {
|
2023-02-27 03:12:40 +00:00
|
|
|
await processNewItems(items, client.profile.username, null);
|
2022-11-30 00:56:14 +00:00
|
|
|
});
|
2022-11-18 23:31:28 +00:00
|
|
|
|
2023-04-28 00:35:46 +00:00
|
|
|
if (parentId.isNotEmpty &&
|
|
|
|
getIt<FriendicaVersionChecker>()
|
|
|
|
.canUseFeature(RelaticaFeatures.reshareIdFix)) {
|
|
|
|
final parentIdForCall = mapInteractionId(parentId);
|
|
|
|
await client
|
|
|
|
.getPostOrComment(parentIdForCall, fullContext: false)
|
|
|
|
.withResult((entries) =>
|
2023-04-29 01:28:43 +00:00
|
|
|
parentId = entries.isEmpty ? '' : entries.first.parentId)
|
2023-04-28 00:35:46 +00:00
|
|
|
.andThenAsync((rootItems) async => await client
|
|
|
|
.getPostOrComment(idForCall, fullContext: true)
|
|
|
|
.transformAsync(
|
|
|
|
(contextItems) async => [...rootItems, ...contextItems]))
|
|
|
|
.withResult((items) async {
|
|
|
|
await processNewItems(items, client.profile.username, null);
|
|
|
|
});
|
|
|
|
}
|
2022-11-18 23:31:28 +00:00
|
|
|
return result.mapValue((_) {
|
2022-11-21 21:21:45 +00:00
|
|
|
_logger.finest('$id post updated');
|
2023-02-27 03:12:40 +00:00
|
|
|
return _nodeToTreeItem(_getPostRootNode(id)!, client.profile.userId);
|
2022-11-18 23:31:28 +00:00
|
|
|
}).mapError(
|
2023-03-14 21:34:18 +00:00
|
|
|
(error) {
|
2022-11-21 21:21:45 +00:00
|
|
|
_logger.finest('$id error updating: $error');
|
2022-11-22 16:43:16 +00:00
|
|
|
return ExecError(
|
|
|
|
type: ErrorType.localError,
|
|
|
|
message: error.toString(),
|
|
|
|
);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
FutureResult<EntryTreeItem, ExecError> resharePost(String id) async {
|
|
|
|
_logger.finest('Resharing post: $id');
|
2023-04-29 01:28:43 +00:00
|
|
|
final client = StatusesClient(profile);
|
2023-04-27 13:13:13 +00:00
|
|
|
final idForCall = mapInteractionId(id);
|
2022-11-22 16:43:16 +00:00
|
|
|
final result =
|
2023-04-27 13:13:13 +00:00
|
|
|
await client.resharePost(idForCall).andThenSuccessAsync((item) async {
|
2023-02-27 03:12:40 +00:00
|
|
|
await processNewItems([item], client.profile.username, null);
|
2022-11-22 16:43:16 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
return result.mapValue((_) {
|
|
|
|
_logger.finest('$id post updated after reshare');
|
2023-02-27 03:12:40 +00:00
|
|
|
return _nodeToTreeItem(_postNodes[id]!, client.profile.userId);
|
2022-11-22 16:43:16 +00:00
|
|
|
}).mapError(
|
2023-03-14 21:34:18 +00:00
|
|
|
(error) {
|
2022-11-22 16:43:16 +00:00
|
|
|
_logger.finest('$id error updating: $error');
|
|
|
|
return ExecError(
|
|
|
|
type: ErrorType.localError,
|
|
|
|
message: error.toString(),
|
|
|
|
);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-12-27 04:36:04 +00:00
|
|
|
FutureResult<bool, ExecError> unResharePost(String id) async {
|
2022-11-22 16:43:16 +00:00
|
|
|
_logger.finest('Unresharing post: $id');
|
2023-04-29 01:28:43 +00:00
|
|
|
final client = StatusesClient(profile);
|
2023-04-27 13:13:13 +00:00
|
|
|
final idForCall = mapInteractionId(id);
|
2023-02-24 21:08:14 +00:00
|
|
|
final result =
|
2023-04-27 13:13:13 +00:00
|
|
|
await client.unResharePost(idForCall).andThenSuccessAsync((item) async {
|
2023-02-27 03:12:40 +00:00
|
|
|
await processNewItems([item], client.profile.username, null);
|
2022-11-22 16:43:16 +00:00
|
|
|
});
|
|
|
|
|
2022-12-27 04:36:04 +00:00
|
|
|
if (result.isFailure) {
|
|
|
|
return Result.error(result.error);
|
|
|
|
}
|
|
|
|
|
|
|
|
_cleanupEntriesForId(id);
|
|
|
|
notifyListeners();
|
|
|
|
|
|
|
|
return Result.ok(true);
|
2022-11-18 23:31:28 +00:00
|
|
|
}
|
|
|
|
|
2023-03-14 21:34:18 +00:00
|
|
|
FutureResult<EntryTreeItem, ExecError> toggleFavorited(
|
|
|
|
String id, bool newStatus) async {
|
2023-04-27 13:13:13 +00:00
|
|
|
final interactionClient = InteractionsClient(profile);
|
|
|
|
final postsClient = StatusesClient(profile);
|
|
|
|
final idForCall = mapInteractionId(id);
|
|
|
|
final result =
|
|
|
|
await interactionClient.changeFavoriteStatus(idForCall, newStatus);
|
2022-11-18 21:50:15 +00:00
|
|
|
if (result.isFailure) {
|
|
|
|
return result.errorCast();
|
|
|
|
}
|
|
|
|
|
2023-04-27 13:13:13 +00:00
|
|
|
final updateResult =
|
|
|
|
await postsClient.getPostOrComment(id, fullContext: false);
|
|
|
|
if (updateResult.isFailure) {
|
|
|
|
return updateResult.errorCast();
|
|
|
|
}
|
|
|
|
final update = updateResult.value.first;
|
2022-11-21 21:21:45 +00:00
|
|
|
_entries[update.id] = update;
|
|
|
|
final node = update.parentId.isEmpty
|
|
|
|
? _postNodes[update.id]!
|
2023-03-20 02:22:09 +00:00
|
|
|
: _postNodes[_parentPostIds[update.id]]!;
|
2022-11-21 21:21:45 +00:00
|
|
|
|
|
|
|
notifyListeners();
|
2023-04-27 13:13:13 +00:00
|
|
|
return Result.ok(_nodeToTreeItem(node, interactionClient.profile.userId));
|
2022-11-21 21:21:45 +00:00
|
|
|
}
|
2022-11-18 21:50:15 +00:00
|
|
|
|
2022-11-22 19:42:26 +00:00
|
|
|
EntryTreeItem _nodeToTreeItem(_Node node, String currentId) {
|
2022-11-21 21:21:45 +00:00
|
|
|
final childenEntries = <String, EntryTreeItem>{};
|
|
|
|
for (final c in node.children) {
|
2022-11-22 19:42:26 +00:00
|
|
|
childenEntries[c.id] = _nodeToTreeItem(c, currentId);
|
2022-11-18 21:50:15 +00:00
|
|
|
}
|
2022-11-22 19:42:26 +00:00
|
|
|
final entry = _entries[node.id]!;
|
2022-12-28 13:52:55 +00:00
|
|
|
final isMine =
|
|
|
|
entry.authorId == currentId || entry.reshareAuthorId == currentId;
|
2022-11-22 19:42:26 +00:00
|
|
|
return EntryTreeItem(
|
|
|
|
_entries[node.id]!,
|
|
|
|
isMine: isMine,
|
|
|
|
initialChildren: childenEntries,
|
|
|
|
);
|
2022-11-21 21:21:45 +00:00
|
|
|
}
|
2022-12-27 04:36:04 +00:00
|
|
|
|
|
|
|
void _cleanupEntriesForId(String id) {
|
|
|
|
if (_parentPostIds.containsKey(id)) {
|
|
|
|
_parentPostIds.remove(id);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (_entries.containsKey(id)) {
|
|
|
|
_entries.remove(id);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (_postNodes.containsKey(id)) {
|
|
|
|
_postNodes.remove(id);
|
|
|
|
}
|
|
|
|
}
|
2022-11-21 21:21:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
class _Node {
|
|
|
|
final String id;
|
|
|
|
final _children = <String, _Node>{};
|
2022-11-18 21:50:15 +00:00
|
|
|
|
2022-11-21 21:21:45 +00:00
|
|
|
List<_Node> get children => _children.values.toList();
|
|
|
|
|
|
|
|
_Node(this.id, {Map<String, _Node>? initialChildren}) {
|
|
|
|
if (initialChildren != null) {
|
|
|
|
_children.addAll(initialChildren);
|
2022-11-18 21:50:15 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-21 21:21:45 +00:00
|
|
|
void addChild(_Node node) {
|
|
|
|
_children[node.id] = node;
|
|
|
|
}
|
|
|
|
|
2022-12-27 04:36:04 +00:00
|
|
|
_Node? removeChildById(String id) {
|
|
|
|
if (_children.containsKey(id)) {
|
|
|
|
_children.remove(id);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (final c in _children.values) {
|
|
|
|
c.removeChildById(id);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-21 21:21:45 +00:00
|
|
|
_Node? getChildById(String id) {
|
|
|
|
if (_children.containsKey(id)) {
|
|
|
|
return _children[id]!;
|
2022-11-18 21:50:15 +00:00
|
|
|
}
|
|
|
|
|
2022-11-21 21:21:45 +00:00
|
|
|
for (final c in _children.values) {
|
|
|
|
final result = c.getChildById(id);
|
|
|
|
if (result != null) {
|
|
|
|
return result;
|
|
|
|
}
|
2022-11-18 21:50:15 +00:00
|
|
|
}
|
2022-11-21 21:21:45 +00:00
|
|
|
|
|
|
|
return null;
|
2022-11-18 21:50:15 +00:00
|
|
|
}
|
2022-11-21 21:21:45 +00:00
|
|
|
|
|
|
|
@override
|
|
|
|
bool operator ==(Object other) =>
|
|
|
|
identical(this, other) ||
|
2023-03-14 21:34:18 +00:00
|
|
|
other is _Node && runtimeType == other.runtimeType && id == other.id;
|
2022-11-21 21:21:45 +00:00
|
|
|
|
|
|
|
@override
|
|
|
|
int get hashCode => id.hashCode;
|
2022-11-17 16:04:14 +00:00
|
|
|
}
|