2022-11-17 16:04:14 +00:00
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:logging/logging.dart';
|
|
|
|
import 'package:result_monad/result_monad.dart';
|
|
|
|
|
|
|
|
import '../friendica_client.dart';
|
|
|
|
import '../globals.dart';
|
|
|
|
import '../models/TimelineIdentifiers.dart';
|
|
|
|
import '../models/entry_tree_item.dart';
|
|
|
|
import '../models/exec_error.dart';
|
|
|
|
import '../models/timeline_entry.dart';
|
|
|
|
import 'auth_service.dart';
|
|
|
|
|
|
|
|
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>{};
|
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-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');
|
|
|
|
final auth = getIt<AuthService>();
|
2022-11-30 00:56:14 +00:00
|
|
|
final postNode = _getPostRootNode(id);
|
2022-11-23 20:48:09 +00:00
|
|
|
if (postNode == null) {
|
|
|
|
return Result.error(ExecError(
|
|
|
|
type: ErrorType.notFound,
|
|
|
|
message: 'Unknown post id: $id',
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
|
|
|
return Result.ok(_nodeToTreeItem(postNode, auth.currentId));
|
|
|
|
}
|
|
|
|
|
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',
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
|
|
|
FutureResult<bool, ExecError> createNewStatus(String text,
|
|
|
|
{String spoilerText = '', String inReplyToId = ''}) async {
|
2022-11-22 18:55:50 +00:00
|
|
|
_logger.finest('Creating new post: $text');
|
|
|
|
final auth = getIt<AuthService>();
|
|
|
|
final clientResult = auth.currentClient;
|
|
|
|
if (clientResult.isFailure) {
|
|
|
|
_logger.severe('Error getting Friendica client: ${clientResult.error}');
|
|
|
|
return clientResult.errorCast();
|
|
|
|
}
|
|
|
|
|
|
|
|
final client = clientResult.value;
|
|
|
|
final result = await client
|
2022-11-23 02:59:08 +00:00
|
|
|
.createNewStatus(
|
2022-11-22 18:55:50 +00:00
|
|
|
text: text,
|
|
|
|
spoilerText: spoilerText,
|
2022-11-23 02:59:08 +00:00
|
|
|
inReplyToId: inReplyToId,
|
2022-11-22 18:55:50 +00:00
|
|
|
)
|
|
|
|
.andThenSuccessAsync((item) async {
|
|
|
|
await processNewItems([item], client.credentials.username, null);
|
|
|
|
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(
|
|
|
|
(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');
|
|
|
|
final auth = getIt<AuthService>();
|
|
|
|
final clientResult = auth.currentClient;
|
|
|
|
if (clientResult.isFailure) {
|
|
|
|
_logger.severe('Error getting Friendica client: ${clientResult.error}');
|
|
|
|
return clientResult.errorCast();
|
|
|
|
}
|
|
|
|
|
|
|
|
final client = clientResult.value;
|
2022-11-18 23:31:28 +00:00
|
|
|
final itemsResult =
|
2022-11-22 14:54:10 +00:00
|
|
|
await client.getTimeline(type: type, maxId: maxId, sinceId: sinceId);
|
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));
|
|
|
|
final updatedPosts =
|
2022-11-22 19:42:26 +00:00
|
|
|
await processNewItems(itemsResult.value, auth.currentId, client);
|
2022-11-21 21:21:45 +00:00
|
|
|
_logger.finest(() {
|
|
|
|
final postCount = _entries.values.where((e) => e.parentId.isEmpty).length;
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<List<EntryTreeItem>> processNewItems(
|
|
|
|
List<TimelineEntry> items,
|
2022-11-22 19:42:26 +00:00
|
|
|
String currentId,
|
2022-11-17 16:04:14 +00:00
|
|
|
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) {
|
2022-11-17 16:04:14 +00:00
|
|
|
await client
|
|
|
|
?.getPostOrComment(o.id, fullContext: true)
|
|
|
|
.andThenSuccessAsync((items) async {
|
2022-11-21 21:21:45 +00:00
|
|
|
final parentPostId = items.firstWhere((e) => e.parentId.isEmpty).id;
|
|
|
|
_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
|
|
|
|
|
|
|
allSeenItems.sort((i1, i2) => int.parse(i1.id).compareTo(int.parse(i2.id)));
|
|
|
|
final postNodesToReturn = <_Node>{};
|
|
|
|
for (final item in allSeenItems) {
|
|
|
|
if (item.parentId.isEmpty) {
|
|
|
|
final postNode = _postNodes.putIfAbsent(item.id, () => _Node(item.id));
|
|
|
|
postNodesToReturn.add(postNode);
|
|
|
|
} else {
|
2022-11-23 02:59:08 +00:00
|
|
|
final parentParentPostId = _postNodes.containsKey(item.parentId)
|
|
|
|
? item.parentId
|
|
|
|
: _parentPostIds[item.parentId];
|
|
|
|
final parentPostNode = _postNodes[parentParentPostId]!;
|
2022-11-21 21:21:45 +00:00
|
|
|
postNodesToReturn.add(parentPostNode);
|
2022-11-23 02:59:08 +00:00
|
|
|
_parentPostIds[item.id] = parentPostNode.id;
|
2022-11-21 21:21:45 +00:00
|
|
|
if (parentPostNode.getChildById(item.id) == null) {
|
|
|
|
final newNode = _Node(item.id);
|
|
|
|
final injectionNode = parentPostNode.id == item.parentId
|
|
|
|
? parentPostNode
|
|
|
|
: parentPostNode.getChildById(item.parentId)!;
|
|
|
|
injectionNode.addChild(newNode);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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(
|
|
|
|
'Completed processing new items ${client == null ? 'sub level' : 'top level'}');
|
|
|
|
return updatedPosts;
|
|
|
|
}
|
2022-11-18 21:50:15 +00:00
|
|
|
|
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');
|
2022-11-18 23:31:28 +00:00
|
|
|
final auth = getIt<AuthService>();
|
|
|
|
final clientResult = auth.currentClient;
|
|
|
|
if (clientResult.isFailure) {
|
|
|
|
_logger.severe('Error getting Friendica client: ${clientResult.error}');
|
|
|
|
return clientResult.errorCast();
|
|
|
|
}
|
|
|
|
|
|
|
|
final client = clientResult.value;
|
|
|
|
final result = await client
|
2022-11-21 21:21:45 +00:00
|
|
|
.getPostOrComment(id, fullContext: false)
|
2022-11-30 00:56:14 +00:00
|
|
|
.andThenAsync((rootItems) async => await client
|
|
|
|
.getPostOrComment(id, fullContext: true)
|
|
|
|
.andThenSuccessAsync(
|
|
|
|
(contextItems) async => [...rootItems, ...contextItems]))
|
2022-11-18 23:31:28 +00:00
|
|
|
.andThenSuccessAsync((items) async {
|
2022-11-30 00:56:14 +00:00
|
|
|
await processNewItems(items, client.credentials.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');
|
2022-11-30 00:56:14 +00:00
|
|
|
return _nodeToTreeItem(_getPostRootNode(id)!, auth.currentId);
|
2022-11-18 23:31:28 +00:00
|
|
|
}).mapError(
|
|
|
|
(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');
|
|
|
|
final auth = getIt<AuthService>();
|
|
|
|
final clientResult = auth.currentClient;
|
|
|
|
if (clientResult.isFailure) {
|
|
|
|
_logger.severe('Error getting Friendica client: ${clientResult.error}');
|
|
|
|
return clientResult.errorCast();
|
|
|
|
}
|
|
|
|
|
|
|
|
final client = clientResult.value;
|
|
|
|
final result =
|
|
|
|
await client.resharePost(id).andThenSuccessAsync((item) async {
|
|
|
|
await processNewItems([item], client.credentials.username, null);
|
|
|
|
});
|
|
|
|
|
|
|
|
return result.mapValue((_) {
|
|
|
|
_logger.finest('$id post updated after reshare');
|
2022-11-22 19:42:26 +00:00
|
|
|
return _nodeToTreeItem(_postNodes[id]!, auth.currentId);
|
2022-11-22 16:43:16 +00:00
|
|
|
}).mapError(
|
|
|
|
(error) {
|
|
|
|
_logger.finest('$id error updating: $error');
|
|
|
|
return ExecError(
|
|
|
|
type: ErrorType.localError,
|
|
|
|
message: error.toString(),
|
|
|
|
);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
FutureResult<EntryTreeItem, ExecError> unResharePost(String id) async {
|
|
|
|
_logger.finest('Unresharing post: $id');
|
|
|
|
final auth = getIt<AuthService>();
|
|
|
|
final clientResult = auth.currentClient;
|
|
|
|
if (clientResult.isFailure) {
|
|
|
|
_logger.severe('Error getting Friendica client: ${clientResult.error}');
|
|
|
|
return clientResult.errorCast();
|
|
|
|
}
|
|
|
|
|
|
|
|
final client = clientResult.value;
|
|
|
|
final result =
|
|
|
|
await client.unResharePost(id).andThenSuccessAsync((item) async {
|
|
|
|
await processNewItems([item], client.credentials.username, null);
|
|
|
|
});
|
|
|
|
|
|
|
|
return result.mapValue((_) {
|
|
|
|
_logger.finest('$id post updated after unreshare');
|
2022-11-22 19:42:26 +00:00
|
|
|
return _nodeToTreeItem(_postNodes[id]!, auth.currentId);
|
2022-11-22 16:43:16 +00:00
|
|
|
}).mapError(
|
|
|
|
(error) {
|
|
|
|
_logger.finest('$id error updating: $error');
|
2022-11-18 23:31:28 +00:00
|
|
|
return ExecError(
|
|
|
|
type: ErrorType.localError,
|
|
|
|
message: error.toString(),
|
|
|
|
);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-11-18 21:50:15 +00:00
|
|
|
FutureResult<EntryTreeItem, ExecError> toggleFavorited(
|
2022-11-21 21:21:45 +00:00
|
|
|
String id, bool newStatus) async {
|
2022-11-18 21:50:15 +00:00
|
|
|
final auth = getIt<AuthService>();
|
|
|
|
final clientResult = auth.currentClient;
|
|
|
|
if (clientResult.isFailure) {
|
|
|
|
_logger.severe('Error getting Friendica client: ${clientResult.error}');
|
|
|
|
return clientResult.errorCast();
|
|
|
|
}
|
|
|
|
final client = clientResult.value;
|
|
|
|
final result = await client.changeFavoriteStatus(id, newStatus);
|
|
|
|
if (result.isFailure) {
|
|
|
|
return result.errorCast();
|
|
|
|
}
|
|
|
|
|
|
|
|
final update = result.value;
|
2022-11-21 21:21:45 +00:00
|
|
|
_entries[update.id] = update;
|
|
|
|
final node = update.parentId.isEmpty
|
|
|
|
? _postNodes[update.id]!
|
|
|
|
: _postNodes[update.parentId]!.getChildById(update.id)!;
|
|
|
|
|
|
|
|
notifyListeners();
|
2022-11-22 19:42:26 +00:00
|
|
|
return Result.ok(_nodeToTreeItem(node, auth.currentId));
|
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]!;
|
|
|
|
final isMine = entry.authorId == currentId;
|
|
|
|
return EntryTreeItem(
|
|
|
|
_entries[node.id]!,
|
|
|
|
isMine: isMine,
|
|
|
|
initialChildren: childenEntries,
|
|
|
|
);
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
_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) ||
|
|
|
|
other is _Node && runtimeType == other.runtimeType && id == other.id;
|
|
|
|
|
|
|
|
@override
|
|
|
|
int get hashCode => id.hashCode;
|
2022-11-17 16:04:14 +00:00
|
|
|
}
|