kopia lustrzana https://gitlab.com/mysocialportal/relatica
Refactor TimelineManager and EntryManager to Riverpod providers
rodzic
b8b2ea7d26
commit
fc6e17f133
|
@ -1,10 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../riverpod_controllers/hashtag_service.dart';
|
||||
import '../../services/entry_manager_service.dart';
|
||||
import '../../utils/active_profile_selector.dart';
|
||||
|
||||
class HashtagAutocompleteOptions extends ConsumerWidget {
|
||||
const HashtagAutocompleteOptions({
|
||||
|
@ -20,12 +17,14 @@ class HashtagAutocompleteOptions extends ConsumerWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final manager = context
|
||||
.read<ActiveProfileSelector<EntryManagerService>>()
|
||||
.activeEntry
|
||||
.value;
|
||||
final postTreeHashtags =
|
||||
manager.getPostTreeHashtags(id).getValueOrElse(() => [])..sort();
|
||||
//TODO Replace with new Riverpod hashtags services
|
||||
// final manager = context
|
||||
// .read<ActiveProfileSelector<EntryManagerService>>()
|
||||
// .activeEntry
|
||||
// .value;
|
||||
// final postTreeHashtags =
|
||||
// manager.getPostTreeHashtags(id).getValueOrElse(() => [])..sort();
|
||||
final postTreeHashtags = [];
|
||||
final hashtagsFromService =
|
||||
ref.watch(hashtagServiceProvider(searchString: query));
|
||||
final hashtags = [...postTreeHashtags, ...hashtagsFromService];
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../models/connection.dart';
|
||||
import '../../services/connections_manager.dart';
|
||||
import '../../services/entry_manager_service.dart';
|
||||
import '../../utils/active_profile_selector.dart';
|
||||
import '../image_control.dart';
|
||||
|
||||
class MentionAutocompleteOptions extends StatelessWidget {
|
||||
|
@ -21,28 +17,30 @@ class MentionAutocompleteOptions extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final entryManager = context
|
||||
.read<ActiveProfileSelector<EntryManagerService>>()
|
||||
.activeEntry
|
||||
.value;
|
||||
|
||||
final connectionManager = context
|
||||
.read<ActiveProfileSelector<ConnectionsManager>>()
|
||||
.activeEntry
|
||||
.value;
|
||||
|
||||
final postTreeUsers = entryManager
|
||||
.getPostTreeConnectionIds(id)
|
||||
.getValueOrElse(() => [])
|
||||
.map((id) => connectionManager.getById(id))
|
||||
.where((result) => result.isSuccess)
|
||||
.map((result) => result.value)
|
||||
.toList()
|
||||
..sort((u1, u2) => u1.name.compareTo(u2.name));
|
||||
|
||||
final knownUsers = connectionManager.getKnownUsersByName(query);
|
||||
|
||||
final users = [...postTreeUsers, ...knownUsers];
|
||||
//TODO Replace with new Riverpod mentions service
|
||||
// final entryManager = context
|
||||
// .read<ActiveProfileSelector<EntryManagerService>>()
|
||||
// .activeEntry
|
||||
// .value;
|
||||
//
|
||||
// final connectionManager = context
|
||||
// .read<ActiveProfileSelector<ConnectionsManager>>()
|
||||
// .activeEntry
|
||||
// .value;
|
||||
//
|
||||
// final postTreeUsers = entryManager
|
||||
// .getPostTreeConnectionIds(id)
|
||||
// .getValueOrElse(() => [])
|
||||
// .map((id) => connectionManager.getById(id))
|
||||
// .where((result) => result.isSuccess)
|
||||
// .map((result) => result.value)
|
||||
// .toList()
|
||||
// ..sort((u1, u2) => u1.name.compareTo(u2.name));
|
||||
//
|
||||
// final knownUsers = connectionManager.getKnownUsersByName(query);
|
||||
//
|
||||
// final users = [...postTreeUsers, ...knownUsers];
|
||||
final users = [];
|
||||
|
||||
if (users.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
|
|
|
@ -6,13 +6,14 @@ import 'package:provider/provider.dart';
|
|||
import 'package:result_monad/result_monad.dart';
|
||||
|
||||
import '../globals.dart';
|
||||
import '../models/auth/profile.dart';
|
||||
import '../models/exec_error.dart';
|
||||
import '../models/user_notification.dart';
|
||||
import '../riverpod_controllers/entry_tree_item_services.dart';
|
||||
import '../riverpod_controllers/notification_services.dart';
|
||||
import '../routes.dart';
|
||||
import '../services/auth_service.dart';
|
||||
import '../services/connections_manager.dart';
|
||||
import '../services/timeline_manager.dart';
|
||||
import '../utils/active_profile_selector.dart';
|
||||
import '../utils/dateutils.dart';
|
||||
import '../utils/snackbar_builder.dart';
|
||||
|
@ -29,16 +30,18 @@ class NotificationControl extends ConsumerWidget {
|
|||
required this.notification,
|
||||
});
|
||||
|
||||
Future<void> _goToStatus(BuildContext context) async {
|
||||
final manager =
|
||||
getIt<ActiveProfileSelector<TimelineManager>>().activeEntry.value;
|
||||
final existingPostData = manager.getPostTreeEntryBy(notification.iid);
|
||||
Future<void> _goToStatus(
|
||||
BuildContext context, WidgetRef ref, Profile profile) async {
|
||||
final existingPostData =
|
||||
ref.read(postTreeEntryByIdProvider(profile, notification.iid));
|
||||
if (existingPostData.isSuccess) {
|
||||
context
|
||||
.push('/post/view/${existingPostData.value.id}/${notification.iid}');
|
||||
return;
|
||||
}
|
||||
final loadedPost = await manager.refreshStatusChain(notification.iid);
|
||||
final loadedPost = await ref
|
||||
.read(timelineUpdaterProvider(profile).notifier)
|
||||
.refreshStatusChain(notification.iid);
|
||||
if (loadedPost.isSuccess) {
|
||||
if (context.mounted) {
|
||||
context.push('/post/view/${loadedPost.value.id}/${notification.iid}');
|
||||
|
@ -105,7 +108,7 @@ class NotificationControl extends ConsumerWidget {
|
|||
case NotificationType.reblog:
|
||||
case NotificationType.status:
|
||||
onTapCallFunction = () async {
|
||||
await _goToStatus(context);
|
||||
await _goToStatus(context, ref, profile);
|
||||
};
|
||||
break;
|
||||
case NotificationType.direct_message:
|
||||
|
|
|
@ -6,17 +6,17 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
import 'package:go_router/go_router.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:relatica/riverpod_controllers/timeline_entry_filter_services.dart';
|
||||
import 'package:result_monad/result_monad.dart';
|
||||
|
||||
import '../../globals.dart';
|
||||
import '../../models/auth/profile.dart';
|
||||
import '../../models/filters/timeline_entry_filter.dart';
|
||||
import '../../models/flattened_tree_item.dart';
|
||||
import '../../models/timeline_entry.dart';
|
||||
import '../../riverpod_controllers/entry_tree_item_services.dart';
|
||||
import '../../riverpod_controllers/settings_services.dart';
|
||||
import '../../riverpod_controllers/timeline_entry_filter_services.dart';
|
||||
import '../../services/auth_service.dart';
|
||||
import '../../services/timeline_manager.dart';
|
||||
import '../../utils/active_profile_selector.dart';
|
||||
import '../../utils/clipboard_utils.dart';
|
||||
import '../../utils/filter_runner.dart';
|
||||
import '../../utils/html_to_edit_text_helper.dart';
|
||||
|
@ -104,7 +104,7 @@ class _StatusControlState extends ConsumerState<FlattenedTreeEntryControl> {
|
|||
if (filteringInfo.isFiltered && !showFilteredPost) {
|
||||
body = buildHiddenBody(context, filteringInfo);
|
||||
} else {
|
||||
body = buildMainWidgetBody(context);
|
||||
body = buildMainWidgetBody(context, profile);
|
||||
}
|
||||
|
||||
final decoration = isPost
|
||||
|
@ -145,7 +145,7 @@ class _StatusControlState extends ConsumerState<FlattenedTreeEntryControl> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget buildMainWidgetBody(BuildContext context) {
|
||||
Widget buildMainWidgetBody(BuildContext context, Profile profile) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(5.0),
|
||||
child: Column(
|
||||
|
@ -165,7 +165,7 @@ class _StatusControlState extends ConsumerState<FlattenedTreeEntryControl> {
|
|||
showFilteredPost = false;
|
||||
}),
|
||||
icon: const Icon(Icons.hide_source)),
|
||||
buildMenuControl(context),
|
||||
buildMenuControl(context, profile),
|
||||
],
|
||||
),
|
||||
const VerticalPadding(
|
||||
|
@ -280,7 +280,7 @@ class _StatusControlState extends ConsumerState<FlattenedTreeEntryControl> {
|
|||
itemCount: items.length));
|
||||
}
|
||||
|
||||
Widget buildMenuControl(BuildContext context) {
|
||||
Widget buildMenuControl(BuildContext context, Profile profile) {
|
||||
const editStatus = 'Edit';
|
||||
const deleteStatus = 'Delete';
|
||||
const divider = 'Divider';
|
||||
|
@ -326,7 +326,7 @@ class _StatusControlState extends ConsumerState<FlattenedTreeEntryControl> {
|
|||
}
|
||||
break;
|
||||
case deleteStatus:
|
||||
deleteEntry();
|
||||
deleteEntry(profile);
|
||||
break;
|
||||
case openExternal:
|
||||
await openUrlStringInSystembrowser(
|
||||
|
@ -376,17 +376,16 @@ class _StatusControlState extends ConsumerState<FlattenedTreeEntryControl> {
|
|||
});
|
||||
}
|
||||
|
||||
Future<void> deleteEntry() async {
|
||||
Future<void> deleteEntry(Profile profile) async {
|
||||
setState(() {
|
||||
isProcessing = true;
|
||||
});
|
||||
final confirm =
|
||||
await showYesNoDialog(context, 'Delete ${isPost ? "Post" : "Comment"}');
|
||||
if (confirm == true) {
|
||||
await getIt<ActiveProfileSelector<TimelineManager>>()
|
||||
.activeEntry
|
||||
.transformAsync(
|
||||
(tm) async => await tm.deleteEntryById(item.timelineEntry.id))
|
||||
await ref
|
||||
.read(timelineUpdaterProvider(profile).notifier)
|
||||
.deleteEntryById(item.timelineEntry.id)
|
||||
.match(onSuccess: (_) {
|
||||
isProcessing = false;
|
||||
if (context.canPop()) {
|
||||
|
|
|
@ -2,15 +2,17 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:result_monad/result_monad.dart';
|
||||
|
||||
import '../../globals.dart';
|
||||
import '../../models/auth/profile.dart';
|
||||
import '../../models/exec_error.dart';
|
||||
import '../../models/timeline_entry.dart';
|
||||
import '../../riverpod_controllers/timeline_entry_services.dart';
|
||||
import '../../services/auth_service.dart';
|
||||
import '../../services/feature_version_checker.dart';
|
||||
import '../../services/fediverse_server_validator.dart';
|
||||
import '../../services/timeline_manager.dart';
|
||||
import '../../utils/active_profile_selector.dart';
|
||||
import '../../utils/dateutils.dart';
|
||||
import '../../utils/interaction_availability_util.dart';
|
||||
import '../../utils/snackbar_builder.dart';
|
||||
|
@ -50,20 +52,18 @@ class _InteractionsBarControlState
|
|||
|
||||
int get likes => widget.entry.engagementSummary.favoritesCount;
|
||||
|
||||
Future<void> toggleFavorited() async {
|
||||
Future<void> toggleFavorited(Profile profile) async {
|
||||
setState(() {
|
||||
isProcessing = true;
|
||||
});
|
||||
final newState = !isFavorited;
|
||||
_logger.fine('Trying to toggle favorite from $isFavorited to $newState');
|
||||
final result = await getIt<ActiveProfileSelector<TimelineManager>>()
|
||||
.activeEntry
|
||||
.andThenAsync(
|
||||
(tm) async => await tm.toggleFavorited(widget.entry.id, newState));
|
||||
final result = await ref
|
||||
.read(timelineEntryManagerProvider(profile, widget.entry.id).notifier)
|
||||
.toggleFavorited(newState);
|
||||
result.match(onSuccess: (update) {
|
||||
setState(() {
|
||||
_logger.fine(
|
||||
'Success toggling! $isFavorited -> ${update.entry.isFavorited}');
|
||||
_logger.fine('Success toggling! $isFavorited -> ${update.isFavorited}');
|
||||
});
|
||||
}, onError: (error) {
|
||||
buildSnackbar(context, 'Error toggling like status: $error');
|
||||
|
@ -73,7 +73,7 @@ class _InteractionsBarControlState
|
|||
});
|
||||
}
|
||||
|
||||
Future<void> resharePost() async {
|
||||
Future<void> resharePost(Profile profile) async {
|
||||
setState(() {
|
||||
isProcessing = true;
|
||||
});
|
||||
|
@ -99,9 +99,9 @@ class _InteractionsBarControlState
|
|||
|
||||
final id = widget.entry.id;
|
||||
_logger.fine('Trying to reshare $id');
|
||||
final result = await getIt<ActiveProfileSelector<TimelineManager>>()
|
||||
.activeEntry
|
||||
.andThenAsync((tm) async => await tm.resharePost(id));
|
||||
final result = await await ref
|
||||
.read(timelineEntryManagerProvider(profile, widget.entry.id).notifier)
|
||||
.resharePost();
|
||||
|
||||
result.match(onSuccess: (update) {
|
||||
setState(() {
|
||||
|
@ -139,15 +139,15 @@ class _InteractionsBarControlState
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> unResharePost() async {
|
||||
Future<void> unResharePost(Profile profile) async {
|
||||
setState(() {
|
||||
isProcessing = true;
|
||||
});
|
||||
final id = widget.entry.id;
|
||||
_logger.fine('Trying to un-reshare $id');
|
||||
final result = await getIt<ActiveProfileSelector<TimelineManager>>()
|
||||
.activeEntry
|
||||
.andThenAsync((tm) async => await tm.unResharePost(id));
|
||||
final result = await await ref
|
||||
.read(timelineEntryManagerProvider(profile, widget.entry.id).notifier)
|
||||
.unResharePost();
|
||||
result.match(onSuccess: (update) {
|
||||
setState(() {
|
||||
_logger.fine('Success un-resharing post by ${widget.entry.author}');
|
||||
|
@ -181,7 +181,7 @@ class _InteractionsBarControlState
|
|||
);
|
||||
}
|
||||
|
||||
Widget buildLikeButton() {
|
||||
Widget buildLikeButton(Profile profile) {
|
||||
final canReact = widget.entry.getCanReact(ref);
|
||||
final tooltip =
|
||||
canReact.canDo ? 'Press to toggle like/unlike' : canReact.reason;
|
||||
|
@ -190,7 +190,7 @@ class _InteractionsBarControlState
|
|||
likes,
|
||||
true,
|
||||
tooltip,
|
||||
canReact.canDo ? () async => await toggleFavorited() : null,
|
||||
canReact.canDo ? () async => await toggleFavorited(profile) : null,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -207,7 +207,7 @@ class _InteractionsBarControlState
|
|||
);
|
||||
}
|
||||
|
||||
Widget buildReshareButton() {
|
||||
Widget buildReshareButton(Profile profile) {
|
||||
final reshareable = widget.entry.getIsReshareable(ref, widget.isMine);
|
||||
final canReshare = reshareable.canDo;
|
||||
late final String tooltip;
|
||||
|
@ -222,8 +222,9 @@ class _InteractionsBarControlState
|
|||
true,
|
||||
tooltip,
|
||||
canReshare && !isProcessing
|
||||
? () async =>
|
||||
youReshared ? await unResharePost() : await resharePost()
|
||||
? () async => youReshared
|
||||
? await unResharePost(profile)
|
||||
: await resharePost(profile)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
@ -231,12 +232,13 @@ class _InteractionsBarControlState
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_logger.finest('Building: ${widget.entry.toShortString()}');
|
||||
final profile = context.watch<AccountsService>().currentProfile;
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
buildLikeButton(),
|
||||
buildLikeButton(profile),
|
||||
buildCommentButton(),
|
||||
if (widget.entry.parentId.isEmpty) buildReshareButton(),
|
||||
if (widget.entry.parentId.isEmpty) buildReshareButton(profile),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,17 +4,15 @@ import 'package:logging/logging.dart';
|
|||
import 'package:provider/provider.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
|
||||
import '../../models/entry_tree_item.dart';
|
||||
import '../../models/flattened_tree_item.dart';
|
||||
import '../../models/timeline_entry.dart';
|
||||
import '../../riverpod_controllers/entry_tree_item_services.dart';
|
||||
import '../../riverpod_controllers/timeline_entry_services.dart';
|
||||
import '../../services/auth_service.dart';
|
||||
import '../../services/timeline_manager.dart';
|
||||
import '../../utils/active_profile_selector.dart';
|
||||
import '../../utils/entry_tree_item_flattening.dart';
|
||||
import 'flattened_tree_entry_control.dart';
|
||||
|
||||
class PostControl extends ConsumerStatefulWidget {
|
||||
final EntryTreeItem originalItem;
|
||||
final String id;
|
||||
final String scrollToId;
|
||||
final bool openRemote;
|
||||
final bool showStatusOpenButton;
|
||||
|
@ -22,7 +20,7 @@ class PostControl extends ConsumerStatefulWidget {
|
|||
|
||||
const PostControl({
|
||||
super.key,
|
||||
required this.originalItem,
|
||||
required this.id,
|
||||
required this.scrollToId,
|
||||
required this.openRemote,
|
||||
required this.showStatusOpenButton,
|
||||
|
@ -40,10 +38,6 @@ class _PostControlState extends ConsumerState<PostControl> {
|
|||
final ItemPositionsListener itemPositionsListener =
|
||||
ItemPositionsListener.create();
|
||||
|
||||
EntryTreeItem get item => widget.originalItem;
|
||||
|
||||
TimelineEntry get entry => item.entry;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
@ -52,17 +46,19 @@ class _PostControlState extends ConsumerState<PostControl> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final profile = context.watch<AccountsService>().currentProfile;
|
||||
context.watch<ActiveProfileSelector<TimelineManager>>();
|
||||
_logger.finest('Building ${item.entry.toShortString()}');
|
||||
//TODO Handle it not existing
|
||||
final item = ref.watch(entryTreeManagerProvider(profile, widget.id)).value;
|
||||
ref.watch(timelineEntryManagerProvider(profile, widget.id));
|
||||
if (!widget.isRoot) {
|
||||
return FlattenedTreeEntryControl(
|
||||
originalItem: item.flatten(topLevelOnly: true).first,
|
||||
originalItem:
|
||||
item.flatten(topLevelOnly: true, profile: profile, ref: ref).first,
|
||||
openRemote: widget.openRemote,
|
||||
showStatusOpenButton: widget.showStatusOpenButton,
|
||||
);
|
||||
}
|
||||
|
||||
final items = widget.originalItem.flatten();
|
||||
final items = item.flatten(profile: profile, ref: ref);
|
||||
return buildListView(context, items);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,31 +1,33 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:relatica/controls/padding.dart';
|
||||
import 'package:relatica/globals.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
|
||||
import '../../models/auth/profile.dart';
|
||||
import '../../models/timeline_identifiers.dart';
|
||||
import '../../riverpod_controllers/timeline_services.dart';
|
||||
import '../../services/auth_service.dart';
|
||||
import '../../services/network_status_service.dart';
|
||||
import '../../services/timeline_manager.dart';
|
||||
import '../../utils/active_profile_selector.dart';
|
||||
import 'post_control.dart';
|
||||
|
||||
class TimelinePanel extends StatelessWidget {
|
||||
class TimelinePanel extends ConsumerWidget {
|
||||
static final _logger = Logger('$TimelinePanel');
|
||||
final TimelineIdentifiers timeline;
|
||||
final controller = ItemScrollController();
|
||||
|
||||
TimelinePanel({super.key, required this.timeline});
|
||||
|
||||
Future<void> update(BuildContext context, TimelineManager manager) async {
|
||||
Future<void> update(
|
||||
BuildContext context, Profile profile, WidgetRef ref) async {
|
||||
final confirm =
|
||||
await showYesNoDialog(context, 'Reload timeline from scratch?');
|
||||
if (confirm == true) {
|
||||
await manager.updateTimeline(
|
||||
timeline,
|
||||
TimelineRefreshType.refresh,
|
||||
);
|
||||
await ref
|
||||
.read(timelineManagerProvider(profile, timeline).notifier)
|
||||
.updateTimeline(TimelineRefreshType.refresh);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -34,19 +36,16 @@ class TimelinePanel extends StatelessWidget {
|
|||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
_logger.finer('Build');
|
||||
final nss = getIt<NetworkStatusService>();
|
||||
final manager = context
|
||||
.watch<ActiveProfileSelector<TimelineManager>>()
|
||||
.activeEntry
|
||||
.value;
|
||||
final items = manager.getTimeline(timeline);
|
||||
final profile = context.watch<AccountsService>().currentProfile;
|
||||
final postIds = ref.watch(timelineManagerProvider(profile, timeline)).posts;
|
||||
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: nss.timelineLoadingStatus,
|
||||
builder: (BuildContext context, bool loading, Widget? _) {
|
||||
if (items.isEmpty && loading) {
|
||||
if (postIds.isEmpty && loading) {
|
||||
return const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
@ -57,7 +56,7 @@ class TimelinePanel extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
|
||||
if (items.isEmpty) {
|
||||
if (postIds.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
@ -65,8 +64,10 @@ class TimelinePanel extends StatelessWidget {
|
|||
const Text('No posts loaded'),
|
||||
const VerticalPadding(),
|
||||
ElevatedButton(
|
||||
onPressed: () => manager.updateTimeline(
|
||||
timeline, TimelineRefreshType.refresh),
|
||||
onPressed: () => ref
|
||||
.read(
|
||||
timelineManagerProvider(profile, timeline).notifier)
|
||||
.updateTimeline(TimelineRefreshType.refresh),
|
||||
child: const Text('Load Posts'))
|
||||
],
|
||||
),
|
||||
|
@ -75,43 +76,46 @@ class TimelinePanel extends StatelessWidget {
|
|||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
update(context, manager);
|
||||
update(context, profile, ref);
|
||||
return;
|
||||
},
|
||||
child: ScrollablePositionedList.separated(
|
||||
itemScrollController: controller,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
separatorBuilder: (context, index) =>
|
||||
index == 0 || index == items.length
|
||||
index == 0 || index == postIds.length
|
||||
? const SizedBox()
|
||||
: const Divider(),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0) {
|
||||
return TextButton(
|
||||
onPressed: () async => await manager.updateTimeline(
|
||||
timeline, TimelineRefreshType.loadNewer),
|
||||
onPressed: () async => await ref
|
||||
.read(
|
||||
timelineManagerProvider(profile, timeline).notifier)
|
||||
.updateTimeline(TimelineRefreshType.loadNewer),
|
||||
child: const Text('Load newer posts'));
|
||||
}
|
||||
|
||||
if (index == items.length + 1) {
|
||||
if (index == postIds.length + 1) {
|
||||
return TextButton(
|
||||
onPressed: () async => await manager.updateTimeline(
|
||||
timeline, TimelineRefreshType.loadOlder),
|
||||
onPressed: () async => await ref
|
||||
.read(
|
||||
timelineManagerProvider(profile, timeline).notifier)
|
||||
.updateTimeline(TimelineRefreshType.loadOlder),
|
||||
child: const Text('Load older posts'));
|
||||
}
|
||||
final itemIndex = index - 1;
|
||||
final item = items[itemIndex];
|
||||
TimelinePanel._logger.finest(
|
||||
'Building item: $itemIndex: ${item.entry.toShortString()}');
|
||||
final id = postIds[itemIndex];
|
||||
TimelinePanel._logger.finest('Building item: $itemIndex: $id');
|
||||
return PostControl(
|
||||
originalItem: item,
|
||||
scrollToId: item.id,
|
||||
id: id,
|
||||
scrollToId: id,
|
||||
openRemote: false,
|
||||
showStatusOpenButton: true,
|
||||
isRoot: false,
|
||||
);
|
||||
},
|
||||
itemCount: items.length + 2,
|
||||
itemCount: postIds.length + 2,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
|
@ -17,7 +17,6 @@ import 'models/auth/profile.dart';
|
|||
import 'models/instance_info.dart';
|
||||
import 'services/auth_service.dart';
|
||||
import 'services/connections_manager.dart';
|
||||
import 'services/entry_manager_service.dart';
|
||||
import 'services/feature_version_checker.dart';
|
||||
import 'services/fediverse_server_validator.dart';
|
||||
import 'services/follow_requests_manager.dart';
|
||||
|
@ -26,7 +25,6 @@ import 'services/network_status_service.dart';
|
|||
import 'services/persistent_info_service.dart';
|
||||
import 'services/reshared_via_service.dart';
|
||||
import 'services/secrets_service.dart';
|
||||
import 'services/timeline_manager.dart';
|
||||
import 'utils/active_profile_selector.dart';
|
||||
|
||||
final _logger = Logger('DI_Init');
|
||||
|
@ -90,18 +88,6 @@ Future<void> dependencyInjectionInitialization() async {
|
|||
getIt.registerSingleton<ActiveProfileSelector<GalleryService>>(
|
||||
ActiveProfileSelector((p) => GalleryService(p))
|
||||
..subscribeToProfileSwaps());
|
||||
getIt.registerSingleton<ActiveProfileSelector<EntryManagerService>>(
|
||||
ActiveProfileSelector((p) => EntryManagerService(p))
|
||||
..subscribeToProfileSwaps());
|
||||
getIt.registerSingleton<ActiveProfileSelector<TimelineManager>>(
|
||||
ActiveProfileSelector((p) => TimelineManager(
|
||||
p,
|
||||
getIt<ActiveProfileSelector<ICirclesRepo>>().getForProfile(p).value,
|
||||
getIt<ActiveProfileSelector<EntryManagerService>>()
|
||||
.getForProfile(p)
|
||||
.value,
|
||||
))
|
||||
..subscribeToProfileSwaps());
|
||||
getIt.registerSingleton<ActiveProfileSelector<FollowRequestsManager>>(
|
||||
ActiveProfileSelector((p) => FollowRequestsManager(p))
|
||||
..subscribeToProfileSwaps());
|
||||
|
@ -134,12 +120,6 @@ void clearCaches() {
|
|||
_logger.severe('Error clearing IConnections Repo: $error'),
|
||||
);
|
||||
|
||||
getIt<ActiveProfileSelector<EntryManagerService>>().activeEntry.match(
|
||||
onSuccess: (service) => service.clear(),
|
||||
onError: (error) =>
|
||||
_logger.severe('Error clearing EntryManagerService Repo: $error'),
|
||||
);
|
||||
|
||||
getIt<ActiveProfileSelector<FollowRequestsManager>>().activeEntry.match(
|
||||
onSuccess: (manager) => manager.clear(),
|
||||
onError: (error) =>
|
||||
|
@ -151,10 +131,4 @@ void clearCaches() {
|
|||
onError: (error) =>
|
||||
_logger.severe('Error clearing GalleryService Repo: $error'),
|
||||
);
|
||||
|
||||
getIt<ActiveProfileSelector<TimelineManager>>().activeEntry.match(
|
||||
onSuccess: (manager) => manager.clear(),
|
||||
onError: (error) =>
|
||||
_logger.severe('Error clearing TimelineManager Repo: $error'),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -17,10 +17,8 @@ import 'riverpod_controllers/settings_services.dart';
|
|||
import 'routes.dart';
|
||||
import 'services/auth_service.dart';
|
||||
import 'services/connections_manager.dart';
|
||||
import 'services/entry_manager_service.dart';
|
||||
import 'services/follow_requests_manager.dart';
|
||||
import 'services/gallery_service.dart';
|
||||
import 'services/timeline_manager.dart';
|
||||
import 'update_timer_initialization.dart';
|
||||
import 'utils/active_profile_selector.dart';
|
||||
import 'utils/app_scrolling_behavior.dart';
|
||||
|
@ -117,17 +115,10 @@ class _AppState extends fr.ConsumerState<App> {
|
|||
create: (_) => getIt<ActiveProfileSelector<ConnectionsManager>>(),
|
||||
lazy: true,
|
||||
),
|
||||
ChangeNotifierProvider<ActiveProfileSelector<EntryManagerService>>(
|
||||
create: (_) => getIt<ActiveProfileSelector<EntryManagerService>>(),
|
||||
lazy: true,
|
||||
),
|
||||
ChangeNotifierProvider<ActiveProfileSelector<GalleryService>>(
|
||||
create: (_) => getIt<ActiveProfileSelector<GalleryService>>(),
|
||||
lazy: true,
|
||||
),
|
||||
ChangeNotifierProvider<ActiveProfileSelector<TimelineManager>>(
|
||||
create: (_) => getIt<ActiveProfileSelector<TimelineManager>>(),
|
||||
),
|
||||
ChangeNotifierProvider<ActiveProfileSelector<FollowRequestsManager>>(
|
||||
create: (_) =>
|
||||
getIt<ActiveProfileSelector<FollowRequestsManager>>(),
|
||||
|
|
|
@ -1,86 +1,37 @@
|
|||
import 'timeline_entry.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class EntryTreeItem {
|
||||
final TimelineEntry entry;
|
||||
final String id;
|
||||
final bool isMine;
|
||||
bool isOrphaned;
|
||||
|
||||
final _children = <String, EntryTreeItem>{};
|
||||
final _children = <String>{};
|
||||
|
||||
EntryTreeItem(this.entry,
|
||||
EntryTreeItem(this.id,
|
||||
{this.isMine = true,
|
||||
this.isOrphaned = false,
|
||||
Map<String, EntryTreeItem>? initialChildren}) {
|
||||
Iterable<String>? initialChildren}) {
|
||||
_children.addAll(initialChildren ?? {});
|
||||
}
|
||||
|
||||
factory EntryTreeItem.empty() => EntryTreeItem(TimelineEntry());
|
||||
List<String> get children => List.unmodifiable(_children);
|
||||
|
||||
EntryTreeItem copy({required TimelineEntry entry}) => EntryTreeItem(
|
||||
entry,
|
||||
isMine: isMine,
|
||||
isOrphaned: isOrphaned,
|
||||
initialChildren: _children,
|
||||
);
|
||||
|
||||
String get id => entry.id;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
void removeChildById(String id) {
|
||||
if (_children.containsKey(id)) {
|
||||
_children.remove(id);
|
||||
}
|
||||
|
||||
for (final c in _children.values) {
|
||||
c.removeChildById(id);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
int get totalChildren {
|
||||
int t = _children.length;
|
||||
for (final c in _children.values) {
|
||||
t += c.totalChildren;
|
||||
}
|
||||
|
||||
return t;
|
||||
}
|
||||
|
||||
List<EntryTreeItem> get children => List.unmodifiable(_children.values);
|
||||
factory EntryTreeItem.empty() => EntryTreeItem('');
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is EntryTreeItem &&
|
||||
runtimeType == other.runtimeType &&
|
||||
entry == other.entry &&
|
||||
id == other.id &&
|
||||
isMine == other.isMine &&
|
||||
isOrphaned == other.isOrphaned &&
|
||||
_children == other._children;
|
||||
setEquals(_children, other._children);
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
entry.hashCode ^
|
||||
id.hashCode ^
|
||||
isMine.hashCode ^
|
||||
isOrphaned.hashCode ^
|
||||
_children.hashCode;
|
||||
Object.hashAll(_children);
|
||||
}
|
||||
|
|
|
@ -1,13 +1,20 @@
|
|||
import 'entry_tree_item.dart';
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'timeline_identifiers.dart';
|
||||
|
||||
const defaultLowestId = 9223372036854775807;
|
||||
const defaultHighestId = 0;
|
||||
|
||||
enum InsertionType {
|
||||
beginning,
|
||||
end,
|
||||
}
|
||||
|
||||
class Timeline {
|
||||
final TimelineIdentifiers id;
|
||||
final List<EntryTreeItem> _posts = [];
|
||||
final Map<String, EntryTreeItem> _postsById = {};
|
||||
final TimelineIdentifiers timelineId;
|
||||
final List<String> _postIds = [];
|
||||
int _lowestStatusId = defaultLowestId;
|
||||
int _highestStatusId = defaultHighestId;
|
||||
|
||||
|
@ -15,17 +22,23 @@ class Timeline {
|
|||
|
||||
int get lowestStatusId => _lowestStatusId;
|
||||
|
||||
Timeline(this.id, {List<EntryTreeItem>? initialPosts}) {
|
||||
if (initialPosts != null) {
|
||||
addOrUpdate(initialPosts);
|
||||
Timeline(this.timelineId, {List<String>? initialPostIds}) {
|
||||
if (initialPostIds != null) {
|
||||
_postIds.addAll(initialPostIds);
|
||||
}
|
||||
}
|
||||
|
||||
List<EntryTreeItem> get posts => List.unmodifiable(_posts);
|
||||
List<String> get posts => UnmodifiableListView(_postIds);
|
||||
|
||||
void addOrUpdate(List<EntryTreeItem> newPosts) {
|
||||
for (final p in newPosts) {
|
||||
final id = int.parse(p.id);
|
||||
Timeline addOrUpdate(
|
||||
List<String> newIds, {
|
||||
required InsertionType insertionType,
|
||||
}) {
|
||||
final newIdsSet = Set.from(newIds);
|
||||
final updatedIds = _postIds.where((i) => !newIdsSet.contains(i)).toList();
|
||||
|
||||
for (final idString in newIds) {
|
||||
final id = int.parse(idString);
|
||||
if (_lowestStatusId > id) {
|
||||
_lowestStatusId = id;
|
||||
}
|
||||
|
@ -33,48 +46,44 @@ class Timeline {
|
|||
if (_highestStatusId < id) {
|
||||
_highestStatusId = id;
|
||||
}
|
||||
_postsById[p.id] = p;
|
||||
}
|
||||
_posts.clear();
|
||||
_posts.addAll(_postsById.values);
|
||||
_posts.sort((p1, p2) {
|
||||
final id1 = num.parse(p1.id);
|
||||
final id2 = num.parse(p2.id);
|
||||
return id2.compareTo(id1);
|
||||
// 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;
|
||||
switch (insertionType) {
|
||||
case InsertionType.beginning:
|
||||
updatedIds.insertAll(0, newIds);
|
||||
break;
|
||||
case InsertionType.end:
|
||||
updatedIds.addAll(newIds);
|
||||
break;
|
||||
}
|
||||
|
||||
final existingIds = <String>{};
|
||||
for (int i = 0; i < updatedIds.length; i++) {
|
||||
final id = updatedIds[i];
|
||||
if (existingIds.contains(id)) {
|
||||
updatedIds.removeAt(i);
|
||||
} else {
|
||||
existingIds.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
return changed;
|
||||
updatedIds.sort((p1, p2) {
|
||||
final id1 = num.parse(p1);
|
||||
final id2 = num.parse(p2);
|
||||
return id2.compareTo(id1);
|
||||
});
|
||||
|
||||
return Timeline(timelineId, initialPostIds: updatedIds)
|
||||
.._lowestStatusId = lowestStatusId
|
||||
.._highestStatusId = highestStatusId;
|
||||
}
|
||||
|
||||
void removeTimelineEntry(String id) {
|
||||
if (_postsById.containsKey(id)) {
|
||||
final post = _postsById.remove(id);
|
||||
_posts.remove(post);
|
||||
return;
|
||||
}
|
||||
|
||||
for (final p in _posts) {
|
||||
p.removeChildById(id);
|
||||
}
|
||||
_postIds.remove(id);
|
||||
}
|
||||
|
||||
void clear() {
|
||||
_posts.clear();
|
||||
_postsById.clear();
|
||||
_postIds.clear();
|
||||
_lowestStatusId = defaultLowestId;
|
||||
_highestStatusId = defaultHighestId;
|
||||
}
|
||||
|
@ -82,8 +91,11 @@ class Timeline {
|
|||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is Timeline && runtimeType == other.runtimeType && id == other.id;
|
||||
other is Timeline &&
|
||||
runtimeType == other.runtimeType &&
|
||||
timelineId == other.timelineId &&
|
||||
listEquals(posts, other.posts);
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode;
|
||||
int get hashCode => timelineId.hashCode ^ Object.hashAll(_postIds);
|
||||
}
|
||||
|
|
|
@ -75,8 +75,9 @@ class TimelineIdentifiers {
|
|||
other is TimelineIdentifiers &&
|
||||
runtimeType == other.runtimeType &&
|
||||
timeline == other.timeline &&
|
||||
auxData == other.auxData;
|
||||
auxData == other.auxData &&
|
||||
label == other.label;
|
||||
|
||||
@override
|
||||
int get hashCode => timeline.hashCode ^ auxData.hashCode;
|
||||
int get hashCode => timeline.hashCode ^ auxData.hashCode ^ label.hashCode;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,547 @@
|
|||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:result_monad/result_monad.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../friendica_client/friendica_client.dart';
|
||||
import '../friendica_client/paging_data.dart';
|
||||
import '../models/auth/profile.dart';
|
||||
import '../models/entry_tree_item.dart';
|
||||
import '../models/exec_error.dart';
|
||||
import '../models/image_entry.dart';
|
||||
import '../models/media_attachment_uploads/media_upload_attachment.dart';
|
||||
import '../models/media_attachment_uploads/new_entry_media_items.dart';
|
||||
import '../models/timeline_entry.dart';
|
||||
import '../models/timeline_identifiers.dart';
|
||||
import '../models/visibility.dart';
|
||||
import '../utils/media_upload_attachment_helper.dart';
|
||||
import 'timeline_entry_services.dart';
|
||||
|
||||
part 'entry_tree_item_services.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
Map<String, String> _parentPostIds(_ParentPostIdsRef ref, Profile profile) {
|
||||
return {};
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
Map<String, _Node> _postNodes(_PostNodesRef ref, Profile profile) {
|
||||
return {};
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
Map<String, EntryTreeItem> _entryTreeItems(
|
||||
_EntryTreeItemsRef ref, Profile profile) =>
|
||||
{};
|
||||
|
||||
final _pteLogger = Logger('PostTreeEntryByIdProvider');
|
||||
|
||||
@riverpod
|
||||
Result<EntryTreeItem, ExecError> postTreeEntryById(
|
||||
PostTreeEntryByIdRef ref, Profile profile, String id) {
|
||||
_pteLogger.finest('Building for $id for $profile');
|
||||
final isPostNode = ref.watch(_postNodesProvider(profile)).containsKey(id);
|
||||
_pteLogger.finest('$id ${isPostNode ? "is a post" : "is a comment"}');
|
||||
final postId =
|
||||
isPostNode ? id : ref.watch(_parentPostIdsProvider(profile))[id];
|
||||
if (postId == null) {
|
||||
_pteLogger.finest('No post entry found for $id for $profile');
|
||||
return buildErrorResult(
|
||||
type: ErrorType.notFound, message: 'No post entry found for id: $id');
|
||||
}
|
||||
|
||||
final entry = ref.watch(entryTreeManagerProvider(profile, postId));
|
||||
_pteLogger.finest('Result from ETM: $entry');
|
||||
return entry;
|
||||
}
|
||||
|
||||
final _etmLogger = Logger('EntryTreeManagerProvider');
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class EntryTreeManager extends _$EntryTreeManager {
|
||||
late Profile userProfile;
|
||||
var entryId = '';
|
||||
|
||||
@override
|
||||
Result<EntryTreeItem, ExecError> build(Profile profile, String id) {
|
||||
_etmLogger.finest('Building for $id for $profile');
|
||||
// ref.cacheFor(const Duration(hours: 1));
|
||||
entryId = id;
|
||||
userProfile = profile;
|
||||
final entries = ref.watch(_entryTreeItemsProvider(profile));
|
||||
final entry = entries[id];
|
||||
if (entry == null) {
|
||||
_etmLogger.finest('EntryTreeItem for $id for $profile not found');
|
||||
return buildErrorResult(
|
||||
type: ErrorType.notFound,
|
||||
message: '$id not found',
|
||||
);
|
||||
}
|
||||
|
||||
_etmLogger.finest('Return entry for $id for $profile');
|
||||
return Result.ok(entry);
|
||||
}
|
||||
|
||||
Result<EntryTreeItem, ExecError> upsert(EntryTreeItem entry) {
|
||||
_etmLogger.finest('Upserting entry for $id for $profile');
|
||||
if (entry.id != entryId) {
|
||||
return buildErrorResult(
|
||||
type: ErrorType.argumentError,
|
||||
message:
|
||||
'Trying to add an entry to a provider that does not match the id: $id',
|
||||
);
|
||||
} else {
|
||||
ref.read(_entryTreeItemsProvider(userProfile))[entryId] = entry;
|
||||
}
|
||||
|
||||
if (state.isFailure || entry != state.value) {
|
||||
state = Result.ok(entry);
|
||||
ref.invalidateSelf(); //TODO Confirm need to invalidate (I don't think I do any longer)
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
void remove() {
|
||||
_etmLogger.finest('Removing for $entryId for $profile');
|
||||
ref.read(_entryTreeItemsProvider(userProfile)).remove(entryId);
|
||||
}
|
||||
}
|
||||
|
||||
final _tluLogger = Logger('TimelineUpdater');
|
||||
|
||||
@riverpod
|
||||
class TimelineUpdater extends _$TimelineUpdater {
|
||||
@override
|
||||
bool build(Profile userProfile) {
|
||||
return true;
|
||||
}
|
||||
|
||||
FutureResult<List<EntryTreeItem>, ExecError> updateTimeline(
|
||||
TimelineIdentifiers type, int maxId, int sinceId) async {
|
||||
_tluLogger.fine(() => 'Updating timeline');
|
||||
final client = TimelineClient(userProfile);
|
||||
final itemsResult = await client.getTimeline(
|
||||
type: type,
|
||||
page: PagingData(
|
||||
maxId: maxId > 0 ? maxId : null,
|
||||
sinceId: sinceId > 0 ? sinceId : null,
|
||||
),
|
||||
);
|
||||
if (itemsResult.isFailure) {
|
||||
_tluLogger.severe('Error getting timeline: ${itemsResult.error}');
|
||||
return itemsResult.errorCast();
|
||||
}
|
||||
|
||||
itemsResult.value.sort((t1, t2) => t1.id.compareTo(t2.id));
|
||||
final updatedPosts = await _processNewItems(
|
||||
itemsResult.value,
|
||||
userProfile.userId,
|
||||
);
|
||||
return Result.ok(updatedPosts);
|
||||
}
|
||||
|
||||
FutureResult<EntryTreeItem, ExecError> refreshStatusChain(String id) async {
|
||||
_tluLogger.fine('Refreshing post: $id');
|
||||
final client = StatusesClient(userProfile);
|
||||
final postResult = client.getPostOrComment(id, fullContext: false);
|
||||
final contextResult = client.getPostOrComment(id, fullContext: true);
|
||||
|
||||
final results = await Future.wait([postResult, contextResult]);
|
||||
final hadError = results.map((r) => r.isFailure).reduce((e, s) => e && s);
|
||||
final entries = results
|
||||
.map((r) => r.getValueOrElse(() => []))
|
||||
.expand((i) => i)
|
||||
.toList();
|
||||
if (entries.isNotEmpty) {
|
||||
_cleanupEntriesForId(id);
|
||||
//ID may be wrong, have to think about if this always needs to be a post ID. Maybe this field is superfluous
|
||||
await _processNewItems(entries, client.profile.userId);
|
||||
}
|
||||
|
||||
if (hadError) {
|
||||
return Result.error(results.firstWhere((r) => r.isFailure).error);
|
||||
} else {
|
||||
final resultFromProvider =
|
||||
ref.read(postTreeEntryByIdProvider(userProfile, id));
|
||||
return resultFromProvider;
|
||||
}
|
||||
}
|
||||
|
||||
FutureResult<bool, ExecError> deleteEntryById(String id) async {
|
||||
// TODO confirm that the entry no longer appears in the UI even if the timeline or chain has the ID
|
||||
_tluLogger.finest('Delete entry: $id');
|
||||
final result = await StatusesClient(userProfile)
|
||||
.deleteEntryById(id)
|
||||
.withResult((_) => _cleanupEntriesForId(id));
|
||||
|
||||
return result.execErrorCast();
|
||||
}
|
||||
|
||||
void _cleanupEntriesForId(String id) {
|
||||
final parentPostIds = ref.read(_parentPostIdsProvider(userProfile));
|
||||
final postNodes = ref.read(_postNodesProvider(userProfile));
|
||||
if (parentPostIds.containsKey(id)) {
|
||||
final parentPostId = parentPostIds.remove(id);
|
||||
final parentPostNode = postNodes[parentPostId];
|
||||
parentPostNode?.removeChildById(id);
|
||||
}
|
||||
|
||||
ref.read(entryTreeManagerProvider(userProfile, id).notifier).remove();
|
||||
|
||||
if (postNodes.containsKey(id)) {
|
||||
postNodes.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<EntryTreeItem>> _processNewItems(
|
||||
List<TimelineEntry> items,
|
||||
String currentId,
|
||||
) async {
|
||||
_tluLogger.fine('Processing new items: ${items.map((e) => e.id).toList()}');
|
||||
items.sort((i1, i2) => int.parse(i1.id).compareTo(int.parse(i2.id)));
|
||||
final allSeenItems = [...items];
|
||||
for (final item in items) {
|
||||
_tluLogger.finest('Upserting entry for ${item.id}');
|
||||
ref
|
||||
.read(timelineEntryManagerProvider(userProfile, item.id).notifier)
|
||||
.upsert(item);
|
||||
}
|
||||
|
||||
final orphans = <TimelineEntry>[];
|
||||
for (final item in items) {
|
||||
if (item.parentId.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
ref.read(timelineEntryManagerProvider(userProfile, item.parentId)).match(
|
||||
onSuccess: (parent) {
|
||||
if (parent.parentId.isEmpty) {
|
||||
ref.read(_parentPostIdsProvider(userProfile))[item.id] = parent.id;
|
||||
}
|
||||
}, onError: (_) {
|
||||
orphans.add(item);
|
||||
});
|
||||
}
|
||||
for (final o in orphans) {
|
||||
await StatusesClient(userProfile)
|
||||
.getPostOrComment(o.id, fullContext: true)
|
||||
.andThenSuccessAsync((items) async {
|
||||
final parentPostId = items.firstWhere((e) => e.parentId.isEmpty).id;
|
||||
ref.read(_parentPostIdsProvider(userProfile))[o.id] = parentPostId;
|
||||
allSeenItems.addAll(items);
|
||||
for (final item in items) {
|
||||
ref
|
||||
.read(timelineEntryManagerProvider(userProfile, item.id).notifier)
|
||||
.upsert(item);
|
||||
ref.read(_parentPostIdsProvider(userProfile))[item.id] = parentPostId;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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));
|
||||
});
|
||||
final postNodesToReturn = <_Node>{};
|
||||
var lastCount = 0;
|
||||
final postNodes = ref.read(_postNodesProvider(userProfile));
|
||||
final parentPostIds = ref.read(_parentPostIdsProvider(userProfile));
|
||||
while (allSeenItems.isNotEmpty) {
|
||||
final seenItemsCopy = [...allSeenItems];
|
||||
for (final item in seenItemsCopy) {
|
||||
if (item.parentId.isEmpty) {
|
||||
final postNode = postNodes.putIfAbsent(item.id, () => _Node(item.id));
|
||||
postNodesToReturn.add(postNode);
|
||||
allSeenItems.remove(item);
|
||||
} else {
|
||||
final parentParentPostId = postNodes.containsKey(item.parentId)
|
||||
? item.parentId
|
||||
: parentPostIds[item.parentId];
|
||||
if (postNodes[parentParentPostId] == null) {
|
||||
_tluLogger.severe(
|
||||
'Error finding parent ${item.parentId} for entry ${item.id}');
|
||||
continue;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
allSeenItems.remove(item);
|
||||
}
|
||||
}
|
||||
if (allSeenItems.isNotEmpty && allSeenItems.length == lastCount) {
|
||||
_tluLogger.severe(
|
||||
'Had ${allSeenItems.length} items left over after all iterations');
|
||||
break;
|
||||
}
|
||||
lastCount = allSeenItems.length;
|
||||
}
|
||||
|
||||
final updatedPosts = postNodesToReturn
|
||||
.map((node) => _nodeToTreeItem(node, currentId))
|
||||
.toList();
|
||||
|
||||
_tluLogger.finest('Completed processing new items');
|
||||
return updatedPosts;
|
||||
}
|
||||
|
||||
EntryTreeItem _nodeToTreeItem(_Node node, String currentId) {
|
||||
final childenEntries = <String>[];
|
||||
for (final c in node.children) {
|
||||
childenEntries.add(c.id);
|
||||
_nodeToTreeItem(c, currentId);
|
||||
}
|
||||
final entryId = node.id;
|
||||
final isMine = ref
|
||||
.read(timelineEntryManagerProvider(userProfile, entryId))
|
||||
.fold(onSuccess: (t) => t.authorId, onError: (_) => '') ==
|
||||
currentId;
|
||||
final rval = EntryTreeItem(
|
||||
entryId,
|
||||
isMine: isMine,
|
||||
initialChildren: childenEntries,
|
||||
);
|
||||
|
||||
ref
|
||||
.read(entryTreeManagerProvider(userProfile, node.id).notifier)
|
||||
.upsert(rval);
|
||||
return rval;
|
||||
}
|
||||
}
|
||||
|
||||
final _swLogger = Logger('StatusWriter');
|
||||
|
||||
@riverpod
|
||||
class StatusWriter extends _$StatusWriter {
|
||||
@override
|
||||
bool build(Profile userProfile) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO move to RP timeline entry services
|
||||
FutureResult<TimelineEntry, ExecError> createNewStatus(
|
||||
String text, {
|
||||
String spoilerText = '',
|
||||
String inReplyToId = '',
|
||||
required NewEntryMediaItems mediaItems,
|
||||
required List<ImageEntry> existingMediaItems,
|
||||
required Visibility visibility,
|
||||
}) async {
|
||||
_swLogger.info('Creating new post: $text');
|
||||
final mediaIds = existingMediaItems.map((m) => m.scales.first.id).toList();
|
||||
for (final item in mediaItems.attachments) {
|
||||
if (item.isExistingServerItem) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final result =
|
||||
await _uploadMediaItems(item, mediaItems.albumName, visibility)
|
||||
.withResult((newId) => mediaIds.add(newId));
|
||||
|
||||
if (result.isFailure) {
|
||||
return Result.error(ExecError(
|
||||
type: ErrorType.localError,
|
||||
message: 'Error uploading image: ${result.error}'));
|
||||
}
|
||||
}
|
||||
|
||||
final result = await StatusesClient(userProfile)
|
||||
.createNewStatus(
|
||||
text: text,
|
||||
spoilerText: spoilerText,
|
||||
inReplyToId: inReplyToId,
|
||||
mediaIds: mediaIds,
|
||||
visibility: visibility)
|
||||
.withResult((item) async {
|
||||
ref
|
||||
.read(timelineEntryManagerProvider(userProfile, item.id).notifier)
|
||||
.upsert(item);
|
||||
})
|
||||
.withResult((item) async {
|
||||
if (inReplyToId.isNotEmpty) {
|
||||
late final String rootPostId;
|
||||
if (ref
|
||||
.read(_postNodesProvider(userProfile))
|
||||
.containsKey(inReplyToId)) {
|
||||
rootPostId = inReplyToId;
|
||||
} else {
|
||||
rootPostId =
|
||||
ref.read(_parentPostIdsProvider(userProfile))[inReplyToId]!;
|
||||
}
|
||||
await ref
|
||||
.read(timelineUpdaterProvider(userProfile).notifier)
|
||||
.refreshStatusChain(rootPostId);
|
||||
}
|
||||
})
|
||||
.withResult((status) => _swLogger.finest('${status.id} status created'))
|
||||
.withError((error) => _swLogger.finest('Error creating post: $error'));
|
||||
|
||||
return result.execErrorCast();
|
||||
}
|
||||
|
||||
FutureResult<TimelineEntry, ExecError> editStatus(
|
||||
String statusId,
|
||||
String text, {
|
||||
String spoilerText = '',
|
||||
required NewEntryMediaItems mediaItems,
|
||||
required List<ImageEntry> existingMediaItems,
|
||||
required Visibility newMediaItemVisibility,
|
||||
}) async {
|
||||
_swLogger.info('Editing post: $text');
|
||||
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 result = await _uploadMediaItems(
|
||||
item, mediaItems.albumName, newMediaItemVisibility)
|
||||
.withResult((newId) => mediaIds.add(newId));
|
||||
|
||||
if (result.isFailure) {
|
||||
return Result.error(ExecError(
|
||||
type: ErrorType.localError,
|
||||
message: 'Error uploading image: ${result.error}'));
|
||||
}
|
||||
}
|
||||
|
||||
final result = await StatusesClient(userProfile)
|
||||
.editStatus(
|
||||
id: statusId,
|
||||
text: text,
|
||||
spoilerText: spoilerText,
|
||||
mediaIds: mediaIds)
|
||||
.withResult((item) async {
|
||||
ref
|
||||
.read(timelineEntryManagerProvider(userProfile, item.id).notifier)
|
||||
.upsert(item);
|
||||
})
|
||||
.withResult((item) async {
|
||||
final inReplyToId = item.parentId;
|
||||
if (inReplyToId.isNotEmpty) {
|
||||
late final String rootPostId;
|
||||
if (ref
|
||||
.read(_postNodesProvider(userProfile))
|
||||
.containsKey(inReplyToId)) {
|
||||
rootPostId = inReplyToId;
|
||||
} else {
|
||||
rootPostId =
|
||||
ref.read(_parentPostIdsProvider(userProfile))[inReplyToId]!;
|
||||
}
|
||||
await ref
|
||||
.read(timelineUpdaterProvider(userProfile).notifier)
|
||||
.refreshStatusChain(rootPostId);
|
||||
}
|
||||
})
|
||||
.withResult((status) => _swLogger.finest('${status.id} status created'))
|
||||
.withError((error) => _swLogger.finest('Error creating post: $error'));
|
||||
|
||||
return result.execErrorCast();
|
||||
}
|
||||
|
||||
FutureResult<String, ExecError> _uploadMediaItems(
|
||||
MediaUploadAttachment item,
|
||||
String albumName,
|
||||
Visibility visibility,
|
||||
) async {
|
||||
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(
|
||||
(imageBytes) async =>
|
||||
await RemoteFileClient(userProfile).uploadFileAsAttachment(
|
||||
bytes: imageBytes,
|
||||
album: albumName,
|
||||
description: item.description,
|
||||
fileName: filename,
|
||||
visibility: visibility,
|
||||
),
|
||||
)
|
||||
.transform((v) => v.scales.first.id);
|
||||
|
||||
return uploadResult.execErrorCast();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
void removeChildById(String id) {
|
||||
if (_children.containsKey(id)) {
|
||||
_children.remove(id);
|
||||
}
|
||||
|
||||
for (final c in _children.values) {
|
||||
c.removeChildById(id);
|
||||
}
|
||||
}
|
||||
|
||||
_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;
|
||||
}
|
Plik diff jest za duży
Load Diff
|
@ -3,9 +3,6 @@ import 'dart:io';
|
|||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../models/auth/profile.dart';
|
||||
import '../services/entry_manager_service.dart';
|
||||
|
||||
part 'globals_services.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
|
@ -15,8 +12,3 @@ SharedPreferences sharedPreferences(SharedPreferencesRef ref) =>
|
|||
@Riverpod(keepAlive: true)
|
||||
Directory applicationSupportDirectory(ApplicationSupportDirectoryRef ref) =>
|
||||
throw UnimplementedError();
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
EntryManagerService entryManagerService(
|
||||
EntryManagerServiceRef ref, Profile profile) =>
|
||||
EntryManagerService(profile);
|
||||
|
|
|
@ -37,154 +37,5 @@ final applicationSupportDirectoryProvider = Provider<Directory>.internal(
|
|||
);
|
||||
|
||||
typedef ApplicationSupportDirectoryRef = ProviderRef<Directory>;
|
||||
String _$entryManagerServiceHash() =>
|
||||
r'c3fdb3153999b0b9053b3a4aff0e6834b5a76d12';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
_SystemHash._();
|
||||
|
||||
static int combine(int hash, int value) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + value);
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||
return hash ^ (hash >> 6);
|
||||
}
|
||||
|
||||
static int finish(int hash) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||
// ignore: parameter_assignments
|
||||
hash = hash ^ (hash >> 11);
|
||||
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||
}
|
||||
}
|
||||
|
||||
/// See also [entryManagerService].
|
||||
@ProviderFor(entryManagerService)
|
||||
const entryManagerServiceProvider = EntryManagerServiceFamily();
|
||||
|
||||
/// See also [entryManagerService].
|
||||
class EntryManagerServiceFamily extends Family<EntryManagerService> {
|
||||
/// See also [entryManagerService].
|
||||
const EntryManagerServiceFamily();
|
||||
|
||||
/// See also [entryManagerService].
|
||||
EntryManagerServiceProvider call(
|
||||
Profile profile,
|
||||
) {
|
||||
return EntryManagerServiceProvider(
|
||||
profile,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
EntryManagerServiceProvider getProviderOverride(
|
||||
covariant EntryManagerServiceProvider provider,
|
||||
) {
|
||||
return call(
|
||||
provider.profile,
|
||||
);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'entryManagerServiceProvider';
|
||||
}
|
||||
|
||||
/// See also [entryManagerService].
|
||||
class EntryManagerServiceProvider extends Provider<EntryManagerService> {
|
||||
/// See also [entryManagerService].
|
||||
EntryManagerServiceProvider(
|
||||
Profile profile,
|
||||
) : this._internal(
|
||||
(ref) => entryManagerService(
|
||||
ref as EntryManagerServiceRef,
|
||||
profile,
|
||||
),
|
||||
from: entryManagerServiceProvider,
|
||||
name: r'entryManagerServiceProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$entryManagerServiceHash,
|
||||
dependencies: EntryManagerServiceFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
EntryManagerServiceFamily._allTransitiveDependencies,
|
||||
profile: profile,
|
||||
);
|
||||
|
||||
EntryManagerServiceProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.profile,
|
||||
}) : super.internal();
|
||||
|
||||
final Profile profile;
|
||||
|
||||
@override
|
||||
Override overrideWith(
|
||||
EntryManagerService Function(EntryManagerServiceRef provider) create,
|
||||
) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: EntryManagerServiceProvider._internal(
|
||||
(ref) => create(ref as EntryManagerServiceRef),
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
profile: profile,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
ProviderElement<EntryManagerService> createElement() {
|
||||
return _EntryManagerServiceProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is EntryManagerServiceProvider && other.profile == profile;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, profile.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
mixin EntryManagerServiceRef on ProviderRef<EntryManagerService> {
|
||||
/// The parameter `profile` of this provider.
|
||||
Profile get profile;
|
||||
}
|
||||
|
||||
class _EntryManagerServiceProviderElement
|
||||
extends ProviderElement<EntryManagerService> with EntryManagerServiceRef {
|
||||
_EntryManagerServiceProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
Profile get profile => (origin as EntryManagerServiceProvider).profile;
|
||||
}
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import 'package:logging/logging.dart';
|
||||
import 'package:result_monad/result_monad.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../friendica_client/friendica_client.dart';
|
||||
import '../models/auth/profile.dart';
|
||||
import '../models/exec_error.dart';
|
||||
import '../models/timeline_entry.dart';
|
||||
import 'rp_provider_extension.dart';
|
||||
import 'reshared_via_services.dart';
|
||||
|
||||
part 'timeline_entry_services.g.dart';
|
||||
|
||||
|
@ -13,28 +15,35 @@ Map<String, TimelineEntry> _timelineEntries(
|
|||
_TimelineEntriesRef ref, Profile profile) =>
|
||||
{};
|
||||
|
||||
@riverpod
|
||||
final _logger = Logger('TimelineEntryManagerProvider');
|
||||
|
||||
//TODO Test using cacheFor to let it expire
|
||||
@Riverpod(keepAlive: true)
|
||||
class TimelineEntryManager extends _$TimelineEntryManager {
|
||||
late Profile userProfile;
|
||||
var entryId = '';
|
||||
|
||||
@override
|
||||
Result<TimelineEntry, ExecError> build(Profile profile, String id) {
|
||||
ref.cacheFor(const Duration(hours: 1));
|
||||
_logger.finest('Building for $id for $profile');
|
||||
//ref.cacheFor(const Duration(hours: 1));
|
||||
entryId = id;
|
||||
userProfile = profile;
|
||||
final entry = ref.watch(_timelineEntriesProvider(profile))[id];
|
||||
if (entry == null) {
|
||||
_logger.finest('Entry not found for $id for $profile');
|
||||
return buildErrorResult(
|
||||
type: ErrorType.notFound,
|
||||
message: '$id not found',
|
||||
);
|
||||
}
|
||||
|
||||
_logger.finest('For $id for $profile returning: \n$entry');
|
||||
return Result.ok(entry);
|
||||
}
|
||||
|
||||
Result<TimelineEntry, ExecError> upsert(TimelineEntry entry) {
|
||||
_logger.fine('Upserting entry for ${entry.id} for $userProfile');
|
||||
if (entry.id != entryId) {
|
||||
return buildErrorResult(
|
||||
type: ErrorType.argumentError,
|
||||
|
@ -47,13 +56,54 @@ class TimelineEntryManager extends _$TimelineEntryManager {
|
|||
|
||||
if (state.isFailure || entry != state.value) {
|
||||
state = Result.ok(entry);
|
||||
ref.invalidateSelf();
|
||||
ref.invalidateSelf(); //Confirm I need to do this, I don't think I do
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
FutureResult<TimelineEntry, ExecError> toggleFavorited(bool newStatus) async {
|
||||
final interactionClient = InteractionsClient(userProfile);
|
||||
final result = await interactionClient
|
||||
.changeFavoriteStatus(entryId, newStatus)
|
||||
.withResult((update) {
|
||||
_logger.fine('Updating $entryId for $userProfile to: \n$update');
|
||||
ref.read(_timelineEntriesProvider(userProfile))[entryId] = update;
|
||||
});
|
||||
if (result.isSuccess) {
|
||||
state = result.execErrorCast();
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
FutureResult<TimelineEntry, ExecError> resharePost() async {
|
||||
final client = StatusesClient(profile);
|
||||
final result = await client.resharePost(entryId).withResult((item) async {
|
||||
ref
|
||||
.read(resharedViaProvider(userProfile, entryId).notifier)
|
||||
.upsertResharedVia(profile.id);
|
||||
}).withResult((update) =>
|
||||
ref.read(_timelineEntriesProvider(userProfile))[entryId] = update);
|
||||
|
||||
state = result.execErrorCast();
|
||||
return state;
|
||||
}
|
||||
|
||||
FutureResult<TimelineEntry, ExecError> unResharePost() async {
|
||||
final client = StatusesClient(profile);
|
||||
final result = await client.unResharePost(entryId).withResult((item) async {
|
||||
ref
|
||||
.read(resharedViaProvider(userProfile, entryId).notifier)
|
||||
.upsertRemovedSharer(profile.id);
|
||||
}).withResult((update) =>
|
||||
ref.read(_timelineEntriesProvider(userProfile))[entryId] = update);
|
||||
|
||||
state = result.execErrorCast();
|
||||
return state;
|
||||
}
|
||||
|
||||
void remove() {
|
||||
ref.read(_timelineEntriesProvider(userProfile)).remove(entryId);
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -157,10 +157,10 @@ class _TimelineEntriesProviderElement
|
|||
}
|
||||
|
||||
String _$timelineEntryManagerHash() =>
|
||||
r'8e8cf823e4721ff139a78084a17cd06a712e0a44';
|
||||
r'2d0a706f65beeff92606c826d85550a533d6a56e';
|
||||
|
||||
abstract class _$TimelineEntryManager
|
||||
extends BuildlessAutoDisposeNotifier<Result<TimelineEntry, ExecError>> {
|
||||
extends BuildlessNotifier<Result<TimelineEntry, ExecError>> {
|
||||
late final Profile profile;
|
||||
late final String id;
|
||||
|
||||
|
@ -217,7 +217,7 @@ class TimelineEntryManagerFamily
|
|||
}
|
||||
|
||||
/// See also [TimelineEntryManager].
|
||||
class TimelineEntryManagerProvider extends AutoDisposeNotifierProviderImpl<
|
||||
class TimelineEntryManagerProvider extends NotifierProviderImpl<
|
||||
TimelineEntryManager, Result<TimelineEntry, ExecError>> {
|
||||
/// See also [TimelineEntryManager].
|
||||
TimelineEntryManagerProvider(
|
||||
|
@ -284,7 +284,7 @@ class TimelineEntryManagerProvider extends AutoDisposeNotifierProviderImpl<
|
|||
}
|
||||
|
||||
@override
|
||||
AutoDisposeNotifierProviderElement<TimelineEntryManager,
|
||||
NotifierProviderElement<TimelineEntryManager,
|
||||
Result<TimelineEntry, ExecError>> createElement() {
|
||||
return _TimelineEntryManagerProviderElement(this);
|
||||
}
|
||||
|
@ -307,7 +307,7 @@ class TimelineEntryManagerProvider extends AutoDisposeNotifierProviderImpl<
|
|||
}
|
||||
|
||||
mixin TimelineEntryManagerRef
|
||||
on AutoDisposeNotifierProviderRef<Result<TimelineEntry, ExecError>> {
|
||||
on NotifierProviderRef<Result<TimelineEntry, ExecError>> {
|
||||
/// The parameter `profile` of this provider.
|
||||
Profile get profile;
|
||||
|
||||
|
@ -315,9 +315,9 @@ mixin TimelineEntryManagerRef
|
|||
String get id;
|
||||
}
|
||||
|
||||
class _TimelineEntryManagerProviderElement
|
||||
extends AutoDisposeNotifierProviderElement<TimelineEntryManager,
|
||||
Result<TimelineEntry, ExecError>> with TimelineEntryManagerRef {
|
||||
class _TimelineEntryManagerProviderElement extends NotifierProviderElement<
|
||||
TimelineEntryManager,
|
||||
Result<TimelineEntry, ExecError>> with TimelineEntryManagerRef {
|
||||
_TimelineEntryManagerProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
import 'package:logging/logging.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../models/auth/profile.dart';
|
||||
import '../models/timeline.dart';
|
||||
import '../models/timeline_identifiers.dart';
|
||||
import 'entry_tree_item_services.dart';
|
||||
|
||||
part 'timeline_services.g.dart';
|
||||
|
||||
final _tmpLogger = Logger('TimelineManagerProvider');
|
||||
|
||||
enum TimelineRefreshType {
|
||||
refresh,
|
||||
loadOlder,
|
||||
loadNewer,
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class TimelineManager extends _$TimelineManager {
|
||||
@override
|
||||
Timeline build(Profile userProfile, TimelineIdentifiers timelineId) {
|
||||
_tmpLogger.info('Building for $userProfile for $timelineId');
|
||||
return Timeline(timelineId);
|
||||
}
|
||||
|
||||
Future<void> updateTimeline(
|
||||
TimelineRefreshType refreshType,
|
||||
) async {
|
||||
_tmpLogger.info(
|
||||
'Updating w/$refreshType for timeline $timelineId for profile $userProfile ');
|
||||
late final int lowestId;
|
||||
late final int highestId;
|
||||
late final InsertionType insertionType;
|
||||
|
||||
switch (refreshType) {
|
||||
case TimelineRefreshType.refresh:
|
||||
lowestId = 0;
|
||||
highestId = 0;
|
||||
insertionType = InsertionType.end;
|
||||
break;
|
||||
case TimelineRefreshType.loadOlder:
|
||||
lowestId = state.lowestStatusId;
|
||||
highestId = 0;
|
||||
insertionType = InsertionType.end;
|
||||
break;
|
||||
case TimelineRefreshType.loadNewer:
|
||||
lowestId = 0;
|
||||
highestId = state.highestStatusId;
|
||||
insertionType = InsertionType.beginning;
|
||||
break;
|
||||
}
|
||||
(await ref
|
||||
.read(timelineUpdaterProvider(userProfile).notifier)
|
||||
.updateTimeline(timelineId, lowestId, highestId))
|
||||
.match(onSuccess: (posts) {
|
||||
_tmpLogger
|
||||
.finest('Posts returned for adding to $timelineId: ${posts.length}');
|
||||
final oldState = state;
|
||||
final newState = state = state.addOrUpdate(
|
||||
posts.map((p) => p.id).toList(),
|
||||
insertionType: insertionType,
|
||||
);
|
||||
_tmpLogger.finest(
|
||||
'Old post count: ${oldState.posts.length}, New Post count: ${newState.posts.length}');
|
||||
_tmpLogger.finest('Old state == New state? ${oldState == newState}');
|
||||
}, onError: (error) {
|
||||
_tmpLogger.severe('Error updating timeline: $timelineId}');
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,196 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'timeline_services.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$timelineManagerHash() => r'b4b0676646b2efc9ce54e2790b4188683c586767';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
_SystemHash._();
|
||||
|
||||
static int combine(int hash, int value) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + value);
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||
return hash ^ (hash >> 6);
|
||||
}
|
||||
|
||||
static int finish(int hash) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||
// ignore: parameter_assignments
|
||||
hash = hash ^ (hash >> 11);
|
||||
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _$TimelineManager extends BuildlessNotifier<Timeline> {
|
||||
late final Profile userProfile;
|
||||
late final TimelineIdentifiers timelineId;
|
||||
|
||||
Timeline build(
|
||||
Profile userProfile,
|
||||
TimelineIdentifiers timelineId,
|
||||
);
|
||||
}
|
||||
|
||||
/// See also [TimelineManager].
|
||||
@ProviderFor(TimelineManager)
|
||||
const timelineManagerProvider = TimelineManagerFamily();
|
||||
|
||||
/// See also [TimelineManager].
|
||||
class TimelineManagerFamily extends Family<Timeline> {
|
||||
/// See also [TimelineManager].
|
||||
const TimelineManagerFamily();
|
||||
|
||||
/// See also [TimelineManager].
|
||||
TimelineManagerProvider call(
|
||||
Profile userProfile,
|
||||
TimelineIdentifiers timelineId,
|
||||
) {
|
||||
return TimelineManagerProvider(
|
||||
userProfile,
|
||||
timelineId,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
TimelineManagerProvider getProviderOverride(
|
||||
covariant TimelineManagerProvider provider,
|
||||
) {
|
||||
return call(
|
||||
provider.userProfile,
|
||||
provider.timelineId,
|
||||
);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'timelineManagerProvider';
|
||||
}
|
||||
|
||||
/// See also [TimelineManager].
|
||||
class TimelineManagerProvider
|
||||
extends NotifierProviderImpl<TimelineManager, Timeline> {
|
||||
/// See also [TimelineManager].
|
||||
TimelineManagerProvider(
|
||||
Profile userProfile,
|
||||
TimelineIdentifiers timelineId,
|
||||
) : this._internal(
|
||||
() => TimelineManager()
|
||||
..userProfile = userProfile
|
||||
..timelineId = timelineId,
|
||||
from: timelineManagerProvider,
|
||||
name: r'timelineManagerProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$timelineManagerHash,
|
||||
dependencies: TimelineManagerFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
TimelineManagerFamily._allTransitiveDependencies,
|
||||
userProfile: userProfile,
|
||||
timelineId: timelineId,
|
||||
);
|
||||
|
||||
TimelineManagerProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.userProfile,
|
||||
required this.timelineId,
|
||||
}) : super.internal();
|
||||
|
||||
final Profile userProfile;
|
||||
final TimelineIdentifiers timelineId;
|
||||
|
||||
@override
|
||||
Timeline runNotifierBuild(
|
||||
covariant TimelineManager notifier,
|
||||
) {
|
||||
return notifier.build(
|
||||
userProfile,
|
||||
timelineId,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Override overrideWith(TimelineManager Function() create) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: TimelineManagerProvider._internal(
|
||||
() => create()
|
||||
..userProfile = userProfile
|
||||
..timelineId = timelineId,
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
userProfile: userProfile,
|
||||
timelineId: timelineId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
NotifierProviderElement<TimelineManager, Timeline> createElement() {
|
||||
return _TimelineManagerProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is TimelineManagerProvider &&
|
||||
other.userProfile == userProfile &&
|
||||
other.timelineId == timelineId;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, userProfile.hashCode);
|
||||
hash = _SystemHash.combine(hash, timelineId.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
mixin TimelineManagerRef on NotifierProviderRef<Timeline> {
|
||||
/// The parameter `userProfile` of this provider.
|
||||
Profile get userProfile;
|
||||
|
||||
/// The parameter `timelineId` of this provider.
|
||||
TimelineIdentifiers get timelineId;
|
||||
}
|
||||
|
||||
class _TimelineManagerProviderElement
|
||||
extends NotifierProviderElement<TimelineManager, Timeline>
|
||||
with TimelineManagerRef {
|
||||
_TimelineManagerProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
Profile get userProfile => (origin as TimelineManagerProvider).userProfile;
|
||||
@override
|
||||
TimelineIdentifiers get timelineId =>
|
||||
(origin as TimelineManagerProvider).timelineId;
|
||||
}
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
|
@ -18,6 +18,7 @@ import '../controls/standard_appbar.dart';
|
|||
import '../controls/timeline/status_header_control.dart';
|
||||
import '../controls/visibility_dialog.dart';
|
||||
import '../globals.dart';
|
||||
import '../models/auth/profile.dart';
|
||||
import '../models/exec_error.dart';
|
||||
import '../models/image_entry.dart';
|
||||
import '../models/link_preview_data.dart';
|
||||
|
@ -26,11 +27,12 @@ import '../models/timeline_entry.dart';
|
|||
import '../models/timeline_grouping_list_data.dart';
|
||||
import '../models/visibility.dart';
|
||||
import '../riverpod_controllers/circles_repo_services.dart';
|
||||
import '../riverpod_controllers/entry_tree_item_services.dart';
|
||||
import '../riverpod_controllers/timeline_entry_services.dart';
|
||||
import '../serializers/friendica/link_preview_friendica_extensions.dart';
|
||||
import '../services/auth_service.dart';
|
||||
import '../services/connections_manager.dart';
|
||||
import '../services/feature_version_checker.dart';
|
||||
import '../services/timeline_manager.dart';
|
||||
import '../utils/active_profile_selector.dart';
|
||||
import '../utils/html_to_edit_text_helper.dart';
|
||||
import '../utils/opengraph_preview_grabber.dart';
|
||||
|
@ -78,11 +80,9 @@ class _EditorScreenState extends ConsumerState<EditorScreen> {
|
|||
void initState() {
|
||||
super.initState();
|
||||
if (isComment) {
|
||||
final manager = context
|
||||
.read<ActiveProfileSelector<TimelineManager>>()
|
||||
.activeEntry
|
||||
.value;
|
||||
manager.getEntryById(widget.parentId).match(onSuccess: (entry) {
|
||||
final profile = context.read<AccountsService>().currentProfile;
|
||||
ref.read(timelineEntryManagerProvider(profile, widget.parentId)).match(
|
||||
onSuccess: (entry) {
|
||||
spoilerController.text = entry.spoilerText;
|
||||
parentEntry = entry;
|
||||
visibility = entry.visibility;
|
||||
|
@ -101,10 +101,9 @@ class _EditorScreenState extends ConsumerState<EditorScreen> {
|
|||
void restoreStatusData() async {
|
||||
_logger.finer('Attempting to load status for editing');
|
||||
loaded = false;
|
||||
final result = await getIt<ActiveProfileSelector<TimelineManager>>()
|
||||
.activeEntry
|
||||
.andThenAsync((manager) async => manager.getEntryById(widget.id));
|
||||
result.match(onSuccess: (entry) {
|
||||
final profile = context.read<AccountsService>().currentProfile;
|
||||
ref.read(timelineEntryManagerProvider(profile, widget.id)).match(
|
||||
onSuccess: (entry) {
|
||||
_logger.finer('Loading status ${widget.id} information into fields');
|
||||
contentController.text = htmlToSimpleText(entry.body);
|
||||
spoilerController.text = entry.spoilerText;
|
||||
|
@ -144,8 +143,7 @@ class _EditorScreenState extends ConsumerState<EditorScreen> {
|
|||
existingMediaItems.isEmpty &&
|
||||
newMediaItems.attachments.isEmpty;
|
||||
|
||||
Future<void> createStatus(
|
||||
BuildContext context, TimelineManager manager) async {
|
||||
Future<void> createStatus(BuildContext context, Profile profile) async {
|
||||
if (isSubmitting) {
|
||||
return;
|
||||
}
|
||||
|
@ -159,15 +157,16 @@ class _EditorScreenState extends ConsumerState<EditorScreen> {
|
|||
isSubmitting = true;
|
||||
});
|
||||
|
||||
final result = await manager
|
||||
final result = await ref
|
||||
.read(statusWriterProvider(profile).notifier)
|
||||
.createNewStatus(
|
||||
bodyText,
|
||||
spoilerText: spoilerController.text,
|
||||
inReplyToId: widget.parentId,
|
||||
newMediaItems: newMediaItems,
|
||||
existingMediaItems: existingMediaItems,
|
||||
visibility: visibility,
|
||||
)
|
||||
bodyText,
|
||||
spoilerText: spoilerController.text,
|
||||
inReplyToId: widget.parentId,
|
||||
mediaItems: newMediaItems,
|
||||
existingMediaItems: existingMediaItems,
|
||||
visibility: visibility,
|
||||
)
|
||||
.withError((error) {
|
||||
buildSnackbar(context, 'Error posting: $error');
|
||||
logError(error, _logger);
|
||||
|
@ -186,7 +185,7 @@ class _EditorScreenState extends ConsumerState<EditorScreen> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> editStatus(BuildContext context, TimelineManager manager) async {
|
||||
Future<void> editStatus(BuildContext context, Profile profile) async {
|
||||
if (isSubmitting) {
|
||||
return;
|
||||
}
|
||||
|
@ -200,16 +199,16 @@ class _EditorScreenState extends ConsumerState<EditorScreen> {
|
|||
isSubmitting = true;
|
||||
});
|
||||
|
||||
final result = await manager
|
||||
final result = await ref
|
||||
.read(statusWriterProvider(profile).notifier)
|
||||
.editStatus(
|
||||
widget.id,
|
||||
bodyText,
|
||||
spoilerText: spoilerController.text,
|
||||
inReplyToId: widget.parentId,
|
||||
newMediaItems: newMediaItems,
|
||||
existingMediaItems: existingMediaItems,
|
||||
newMediaItemVisibility: visibility,
|
||||
)
|
||||
widget.id,
|
||||
bodyText,
|
||||
spoilerText: spoilerController.text,
|
||||
mediaItems: newMediaItems,
|
||||
existingMediaItems: existingMediaItems,
|
||||
newMediaItemVisibility: visibility,
|
||||
)
|
||||
.withError((error) {
|
||||
buildSnackbar(context, 'Error updating $statusType: $error');
|
||||
logError(error, _logger);
|
||||
|
@ -231,11 +230,7 @@ class _EditorScreenState extends ConsumerState<EditorScreen> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_logger.finer('Build editor $isComment $parentEntry');
|
||||
final manager = context
|
||||
.read<ActiveProfileSelector<TimelineManager>>()
|
||||
.activeEntry
|
||||
.value;
|
||||
|
||||
final profile = context.watch<AccountsService>().currentProfile;
|
||||
final vc = getIt<FriendicaVersionChecker>();
|
||||
final canEdit = vc.canUseFeature(RelaticaFeatures.statusEditing);
|
||||
final canSpoilerText = vc.canUseFeature(RelaticaFeatures.postSpoilerText) ||
|
||||
|
@ -298,7 +293,7 @@ class _EditorScreenState extends ConsumerState<EditorScreen> {
|
|||
MediaUploadsControl(
|
||||
entryMediaItems: newMediaItems,
|
||||
),
|
||||
buildButtonBar(context, manager),
|
||||
buildButtonBar(context, profile),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -415,19 +410,19 @@ class _EditorScreenState extends ConsumerState<EditorScreen> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget buildButtonBar(BuildContext context, TimelineManager manager) {
|
||||
Widget buildButtonBar(BuildContext context, Profile profile) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (!widget.forEditing)
|
||||
ElevatedButton(
|
||||
onPressed:
|
||||
isSubmitting ? null : () => createStatus(context, manager),
|
||||
isSubmitting ? null : () => createStatus(context, profile),
|
||||
child: const Text('Submit'),
|
||||
),
|
||||
if (widget.forEditing)
|
||||
ElevatedButton(
|
||||
onPressed: isSubmitting ? null : () => editStatus(context, manager),
|
||||
onPressed: isSubmitting ? null : () => editStatus(context, profile),
|
||||
child: const Text('Edit'),
|
||||
),
|
||||
const HorizontalPadding(),
|
||||
|
|
|
@ -17,10 +17,9 @@ import '../models/timeline_grouping_list_data.dart';
|
|||
import '../models/timeline_identifiers.dart';
|
||||
import '../riverpod_controllers/circles_repo_services.dart';
|
||||
import '../riverpod_controllers/focus_mode.dart';
|
||||
import '../riverpod_controllers/timeline_services.dart';
|
||||
import '../services/auth_service.dart';
|
||||
import '../services/network_status_service.dart';
|
||||
import '../services/timeline_manager.dart';
|
||||
import '../utils/active_profile_selector.dart';
|
||||
|
||||
class HomeScreen extends ConsumerStatefulWidget {
|
||||
const HomeScreen({super.key});
|
||||
|
@ -34,20 +33,20 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
|
|||
|
||||
TimelineIdentifiers currentTimeline = TimelineIdentifiers.home();
|
||||
|
||||
void updateTimeline(TimelineManager manager) {
|
||||
void updateTimeline(Profile profile) {
|
||||
_logger.finer('Updating timeline: $currentTimeline');
|
||||
Future.delayed(const Duration(milliseconds: 100), () async {
|
||||
await manager.updateTimeline(
|
||||
currentTimeline, TimelineRefreshType.refresh);
|
||||
await ref
|
||||
.read(timelineManagerProvider(profile, currentTimeline).notifier)
|
||||
.updateTimeline(TimelineRefreshType.refresh);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
getIt<ActiveProfileSelector<TimelineManager>>()
|
||||
.activeEntry
|
||||
.andThenSuccess((m) => updateTimeline(m));
|
||||
final profile = context.read<AccountsService>().currentProfile;
|
||||
updateTimeline(profile);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -113,11 +112,6 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
|
|||
TimelineType.local,
|
||||
];
|
||||
|
||||
final manager = context
|
||||
.watch<ActiveProfileSelector<TimelineManager>>()
|
||||
.activeEntry
|
||||
.value;
|
||||
|
||||
final circles = ref.watch(timelineGroupingListProvider(
|
||||
profile,
|
||||
GroupingType.circle,
|
||||
|
@ -210,7 +204,7 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
|
|||
return;
|
||||
}
|
||||
currentTimeline = value;
|
||||
updateTimeline(manager);
|
||||
updateTimeline(profile);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../controls/linear_status_indicator.dart';
|
||||
|
@ -6,11 +7,11 @@ import '../controls/responsive_max_width.dart' show ResponsiveMaxWidth;
|
|||
import '../controls/standard_appbar.dart';
|
||||
import '../controls/timeline/post_control.dart';
|
||||
import '../globals.dart';
|
||||
import '../riverpod_controllers/entry_tree_item_services.dart';
|
||||
import '../services/auth_service.dart';
|
||||
import '../services/network_status_service.dart';
|
||||
import '../services/timeline_manager.dart';
|
||||
import '../utils/active_profile_selector.dart';
|
||||
|
||||
class PostScreen extends StatefulWidget {
|
||||
class PostScreen extends ConsumerStatefulWidget {
|
||||
final String id;
|
||||
|
||||
final String goToId;
|
||||
|
@ -22,39 +23,39 @@ class PostScreen extends StatefulWidget {
|
|||
});
|
||||
|
||||
@override
|
||||
State<PostScreen> createState() => _PostScreenState();
|
||||
ConsumerState<PostScreen> createState() => _PostScreenState();
|
||||
}
|
||||
|
||||
class _PostScreenState extends State<PostScreen> {
|
||||
class _PostScreenState extends ConsumerState<PostScreen> {
|
||||
bool firstDraw = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Future.delayed(const Duration(milliseconds: 500), () async {
|
||||
getIt<ActiveProfileSelector<TimelineManager>>()
|
||||
.activeEntry
|
||||
.andThenSuccess(
|
||||
(m) => m.refreshStatusChain(widget.id),
|
||||
);
|
||||
final profile = context.read<AccountsService>().currentProfile;
|
||||
Future.delayed(const Duration(milliseconds: 100), () async {
|
||||
await ref
|
||||
.read(timelineUpdaterProvider(profile).notifier)
|
||||
.refreshStatusChain(widget.id);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final nss = getIt<NetworkStatusService>();
|
||||
final manager = context
|
||||
.watch<ActiveProfileSelector<TimelineManager>>()
|
||||
.activeEntry
|
||||
.value;
|
||||
final body = manager.getPostTreeEntryBy(widget.id).fold(
|
||||
final profile = context.watch<AccountsService>().currentProfile;
|
||||
final entryResult =
|
||||
ref.watch(postTreeEntryByIdProvider(profile, widget.id));
|
||||
final body = entryResult.fold(
|
||||
onSuccess: (post) => RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
manager.refreshStatusChain(widget.id);
|
||||
await ref
|
||||
.read(timelineUpdaterProvider(profile).notifier)
|
||||
.refreshStatusChain(widget.id);
|
||||
return;
|
||||
},
|
||||
child: PostControl(
|
||||
originalItem: post,
|
||||
id: post.id,
|
||||
scrollToId: widget.goToId,
|
||||
openRemote: true,
|
||||
showStatusOpenButton: false,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
@ -17,22 +18,22 @@ import '../models/connection.dart';
|
|||
import '../models/search_results.dart';
|
||||
import '../models/search_types.dart';
|
||||
import '../models/timeline_entry.dart';
|
||||
import '../riverpod_controllers/entry_tree_item_services.dart';
|
||||
import '../routes.dart';
|
||||
import '../services/auth_service.dart';
|
||||
import '../services/connections_manager.dart';
|
||||
import '../services/entry_manager_service.dart';
|
||||
import '../services/network_status_service.dart';
|
||||
import '../utils/active_profile_selector.dart';
|
||||
import '../utils/snackbar_builder.dart';
|
||||
|
||||
class SearchScreen extends StatefulWidget {
|
||||
class SearchScreen extends ConsumerStatefulWidget {
|
||||
const SearchScreen({super.key});
|
||||
|
||||
@override
|
||||
State<SearchScreen> createState() => _SearchScreenState();
|
||||
ConsumerState<SearchScreen> createState() => _SearchScreenState();
|
||||
}
|
||||
|
||||
class _SearchScreenState extends State<SearchScreen> {
|
||||
class _SearchScreenState extends ConsumerState<SearchScreen> {
|
||||
static const limit = 50;
|
||||
static final _logger = Logger('$SearchScreen');
|
||||
var searchTextController = TextEditingController();
|
||||
|
@ -281,7 +282,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||
child: const Text('Load more results'),
|
||||
);
|
||||
}
|
||||
return buildStatusListTile(statuses[index]);
|
||||
return buildStatusListTile(profile, statuses[index]);
|
||||
},
|
||||
itemCount: statuses.length + 1,
|
||||
);
|
||||
|
@ -294,7 +295,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||
|
||||
return ListView(physics: const AlwaysScrollableScrollPhysics(), children: [
|
||||
if (searchResult.statuses.isNotEmpty)
|
||||
buildStatusListTile(searchResult.statuses.first),
|
||||
buildStatusListTile(profile, searchResult.statuses.first),
|
||||
if (searchResult.accounts.isNotEmpty)
|
||||
buildConnectionListTile(searchResult.accounts.first),
|
||||
]);
|
||||
|
@ -340,11 +341,11 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget buildStatusListTile(TimelineEntry status) {
|
||||
Widget buildStatusListTile(Profile profile, TimelineEntry status) {
|
||||
return SearchResultStatusControl(status, () async {
|
||||
final result = await getIt<ActiveProfileSelector<EntryManagerService>>()
|
||||
.activeEntry
|
||||
.andThenAsync((em) async => em.refreshStatusChain(status.id));
|
||||
final result = await ref
|
||||
.read(timelineUpdaterProvider(profile).notifier)
|
||||
.refreshStatusChain(status.id);
|
||||
if (context.mounted) {
|
||||
result.match(
|
||||
onSuccess: (entry) =>
|
||||
|
|
|
@ -1,44 +1,50 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../controls/linear_status_indicator.dart';
|
||||
import '../controls/standard_appbar.dart';
|
||||
import '../controls/timeline/timeline_panel.dart';
|
||||
import '../globals.dart';
|
||||
import '../models/auth/profile.dart';
|
||||
import '../models/timeline_identifiers.dart';
|
||||
import '../riverpod_controllers/timeline_services.dart';
|
||||
import '../services/auth_service.dart';
|
||||
import '../services/network_status_service.dart';
|
||||
import '../services/timeline_manager.dart';
|
||||
import '../utils/active_profile_selector.dart';
|
||||
|
||||
class UserPostsScreen extends StatefulWidget {
|
||||
class UserPostsScreen extends ConsumerStatefulWidget {
|
||||
final String userId;
|
||||
|
||||
const UserPostsScreen({super.key, required this.userId});
|
||||
|
||||
@override
|
||||
State<UserPostsScreen> createState() => _UserPostsScreenState();
|
||||
ConsumerState<UserPostsScreen> createState() => _UserPostsScreenState();
|
||||
}
|
||||
|
||||
class _UserPostsScreenState extends State<UserPostsScreen> {
|
||||
class _UserPostsScreenState extends ConsumerState<UserPostsScreen> {
|
||||
late final TimelineIdentifiers timeline;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final profile = context.read<AccountsService>().currentProfile;
|
||||
timeline = TimelineIdentifiers.profile(widget.userId);
|
||||
getIt<ActiveProfileSelector<TimelineManager>>()
|
||||
.activeEntry
|
||||
.andThenSuccess((m) => updateTimeline(m));
|
||||
updateTimeline(profile);
|
||||
}
|
||||
|
||||
void updateTimeline(TimelineManager manager) {
|
||||
void updateTimeline(Profile profile) {
|
||||
Future.delayed(const Duration(milliseconds: 100), () async {
|
||||
await manager.updateTimeline(timeline, TimelineRefreshType.refresh);
|
||||
await ref
|
||||
.read(timelineManagerProvider(profile, timeline).notifier)
|
||||
.updateTimeline(TimelineRefreshType.refresh);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final nss = getIt<NetworkStatusService>();
|
||||
final profile = context.watch<AccountsService>().currentProfile;
|
||||
ref.watch(timelineManagerProvider(profile, timeline));
|
||||
|
||||
return Scaffold(
|
||||
appBar: StandardAppBar.build(
|
||||
|
|
|
@ -1,650 +0,0 @@
|
|||
import 'package:flutter/material.dart' hide Visibility;
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:result_monad/result_monad.dart';
|
||||
|
||||
import '../friendica_client/friendica_client.dart';
|
||||
import '../friendica_client/paging_data.dart';
|
||||
import '../globals.dart';
|
||||
import '../models/auth/profile.dart';
|
||||
import '../models/entry_tree_item.dart';
|
||||
import '../models/exec_error.dart';
|
||||
import '../models/image_entry.dart';
|
||||
import '../models/media_attachment_uploads/new_entry_media_items.dart';
|
||||
import '../models/timeline_entry.dart';
|
||||
import '../models/timeline_identifiers.dart';
|
||||
import '../models/visibility.dart';
|
||||
import '../utils/active_profile_selector.dart';
|
||||
import '../utils/media_upload_attachment_helper.dart';
|
||||
import 'reshared_via_service.dart';
|
||||
|
||||
class EntryManagerService extends ChangeNotifier {
|
||||
static final _logger = Logger('$EntryManagerService');
|
||||
final _entries = <String, TimelineEntry>{};
|
||||
final _parentPostIds = <String, String>{};
|
||||
final _postNodes = <String, _Node>{};
|
||||
final _postThreadHashtags = <String, Set<String>>{};
|
||||
final _postTreeConnections = <String, Set<String>>{};
|
||||
final Profile profile;
|
||||
|
||||
EntryManagerService(this.profile);
|
||||
|
||||
void clear() {
|
||||
_entries.clear();
|
||||
_parentPostIds.clear();
|
||||
_postNodes.clear();
|
||||
}
|
||||
|
||||
_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];
|
||||
}
|
||||
|
||||
Result<EntryTreeItem, ExecError> getPostTreeEntryBy(String id) {
|
||||
_logger.finest('Getting post: $id');
|
||||
final idForCall = id;
|
||||
final postNode = _getPostRootNode(idForCall);
|
||||
if (postNode == null) {
|
||||
return Result.error(ExecError(
|
||||
type: ErrorType.notFound,
|
||||
message: 'Unknown post id: $id',
|
||||
));
|
||||
}
|
||||
|
||||
return Result.ok(_nodeToTreeItem(postNode, profile.userId));
|
||||
}
|
||||
|
||||
Result<List<String>, ExecError> getPostTreeHashtags(String id) {
|
||||
final postId = _getPostRootNode(id)?.id ?? '';
|
||||
if (postId.isEmpty) {
|
||||
return buildErrorResult(
|
||||
type: ErrorType.notFound,
|
||||
message: 'Root Post ID not found for $id',
|
||||
);
|
||||
}
|
||||
final hashtags = _postThreadHashtags[postId]?.toList() ?? [];
|
||||
|
||||
return Result.ok(hashtags);
|
||||
}
|
||||
|
||||
Result<List<String>, ExecError> getPostTreeConnectionIds(String id) {
|
||||
final postId = _getPostRootNode(id)?.id ?? '';
|
||||
if (postId.isEmpty) {
|
||||
return buildErrorResult(
|
||||
type: ErrorType.notFound,
|
||||
message: 'Root Post ID not found for $id',
|
||||
);
|
||||
}
|
||||
final hashtags = _postTreeConnections[postId]?.toList() ?? [];
|
||||
|
||||
return Result.ok(hashtags);
|
||||
}
|
||||
|
||||
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> deleteEntryById(String id) async {
|
||||
_logger.finest('Delete entry: $id');
|
||||
final result = await StatusesClient(profile).deleteEntryById(id);
|
||||
if (result.isFailure) {
|
||||
return result.errorCast();
|
||||
}
|
||||
|
||||
_cleanupEntriesForId(id);
|
||||
notifyListeners();
|
||||
return Result.ok(true);
|
||||
}
|
||||
|
||||
FutureResult<bool, ExecError> createNewStatus(
|
||||
String text, {
|
||||
String spoilerText = '',
|
||||
String inReplyToId = '',
|
||||
required NewEntryMediaItems mediaItems,
|
||||
required List<ImageEntry> existingMediaItems,
|
||||
required Visibility visibility,
|
||||
}) async {
|
||||
_logger.finest('Creating new post: $text');
|
||||
final mediaIds = existingMediaItems.map((m) => 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(
|
||||
(imageBytes) async =>
|
||||
await RemoteFileClient(profile).uploadFileAsAttachment(
|
||||
bytes: imageBytes,
|
||||
album: mediaItems.albumName,
|
||||
description: item.description,
|
||||
fileName: filename,
|
||||
visibility: visibility,
|
||||
),
|
||||
);
|
||||
if (uploadResult.isSuccess) {
|
||||
mediaIds.add(uploadResult.value.scales.first.id);
|
||||
} else {
|
||||
return Result.error(ExecError(
|
||||
type: ErrorType.localError,
|
||||
message: 'Error uploading image: ${uploadResult.error}'));
|
||||
}
|
||||
}
|
||||
|
||||
final result = await StatusesClient(profile)
|
||||
.createNewStatus(
|
||||
text: text,
|
||||
spoilerText: spoilerText,
|
||||
inReplyToId: inReplyToId,
|
||||
mediaIds: mediaIds,
|
||||
visibility: visibility)
|
||||
.andThenSuccessAsync((item) async {
|
||||
await processNewItems([item], profile.username, null);
|
||||
return item;
|
||||
}).andThenSuccessAsync((item) async {
|
||||
if (inReplyToId.isNotEmpty) {
|
||||
late final String 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(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
FutureResult<bool, ExecError> editStatus(
|
||||
String id,
|
||||
String text, {
|
||||
String spoilerText = '',
|
||||
required NewEntryMediaItems mediaItems,
|
||||
required List<ImageEntry> existingMediaItems,
|
||||
required Visibility newMediaItemVisibility,
|
||||
}) async {
|
||||
_logger.finest('Editing post: $text');
|
||||
final idForCall = id;
|
||||
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(
|
||||
(imageBytes) async => await RemoteFileClient(profile)
|
||||
.uploadFileAsAttachment(
|
||||
bytes: imageBytes,
|
||||
album: mediaItems.albumName,
|
||||
description: item.description,
|
||||
fileName: filename,
|
||||
visibility: newMediaItemVisibility),
|
||||
);
|
||||
if (uploadResult.isSuccess) {
|
||||
mediaIds.add(uploadResult.value.scales.first.id);
|
||||
} else {
|
||||
return Result.error(ExecError(
|
||||
type: ErrorType.localError,
|
||||
message: 'Error uploading image: ${uploadResult.error}'));
|
||||
}
|
||||
}
|
||||
|
||||
final result = await StatusesClient(profile)
|
||||
.editStatus(
|
||||
id: idForCall,
|
||||
text: text,
|
||||
spoilerText: spoilerText,
|
||||
mediaIds: mediaIds)
|
||||
.andThenSuccessAsync((item) async {
|
||||
await processNewItems([item], profile.username, null);
|
||||
return item;
|
||||
}).andThenSuccessAsync((item) async {
|
||||
final inReplyToId = item.parentId;
|
||||
if (inReplyToId.isNotEmpty) {
|
||||
late final String 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(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
FutureResult<List<EntryTreeItem>, ExecError> updateTimeline(
|
||||
TimelineIdentifiers type, int maxId, int sinceId) async {
|
||||
_logger.fine(() => 'Updating timeline');
|
||||
final client = TimelineClient(profile);
|
||||
final itemsResult = await client.getTimeline(
|
||||
type: type,
|
||||
page: PagingData(
|
||||
maxId: maxId > 0 ? maxId : null,
|
||||
sinceId: sinceId > 0 ? sinceId : null,
|
||||
),
|
||||
);
|
||||
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 =
|
||||
await processNewItems(itemsResult.value, profile.userId, client);
|
||||
_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);
|
||||
}
|
||||
|
||||
Future<List<EntryTreeItem>> processNewItems(
|
||||
List<TimelineEntry> items,
|
||||
String currentId,
|
||||
FriendicaClient? client,
|
||||
) async {
|
||||
items.sort((i1, i2) => int.parse(i1.id).compareTo(int.parse(i2.id)));
|
||||
final allSeenItems = [...items];
|
||||
for (final item in items) {
|
||||
_entries[item.id] = item;
|
||||
}
|
||||
|
||||
final orphans = <TimelineEntry>[];
|
||||
for (final item in items) {
|
||||
if (item.parentId.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final parent = _entries[item.parentId];
|
||||
if (parent == null) {
|
||||
orphans.add(item);
|
||||
} else {
|
||||
if (parent.parentId.isEmpty) {
|
||||
_parentPostIds[item.id] = parent.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (final o in orphans) {
|
||||
await StatusesClient(profile)
|
||||
.getPostOrComment(o.id, fullContext: true)
|
||||
.andThenSuccessAsync((items) async {
|
||||
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) {
|
||||
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));
|
||||
});
|
||||
final postNodesToReturn = <_Node>{};
|
||||
var lastCount = 0;
|
||||
while (allSeenItems.isNotEmpty) {
|
||||
final seenItemsCopy = [...allSeenItems];
|
||||
for (final item in seenItemsCopy) {
|
||||
if (item.parentId.isEmpty) {
|
||||
final postNode =
|
||||
_postNodes.putIfAbsent(item.id, () => _Node(item.id));
|
||||
final pth = _postThreadHashtags.putIfAbsent(item.id, () => {});
|
||||
final ptc = _postTreeConnections.putIfAbsent(item.id, () => {});
|
||||
pth.addAll(item.tags);
|
||||
ptc.add(item.authorId);
|
||||
ptc.add(item.parentAuthorId);
|
||||
postNodesToReturn.add(postNode);
|
||||
allSeenItems.remove(item);
|
||||
} else {
|
||||
final parentParentPostId = _postNodes.containsKey(item.parentId)
|
||||
? item.parentId
|
||||
: _parentPostIds[item.parentId];
|
||||
if (_postNodes[parentParentPostId] == null) {
|
||||
_logger.severe(
|
||||
'Error finding parent ${item.parentId} for entry ${item.id}');
|
||||
continue;
|
||||
}
|
||||
final pth =
|
||||
_postThreadHashtags.putIfAbsent(parentParentPostId!, () => {});
|
||||
final ptc =
|
||||
_postTreeConnections.putIfAbsent(parentParentPostId, () => {});
|
||||
pth.addAll(item.tags);
|
||||
ptc.add(item.authorId);
|
||||
ptc.add(item.parentAuthorId);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
allSeenItems.remove(item);
|
||||
}
|
||||
}
|
||||
if (allSeenItems.isNotEmpty && allSeenItems.length == lastCount) {
|
||||
_logger.severe(
|
||||
'Had ${allSeenItems.length} items left over after all iterations');
|
||||
break;
|
||||
}
|
||||
lastCount = allSeenItems.length;
|
||||
}
|
||||
|
||||
final updatedPosts = postNodesToReturn
|
||||
.map((node) => _nodeToTreeItem(node, currentId))
|
||||
.toList();
|
||||
|
||||
_logger.finest(
|
||||
'Completed processing new items ${client == null ? 'sub level' : 'top level'}');
|
||||
return updatedPosts;
|
||||
}
|
||||
|
||||
FutureResult<EntryTreeItem, ExecError> refreshStatusChain(String id) async {
|
||||
_logger.finest('Refreshing post: $id');
|
||||
final client = StatusesClient(profile);
|
||||
final result = await client
|
||||
.getPostOrComment(id, fullContext: false)
|
||||
.andThenAsync((rootItems) async => await client
|
||||
.getPostOrComment(id, fullContext: true)
|
||||
.andThenSuccessAsync(
|
||||
(contextItems) async => [...rootItems, ...contextItems]))
|
||||
.withResult((items) async {
|
||||
_cleanupEntriesForId(id);
|
||||
await processNewItems(items, client.profile.username, null);
|
||||
});
|
||||
|
||||
return result.mapValue((_) {
|
||||
_logger.finest('$id post updated');
|
||||
return _nodeToTreeItem(_getPostRootNode(id)!, client.profile.userId);
|
||||
}).mapError(
|
||||
(error) {
|
||||
_logger.finest('$id error updating: $error');
|
||||
return ExecError(
|
||||
type: ErrorType.localError,
|
||||
message: error.toString(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
FutureResult<EntryTreeItem, ExecError> resharePost(String id) async {
|
||||
_logger.finest('Resharing post: $id');
|
||||
final resharedViaService = getIt<ActiveProfileSelector<ReshareViaService>>()
|
||||
.getForProfile(profile)
|
||||
.fold(
|
||||
onSuccess: (s) => s,
|
||||
onError: (error) {
|
||||
_logger.severe('Error getting reshared via service: $error');
|
||||
return null;
|
||||
});
|
||||
final client = StatusesClient(profile);
|
||||
final idForCall = id;
|
||||
final result =
|
||||
await client.resharePost(idForCall).andThenSuccessAsync((item) async {
|
||||
resharedViaService?.upsertResharedVia(postId: id, resharerId: profile.id);
|
||||
await processNewItems([item], client.profile.username, null);
|
||||
});
|
||||
|
||||
return result
|
||||
.mapValue((_) {
|
||||
_logger.finest('$id post updated after reshare');
|
||||
return _nodeToTreeItem(_postNodes[id]!, client.profile.userId);
|
||||
})
|
||||
.withResult((_) => notifyListeners())
|
||||
.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 resharedViaService = getIt<ActiveProfileSelector<ReshareViaService>>()
|
||||
.getForProfile(profile)
|
||||
.fold(
|
||||
onSuccess: (s) => s,
|
||||
onError: (error) {
|
||||
_logger.severe('Error getting reshared via service: $error');
|
||||
return null;
|
||||
});
|
||||
final client = StatusesClient(profile);
|
||||
final idForCall = id;
|
||||
final result =
|
||||
await client.unResharePost(idForCall).andThenSuccessAsync((item) async {
|
||||
resharedViaService?.upsertRemovedSharer(
|
||||
postId: id, resharerId: profile.id);
|
||||
await processNewItems([item], client.profile.username, null);
|
||||
});
|
||||
|
||||
if (result.isFailure) {
|
||||
return Result.error(result.error);
|
||||
}
|
||||
|
||||
return result
|
||||
.mapValue((_) {
|
||||
_logger.finest('$id post updated after reshare');
|
||||
return _nodeToTreeItem(_postNodes[id]!, client.profile.userId);
|
||||
})
|
||||
.withResult((_) => notifyListeners())
|
||||
.mapError(
|
||||
(error) {
|
||||
_logger.finest('$id error updating: $error');
|
||||
return ExecError(
|
||||
type: ErrorType.localError,
|
||||
message: error.toString(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
FutureResult<EntryTreeItem, ExecError> toggleFavorited(
|
||||
String id, bool newStatus) async {
|
||||
final interactionClient = InteractionsClient(profile);
|
||||
final postsClient = StatusesClient(profile);
|
||||
final idForCall = id;
|
||||
final result =
|
||||
await interactionClient.changeFavoriteStatus(idForCall, newStatus);
|
||||
if (result.isFailure) {
|
||||
return result.errorCast();
|
||||
}
|
||||
|
||||
final updateResult =
|
||||
await postsClient.getPostOrComment(id, fullContext: false);
|
||||
if (updateResult.isFailure) {
|
||||
return updateResult.errorCast();
|
||||
}
|
||||
final update = updateResult.value.first;
|
||||
_entries[update.id] = update;
|
||||
final node = update.parentId.isEmpty
|
||||
? _postNodes[update.id]!
|
||||
: _postNodes[_parentPostIds[update.id]]!;
|
||||
|
||||
notifyListeners();
|
||||
return Result.ok(_nodeToTreeItem(node, interactionClient.profile.userId));
|
||||
}
|
||||
|
||||
EntryTreeItem _nodeToTreeItem(_Node node, String currentId) {
|
||||
final childenEntries = <String, EntryTreeItem>{};
|
||||
for (final c in node.children) {
|
||||
childenEntries[c.id] = _nodeToTreeItem(c, currentId);
|
||||
}
|
||||
final entry = _entries[node.id]!;
|
||||
final isMine = entry.authorId == currentId;
|
||||
return EntryTreeItem(
|
||||
_entries[node.id]!,
|
||||
isMine: isMine,
|
||||
initialChildren: childenEntries,
|
||||
);
|
||||
}
|
||||
|
||||
void _cleanupEntriesForId(String id) {
|
||||
if (_parentPostIds.containsKey(id)) {
|
||||
final parentPostId = _parentPostIds.remove(id);
|
||||
final parentPostNode = _postNodes[parentPostId];
|
||||
parentPostNode?.removeChildById(id);
|
||||
}
|
||||
|
||||
if (_entries.containsKey(id)) {
|
||||
_entries.remove(id);
|
||||
}
|
||||
|
||||
if (_postNodes.containsKey(id)) {
|
||||
_postNodes.remove(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
void removeChildById(String id) {
|
||||
if (_children.containsKey(id)) {
|
||||
_children.remove(id);
|
||||
}
|
||||
|
||||
for (final c in _children.values) {
|
||||
c.removeChildById(id);
|
||||
}
|
||||
}
|
||||
|
||||
_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;
|
||||
}
|
|
@ -1,216 +0,0 @@
|
|||
import 'package:flutter/material.dart' hide Visibility;
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:result_monad/result_monad.dart';
|
||||
|
||||
import '../data/interfaces/circles_repo_intf.dart';
|
||||
import '../models/auth/profile.dart';
|
||||
import '../models/entry_tree_item.dart';
|
||||
import '../models/exec_error.dart';
|
||||
import '../models/image_entry.dart';
|
||||
import '../models/media_attachment_uploads/new_entry_media_items.dart';
|
||||
import '../models/timeline.dart';
|
||||
import '../models/timeline_entry.dart';
|
||||
import '../models/timeline_identifiers.dart';
|
||||
import '../models/visibility.dart';
|
||||
import 'entry_manager_service.dart';
|
||||
|
||||
enum TimelineRefreshType {
|
||||
refresh,
|
||||
loadOlder,
|
||||
loadNewer,
|
||||
}
|
||||
|
||||
class TimelineManager extends ChangeNotifier {
|
||||
static final _logger = Logger('$TimelineManager');
|
||||
|
||||
final ICirclesRepo circlesRepo;
|
||||
final EntryManagerService entryManagerService;
|
||||
var circlesNotInitialized = true;
|
||||
final Profile profile;
|
||||
|
||||
final cachedTimelines = <TimelineIdentifiers, Timeline>{};
|
||||
|
||||
TimelineManager(this.profile, this.circlesRepo, this.entryManagerService);
|
||||
|
||||
void clear() {
|
||||
circlesNotInitialized = true;
|
||||
cachedTimelines.clear();
|
||||
entryManagerService.clear();
|
||||
circlesRepo.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
FutureResult<bool, ExecError> createNewStatus(
|
||||
String text, {
|
||||
String spoilerText = '',
|
||||
String inReplyToId = '',
|
||||
required NewEntryMediaItems newMediaItems,
|
||||
required List<ImageEntry> existingMediaItems,
|
||||
required Visibility visibility,
|
||||
}) async {
|
||||
final result = await entryManagerService.createNewStatus(
|
||||
text,
|
||||
spoilerText: spoilerText,
|
||||
inReplyToId: inReplyToId,
|
||||
mediaItems: newMediaItems,
|
||||
existingMediaItems: existingMediaItems,
|
||||
visibility: visibility,
|
||||
);
|
||||
if (result.isSuccess) {
|
||||
_logger.finest('Notifying listeners of new status created');
|
||||
notifyListeners();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
FutureResult<bool, ExecError> editStatus(
|
||||
String id,
|
||||
String text, {
|
||||
String spoilerText = '',
|
||||
String inReplyToId = '',
|
||||
required NewEntryMediaItems newMediaItems,
|
||||
required List<ImageEntry> existingMediaItems,
|
||||
required Visibility newMediaItemVisibility,
|
||||
}) async {
|
||||
final result = await entryManagerService.editStatus(id, text,
|
||||
spoilerText: spoilerText,
|
||||
mediaItems: newMediaItems,
|
||||
existingMediaItems: existingMediaItems,
|
||||
newMediaItemVisibility: newMediaItemVisibility);
|
||||
if (result.isSuccess) {
|
||||
_logger.finest('Notifying listeners of updated status');
|
||||
notifyListeners();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Result<TimelineEntry, ExecError> getEntryById(String id) {
|
||||
_logger.finest('Getting entry for $id');
|
||||
return entryManagerService.getEntryById(id);
|
||||
}
|
||||
|
||||
FutureResult<bool, ExecError> deleteEntryById(String id) async {
|
||||
_logger.finest('Delete entry for $id');
|
||||
final result = await entryManagerService.deleteEntryById(id);
|
||||
|
||||
if (result.isSuccess) {
|
||||
for (final t in cachedTimelines.values) {
|
||||
t.removeTimelineEntry(id);
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
return result;
|
||||
}
|
||||
|
||||
Result<EntryTreeItem, ExecError> getPostTreeEntryBy(String id) {
|
||||
_logger.finest('Getting post for $id');
|
||||
return entryManagerService.getPostTreeEntryBy(id);
|
||||
}
|
||||
|
||||
// refresh timeline gets statuses newer than the newest in that timeline
|
||||
List<EntryTreeItem> getTimeline(TimelineIdentifiers type) {
|
||||
_logger.finest('Getting timeline $type');
|
||||
return cachedTimelines.putIfAbsent(type, () => Timeline(type)).posts;
|
||||
}
|
||||
|
||||
///
|
||||
/// id is the id of a post or comment in the chain, including the original post.
|
||||
FutureResult<EntryTreeItem, ExecError> refreshStatusChain(String id) async {
|
||||
_logger.finest('Refreshing post $id');
|
||||
final result = await entryManagerService.refreshStatusChain(id);
|
||||
if (result.isSuccess) {
|
||||
for (final t in cachedTimelines.values) {
|
||||
t.addOrUpdate([result.value]);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
FutureResult<EntryTreeItem, ExecError> resharePost(String id) async {
|
||||
final result = await entryManagerService.resharePost(id);
|
||||
final empty = EntryTreeItem.empty();
|
||||
if (result.isSuccess) {
|
||||
for (final t in cachedTimelines.values) {
|
||||
if (t.posts.firstWhere((p) => p.id == id, orElse: () => empty) !=
|
||||
empty) {
|
||||
t.addOrUpdate([result.value]);
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
FutureResult<EntryTreeItem, ExecError> unResharePost(String id) async {
|
||||
final result = await entryManagerService.unResharePost(id);
|
||||
final empty = EntryTreeItem.empty();
|
||||
if (result.isSuccess) {
|
||||
for (final t in cachedTimelines.values) {
|
||||
if (t.posts.firstWhere((p) => p.id == id, orElse: () => empty) !=
|
||||
empty) {
|
||||
t.addOrUpdate([result.value]);
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<void> updateTimeline(
|
||||
TimelineIdentifiers type,
|
||||
TimelineRefreshType refreshType,
|
||||
) async {
|
||||
_logger.finest('Updating w/$refreshType for timeline $type ');
|
||||
final timeline = cachedTimelines.putIfAbsent(type, () => Timeline(type));
|
||||
late final int lowestId;
|
||||
late final int highestId;
|
||||
switch (refreshType) {
|
||||
case TimelineRefreshType.refresh:
|
||||
timeline.clear();
|
||||
lowestId = 0;
|
||||
highestId = 0;
|
||||
break;
|
||||
case TimelineRefreshType.loadOlder:
|
||||
lowestId = timeline.lowestStatusId;
|
||||
highestId = 0;
|
||||
break;
|
||||
case TimelineRefreshType.loadNewer:
|
||||
lowestId = 0;
|
||||
highestId = timeline.highestStatusId;
|
||||
break;
|
||||
}
|
||||
(await entryManagerService.updateTimeline(type, lowestId, highestId)).match(
|
||||
onSuccess: (posts) {
|
||||
_logger.finest('Posts returned for adding to $type: ${posts.length}');
|
||||
timeline.addOrUpdate(posts);
|
||||
notifyListeners();
|
||||
}, onError: (error) {
|
||||
_logger.severe('Error getting timeline: $type}');
|
||||
});
|
||||
}
|
||||
|
||||
FutureResult<EntryTreeItem, ExecError> toggleFavorited(
|
||||
String id, bool newStatus) async {
|
||||
_logger.finer('Attempting toggling favorite $id to $newStatus');
|
||||
final result = await entryManagerService.toggleFavorited(id, newStatus);
|
||||
if (result.isFailure) {
|
||||
_logger.info('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;
|
||||
}
|
||||
// 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
|
||||
// If our own has delete
|
||||
}
|
|
@ -1,14 +1,27 @@
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../models/auth/profile.dart';
|
||||
import '../models/entry_tree_item.dart';
|
||||
import '../models/flattened_tree_item.dart';
|
||||
import '../models/timeline_entry.dart';
|
||||
import '../riverpod_controllers/entry_tree_item_services.dart';
|
||||
import '../riverpod_controllers/timeline_entry_services.dart';
|
||||
|
||||
extension FlatteningExtensions on EntryTreeItem {
|
||||
static const baseLevel = 0;
|
||||
|
||||
List<FlattenedTreeItem> flatten(
|
||||
{int level = baseLevel, bool topLevelOnly = false}) {
|
||||
{int level = baseLevel,
|
||||
bool topLevelOnly = false,
|
||||
required Profile profile,
|
||||
required WidgetRef ref}) {
|
||||
final items = <FlattenedTreeItem>[];
|
||||
|
||||
// TODO handle if entries aren't in manager
|
||||
final entryForItem =
|
||||
ref.read(timelineEntryManagerProvider(profile, id)).value;
|
||||
final myEntry = FlattenedTreeItem(
|
||||
timelineEntry: entry,
|
||||
timelineEntry: entryForItem,
|
||||
isMine: isMine,
|
||||
level: level,
|
||||
);
|
||||
|
@ -18,16 +31,19 @@ extension FlatteningExtensions on EntryTreeItem {
|
|||
return items;
|
||||
}
|
||||
|
||||
final sortedChildren = [...children];
|
||||
sortedChildren.sort((c1, c2) =>
|
||||
c1.entry.creationTimestamp.compareTo(c2.entry.creationTimestamp));
|
||||
final sortedChildren = children.map((id) {
|
||||
final tree = ref.read(entryTreeManagerProvider(profile, id)).value;
|
||||
final entry = ref.read(timelineEntryManagerProvider(profile, id)).value;
|
||||
return _EntryTreeItemWithEntity(entry, tree);
|
||||
}).toList();
|
||||
for (final child in sortedChildren) {
|
||||
int childLevel = level + 1;
|
||||
if (child.entry.authorId == entry.authorId && level != baseLevel) {
|
||||
if (child.entry.authorId == entryForItem.authorId && level != baseLevel) {
|
||||
childLevel = level;
|
||||
}
|
||||
|
||||
final childItems = child.flatten(level: childLevel);
|
||||
final childItems =
|
||||
child.tree.flatten(level: childLevel, profile: profile, ref: ref);
|
||||
childItems.sort((c1, c2) {
|
||||
if (c2.level == c1.level) {
|
||||
return c1.timelineEntry.creationTimestamp
|
||||
|
@ -51,3 +67,10 @@ extension FlatteningExtensions on EntryTreeItem {
|
|||
return items;
|
||||
}
|
||||
}
|
||||
|
||||
class _EntryTreeItemWithEntity {
|
||||
final TimelineEntry entry;
|
||||
final EntryTreeItem tree;
|
||||
|
||||
_EntryTreeItemWithEntity(this.entry, this.tree);
|
||||
}
|
||||
|
|
|
@ -1,271 +1,268 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:relatica/globals.dart';
|
||||
import 'package:relatica/models/entry_tree_item.dart';
|
||||
import 'package:relatica/models/timeline_entry.dart';
|
||||
import 'package:relatica/utils/entry_tree_item_flattening.dart';
|
||||
|
||||
|
||||
void main() {
|
||||
group('Flattening Tests', () {
|
||||
test('Single entry no children', () {
|
||||
final entry = TimelineEntry.randomBuilt();
|
||||
final treeItem = EntryTreeItem(entry);
|
||||
final flattened = treeItem.flatten();
|
||||
expect(flattened.length, equals(1));
|
||||
expect(flattened.first.isMine, equals(treeItem.isMine));
|
||||
expect(flattened.first.timelineEntry, equals(treeItem.entry));
|
||||
});
|
||||
|
||||
test('Entry with two children', () {
|
||||
final post = TimelineEntry(id: '0');
|
||||
final children = {
|
||||
'1': EntryTreeItem(
|
||||
TimelineEntry(id: '1'),
|
||||
),
|
||||
'2': EntryTreeItem(
|
||||
TimelineEntry(id: '2'),
|
||||
),
|
||||
};
|
||||
final treeItem = EntryTreeItem(post, initialChildren: children);
|
||||
final flattened = treeItem.flatten();
|
||||
expect(flattened.length, equals(3));
|
||||
expect(
|
||||
flattened.map((e) => int.parse(e.timelineEntry.id)).toList(),
|
||||
equals([0, 1, 2]),
|
||||
);
|
||||
expect(
|
||||
flattened.map((e) => e.level).toList(),
|
||||
equals([0, 1, 1]),
|
||||
);
|
||||
});
|
||||
|
||||
test('Entry with nesting children different authors', () {
|
||||
final post = TimelineEntry(id: '0');
|
||||
final children = {
|
||||
'1': EntryTreeItem(TimelineEntry(id: '1', authorId: randomId()),
|
||||
initialChildren: {
|
||||
'2': EntryTreeItem(
|
||||
TimelineEntry(id: '2', authorId: randomId()),
|
||||
),
|
||||
}),
|
||||
};
|
||||
final treeItem = EntryTreeItem(post, initialChildren: children);
|
||||
final flattened = treeItem.flatten();
|
||||
expect(flattened.length, equals(3));
|
||||
expect(
|
||||
flattened.map((e) => int.parse(e.timelineEntry.id)).toList(),
|
||||
equals([0, 1, 2]),
|
||||
);
|
||||
expect(
|
||||
flattened.map((e) => e.level).toList(),
|
||||
equals([0, 1, 2]),
|
||||
);
|
||||
});
|
||||
|
||||
test('Entry with nesting children same authors', () {
|
||||
final post = TimelineEntry(id: '0');
|
||||
final children = {
|
||||
'1': EntryTreeItem(TimelineEntry(id: '1'), initialChildren: {
|
||||
'2': EntryTreeItem(
|
||||
TimelineEntry(id: '2'),
|
||||
),
|
||||
}),
|
||||
};
|
||||
final treeItem = EntryTreeItem(post, initialChildren: children);
|
||||
final flattened = treeItem.flatten();
|
||||
expect(flattened.length, equals(3));
|
||||
expect(
|
||||
flattened.map((e) => int.parse(e.timelineEntry.id)).toList(),
|
||||
equals([0, 1, 2]),
|
||||
);
|
||||
expect(
|
||||
flattened.map((e) => e.level).toList(),
|
||||
equals([0, 1, 1]),
|
||||
);
|
||||
});
|
||||
|
||||
test('Entry fully nested children', () {
|
||||
var stamp = 0;
|
||||
final post =
|
||||
TimelineEntry(id: '0', authorId: 'a0', creationTimestamp: stamp++);
|
||||
final children = {
|
||||
'1': EntryTreeItem(
|
||||
TimelineEntry(id: '1', creationTimestamp: stamp++),
|
||||
initialChildren: {
|
||||
'1.1': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '1.1',
|
||||
authorId: randomId(),
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
),
|
||||
'1.2': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '1.2',
|
||||
authorId: randomId(),
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
),
|
||||
'1.3': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '1.3',
|
||||
authorId: randomId(),
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
'2': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '2',
|
||||
authorId: randomId(),
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
initialChildren: {
|
||||
'2.1': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '2.1',
|
||||
authorId: randomId(),
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
),
|
||||
'2.2': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '2.2',
|
||||
authorId: randomId(),
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
initialChildren: {
|
||||
'2.2.1': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '2.2.1',
|
||||
authorId: 'a1',
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
initialChildren: {
|
||||
'2.2.1.1': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '2.2.1.1',
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
),
|
||||
'2.2.1.2': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '2.2.1.2',
|
||||
authorId: 'a1',
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
'2.2.2': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '2.2.2',
|
||||
authorId: 'a2',
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
initialChildren: {
|
||||
'2.2.2.1': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '2.2.2.1',
|
||||
creationTimestamp: (stamp++) + 100,
|
||||
),
|
||||
),
|
||||
'2.2.2.2': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '2.2.2.2',
|
||||
authorId: 'a2',
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
),
|
||||
'2.2.2.3': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '2.2.2.3',
|
||||
authorId: 'a2',
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
),
|
||||
'2.2.2.4': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '2.2.2.4',
|
||||
authorId: 'a0',
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
'2.3': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '2.3',
|
||||
authorId: randomId(),
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
'3': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '3',
|
||||
authorId: 'a0',
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
initialChildren: {
|
||||
'3.1': EntryTreeItem(TimelineEntry(
|
||||
id: '3.1',
|
||||
authorId: randomId(),
|
||||
creationTimestamp: stamp++,
|
||||
)),
|
||||
'3.2': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '3.2',
|
||||
authorId: 'a0',
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
),
|
||||
'3.3': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '3.3',
|
||||
authorId: randomId(),
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
};
|
||||
final treeItem = EntryTreeItem(post, initialChildren: children);
|
||||
final flattened = treeItem.flatten();
|
||||
expect(flattened.length, equals(21));
|
||||
expect(
|
||||
flattened.map((e) => e.timelineEntry.id).toList(),
|
||||
equals([
|
||||
'0',
|
||||
'1',
|
||||
'1.1',
|
||||
'1.2',
|
||||
'1.3',
|
||||
'2',
|
||||
'2.1',
|
||||
'2.2',
|
||||
'2.2.1',
|
||||
'2.2.1.2',
|
||||
'2.2.1.1',
|
||||
'2.2.2',
|
||||
'2.2.2.2',
|
||||
'2.2.2.3',
|
||||
'2.2.2.4',
|
||||
'2.2.2.1',
|
||||
'2.3',
|
||||
'3',
|
||||
'3.2',
|
||||
'3.1',
|
||||
'3.3',
|
||||
]),
|
||||
);
|
||||
expect(
|
||||
flattened.map((e) => e.level).toList(),
|
||||
equals([0, 1, 2, 2, 2, 1, 2, 2, 3, 3, 4, 3, 3, 3, 4, 4, 2, 1, 1, 2, 2]),
|
||||
);
|
||||
});
|
||||
});
|
||||
//TODO Maketests work with Riverpod
|
||||
// group('Flattening Tests', () {
|
||||
// test('Single entry no children', () {
|
||||
// final entry = TimelineEntry.randomBuilt();
|
||||
// final treeItem = EntryTreeItem(entry.id);
|
||||
// final flattened = treeItem.flatten();
|
||||
// expect(flattened.length, equals(1));
|
||||
// expect(flattened.first.isMine, equals(treeItem.isMine));
|
||||
// expect(flattened.first.timelineEntry, equals(treeItem.entry));
|
||||
// });
|
||||
//
|
||||
// test('Entry with two children', () {
|
||||
// final post = TimelineEntry(id: '0');
|
||||
// final children = {
|
||||
// '1': EntryTreeItem(
|
||||
// TimelineEntry(id: '1'),
|
||||
// ),
|
||||
// '2': EntryTreeItem(
|
||||
// TimelineEntry(id: '2'),
|
||||
// ),
|
||||
// };
|
||||
// final treeItem = EntryTreeItem(post, initialChildren: children);
|
||||
// final flattened = treeItem.flatten();
|
||||
// expect(flattened.length, equals(3));
|
||||
// expect(
|
||||
// flattened.map((e) => int.parse(e.timelineEntry.id)).toList(),
|
||||
// equals([0, 1, 2]),
|
||||
// );
|
||||
// expect(
|
||||
// flattened.map((e) => e.level).toList(),
|
||||
// equals([0, 1, 1]),
|
||||
// );
|
||||
// });
|
||||
//
|
||||
// test('Entry with nesting children different authors', () {
|
||||
// final post = TimelineEntry(id: '0');
|
||||
// final children = {
|
||||
// '1': EntryTreeItem(TimelineEntry(id: '1', authorId: randomId()),
|
||||
// initialChildren: {
|
||||
// '2': EntryTreeItem(
|
||||
// TimelineEntry(id: '2', authorId: randomId()),
|
||||
// ),
|
||||
// }),
|
||||
// };
|
||||
// final treeItem = EntryTreeItem(post, initialChildren: children);
|
||||
// final flattened = treeItem.flatten();
|
||||
// expect(flattened.length, equals(3));
|
||||
// expect(
|
||||
// flattened.map((e) => int.parse(e.timelineEntry.id)).toList(),
|
||||
// equals([0, 1, 2]),
|
||||
// );
|
||||
// expect(
|
||||
// flattened.map((e) => e.level).toList(),
|
||||
// equals([0, 1, 2]),
|
||||
// );
|
||||
// });
|
||||
//
|
||||
// test('Entry with nesting children same authors', () {
|
||||
// final post = TimelineEntry(id: '0');
|
||||
// final children = {
|
||||
// '1': EntryTreeItem(TimelineEntry(id: '1'), initialChildren: {
|
||||
// '2': EntryTreeItem(
|
||||
// TimelineEntry(id: '2'),
|
||||
// ),
|
||||
// }),
|
||||
// };
|
||||
// final treeItem = EntryTreeItem(post, initialChildren: children);
|
||||
// final flattened = treeItem.flatten();
|
||||
// expect(flattened.length, equals(3));
|
||||
// expect(
|
||||
// flattened.map((e) => int.parse(e.timelineEntry.id)).toList(),
|
||||
// equals([0, 1, 2]),
|
||||
// );
|
||||
// expect(
|
||||
// flattened.map((e) => e.level).toList(),
|
||||
// equals([0, 1, 1]),
|
||||
// );
|
||||
// });
|
||||
//
|
||||
// test('Entry fully nested children', () {
|
||||
// var stamp = 0;
|
||||
// final post =
|
||||
// TimelineEntry(id: '0', authorId: 'a0', creationTimestamp: stamp++);
|
||||
// final children = {
|
||||
// '1': EntryTreeItem(
|
||||
// TimelineEntry(id: '1', creationTimestamp: stamp++),
|
||||
// initialChildren: {
|
||||
// '1.1': EntryTreeItem(
|
||||
// TimelineEntry(
|
||||
// id: '1.1',
|
||||
// authorId: randomId(),
|
||||
// creationTimestamp: stamp++,
|
||||
// ),
|
||||
// ),
|
||||
// '1.2': EntryTreeItem(
|
||||
// TimelineEntry(
|
||||
// id: '1.2',
|
||||
// authorId: randomId(),
|
||||
// creationTimestamp: stamp++,
|
||||
// ),
|
||||
// ),
|
||||
// '1.3': EntryTreeItem(
|
||||
// TimelineEntry(
|
||||
// id: '1.3',
|
||||
// authorId: randomId(),
|
||||
// creationTimestamp: stamp++,
|
||||
// ),
|
||||
// ),
|
||||
// },
|
||||
// ),
|
||||
// '2': EntryTreeItem(
|
||||
// TimelineEntry(
|
||||
// id: '2',
|
||||
// authorId: randomId(),
|
||||
// creationTimestamp: stamp++,
|
||||
// ),
|
||||
// initialChildren: {
|
||||
// '2.1': EntryTreeItem(
|
||||
// TimelineEntry(
|
||||
// id: '2.1',
|
||||
// authorId: randomId(),
|
||||
// creationTimestamp: stamp++,
|
||||
// ),
|
||||
// ),
|
||||
// '2.2': EntryTreeItem(
|
||||
// TimelineEntry(
|
||||
// id: '2.2',
|
||||
// authorId: randomId(),
|
||||
// creationTimestamp: stamp++,
|
||||
// ),
|
||||
// initialChildren: {
|
||||
// '2.2.1': EntryTreeItem(
|
||||
// TimelineEntry(
|
||||
// id: '2.2.1',
|
||||
// authorId: 'a1',
|
||||
// creationTimestamp: stamp++,
|
||||
// ),
|
||||
// initialChildren: {
|
||||
// '2.2.1.1': EntryTreeItem(
|
||||
// TimelineEntry(
|
||||
// id: '2.2.1.1',
|
||||
// creationTimestamp: stamp++,
|
||||
// ),
|
||||
// ),
|
||||
// '2.2.1.2': EntryTreeItem(
|
||||
// TimelineEntry(
|
||||
// id: '2.2.1.2',
|
||||
// authorId: 'a1',
|
||||
// creationTimestamp: stamp++,
|
||||
// ),
|
||||
// ),
|
||||
// },
|
||||
// ),
|
||||
// '2.2.2': EntryTreeItem(
|
||||
// TimelineEntry(
|
||||
// id: '2.2.2',
|
||||
// authorId: 'a2',
|
||||
// creationTimestamp: stamp++,
|
||||
// ),
|
||||
// initialChildren: {
|
||||
// '2.2.2.1': EntryTreeItem(
|
||||
// TimelineEntry(
|
||||
// id: '2.2.2.1',
|
||||
// creationTimestamp: (stamp++) + 100,
|
||||
// ),
|
||||
// ),
|
||||
// '2.2.2.2': EntryTreeItem(
|
||||
// TimelineEntry(
|
||||
// id: '2.2.2.2',
|
||||
// authorId: 'a2',
|
||||
// creationTimestamp: stamp++,
|
||||
// ),
|
||||
// ),
|
||||
// '2.2.2.3': EntryTreeItem(
|
||||
// TimelineEntry(
|
||||
// id: '2.2.2.3',
|
||||
// authorId: 'a2',
|
||||
// creationTimestamp: stamp++,
|
||||
// ),
|
||||
// ),
|
||||
// '2.2.2.4': EntryTreeItem(
|
||||
// TimelineEntry(
|
||||
// id: '2.2.2.4',
|
||||
// authorId: 'a0',
|
||||
// creationTimestamp: stamp++,
|
||||
// ),
|
||||
// ),
|
||||
// },
|
||||
// ),
|
||||
// },
|
||||
// ),
|
||||
// '2.3': EntryTreeItem(
|
||||
// TimelineEntry(
|
||||
// id: '2.3',
|
||||
// authorId: randomId(),
|
||||
// creationTimestamp: stamp++,
|
||||
// ),
|
||||
// ),
|
||||
// },
|
||||
// ),
|
||||
// '3': EntryTreeItem(
|
||||
// TimelineEntry(
|
||||
// id: '3',
|
||||
// authorId: 'a0',
|
||||
// creationTimestamp: stamp++,
|
||||
// ),
|
||||
// initialChildren: {
|
||||
// '3.1': EntryTreeItem(TimelineEntry(
|
||||
// id: '3.1',
|
||||
// authorId: randomId(),
|
||||
// creationTimestamp: stamp++,
|
||||
// )),
|
||||
// '3.2': EntryTreeItem(
|
||||
// TimelineEntry(
|
||||
// id: '3.2',
|
||||
// authorId: 'a0',
|
||||
// creationTimestamp: stamp++,
|
||||
// ),
|
||||
// ),
|
||||
// '3.3': EntryTreeItem(
|
||||
// TimelineEntry(
|
||||
// id: '3.3',
|
||||
// authorId: randomId(),
|
||||
// creationTimestamp: stamp++,
|
||||
// ),
|
||||
// ),
|
||||
// },
|
||||
// ),
|
||||
// };
|
||||
// final treeItem = EntryTreeItem(post, initialChildren: children);
|
||||
// final flattened = treeItem.flatten();
|
||||
// expect(flattened.length, equals(21));
|
||||
// expect(
|
||||
// flattened.map((e) => e.timelineEntry.id).toList(),
|
||||
// equals([
|
||||
// '0',
|
||||
// '1',
|
||||
// '1.1',
|
||||
// '1.2',
|
||||
// '1.3',
|
||||
// '2',
|
||||
// '2.1',
|
||||
// '2.2',
|
||||
// '2.2.1',
|
||||
// '2.2.1.2',
|
||||
// '2.2.1.1',
|
||||
// '2.2.2',
|
||||
// '2.2.2.2',
|
||||
// '2.2.2.3',
|
||||
// '2.2.2.4',
|
||||
// '2.2.2.1',
|
||||
// '2.3',
|
||||
// '3',
|
||||
// '3.2',
|
||||
// '3.1',
|
||||
// '3.3',
|
||||
// ]),
|
||||
// );
|
||||
// expect(
|
||||
// flattened.map((e) => e.level).toList(),
|
||||
// equals([0, 1, 2, 2, 2, 1, 2, 2, 3, 3, 4, 3, 3, 3, 4, 4, 2, 1, 1, 2, 2]),
|
||||
// );
|
||||
// });
|
||||
// });
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue