Merge pull request #224 from krawieck/fix/deduplication-infinite-scroll
This commit is contained in:
commit
772cb5e575
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
- Show avatars setting toggle
|
- Show avatars setting toggle
|
||||||
- Show scores setting toggle
|
- Show scores setting toggle
|
||||||
|
- Default sort type setting
|
||||||
- Default listing type for the home tab setting
|
- Default listing type for the home tab setting
|
||||||
- Import Lemmy settings: long press an account in account settings then choose the import option
|
- Import Lemmy settings: long press an account in account settings then choose the import option
|
||||||
- Editing posts
|
- Editing posts
|
||||||
|
@ -12,6 +13,7 @@
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fixed bug where creating post would crash after uploading a picture
|
- Fixed bug where creating post would crash after uploading a picture
|
||||||
|
- Added deduplication in infinite scrolls
|
||||||
|
|
||||||
## v0.4.2 - 2021-04-12
|
## v0.4.2 - 2021-04-12
|
||||||
|
|
||||||
|
|
|
@ -3,11 +3,13 @@ import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:lemmy_api_client/v3.dart';
|
import 'package:lemmy_api_client/v3.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart' as ul;
|
import 'package:url_launcher/url_launcher.dart' as ul;
|
||||||
|
|
||||||
import '../hooks/delayed_loading.dart';
|
import '../hooks/delayed_loading.dart';
|
||||||
import '../hooks/stores.dart';
|
import '../hooks/stores.dart';
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
|
import '../stores/config_store.dart';
|
||||||
import '../widgets/fullscreenable_image.dart';
|
import '../widgets/fullscreenable_image.dart';
|
||||||
import '../widgets/radio_picker.dart';
|
import '../widgets/radio_picker.dart';
|
||||||
import 'add_instance.dart';
|
import 'add_instance.dart';
|
||||||
|
@ -40,12 +42,27 @@ class AddAccountPage extends HookWidget {
|
||||||
|
|
||||||
handleOnAdd() async {
|
handleOnAdd() async {
|
||||||
try {
|
try {
|
||||||
|
final isFirstAccount = accountsStore.hasNoAccount;
|
||||||
|
|
||||||
loading.start();
|
loading.start();
|
||||||
await accountsStore.addAccount(
|
await accountsStore.addAccount(
|
||||||
selectedInstance.value,
|
selectedInstance.value,
|
||||||
usernameController.text,
|
usernameController.text,
|
||||||
passwordController.text,
|
passwordController.text,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// if first account try to import settings
|
||||||
|
if (isFirstAccount) {
|
||||||
|
try {
|
||||||
|
await context.read<ConfigStore>().importLemmyUserSettings(
|
||||||
|
accountsStore
|
||||||
|
.userDataFor(
|
||||||
|
selectedInstance.value, usernameController.text)!
|
||||||
|
.jwt);
|
||||||
|
// ignore: avoid_catches_without_on_clauses, empty_catches
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
} on Exception catch (err) {
|
} on Exception catch (err) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||||
|
|
|
@ -36,6 +36,7 @@ class CommunitiesListPage extends StatelessWidget {
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
uniqueProp: (item) => item.community.actorId,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -120,6 +120,7 @@ class InboxPage extends HookWidget {
|
||||||
canBeMarkedAsRead: true,
|
canBeMarkedAsRead: true,
|
||||||
hideOnRead: unreadOnly.value,
|
hideOnRead: unreadOnly.value,
|
||||||
),
|
),
|
||||||
|
uniqueProp: (item) => item.comment.apId,
|
||||||
),
|
),
|
||||||
SortableInfiniteList<PersonMentionView>(
|
SortableInfiniteList<PersonMentionView>(
|
||||||
noItems: const Text('no mentions'),
|
noItems: const Text('no mentions'),
|
||||||
|
@ -137,6 +138,7 @@ class InboxPage extends HookWidget {
|
||||||
umv,
|
umv,
|
||||||
hideOnRead: unreadOnly.value,
|
hideOnRead: unreadOnly.value,
|
||||||
),
|
),
|
||||||
|
uniqueProp: (item) => item.personMention.id,
|
||||||
),
|
),
|
||||||
InfiniteScroll<PrivateMessageView>(
|
InfiniteScroll<PrivateMessageView>(
|
||||||
noItems: const Padding(
|
noItems: const Padding(
|
||||||
|
@ -156,6 +158,7 @@ class InboxPage extends HookWidget {
|
||||||
privateMessageView: mv,
|
privateMessageView: mv,
|
||||||
hideOnRead: unreadOnly.value,
|
hideOnRead: unreadOnly.value,
|
||||||
),
|
),
|
||||||
|
uniqueProp: (item) => item.privateMessage.apId,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
@ -108,6 +108,7 @@ class _ManageAccount extends HookWidget {
|
||||||
useTextEditingController(text: user.person.matrixUserId);
|
useTextEditingController(text: user.person.matrixUserId);
|
||||||
final avatar = useRef(user.person.avatar);
|
final avatar = useRef(user.person.avatar);
|
||||||
final banner = useRef(user.person.banner);
|
final banner = useRef(user.person.banner);
|
||||||
|
final showNsfw = useState(user.localUser.showNsfw);
|
||||||
final sendNotificationsToEmail =
|
final sendNotificationsToEmail =
|
||||||
useState(user.localUser.sendNotificationsToEmail);
|
useState(user.localUser.sendNotificationsToEmail);
|
||||||
final newPasswordController = useTextEditingController();
|
final newPasswordController = useTextEditingController();
|
||||||
|
@ -134,7 +135,7 @@ class _ManageAccount extends HookWidget {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await LemmyApiV3(user.instanceHost).run(SaveUserSettings(
|
await LemmyApiV3(user.instanceHost).run(SaveUserSettings(
|
||||||
showNsfw: user.localUser.showNsfw,
|
showNsfw: showNsfw.value,
|
||||||
theme: user.localUser.theme,
|
theme: user.localUser.theme,
|
||||||
defaultSortType: user.localUser.defaultSortType,
|
defaultSortType: user.localUser.defaultSortType,
|
||||||
defaultListingType: user.localUser.defaultListingType,
|
defaultListingType: user.localUser.defaultListingType,
|
||||||
|
@ -318,6 +319,15 @@ class _ManageAccount extends HookWidget {
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
SwitchListTile.adaptive(
|
||||||
|
value: showNsfw.value,
|
||||||
|
onChanged: (checked) {
|
||||||
|
showNsfw.value = checked;
|
||||||
|
},
|
||||||
|
title: Text(L10n.of(context)!.show_nsfw),
|
||||||
|
dense: true,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
SwitchListTile.adaptive(
|
SwitchListTile.adaptive(
|
||||||
value: sendNotificationsToEmail.value,
|
value: sendNotificationsToEmail.value,
|
||||||
onChanged: (checked) {
|
onChanged: (checked) {
|
||||||
|
|
|
@ -116,6 +116,20 @@ class _SearchResultsList extends HookWidget {
|
||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
uniqueProp: (item) {
|
||||||
|
switch (type) {
|
||||||
|
case SearchType.comments:
|
||||||
|
return (item as CommentView).comment.apId;
|
||||||
|
case SearchType.communities:
|
||||||
|
return (item as CommunityView).community.actorId;
|
||||||
|
case SearchType.posts:
|
||||||
|
return (item as PostView).post.apId;
|
||||||
|
case SearchType.users:
|
||||||
|
return (item as PersonViewSafe).person.actorId;
|
||||||
|
default:
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -159,13 +159,6 @@ class GeneralConfigPage extends HookWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SwitchListTile.adaptive(
|
|
||||||
title: Text(L10n.of(context)!.show_nsfw),
|
|
||||||
value: configStore.showNsfw,
|
|
||||||
onChanged: (checked) {
|
|
||||||
configStore.showNsfw = checked;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -54,15 +54,6 @@ class ConfigStore extends ChangeNotifier {
|
||||||
save();
|
save();
|
||||||
}
|
}
|
||||||
|
|
||||||
late bool _showNsfw;
|
|
||||||
@JsonKey(defaultValue: false)
|
|
||||||
bool get showNsfw => _showNsfw;
|
|
||||||
set showNsfw(bool showNsfw) {
|
|
||||||
_showNsfw = showNsfw;
|
|
||||||
notifyListeners();
|
|
||||||
save();
|
|
||||||
}
|
|
||||||
|
|
||||||
late bool _showScores;
|
late bool _showScores;
|
||||||
@JsonKey(defaultValue: true)
|
@JsonKey(defaultValue: true)
|
||||||
bool get showScores => _showScores;
|
bool get showScores => _showScores;
|
||||||
|
@ -95,24 +86,27 @@ class ConfigStore extends ChangeNotifier {
|
||||||
/// Copies over settings from lemmy to [ConfigStore]
|
/// Copies over settings from lemmy to [ConfigStore]
|
||||||
void copyLemmyUserSettings(LocalUserSettings localUserSettings) {
|
void copyLemmyUserSettings(LocalUserSettings localUserSettings) {
|
||||||
// themes from lemmy-ui that are dark mode
|
// themes from lemmy-ui that are dark mode
|
||||||
// const darkModeLemmyUiThemes = {
|
const darkModeLemmyUiThemes = {
|
||||||
// 'solar',
|
'solar',
|
||||||
// 'cyborg',
|
'cyborg',
|
||||||
// 'darkly',
|
'darkly',
|
||||||
// 'vaporwave-dark',
|
'vaporwave-dark',
|
||||||
// // TODO: is it dark theme?
|
'i386',
|
||||||
// 'i386',
|
};
|
||||||
// };
|
|
||||||
|
|
||||||
_showAvatars = localUserSettings.showAvatars;
|
_showAvatars = localUserSettings.showAvatars;
|
||||||
_showNsfw = localUserSettings.showNsfw;
|
_theme = () {
|
||||||
// TODO: should these also be imported? If so, how?
|
if (localUserSettings.theme == 'browser') return ThemeMode.system;
|
||||||
// _theme = darkModeLemmyUiThemes.contains(localUserSettings.theme)
|
|
||||||
// ? ThemeMode.dark
|
if (darkModeLemmyUiThemes.contains(localUserSettings.theme)) {
|
||||||
// : ThemeMode.light;
|
return ThemeMode.dark;
|
||||||
// _locale = L10n.supportedLocales.contains(Locale(localUserSettings.lang))
|
}
|
||||||
// ? Locale(localUserSettings.lang)
|
|
||||||
// : _locale;
|
return ThemeMode.light;
|
||||||
|
}();
|
||||||
|
_locale = L10n.supportedLocales.contains(Locale(localUserSettings.lang))
|
||||||
|
? Locale(localUserSettings.lang)
|
||||||
|
: _locale;
|
||||||
// TODO: add when it is released
|
// TODO: add when it is released
|
||||||
// _showScores = localUserSettings.showScores;
|
// _showScores = localUserSettings.showScores;
|
||||||
_defaultSortType = localUserSettings.defaultSortType;
|
_defaultSortType = localUserSettings.defaultSortType;
|
||||||
|
|
|
@ -13,7 +13,6 @@ ConfigStore _$ConfigStoreFromJson(Map<String, dynamic> json) {
|
||||||
..amoledDarkMode = json['amoledDarkMode'] as bool? ?? false
|
..amoledDarkMode = json['amoledDarkMode'] as bool? ?? false
|
||||||
..locale = LocaleSerde.fromJson(json['locale'] as String?)
|
..locale = LocaleSerde.fromJson(json['locale'] as String?)
|
||||||
..showAvatars = json['showAvatars'] as bool? ?? true
|
..showAvatars = json['showAvatars'] as bool? ?? true
|
||||||
..showNsfw = json['showNsfw'] as bool? ?? false
|
|
||||||
..showScores = json['showScores'] as bool? ?? true
|
..showScores = json['showScores'] as bool? ?? true
|
||||||
..defaultSortType = _sortTypeFromJson(json['defaultSortType'] as String?)
|
..defaultSortType = _sortTypeFromJson(json['defaultSortType'] as String?)
|
||||||
..defaultListingType =
|
..defaultListingType =
|
||||||
|
@ -26,7 +25,6 @@ Map<String, dynamic> _$ConfigStoreToJson(ConfigStore instance) =>
|
||||||
'amoledDarkMode': instance.amoledDarkMode,
|
'amoledDarkMode': instance.amoledDarkMode,
|
||||||
'locale': LocaleSerde.toJson(instance.locale),
|
'locale': LocaleSerde.toJson(instance.locale),
|
||||||
'showAvatars': instance.showAvatars,
|
'showAvatars': instance.showAvatars,
|
||||||
'showNsfw': instance.showNsfw,
|
|
||||||
'showScores': instance.showScores,
|
'showScores': instance.showScores,
|
||||||
'defaultSortType': instance.defaultSortType,
|
'defaultSortType': instance.defaultSortType,
|
||||||
'defaultListingType': instance.defaultListingType,
|
'defaultListingType': instance.defaultListingType,
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:collection';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
@ -43,6 +45,10 @@ class InfiniteScroll<T> extends HookWidget {
|
||||||
/// Widget that will be displayed if there are no items
|
/// Widget that will be displayed if there are no items
|
||||||
final Widget noItems;
|
final Widget noItems;
|
||||||
|
|
||||||
|
/// Maps an item to its unique property that will allow to detect possible
|
||||||
|
/// duplicates thus perfoming deduplication
|
||||||
|
final Object Function(T item)? uniqueProp;
|
||||||
|
|
||||||
const InfiniteScroll({
|
const InfiniteScroll({
|
||||||
this.batchSize = 10,
|
this.batchSize = 10,
|
||||||
this.leading = const SizedBox.shrink(),
|
this.leading = const SizedBox.shrink(),
|
||||||
|
@ -53,31 +59,39 @@ class InfiniteScroll<T> extends HookWidget {
|
||||||
required this.fetcher,
|
required this.fetcher,
|
||||||
this.controller,
|
this.controller,
|
||||||
this.noItems = const SizedBox.shrink(),
|
this.noItems = const SizedBox.shrink(),
|
||||||
|
this.uniqueProp,
|
||||||
}) : assert(batchSize > 0);
|
}) : assert(batchSize > 0);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final data = useState<List<T>>([]);
|
final data = useState<List<T>>([]);
|
||||||
|
// holds unique props of the data
|
||||||
|
final dataSet = useRef(HashSet<Object>());
|
||||||
final hasMore = useRef(true);
|
final hasMore = useRef(true);
|
||||||
|
final page = useRef(1);
|
||||||
final isFetching = useRef(false);
|
final isFetching = useRef(false);
|
||||||
|
|
||||||
|
final uniquePropFunc = uniqueProp ?? (e) => e as Object;
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
if (controller != null) {
|
if (controller != null) {
|
||||||
controller?.clear = () {
|
controller?.clear = () {
|
||||||
data.value = [];
|
data.value = [];
|
||||||
hasMore.current = true;
|
hasMore.current = true;
|
||||||
|
page.current = 1;
|
||||||
|
dataSet.current.clear();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
final page = data.value.length ~/ batchSize + 1;
|
|
||||||
|
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
data.value = [];
|
data.value = [];
|
||||||
hasMore.current = true;
|
hasMore.current = true;
|
||||||
|
page.current = 1;
|
||||||
|
dataSet.current.clear();
|
||||||
|
|
||||||
await HapticFeedback.mediumImpact();
|
await HapticFeedback.mediumImpact();
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
|
@ -107,13 +121,20 @@ class InfiniteScroll<T> extends HookWidget {
|
||||||
// if it's already fetching more, skip
|
// if it's already fetching more, skip
|
||||||
if (!isFetching.current) {
|
if (!isFetching.current) {
|
||||||
isFetching.current = true;
|
isFetching.current = true;
|
||||||
fetcher(page, batchSize).then((newData) {
|
fetcher(page.current, batchSize).then((incoming) {
|
||||||
// if got less than the batchSize, mark the list as done
|
// if got less than the batchSize, mark the list as done
|
||||||
if (newData.length < batchSize) {
|
if (incoming.length < batchSize) {
|
||||||
hasMore.current = false;
|
hasMore.current = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final newData = incoming.where(
|
||||||
|
(e) => !dataSet.current.contains(uniquePropFunc(e)),
|
||||||
|
);
|
||||||
|
|
||||||
// append new data
|
// append new data
|
||||||
data.value = [...data.value, ...newData];
|
data.value = [...data.value, ...newData];
|
||||||
|
dataSet.current.addAll(newData.map(uniquePropFunc));
|
||||||
|
page.current += 1;
|
||||||
}).whenComplete(() => isFetching.current = false);
|
}).whenComplete(() => isFetching.current = false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import 'package:lemmy_api_client/v3.dart';
|
||||||
|
|
||||||
import '../comment_tree.dart';
|
import '../comment_tree.dart';
|
||||||
import '../hooks/infinite_scroll.dart';
|
import '../hooks/infinite_scroll.dart';
|
||||||
|
import '../hooks/stores.dart';
|
||||||
import 'comment.dart';
|
import 'comment.dart';
|
||||||
import 'infinite_scroll.dart';
|
import 'infinite_scroll.dart';
|
||||||
import 'post.dart';
|
import 'post.dart';
|
||||||
|
@ -19,7 +20,11 @@ class SortableInfiniteList<T> extends HookWidget {
|
||||||
final InfiniteScrollController? controller;
|
final InfiniteScrollController? controller;
|
||||||
final Function? onStyleChange;
|
final Function? onStyleChange;
|
||||||
final Widget noItems;
|
final Widget noItems;
|
||||||
final SortType defaultSort;
|
|
||||||
|
/// if no defaultSort is provided, the defaultSortType
|
||||||
|
/// from the configStore will be used
|
||||||
|
final SortType? defaultSort;
|
||||||
|
final Object Function(T item)? uniqueProp;
|
||||||
|
|
||||||
const SortableInfiniteList({
|
const SortableInfiniteList({
|
||||||
required this.fetcher,
|
required this.fetcher,
|
||||||
|
@ -27,15 +32,18 @@ class SortableInfiniteList<T> extends HookWidget {
|
||||||
this.controller,
|
this.controller,
|
||||||
this.onStyleChange,
|
this.onStyleChange,
|
||||||
this.noItems = const SizedBox.shrink(),
|
this.noItems = const SizedBox.shrink(),
|
||||||
this.defaultSort = SortType.active,
|
this.defaultSort,
|
||||||
|
this.uniqueProp,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final defaultSortType =
|
||||||
|
useConfigStoreSelect((store) => store.defaultSortType);
|
||||||
final defaultController = useInfiniteScrollController();
|
final defaultController = useInfiniteScrollController();
|
||||||
final isc = controller ?? defaultController;
|
final isc = controller ?? defaultController;
|
||||||
|
|
||||||
final sort = useState(defaultSort);
|
final sort = useState(defaultSort ?? defaultSortType);
|
||||||
|
|
||||||
void changeSorting(SortType newSort) {
|
void changeSorting(SortType newSort) {
|
||||||
sort.value = newSort;
|
sort.value = newSort;
|
||||||
|
@ -54,49 +62,41 @@ class SortableInfiniteList<T> extends HookWidget {
|
||||||
controller: isc,
|
controller: isc,
|
||||||
batchSize: 20,
|
batchSize: 20,
|
||||||
noItems: noItems,
|
noItems: noItems,
|
||||||
|
uniqueProp: uniqueProp,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class InfinitePostList extends StatelessWidget {
|
class InfinitePostList extends SortableInfiniteList<PostView> {
|
||||||
final FetcherWithSorting<PostView> fetcher;
|
InfinitePostList({
|
||||||
final InfiniteScrollController? controller;
|
required FetcherWithSorting<PostView> fetcher,
|
||||||
|
InfiniteScrollController? controller,
|
||||||
const InfinitePostList({
|
}) : super(
|
||||||
required this.fetcher,
|
itemBuilder: (post) => Column(
|
||||||
this.controller,
|
children: [
|
||||||
});
|
PostWidget(post),
|
||||||
|
const SizedBox(height: 20),
|
||||||
Widget build(BuildContext context) => SortableInfiniteList<PostView>(
|
],
|
||||||
onStyleChange: () {},
|
),
|
||||||
itemBuilder: (post) => Column(
|
fetcher: fetcher,
|
||||||
children: [
|
controller: controller,
|
||||||
PostWidget(post),
|
noItems: const Text('there are no posts'),
|
||||||
const SizedBox(height: 20),
|
uniqueProp: (item) => item.post.apId,
|
||||||
],
|
);
|
||||||
),
|
|
||||||
fetcher: fetcher,
|
|
||||||
controller: controller,
|
|
||||||
noItems: const Text('there are no posts'),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class InfiniteCommentList extends StatelessWidget {
|
class InfiniteCommentList extends SortableInfiniteList<CommentView> {
|
||||||
final FetcherWithSorting<CommentView> fetcher;
|
InfiniteCommentList({
|
||||||
final InfiniteScrollController? controller;
|
required FetcherWithSorting<CommentView> fetcher,
|
||||||
|
InfiniteScrollController? controller,
|
||||||
const InfiniteCommentList({
|
}) : super(
|
||||||
required this.fetcher,
|
itemBuilder: (comment) => CommentWidget(
|
||||||
this.controller,
|
CommentTree(comment),
|
||||||
});
|
detached: true,
|
||||||
|
),
|
||||||
Widget build(BuildContext context) => SortableInfiniteList<CommentView>(
|
fetcher: fetcher,
|
||||||
itemBuilder: (comment) => CommentWidget(
|
controller: controller,
|
||||||
CommentTree(comment),
|
noItems: const Text('there are no comments'),
|
||||||
detached: true,
|
uniqueProp: (item) => item.comment.apId,
|
||||||
),
|
);
|
||||||
fetcher: fetcher,
|
|
||||||
controller: controller,
|
|
||||||
noItems: const Text('there are no comments'),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue