From e0cb1a0a83303e81323bf7cab72d9dce8b9084fb Mon Sep 17 00:00:00 2001 From: shilangyu Date: Tue, 6 Apr 2021 17:52:10 +0200 Subject: [PATCH 1/8] Add deduplication --- lib/pages/communities_list.dart | 1 + lib/pages/inbox.dart | 3 ++ lib/pages/search_results.dart | 14 +++++ lib/widgets/infinite_scroll.dart | 19 ++++++- lib/widgets/sortable_infinite_list.dart | 72 ++++++++++++------------- 5 files changed, 68 insertions(+), 41 deletions(-) diff --git a/lib/pages/communities_list.dart b/lib/pages/communities_list.dart index 3dfd7c5..e9207be 100644 --- a/lib/pages/communities_list.dart +++ b/lib/pages/communities_list.dart @@ -38,6 +38,7 @@ class CommunitiesListPage extends StatelessWidget { ) ], ), + uniqueProp: (item) => item.community.id, ), ); } diff --git a/lib/pages/inbox.dart b/lib/pages/inbox.dart index 3da27cd..a2a86e2 100644 --- a/lib/pages/inbox.dart +++ b/lib/pages/inbox.dart @@ -118,6 +118,7 @@ class InboxPage extends HookWidget { canBeMarkedAsRead: true, hideOnRead: unreadOnly.value, ), + uniqueProp: (item) => item.comment.id, ), SortableInfiniteList( noItems: const Text('no mentions'), @@ -135,6 +136,7 @@ class InboxPage extends HookWidget { umv, hideOnRead: unreadOnly.value, ), + uniqueProp: (item) => item.personMention.id, ), InfiniteScroll( noItems: const Padding( @@ -154,6 +156,7 @@ class InboxPage extends HookWidget { privateMessageView: mv, hideOnRead: unreadOnly.value, ), + uniqueProp: (item) => item.privateMessage.id, ), ], ), diff --git a/lib/pages/search_results.dart b/lib/pages/search_results.dart index b7aa4ce..da9da31 100644 --- a/lib/pages/search_results.dart +++ b/lib/pages/search_results.dart @@ -120,6 +120,20 @@ class _SearchResultsList extends HookWidget { throw UnimplementedError(); } }, + uniqueProp: (item) { + switch (type) { + case SearchType.comments: + return (item as CommentView).comment.id; + case SearchType.communities: + return (item as CommunityView).community.id; + case SearchType.posts: + return (item as PostView).post.id; + case SearchType.users: + return (item as PersonViewSafe).person.id; + default: + return item; + } + }, ); } } diff --git a/lib/widgets/infinite_scroll.dart b/lib/widgets/infinite_scroll.dart index 1ce4b12..0b8ca75 100644 --- a/lib/widgets/infinite_scroll.dart +++ b/lib/widgets/infinite_scroll.dart @@ -1,3 +1,5 @@ +import 'dart:collection'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -47,6 +49,10 @@ class InfiniteScroll extends HookWidget { /// Widget that will be displayed if there are no items 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({ this.batchSize = 10, this.leading = const SizedBox.shrink(), @@ -57,6 +63,7 @@ class InfiniteScroll extends HookWidget { @required this.fetcher, this.controller, this.noItems = const SizedBox.shrink(), + this.uniqueProp, }) : assert(itemBuilder != null), assert(fetcher != null), assert(batchSize > 0); @@ -64,6 +71,8 @@ class InfiniteScroll extends HookWidget { @override Widget build(BuildContext context) { final data = useState>([]); + // holds unique props of the data + final dataSet = useRef(HashSet()); final hasMore = useRef(true); final isFetching = useRef(false); @@ -111,13 +120,19 @@ class InfiniteScroll extends HookWidget { // if it's already fetching more, skip if (!isFetching.current) { isFetching.current = true; - fetcher(page, batchSize).then((newData) { + fetcher(page, batchSize).then((incoming) { // if got less than the batchSize, mark the list as done - if (newData.length < batchSize) { + if (incoming.length < batchSize) { hasMore.current = false; } + + final newData = incoming.where( + (e) => !dataSet.current.contains(uniqueProp?.call(e) ?? e), + ); + // append new data data.value = [...data.value, ...newData]; + dataSet.current.addAll(newData.map(uniqueProp ?? (e) => e)); }).whenComplete(() => isFetching.current = false); } diff --git a/lib/widgets/sortable_infinite_list.dart b/lib/widgets/sortable_infinite_list.dart index 875e050..5a2d9f2 100644 --- a/lib/widgets/sortable_infinite_list.dart +++ b/lib/widgets/sortable_infinite_list.dart @@ -20,6 +20,7 @@ class SortableInfiniteList extends HookWidget { final Function onStyleChange; final Widget noItems; final SortType defaultSort; + final Object Function(T item) uniqueProp; const SortableInfiniteList({ @required this.fetcher, @@ -28,6 +29,7 @@ class SortableInfiniteList extends HookWidget { this.onStyleChange, this.noItems, this.defaultSort = SortType.active, + this.uniqueProp, }) : assert(fetcher != null), assert(itemBuilder != null), assert(defaultSort != null); @@ -56,49 +58,41 @@ class SortableInfiniteList extends HookWidget { controller: isc, batchSize: 20, noItems: noItems, + uniqueProp: uniqueProp, ); } } -class InfinitePostList extends StatelessWidget { - final FetcherWithSorting fetcher; - final InfiniteScrollController controller; - - const InfinitePostList({ - @required this.fetcher, - this.controller, - }) : assert(fetcher != null); - - Widget build(BuildContext context) => SortableInfiniteList( - onStyleChange: () {}, - itemBuilder: (post) => Column( - children: [ - PostWidget(post), - const SizedBox(height: 20), - ], - ), - fetcher: fetcher, - controller: controller, - noItems: const Text('there are no posts'), - ); +class InfinitePostList extends SortableInfiniteList { + InfinitePostList({ + @required FetcherWithSorting fetcher, + InfiniteScrollController controller, + }) : super( + itemBuilder: (post) => Column( + children: [ + PostWidget(post), + const SizedBox(height: 20), + ], + ), + fetcher: fetcher, + controller: controller, + noItems: const Text('there are no posts'), + uniqueProp: (item) => item.post.id, + ); } -class InfiniteCommentList extends StatelessWidget { - final FetcherWithSorting fetcher; - final InfiniteScrollController controller; - - const InfiniteCommentList({ - @required this.fetcher, - this.controller, - }) : assert(fetcher != null); - - Widget build(BuildContext context) => SortableInfiniteList( - itemBuilder: (comment) => CommentWidget( - CommentTree(comment), - detached: true, - ), - fetcher: fetcher, - controller: controller, - noItems: const Text('there are no comments'), - ); +class InfiniteCommentList extends SortableInfiniteList { + InfiniteCommentList({ + @required FetcherWithSorting fetcher, + InfiniteScrollController controller, + }) : super( + itemBuilder: (comment) => CommentWidget( + CommentTree(comment), + detached: true, + ), + fetcher: fetcher, + controller: controller, + noItems: const Text('there are no comments'), + uniqueProp: (item) => item.comment.id, + ); } From 4519077ae20a4382c964f9d4e366708c7f89a740 Mon Sep 17 00:00:00 2001 From: shilangyu Date: Thu, 22 Apr 2021 19:34:35 +0200 Subject: [PATCH 2/8] Bring back show nsfw to manage_account --- lib/pages/manage_account.dart | 12 +++++++++++- lib/pages/settings.dart | 7 ------- lib/stores/config_store.dart | 10 ---------- lib/stores/config_store.g.dart | 2 -- 4 files changed, 11 insertions(+), 20 deletions(-) diff --git a/lib/pages/manage_account.dart b/lib/pages/manage_account.dart index 8f65681..31924f7 100644 --- a/lib/pages/manage_account.dart +++ b/lib/pages/manage_account.dart @@ -108,6 +108,7 @@ class _ManageAccount extends HookWidget { useTextEditingController(text: user.person.matrixUserId); final avatar = useRef(user.person.avatar); final banner = useRef(user.person.banner); + final showNsfw = useState(user.localUser.showNsfw); final sendNotificationsToEmail = useState(user.localUser.sendNotificationsToEmail); final newPasswordController = useTextEditingController(); @@ -134,7 +135,7 @@ class _ManageAccount extends HookWidget { try { await LemmyApiV3(user.instanceHost).run(SaveUserSettings( - showNsfw: user.localUser.showNsfw, + showNsfw: showNsfw.value, theme: user.localUser.theme, defaultSortType: user.localUser.defaultSortType, defaultListingType: user.localUser.defaultListingType, @@ -318,6 +319,15 @@ class _ManageAccount extends HookWidget { obscureText: true, ), 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( value: sendNotificationsToEmail.value, onChanged: (checked) { diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart index c8eb2a5..5ac85fd 100644 --- a/lib/pages/settings.dart +++ b/lib/pages/settings.dart @@ -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; - }, - ), ], ), ); diff --git a/lib/stores/config_store.dart b/lib/stores/config_store.dart index b47e5eb..7bc65d7 100644 --- a/lib/stores/config_store.dart +++ b/lib/stores/config_store.dart @@ -54,15 +54,6 @@ class ConfigStore extends ChangeNotifier { save(); } - late bool _showNsfw; - @JsonKey(defaultValue: false) - bool get showNsfw => _showNsfw; - set showNsfw(bool showNsfw) { - _showNsfw = showNsfw; - notifyListeners(); - save(); - } - late bool _showScores; @JsonKey(defaultValue: true) bool get showScores => _showScores; @@ -105,7 +96,6 @@ class ConfigStore extends ChangeNotifier { // }; _showAvatars = localUserSettings.showAvatars; - _showNsfw = localUserSettings.showNsfw; // TODO: should these also be imported? If so, how? // _theme = darkModeLemmyUiThemes.contains(localUserSettings.theme) // ? ThemeMode.dark diff --git a/lib/stores/config_store.g.dart b/lib/stores/config_store.g.dart index 206d90a..215b72d 100644 --- a/lib/stores/config_store.g.dart +++ b/lib/stores/config_store.g.dart @@ -13,7 +13,6 @@ ConfigStore _$ConfigStoreFromJson(Map json) { ..amoledDarkMode = json['amoledDarkMode'] as bool? ?? false ..locale = LocaleSerde.fromJson(json['locale'] as String?) ..showAvatars = json['showAvatars'] as bool? ?? true - ..showNsfw = json['showNsfw'] as bool? ?? false ..showScores = json['showScores'] as bool? ?? true ..defaultSortType = _sortTypeFromJson(json['defaultSortType'] as String?) ..defaultListingType = @@ -26,7 +25,6 @@ Map _$ConfigStoreToJson(ConfigStore instance) => 'amoledDarkMode': instance.amoledDarkMode, 'locale': LocaleSerde.toJson(instance.locale), 'showAvatars': instance.showAvatars, - 'showNsfw': instance.showNsfw, 'showScores': instance.showScores, 'defaultSortType': instance.defaultSortType, 'defaultListingType': instance.defaultListingType, From 948fff79f3e71cf87d3abad8dc6aa53df48fb222 Mon Sep 17 00:00:00 2001 From: shilangyu Date: Thu, 22 Apr 2021 21:08:30 +0200 Subject: [PATCH 3/8] Add deduplication --- lib/pages/communities_list.dart | 2 +- lib/pages/inbox.dart | 4 ++-- lib/pages/search_results.dart | 8 ++++---- lib/widgets/infinite_scroll.dart | 17 +++++++++++------ lib/widgets/sortable_infinite_list.dart | 4 ++-- 5 files changed, 20 insertions(+), 15 deletions(-) diff --git a/lib/pages/communities_list.dart b/lib/pages/communities_list.dart index 5cfea6a..1d82c25 100644 --- a/lib/pages/communities_list.dart +++ b/lib/pages/communities_list.dart @@ -36,7 +36,7 @@ class CommunitiesListPage extends StatelessWidget { ) ], ), - uniqueProp: (item) => item.community.id, + uniqueProp: (item) => item.community.actorId, ), ); } diff --git a/lib/pages/inbox.dart b/lib/pages/inbox.dart index 45fac14..27bf006 100644 --- a/lib/pages/inbox.dart +++ b/lib/pages/inbox.dart @@ -120,7 +120,7 @@ class InboxPage extends HookWidget { canBeMarkedAsRead: true, hideOnRead: unreadOnly.value, ), - uniqueProp: (item) => item.comment.id, + uniqueProp: (item) => item.comment.apId, ), SortableInfiniteList( noItems: const Text('no mentions'), @@ -158,7 +158,7 @@ class InboxPage extends HookWidget { privateMessageView: mv, hideOnRead: unreadOnly.value, ), - uniqueProp: (item) => item.privateMessage.id, + uniqueProp: (item) => item.privateMessage.apId, ), ], ), diff --git a/lib/pages/search_results.dart b/lib/pages/search_results.dart index 4d1e71c..2e871b9 100644 --- a/lib/pages/search_results.dart +++ b/lib/pages/search_results.dart @@ -119,13 +119,13 @@ class _SearchResultsList extends HookWidget { uniqueProp: (item) { switch (type) { case SearchType.comments: - return (item as CommentView).comment.id; + return (item as CommentView).comment.apId; case SearchType.communities: - return (item as CommunityView).community.id; + return (item as CommunityView).community.actorId; case SearchType.posts: - return (item as PostView).post.id; + return (item as PostView).post.apId; case SearchType.users: - return (item as PersonViewSafe).person.id; + return (item as PersonViewSafe).person.actorId; default: return item; } diff --git a/lib/widgets/infinite_scroll.dart b/lib/widgets/infinite_scroll.dart index f613b05..090339d 100644 --- a/lib/widgets/infinite_scroll.dart +++ b/lib/widgets/infinite_scroll.dart @@ -68,25 +68,30 @@ class InfiniteScroll extends HookWidget { // holds unique props of the data final dataSet = useRef(HashSet()); final hasMore = useRef(true); + final page = useRef(1); final isFetching = useRef(false); + final uniquePropFunc = uniqueProp ?? (e) => e as Object; + useEffect(() { if (controller != null) { controller?.clear = () { data.value = []; hasMore.current = true; + page.current = 1; + dataSet.current.clear(); }; } return null; }, []); - final page = data.value.length ~/ batchSize + 1; - return RefreshIndicator( onRefresh: () async { data.value = []; hasMore.current = true; + page.current = 1; + dataSet.current.clear(); await HapticFeedback.mediumImpact(); await Future.delayed(const Duration(seconds: 1)); @@ -116,20 +121,20 @@ class InfiniteScroll extends HookWidget { // if it's already fetching more, skip if (!isFetching.current) { isFetching.current = true; - fetcher(page, batchSize).then((incoming) { + fetcher(page.current, batchSize).then((incoming) { // if got less than the batchSize, mark the list as done if (incoming.length < batchSize) { hasMore.current = false; } final newData = incoming.where( - (e) => !dataSet.current.contains(uniqueProp?.call(e) ?? e), + (e) => !dataSet.current.contains(uniquePropFunc(e)), ); // append new data data.value = [...data.value, ...newData]; - dataSet.current - .addAll(newData.map(uniqueProp ?? (e) => e as Object)); + dataSet.current.addAll(newData.map(uniquePropFunc)); + page.current += 1; }).whenComplete(() => isFetching.current = false); } diff --git a/lib/widgets/sortable_infinite_list.dart b/lib/widgets/sortable_infinite_list.dart index 010701c..f65356f 100644 --- a/lib/widgets/sortable_infinite_list.dart +++ b/lib/widgets/sortable_infinite_list.dart @@ -75,7 +75,7 @@ class InfinitePostList extends SortableInfiniteList { fetcher: fetcher, controller: controller, noItems: const Text('there are no posts'), - uniqueProp: (item) => item.post.id, + uniqueProp: (item) => item.post.apId, ); } @@ -91,6 +91,6 @@ class InfiniteCommentList extends SortableInfiniteList { fetcher: fetcher, controller: controller, noItems: const Text('there are no comments'), - uniqueProp: (item) => item.comment.id, + uniqueProp: (item) => item.comment.apId, ); } From 847eb3414f5449c836f39010fe828728b293d8a1 Mon Sep 17 00:00:00 2001 From: shilangyu Date: Fri, 23 Apr 2021 19:20:32 +0200 Subject: [PATCH 4/8] Implement default sort type --- lib/widgets/sortable_infinite_list.dart | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/widgets/sortable_infinite_list.dart b/lib/widgets/sortable_infinite_list.dart index f65356f..11767dc 100644 --- a/lib/widgets/sortable_infinite_list.dart +++ b/lib/widgets/sortable_infinite_list.dart @@ -4,6 +4,7 @@ import 'package:lemmy_api_client/v3.dart'; import '../comment_tree.dart'; import '../hooks/infinite_scroll.dart'; +import '../hooks/stores.dart'; import 'comment.dart'; import 'infinite_scroll.dart'; import 'post.dart'; @@ -19,7 +20,10 @@ class SortableInfiniteList extends HookWidget { final InfiniteScrollController? controller; final Function? onStyleChange; 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({ @@ -28,16 +32,18 @@ class SortableInfiniteList extends HookWidget { this.controller, this.onStyleChange, this.noItems = const SizedBox.shrink(), - this.defaultSort = SortType.active, + this.defaultSort, this.uniqueProp, }); @override Widget build(BuildContext context) { + final defaultSortType = + useConfigStoreSelect((store) => store.defaultSortType); final defaultController = useInfiniteScrollController(); final isc = controller ?? defaultController; - final sort = useState(defaultSort); + final sort = useState(defaultSort ?? defaultSortType); void changeSorting(SortType newSort) { sort.value = newSort; From f524dc3884bae3c2d2deab1aa01fa365a2e0b004 Mon Sep 17 00:00:00 2001 From: shilangyu Date: Fri, 23 Apr 2021 19:26:30 +0200 Subject: [PATCH 5/8] Import lang/theme --- lib/stores/config_store.dart | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/lib/stores/config_store.dart b/lib/stores/config_store.dart index 7bc65d7..194357a 100644 --- a/lib/stores/config_store.dart +++ b/lib/stores/config_store.dart @@ -86,23 +86,21 @@ class ConfigStore extends ChangeNotifier { /// Copies over settings from lemmy to [ConfigStore] void copyLemmyUserSettings(LocalUserSettings localUserSettings) { // themes from lemmy-ui that are dark mode - // const darkModeLemmyUiThemes = { - // 'solar', - // 'cyborg', - // 'darkly', - // 'vaporwave-dark', - // // TODO: is it dark theme? - // 'i386', - // }; + const darkModeLemmyUiThemes = { + 'solar', + 'cyborg', + 'darkly', + 'vaporwave-dark', + 'i386', + }; _showAvatars = localUserSettings.showAvatars; - // TODO: should these also be imported? If so, how? - // _theme = darkModeLemmyUiThemes.contains(localUserSettings.theme) - // ? ThemeMode.dark - // : ThemeMode.light; - // _locale = L10n.supportedLocales.contains(Locale(localUserSettings.lang)) - // ? Locale(localUserSettings.lang) - // : _locale; + _theme = darkModeLemmyUiThemes.contains(localUserSettings.theme) + ? ThemeMode.dark + : ThemeMode.light; + _locale = L10n.supportedLocales.contains(Locale(localUserSettings.lang)) + ? Locale(localUserSettings.lang) + : _locale; // TODO: add when it is released // _showScores = localUserSettings.showScores; _defaultSortType = localUserSettings.defaultSortType; From 9b5f13466b6e8e62496458d36220a410ece8bcab Mon Sep 17 00:00:00 2001 From: shilangyu Date: Fri, 23 Apr 2021 19:32:10 +0200 Subject: [PATCH 6/8] Import settings if first account --- lib/pages/add_account.dart | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lib/pages/add_account.dart b/lib/pages/add_account.dart index e0af64c..21de204 100644 --- a/lib/pages/add_account.dart +++ b/lib/pages/add_account.dart @@ -3,11 +3,13 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:lemmy_api_client/v3.dart'; +import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart' as ul; import '../hooks/delayed_loading.dart'; import '../hooks/stores.dart'; import '../l10n/l10n.dart'; +import '../stores/config_store.dart'; import '../widgets/fullscreenable_image.dart'; import '../widgets/radio_picker.dart'; import 'add_instance.dart'; @@ -40,12 +42,27 @@ class AddAccountPage extends HookWidget { handleOnAdd() async { try { + final isFirstAccount = accountsStore.hasNoAccount; + loading.start(); await accountsStore.addAccount( selectedInstance.value, usernameController.text, passwordController.text, ); + + // if first account try to import settings + if (isFirstAccount) { + try { + await context.read().importLemmyUserSettings( + accountsStore + .userDataFor( + selectedInstance.value, usernameController.text)! + .jwt); + // ignore: avoid_catches_without_on_clauses, empty_catches + } catch (e) {} + } + Navigator.of(context).pop(); } on Exception catch (err) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( From efb773676c9a2231c534d60e738d3f4c4c96c63e Mon Sep 17 00:00:00 2001 From: shilangyu Date: Fri, 23 Apr 2021 19:32:20 +0200 Subject: [PATCH 7/8] Add changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f7b9e5..9b65ea4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Show avatars setting toggle - Show scores setting toggle +- Default sort type setting - Default listing type for the home tab setting - Import Lemmy settings: long press an account in account settings then choose the import option - Editing posts @@ -12,6 +13,7 @@ ### Fixed - Fixed bug where creating post would crash after uploading a picture +- Added deduplication in infinite scrolls ## v0.4.2 - 2021-04-12 From 79d77f60df0d1fbddf7f9f8d6aaa334b920958f7 Mon Sep 17 00:00:00 2001 From: shilangyu Date: Wed, 28 Apr 2021 14:39:31 +0200 Subject: [PATCH 8/8] Add browser theme detection --- lib/stores/config_store.dart | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/stores/config_store.dart b/lib/stores/config_store.dart index 194357a..9378fb4 100644 --- a/lib/stores/config_store.dart +++ b/lib/stores/config_store.dart @@ -95,9 +95,15 @@ class ConfigStore extends ChangeNotifier { }; _showAvatars = localUserSettings.showAvatars; - _theme = darkModeLemmyUiThemes.contains(localUserSettings.theme) - ? ThemeMode.dark - : ThemeMode.light; + _theme = () { + if (localUserSettings.theme == 'browser') return ThemeMode.system; + + if (darkModeLemmyUiThemes.contains(localUserSettings.theme)) { + return ThemeMode.dark; + } + + return ThemeMode.light; + }(); _locale = L10n.supportedLocales.contains(Locale(localUserSettings.lang)) ? Locale(localUserSettings.lang) : _locale;