mirror of
https://gitlab.com/mysocialportal/relatica
synced 2024-10-18 13:33:32 +00:00
More accurate timeline caching/querying.
This commit is contained in:
parent
1c12d74e3b
commit
c0598ad58e
11 changed files with 317 additions and 171 deletions
|
@ -1,12 +1,14 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import '../../globals.dart';
|
||||
import '../../models/timeline_entry.dart';
|
||||
import '../../services/entry_manager_service.dart';
|
||||
import '../../services/timeline_manager.dart';
|
||||
import '../../utils/snackbar_builder.dart';
|
||||
|
||||
class InteractionsBarControl extends StatefulWidget {
|
||||
final TimelineEntry entry;
|
||||
|
||||
const InteractionsBarControl({super.key, required this.entry});
|
||||
|
||||
@override
|
||||
|
@ -14,29 +16,25 @@ class InteractionsBarControl extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _InteractionsBarControlState extends State<InteractionsBarControl> {
|
||||
bool isFavorited = false;
|
||||
int reshares = 0;
|
||||
int comments = 0;
|
||||
int likes = 0;
|
||||
static final _logger = Logger('$InteractionsBarControl');
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
isFavorited = widget.entry.isFavorited;
|
||||
comments = widget.entry.engagementSummary.repliesCount;
|
||||
reshares = widget.entry.engagementSummary.rebloggedCount;
|
||||
likes = widget.entry.engagementSummary.favoritesCount;
|
||||
}
|
||||
bool get isFavorited => widget.entry.isFavorited;
|
||||
|
||||
int get reshares => widget.entry.engagementSummary.rebloggedCount;
|
||||
|
||||
int get comments => widget.entry.engagementSummary.repliesCount;
|
||||
|
||||
int get likes => widget.entry.engagementSummary.favoritesCount;
|
||||
|
||||
Future<void> toggleFavorited() async {
|
||||
final newState = !isFavorited;
|
||||
print('Trying to toggle favorite from $isFavorited to $newState');
|
||||
final result = await getIt<EntryManagerService>()
|
||||
_logger.finest('Trying to toggle favorite from $isFavorited to $newState');
|
||||
final result = await getIt<TimelineManager>()
|
||||
.toggleFavorited(widget.entry.id, newState);
|
||||
result.match(onSuccess: (update) {
|
||||
setState(() {
|
||||
print('Success toggling! $isFavorited -> ${update.entry.isFavorited}');
|
||||
isFavorited = update.entry.isFavorited;
|
||||
_logger.finest(
|
||||
'Success toggling! $isFavorited -> ${update.entry.isFavorited}');
|
||||
});
|
||||
}, onError: (error) {
|
||||
buildSnackbar(context, 'Error toggling like status: $error');
|
||||
|
@ -45,13 +43,14 @@ class _InteractionsBarControlState extends State<InteractionsBarControl> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_logger.finest('Building: ${widget.entry.toShortString()}');
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('$likes likes, $reshares reshares, $comments comments'),
|
||||
IconButton(
|
||||
onPressed: toggleFavorited,
|
||||
onPressed: () async => await toggleFavorited(),
|
||||
icon: isFavorited
|
||||
? Icon(Icons.thumb_up)
|
||||
: Icon(Icons.thumb_up_outlined)),
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_portal/services/entry_manager_service.dart';
|
||||
import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:result_monad/result_monad.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
|
@ -12,6 +12,7 @@ import '../../models/entry_tree_item.dart';
|
|||
import '../../models/timeline_entry.dart';
|
||||
import '../../screens/image_viewer_screen.dart';
|
||||
import '../../services/connections_manager.dart';
|
||||
import '../../services/timeline_manager.dart';
|
||||
import '../../utils/dateutils.dart';
|
||||
import '../../utils/snackbar_builder.dart';
|
||||
import '../padding.dart';
|
||||
|
@ -27,17 +28,15 @@ class StatusControl extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _StatusControlState extends State<StatusControl> {
|
||||
late EntryTreeItem item;
|
||||
static final _logger = Logger('$StatusControl');
|
||||
|
||||
EntryTreeItem get item => widget.originalItem;
|
||||
|
||||
TimelineEntry get entry => item.entry;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
item = widget.originalItem;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_logger.finest('Building ${item.entry.toShortString()}');
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
|
@ -196,12 +195,9 @@ class _StatusControlState extends State<StatusControl> {
|
|||
if (entry.parentId.isEmpty)
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
await getIt<EntryManagerService>()
|
||||
.refreshPost(widget.originalItem)
|
||||
.andThenSuccessAsync((newItem) async => setState(() {
|
||||
print('Updated item');
|
||||
item = newItem;
|
||||
}));
|
||||
await getIt<TimelineManager>()
|
||||
.refreshPost(item.id)
|
||||
.andThenSuccessAsync((newItem) async => setState(() {}));
|
||||
},
|
||||
child: Center(
|
||||
child: Text('Load All'),
|
||||
|
|
|
@ -92,12 +92,12 @@ class FriendicaClient {
|
|||
|
||||
FutureResult<List<TimelineEntry>, ExecError> getPostOrComment(String id,
|
||||
{bool fullContext = false}) async {
|
||||
_logger.finest(
|
||||
() => 'Getting entry for status $id, full context? $fullContext');
|
||||
return (await runCatchingAsync(() async {
|
||||
final baseUrl = 'https://$serverName/api/v1/statuses/$id';
|
||||
final url = fullContext ? '$baseUrl/context' : baseUrl;
|
||||
final request = Uri.parse(url);
|
||||
_logger.finest(() =>
|
||||
'Getting entry for status $id, full context? $fullContext : $url');
|
||||
return (await _getApiRequest(request).andThenSuccessAsync((json) async {
|
||||
if (fullContext) {
|
||||
final ancestors = json['ancestors'] as List<dynamic>;
|
||||
|
@ -167,8 +167,8 @@ class FriendicaClient {
|
|||
}
|
||||
|
||||
FutureResult<String, ExecError> _getUrl(Uri url) async {
|
||||
_logger.finest('GET: $url');
|
||||
try {
|
||||
//SecurityContext.defaultContext.setTrustedCertificates('/etc/apache2/certificate/apache-certificate.crt');
|
||||
final response = await http.get(
|
||||
url,
|
||||
headers: {
|
||||
|
@ -191,6 +191,7 @@ class FriendicaClient {
|
|||
|
||||
FutureResult<String, ExecError> _postUrl(
|
||||
Uri url, Map<String, dynamic> body) async {
|
||||
_logger.finest('POST: $url');
|
||||
try {
|
||||
final response = await http.post(
|
||||
url,
|
||||
|
|
|
@ -19,12 +19,12 @@ void main() async {
|
|||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await dotenv.load(fileName: '.env');
|
||||
Logger.root.level = Level.ALL;
|
||||
Logger.root.onRecord.listen((event) {
|
||||
final logName = event.loggerName.isEmpty ? 'ROOT' : event.loggerName;
|
||||
final msg =
|
||||
'${event.level.name} - $logName @ ${event.time}: ${event.message}';
|
||||
print(msg);
|
||||
});
|
||||
// Logger.root.onRecord.listen((event) {
|
||||
// final logName = event.loggerName.isEmpty ? 'ROOT' : event.loggerName;
|
||||
// final msg =
|
||||
// '${event.level.name} - $logName @ ${event.time}: ${event.message}';
|
||||
// print(msg);
|
||||
// });
|
||||
|
||||
final authService = AuthService();
|
||||
final secretsService = SecretsService();
|
||||
|
|
|
@ -7,20 +7,41 @@ class EntryTreeItem {
|
|||
|
||||
final _children = <String, EntryTreeItem>{};
|
||||
|
||||
EntryTreeItem(this.entry, {this.isMine = true, this.isOrphaned = false});
|
||||
EntryTreeItem(this.entry,
|
||||
{this.isMine = true,
|
||||
this.isOrphaned = false,
|
||||
Map<String, EntryTreeItem>? initialChildren}) {
|
||||
_children.addAll(initialChildren ?? {});
|
||||
}
|
||||
|
||||
EntryTreeItem copy({required TimelineEntry entry}) => EntryTreeItem(
|
||||
entry,
|
||||
isMine: isMine,
|
||||
isOrphaned: isOrphaned,
|
||||
initialChildren: _children,
|
||||
);
|
||||
|
||||
String get id => entry.id;
|
||||
|
||||
void addChild(EntryTreeItem child) {
|
||||
void addOrUpdate(EntryTreeItem child) {
|
||||
_children[child.id] = child;
|
||||
}
|
||||
|
||||
EntryTreeItem? getChildById(String id) {
|
||||
if (_children.containsKey(id)) {
|
||||
return _children[id]!;
|
||||
}
|
||||
|
||||
for (final c in _children.values) {
|
||||
final result = c.getChildById(id);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
int get totalChildren {
|
||||
int t = _children.length;
|
||||
for (final c in _children.values) {
|
||||
|
@ -37,8 +58,15 @@ class EntryTreeItem {
|
|||
identical(this, other) ||
|
||||
other is EntryTreeItem &&
|
||||
runtimeType == other.runtimeType &&
|
||||
entry == other.entry;
|
||||
entry == other.entry &&
|
||||
isMine == other.isMine &&
|
||||
isOrphaned == other.isOrphaned &&
|
||||
_children == other._children;
|
||||
|
||||
@override
|
||||
int get hashCode => entry.hashCode;
|
||||
int get hashCode =>
|
||||
entry.hashCode ^
|
||||
isMine.hashCode ^
|
||||
isOrphaned.hashCode ^
|
||||
_children.hashCode;
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import 'entry_tree_item.dart';
|
|||
class Timeline {
|
||||
final TimelineIdentifiers id;
|
||||
final List<EntryTreeItem> _posts = [];
|
||||
final Set<EntryTreeItem> _postsSet = {};
|
||||
final Map<String, EntryTreeItem> _postsById = {};
|
||||
int _lowestStatusId = 0;
|
||||
int _highestStatusId = 0;
|
||||
|
||||
|
@ -12,13 +12,13 @@ class Timeline {
|
|||
|
||||
Timeline(this.id, {List<EntryTreeItem>? initialPosts}) {
|
||||
if (initialPosts != null) {
|
||||
addPosts(initialPosts);
|
||||
addOrUpdate(initialPosts);
|
||||
}
|
||||
}
|
||||
|
||||
List<EntryTreeItem> get posts => List.unmodifiable(_posts);
|
||||
|
||||
void addPosts(List<EntryTreeItem> newPosts) {
|
||||
void addOrUpdate(List<EntryTreeItem> newPosts) {
|
||||
for (final p in newPosts) {
|
||||
final id = int.parse(p.id);
|
||||
if (_lowestStatusId > id) {
|
||||
|
@ -28,18 +28,33 @@ class Timeline {
|
|||
if (_highestStatusId < id) {
|
||||
_highestStatusId = id;
|
||||
}
|
||||
_postsSet.add(p);
|
||||
_postsById[p.id] = p;
|
||||
}
|
||||
_posts.clear();
|
||||
_posts.addAll(_postsSet);
|
||||
_posts.addAll(_postsById.values);
|
||||
_posts.sort((p1, p2) {
|
||||
return p2.entry.backdatedTimestamp.compareTo(p1.entry.backdatedTimestamp);
|
||||
});
|
||||
}
|
||||
|
||||
bool tryUpdateComment(EntryTreeItem comment) {
|
||||
var changed = false;
|
||||
final parentId = comment.entry.parentId;
|
||||
for (final p in _posts) {
|
||||
final parent =
|
||||
p.id == parentId ? p : p.getChildById(comment.entry.parentId);
|
||||
if (parent != null) {
|
||||
parent.addOrUpdate(comment);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
_posts.clear();
|
||||
_postsSet.clear();
|
||||
_postsById.clear();
|
||||
_lowestStatusId = 0;
|
||||
_highestStatusId = 0;
|
||||
}
|
||||
|
|
|
@ -139,7 +139,11 @@ class TimelineEntry {
|
|||
|
||||
@override
|
||||
String toString() {
|
||||
return 'TimelineEntry{id: $id, isReshare: $isReshare, parentId: $parentId, creationTimestamp: $creationTimestamp, modificationTimestamp: $modificationTimestamp, backdatedTimeStamp: $backdatedTimestamp, post: $body, title: $title, author: $author, parentAuthor: $parentAuthor externalLink:$externalLink}';
|
||||
return 'TimelineEntry{id: $id, isReshare: $isReshare, isFavorited: $isFavorited, parentId: $parentId, creationTimestamp: $creationTimestamp, modificationTimestamp: $modificationTimestamp, backdatedTimeStamp: $backdatedTimestamp, post: $body, title: $title, author: $author, parentAuthor: $parentAuthor externalLink:$externalLink}';
|
||||
}
|
||||
|
||||
String toShortString() {
|
||||
return 'TimelineEntry{id: $id, isReshare: $isReshare, isFavorited: $isFavorited, parentId: $parentId}';
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -147,8 +151,47 @@ class TimelineEntry {
|
|||
identical(this, other) ||
|
||||
other is TimelineEntry &&
|
||||
runtimeType == other.runtimeType &&
|
||||
id == other.id;
|
||||
id == other.id &&
|
||||
parentId == other.parentId &&
|
||||
parentAuthor == other.parentAuthor &&
|
||||
parentAuthorId == other.parentAuthorId &&
|
||||
creationTimestamp == other.creationTimestamp &&
|
||||
backdatedTimestamp == other.backdatedTimestamp &&
|
||||
modificationTimestamp == other.modificationTimestamp &&
|
||||
body == other.body &&
|
||||
title == other.title &&
|
||||
isReshare == other.isReshare &&
|
||||
author == other.author &&
|
||||
authorId == other.authorId &&
|
||||
externalLink == other.externalLink &&
|
||||
locationData == other.locationData &&
|
||||
isFavorited == other.isFavorited &&
|
||||
links == other.links &&
|
||||
likes == other.likes &&
|
||||
dislikes == other.dislikes &&
|
||||
mediaAttachments == other.mediaAttachments &&
|
||||
engagementSummary == other.engagementSummary;
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode;
|
||||
int get hashCode =>
|
||||
id.hashCode ^
|
||||
parentId.hashCode ^
|
||||
parentAuthor.hashCode ^
|
||||
parentAuthorId.hashCode ^
|
||||
creationTimestamp.hashCode ^
|
||||
backdatedTimestamp.hashCode ^
|
||||
modificationTimestamp.hashCode ^
|
||||
body.hashCode ^
|
||||
title.hashCode ^
|
||||
isReshare.hashCode ^
|
||||
author.hashCode ^
|
||||
authorId.hashCode ^
|
||||
externalLink.hashCode ^
|
||||
locationData.hashCode ^
|
||||
isFavorited.hashCode ^
|
||||
links.hashCode ^
|
||||
likes.hashCode ^
|
||||
dislikes.hashCode ^
|
||||
mediaAttachments.hashCode ^
|
||||
engagementSummary.hashCode;
|
||||
}
|
||||
|
|
|
@ -76,15 +76,24 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||
print('items count = ${items.length}');
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await manager.refreshTimeline(TimelineIdentifiers.home());
|
||||
await manager.refreshTimeline(TimelineIdentifiers.home(), true);
|
||||
},
|
||||
child: ListView.separated(
|
||||
itemBuilder: (context, index) {
|
||||
print('Building item: $index');
|
||||
return StatusControl(originalItem: items[index]);
|
||||
if (index == 0) {
|
||||
return ElevatedButton(
|
||||
onPressed: () async => await manager.refreshTimeline(
|
||||
TimelineIdentifiers.home(), false),
|
||||
child: Text('Load newer posts'));
|
||||
}
|
||||
final itemIndex = index - 1;
|
||||
final item = items[itemIndex];
|
||||
_logger.finest(
|
||||
'Building item: $itemIndex: ${item.entry.toShortString()}');
|
||||
return StatusControl(originalItem: item);
|
||||
},
|
||||
separatorBuilder: (context, index) => Divider(),
|
||||
itemCount: items.length,
|
||||
itemCount: items.length + 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ extension TimelineEntryMastodonExtensions on TimelineEntry {
|
|||
})
|
||||
: 0;
|
||||
final id = json['id'] ?? '';
|
||||
final isReshare = json.containsKey('reblogged');
|
||||
final isReshare = json['reblogged'] ?? false;
|
||||
final parentId = json['in_reply_to_id'] ?? '';
|
||||
final parentAuthor = json['in_reply_to_account_id'] ?? '';
|
||||
final parentAuthorId = json['in_reply_to_account_id'] ?? '';
|
||||
|
|
|
@ -12,15 +12,13 @@ import 'auth_service.dart';
|
|||
|
||||
class EntryManagerService extends ChangeNotifier {
|
||||
static final _logger = Logger('$EntryManagerService');
|
||||
final Map<String, EntryTreeItem> _posts = {};
|
||||
final List<EntryTreeItem> _orphanedComments = [];
|
||||
final Map<String, EntryTreeItem> _allComments = {};
|
||||
final _entries = <String, TimelineEntry>{};
|
||||
final _parentPostIds = <String, String>{};
|
||||
final _postNodes = <String, _Node>{};
|
||||
|
||||
void clear() {
|
||||
_posts.clear();
|
||||
_orphanedComments.clear();
|
||||
_allComments.clear();
|
||||
notifyListeners();
|
||||
_entries.clear();
|
||||
_parentPostIds.clear();
|
||||
}
|
||||
|
||||
FutureResult<List<EntryTreeItem>, ExecError> updateTimeline(
|
||||
|
@ -45,10 +43,15 @@ class EntryManagerService extends ChangeNotifier {
|
|||
itemsResult.value.sort((t1, t2) => t1.id.compareTo(t2.id));
|
||||
final updatedPosts =
|
||||
await processNewItems(itemsResult.value, myHandle, client);
|
||||
_orphanedComments.removeWhere((element) => !element.isOrphaned);
|
||||
_logger.finest(() =>
|
||||
'End of update # posts: ${_posts.length}, #comments: ${_allComments.length}, #orphans: ${_orphanedComments.length}');
|
||||
notifyListeners();
|
||||
_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';
|
||||
});
|
||||
return Result.ok(updatedPosts);
|
||||
}
|
||||
|
||||
|
@ -57,74 +60,67 @@ class EntryManagerService extends ChangeNotifier {
|
|||
String myHandle,
|
||||
FriendicaClient? client,
|
||||
) async {
|
||||
final updatedPosts = <EntryTreeItem>[];
|
||||
final allSeenItems = [...items];
|
||||
for (final item in items) {
|
||||
late EntryTreeItem entry;
|
||||
final isMine = item.author == myHandle;
|
||||
if (item.parentAuthor.isEmpty) {
|
||||
entry = _posts.putIfAbsent(item.id,
|
||||
() => EntryTreeItem(item, isMine: isMine, isOrphaned: false));
|
||||
updatedPosts.add(entry);
|
||||
_entries[item.id] = item;
|
||||
}
|
||||
|
||||
final orphans = <TimelineEntry>[];
|
||||
for (final item in items) {
|
||||
final parent = _entries[item.parentId];
|
||||
if (parent == null) {
|
||||
orphans.add(item);
|
||||
} else {
|
||||
final parentPost = _posts[item.parentId];
|
||||
bool newEntry = false;
|
||||
entry = _allComments.putIfAbsent(item.id, () {
|
||||
newEntry = true;
|
||||
return EntryTreeItem(
|
||||
item,
|
||||
isMine: isMine,
|
||||
isOrphaned: parentPost == null,
|
||||
);
|
||||
});
|
||||
|
||||
if (parentPost != null) {
|
||||
parentPost.addChild(entry);
|
||||
updatedPosts.add(parentPost);
|
||||
}
|
||||
|
||||
if (newEntry && entry.isOrphaned) {
|
||||
_orphanedComments.add(entry);
|
||||
if (parent.parentId.isEmpty) {
|
||||
_parentPostIds[item.id] = parent.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final orphansToProcess = [];
|
||||
for (final c in _orphanedComments) {
|
||||
final post = _posts[c.entry.parentId];
|
||||
if (post != null) {
|
||||
c.isOrphaned = false;
|
||||
post.addChild(c);
|
||||
updatedPosts.add(post);
|
||||
continue;
|
||||
}
|
||||
|
||||
final comment = _allComments[c.entry.parentId];
|
||||
if (comment != null) {
|
||||
c.isOrphaned = false;
|
||||
comment.addChild(c);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (client != null) {
|
||||
orphansToProcess.add(c);
|
||||
}
|
||||
}
|
||||
|
||||
for (final o in orphansToProcess) {
|
||||
for (final o in orphans) {
|
||||
await client
|
||||
?.getPostOrComment(o.id, fullContext: true)
|
||||
.andThenSuccessAsync((items) async {
|
||||
final moreUpdatedPosts = await processNewItems(items, myHandle, null);
|
||||
updatedPosts.addAll(moreUpdatedPosts);
|
||||
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;
|
||||
}
|
||||
;
|
||||
});
|
||||
}
|
||||
|
||||
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 {
|
||||
final parentPostNode = _postNodes[_parentPostIds[item.id]]!;
|
||||
postNodesToReturn.add(parentPostNode);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final updatedPosts =
|
||||
postNodesToReturn.map((node) => _nodeToTreeItem(node)).toList();
|
||||
|
||||
_logger.finest(
|
||||
'Completed processing new items ${client == null ? 'sub level' : 'top level'}');
|
||||
return updatedPosts;
|
||||
}
|
||||
|
||||
FutureResult<EntryTreeItem, ExecError> refreshPost(EntryTreeItem item) async {
|
||||
_logger.finest('Refreshing post: ${item.id}');
|
||||
FutureResult<EntryTreeItem, ExecError> refreshPost(String id) async {
|
||||
_logger.finest('Refreshing post: $id');
|
||||
final auth = getIt<AuthService>();
|
||||
final clientResult = auth.currentClient;
|
||||
if (clientResult.isFailure) {
|
||||
|
@ -134,18 +130,22 @@ class EntryManagerService extends ChangeNotifier {
|
|||
|
||||
final client = clientResult.value;
|
||||
final result = await client
|
||||
.getPostOrComment(item.id, fullContext: true)
|
||||
.getPostOrComment(id, fullContext: false)
|
||||
.andThenSuccessAsync((items) async {
|
||||
await processNewItems(items, client.credentials.username, null);
|
||||
});
|
||||
await processNewItems(items, client.credentials.username, null);
|
||||
})
|
||||
.andThenAsync(
|
||||
(_) async => await client.getPostOrComment(id, fullContext: true))
|
||||
.andThenSuccessAsync((items) async {
|
||||
await processNewItems(items, client.credentials.username, null);
|
||||
});
|
||||
|
||||
return result.mapValue((_) {
|
||||
_logger.finest('${item.id} post updated');
|
||||
notifyListeners();
|
||||
return _posts[item.id]!;
|
||||
_logger.finest('$id post updated');
|
||||
return _nodeToTreeItem(_postNodes[id]!);
|
||||
}).mapError(
|
||||
(error) {
|
||||
_logger.finest('${item.id} error updating: $error');
|
||||
_logger.finest('$id error updating: $error');
|
||||
return ExecError(
|
||||
type: ErrorType.localError,
|
||||
message: error.toString(),
|
||||
|
@ -155,8 +155,7 @@ class EntryManagerService extends ChangeNotifier {
|
|||
}
|
||||
|
||||
FutureResult<EntryTreeItem, ExecError> toggleFavorited(
|
||||
String id, bool newStatus,
|
||||
{bool notify = false}) async {
|
||||
String id, bool newStatus) async {
|
||||
final auth = getIt<AuthService>();
|
||||
final clientResult = auth.currentClient;
|
||||
if (clientResult.isFailure) {
|
||||
|
@ -170,35 +169,60 @@ class EntryManagerService extends ChangeNotifier {
|
|||
}
|
||||
|
||||
final update = result.value;
|
||||
late EntryTreeItem rval;
|
||||
if (_posts.containsKey(update.id)) {
|
||||
rval = _posts[update.id]!.copy(entry: update);
|
||||
_posts[update.id] = rval;
|
||||
_updateChildrenEntities(rval, update);
|
||||
}
|
||||
_entries[update.id] = update;
|
||||
final node = update.parentId.isEmpty
|
||||
? _postNodes[update.id]!
|
||||
: _postNodes[update.parentId]!.getChildById(update.id)!;
|
||||
|
||||
if (_allComments.containsKey(update.id)) {
|
||||
rval = _allComments[update.id]!.copy(entry: update);
|
||||
_allComments[update.id] = rval;
|
||||
_updateChildrenEntities(rval, update);
|
||||
}
|
||||
|
||||
if (notify) {
|
||||
notifyListeners();
|
||||
}
|
||||
return Result.ok(rval);
|
||||
notifyListeners();
|
||||
return Result.ok(_nodeToTreeItem(node));
|
||||
}
|
||||
|
||||
void _updateChildrenEntities(EntryTreeItem item, TimelineEntry entry) {
|
||||
final updates = item.children.where((element) => element.id == entry.id);
|
||||
for (final u in updates) {
|
||||
final newItem = u.copy(entry: entry);
|
||||
item.children.remove(u);
|
||||
item.children.add(newItem);
|
||||
}
|
||||
|
||||
for (final c in item.children) {
|
||||
_updateChildrenEntities(c, entry);
|
||||
EntryTreeItem _nodeToTreeItem(_Node node) {
|
||||
final childenEntries = <String, EntryTreeItem>{};
|
||||
for (final c in node.children) {
|
||||
childenEntries[c.id] = _nodeToTreeItem(c);
|
||||
}
|
||||
return EntryTreeItem(_entries[node.id]!, initialChildren: childenEntries);
|
||||
}
|
||||
}
|
||||
|
||||
class _Node {
|
||||
final String id;
|
||||
final _children = <String, _Node>{};
|
||||
|
||||
List<_Node> get children => _children.values.toList();
|
||||
|
||||
_Node(this.id, {Map<String, _Node>? initialChildren}) {
|
||||
if (initialChildren != null) {
|
||||
_children.addAll(initialChildren);
|
||||
}
|
||||
}
|
||||
|
||||
void addChild(_Node node) {
|
||||
_children[node.id] = node;
|
||||
}
|
||||
|
||||
_Node? getChildById(String id) {
|
||||
if (_children.containsKey(id)) {
|
||||
return _children[id]!;
|
||||
}
|
||||
|
||||
for (final c in _children.values) {
|
||||
final result = c.getChildById(id);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is _Node && runtimeType == other.runtimeType && id == other.id;
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode;
|
||||
}
|
||||
|
|
|
@ -22,20 +22,32 @@ class TimelineManager extends ChangeNotifier {
|
|||
|
||||
// refresh timeline gets statuses newer than the newest in that timeline
|
||||
Result<List<EntryTreeItem>, ExecError> getTimeline(TimelineIdentifiers type) {
|
||||
_logger.finest('Getting timeline $type');
|
||||
final posts = cachedTimelines[type]?.posts;
|
||||
if (posts != null) {
|
||||
return Result.ok(posts);
|
||||
}
|
||||
|
||||
refreshTimeline(type);
|
||||
refreshTimeline(type, true);
|
||||
|
||||
return Result.ok([]);
|
||||
}
|
||||
|
||||
Future<void> refreshTimeline(TimelineIdentifiers type) async {
|
||||
FutureResult<EntryTreeItem, ExecError> refreshPost(String id) async {
|
||||
final result = await getIt<EntryManagerService>().refreshPost(id);
|
||||
if (result.isSuccess) {
|
||||
for (final t in cachedTimelines.values) {
|
||||
t.addOrUpdate([result.value]);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<void> refreshTimeline(TimelineIdentifiers type, bool reload) async {
|
||||
final timeline = cachedTimelines.putIfAbsent(type, () => Timeline(type));
|
||||
(await getIt<EntryManagerService>()
|
||||
.updateTimeline(type, timeline.highestStatusId))
|
||||
.updateTimeline(type, reload ? 0 : timeline.highestStatusId))
|
||||
.match(onSuccess: (posts) {
|
||||
_logger.finest('Posts returned for adding to $type: ${posts.length}');
|
||||
timeline.addOrUpdate(posts);
|
||||
|
@ -43,19 +55,38 @@ class TimelineManager extends ChangeNotifier {
|
|||
}, onError: (error) {
|
||||
_logger.severe('Error getting timeline: $type}');
|
||||
});
|
||||
}
|
||||
|
||||
FutureResult<EntryTreeItem, ExecError> toggleFavorited(
|
||||
String id, bool newStatus) async {
|
||||
_logger.finest('Attempting toggling favorite $id to $newStatus');
|
||||
final result =
|
||||
await getIt<EntryManagerService>().toggleFavorited(id, newStatus);
|
||||
if (result.isFailure) {
|
||||
_logger.finest('Error toggling favorite $id: ${result.error}');
|
||||
return result;
|
||||
}
|
||||
|
||||
final update = result.value;
|
||||
for (final timeline in cachedTimelines.values) {
|
||||
update.entry.parentId.isEmpty
|
||||
? timeline.addOrUpdate([update])
|
||||
: timeline.tryUpdateComment(update);
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
return result;
|
||||
}
|
||||
// All statuses get dumped into the entity mangager and get full assembled posts out of it
|
||||
// Timeline keeps track of posts level only so can query timeline manager for those
|
||||
// Should put backing store on timelines and entity manager so can recover from restart faster
|
||||
// Have a purge caches button to start that over from scratch
|
||||
// Should have a contacts manager with backing store as well
|
||||
// Timeline view is new control that knows how to load timeline, scrolling around with refresh and get more
|
||||
// Timeline Item view displays itself and children
|
||||
// Has "Add Comment" value
|
||||
// Has like/dislke
|
||||
// Has reshare/quote reshare (if can get that working somehow)
|
||||
// If our own has delete
|
||||
// All statuses get dumped into the entity mangager and get full assembled posts out of it
|
||||
// Timeline keeps track of posts level only so can query timeline manager for those
|
||||
// Should put backing store on timelines and entity manager so can recover from restart faster
|
||||
// Have a purge caches button to start that over from scratch
|
||||
// Should have a contacts manager with backing store as well
|
||||
// Timeline view is new control that knows how to load timeline, scrolling around with refresh and get more
|
||||
// Timeline Item view displays itself and children
|
||||
// Has "Add Comment" value
|
||||
// Has like/dislke
|
||||
// Has reshare/quote reshare (if can get that working somehow)
|
||||
// If our own has delete
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue