diff --git a/lib/account/models/custom_sort_type.dart b/lib/account/models/custom_sort_type.dart new file mode 100644 index 000000000..0ac7f9641 --- /dev/null +++ b/lib/account/models/custom_sort_type.dart @@ -0,0 +1,115 @@ +import 'package:flutter/foundation.dart'; + +import 'package:drift/drift.dart'; +import 'package:lemmy_api_client/v3.dart'; + +import 'package:thunder/core/database/database.dart'; +import 'package:thunder/core/database/type_converters.dart'; +import 'package:thunder/main.dart'; + +class CustomSortType { + /// The type of sort (community/feed) + final SortType sortType; + + /// The account id + final int accountId; + + /// The community id + final int? communityId; + + /// The feed type + final ListingType? feedType; + + const CustomSortType({ + required this.sortType, + required this.accountId, + this.communityId, + this.feedType, + }); + + CustomSortType copyWith({ + SortType? sortType, + int? accountId, + int? communityId, + ListingType? feedType, + }) => + CustomSortType( + sortType: sortType ?? this.sortType, + accountId: accountId ?? this.accountId, + communityId: communityId ?? this.communityId, + feedType: feedType ?? this.feedType, + ); + + /// Create or update a custom sort type in the db + static Future upsertCustomSortType(CustomSortType customSortType) async { + try { + final existingCustomSortType = await (database.select(database.customSortType) + ..where((t) => t.accountId.equals(customSortType.accountId)) + ..where((t) => customSortType.communityId == null ? t.communityId.isNull() : t.communityId.equals(customSortType.communityId!)) + ..where((t) => customSortType.feedType == null ? t.feedType.isNull() : t.feedType.equals(const ListingTypeConverter().toSql(customSortType.feedType!)))) + .getSingleOrNull(); + + if (existingCustomSortType == null) { + final id = await database.into(database.customSortType).insert( + CustomSortTypeCompanion.insert( + sortType: customSortType.sortType, + accountId: customSortType.accountId, + communityId: Value(customSortType.communityId), + feedType: Value(customSortType.feedType), + ), + ); + return customSortType; + } + + await database.update(database.customSortType).replace( + CustomSortTypeCompanion( + id: Value(existingCustomSortType.id), + sortType: Value(customSortType.sortType), + accountId: Value(customSortType.accountId), + communityId: Value(customSortType.communityId), + feedType: Value(customSortType.feedType), + ), + ); + return customSortType; + } catch (e) { + debugPrint(e.toString()); + return null; + } + } + + /// Retrieve a custom sort type from the db + static Future fetchCustomSortType(int accountId, int? communityId, ListingType? feedType) async { + try { + final customSortType = await (database.select(database.customSortType) + ..where((t) => t.accountId.equals(accountId)) + ..where((t) => communityId == null ? t.communityId.isNull() : t.communityId.equals(communityId)) + ..where((t) => feedType == null ? t.feedType.isNull() : t.feedType.equals(const ListingTypeConverter().toSql(feedType)))) + .getSingleOrNull(); + + if (customSortType == null) return null; + + return CustomSortType( + sortType: customSortType.sortType, + accountId: customSortType.accountId, + communityId: customSortType.communityId, + feedType: customSortType.feedType, + ); + } catch (e) { + debugPrint(e.toString()); + return null; + } + } + + /// Delete a custom sort type from the db + static Future deleteCustomSortType(int accountId, int? communityId, ListingType? feedType) async { + try { + await (database.delete(database.customSortType) + ..where((t) => t.accountId.equals(accountId)) + ..where((t) => communityId == null ? t.communityId.isNull() : t.communityId.equals(communityId)) + ..where((t) => feedType == null ? t.feedType.isNull() : t.feedType.equals(const ListingTypeConverter().toSql(feedType)))) + .go(); + } catch (e) { + debugPrint(e.toString()); + } + } +} diff --git a/lib/community/widgets/community_drawer.dart b/lib/community/widgets/community_drawer.dart index 8461f4325..07c36909b 100644 --- a/lib/community/widgets/community_drawer.dart +++ b/lib/community/widgets/community_drawer.dart @@ -107,7 +107,6 @@ class _CommunityDrawerState extends State { context.read().add( FeedFetchedEvent( feedType: FeedType.community, - sortType: authState.getSiteResponse?.myUser?.localUserView.localUser.defaultSortType ?? thunderState.sortTypeForInstance, communityId: community.id, reset: true, ), @@ -336,7 +335,6 @@ class FavoriteCommunities extends StatelessWidget { context.read().add( FeedFetchedEvent( feedType: FeedType.community, - sortType: authState.getSiteResponse?.myUser?.localUserView.localUser.defaultSortType ?? thunderState.sortTypeForInstance, communityId: community.id, reset: true, ), @@ -397,7 +395,6 @@ class ModeratedCommunities extends StatelessWidget { context.read().add( FeedFetchedEvent( feedType: FeedType.community, - sortType: authState.getSiteResponse?.myUser?.localUserView.localUser.defaultSortType ?? thunderState.sortTypeForInstance, communityId: community.id, reset: true, ), diff --git a/lib/core/auth/bloc/auth_bloc.dart b/lib/core/auth/bloc/auth_bloc.dart index c5318a090..350ddc8f9 100644 --- a/lib/core/auth/bloc/auth_bloc.dart +++ b/lib/core/auth/bloc/auth_bloc.dart @@ -37,7 +37,7 @@ class AuthBloc extends Bloc { /// This event occurs whenever you switch to a different authenticated account on((event, emit) async { - emit(state.copyWith(status: AuthStatus.loading, isLoggedIn: false, reload: event.reload)); + emit(state.copyWith(status: AuthStatus.loading, isLoggedIn: false, account: null, reload: event.reload)); Account? account = await Account.fetchAccount(event.accountId); if (account == null) return emit(state.copyWith(status: AuthStatus.success, account: null, isLoggedIn: false)); diff --git a/lib/core/database/database.dart b/lib/core/database/database.dart index fbf075aeb..181b65035 100644 --- a/lib/core/database/database.dart +++ b/lib/core/database/database.dart @@ -4,6 +4,7 @@ import 'package:drift/drift.dart'; import 'package:drift/native.dart'; import 'package:flutter/material.dart' hide Table; import 'package:flutter_file_dialog/flutter_file_dialog.dart'; +import 'package:lemmy_api_client/v3.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:sqlite3/sqlite3.dart'; @@ -15,12 +16,12 @@ import 'package:thunder/drafts/draft_type.dart'; part 'database.g.dart'; -@DriftDatabase(tables: [Accounts, Favorites, LocalSubscriptions, UserLabels, Drafts]) +@DriftDatabase(tables: [Accounts, Favorites, LocalSubscriptions, UserLabels, Drafts, CustomSortType]) class AppDatabase extends _$AppDatabase { AppDatabase() : super(_openConnection()); @override - int get schemaVersion => 3; + int get schemaVersion => 4; @override MigrationStrategy get migration => MigrationStrategy( @@ -39,6 +40,12 @@ class AppDatabase extends _$AppDatabase { await migrator.createTable(drafts); } + // If we are migrating from 3 or lower to anything higher + if (from <= 3 && to > 3) { + // Create the CustomSortType table + await migrator.createTable(customSortType); + } + // --- DOWNGRADES --- // If we are downgrading from 2 or higher to 1 @@ -52,6 +59,12 @@ class AppDatabase extends _$AppDatabase { // Delete the Drafts table await migrator.deleteTable('drafts'); } + + // If we are downgrading from 4 or higher to 3 or lower + if (from >= 4 && to <= 3) { + // Delete the CustomSortType table + await migrator.deleteTable('custom_sort_type'); + } }, ); } diff --git a/lib/core/database/database.g.dart b/lib/core/database/database.g.dart index 01c585134..0c205e415 100644 --- a/lib/core/database/database.g.dart +++ b/lib/core/database/database.g.dart @@ -1179,6 +1179,249 @@ class DraftsCompanion extends UpdateCompanion { } } +class $CustomSortTypeTable extends CustomSortType with TableInfo<$CustomSortTypeTable, CustomSortTypeData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $CustomSortTypeTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn('id', aliasedName, false, + hasAutoIncrement: true, type: DriftSqlType.int, requiredDuringInsert: false, defaultConstraints: GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + static const VerificationMeta _sortTypeMeta = const VerificationMeta('sortType'); + @override + late final GeneratedColumnWithTypeConverter sortType = + GeneratedColumn('sort_type', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true).withConverter($CustomSortTypeTable.$convertersortType); + static const VerificationMeta _accountIdMeta = const VerificationMeta('accountId'); + @override + late final GeneratedColumn accountId = GeneratedColumn('account_id', aliasedName, false, type: DriftSqlType.int, requiredDuringInsert: true); + static const VerificationMeta _communityIdMeta = const VerificationMeta('communityId'); + @override + late final GeneratedColumn communityId = GeneratedColumn('community_id', aliasedName, true, type: DriftSqlType.int, requiredDuringInsert: false); + static const VerificationMeta _feedTypeMeta = const VerificationMeta('feedType'); + @override + late final GeneratedColumnWithTypeConverter feedType = + GeneratedColumn('feed_type', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false).withConverter($CustomSortTypeTable.$converterfeedTypen); + @override + List get $columns => [id, sortType, accountId, communityId, feedType]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'custom_sort_type'; + @override + VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } + context.handle(_sortTypeMeta, const VerificationResult.success()); + if (data.containsKey('account_id')) { + context.handle(_accountIdMeta, accountId.isAcceptableOrUnknown(data['account_id']!, _accountIdMeta)); + } else if (isInserting) { + context.missing(_accountIdMeta); + } + if (data.containsKey('community_id')) { + context.handle(_communityIdMeta, communityId.isAcceptableOrUnknown(data['community_id']!, _communityIdMeta)); + } + context.handle(_feedTypeMeta, const VerificationResult.success()); + return context; + } + + @override + Set get $primaryKey => {id}; + @override + CustomSortTypeData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return CustomSortTypeData( + id: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}id'])!, + sortType: $CustomSortTypeTable.$convertersortType.fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}sort_type'])!), + accountId: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}account_id'])!, + communityId: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}community_id']), + feedType: $CustomSortTypeTable.$converterfeedTypen.fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}feed_type'])), + ); + } + + @override + $CustomSortTypeTable createAlias(String alias) { + return $CustomSortTypeTable(attachedDatabase, alias); + } + + static TypeConverter $convertersortType = const SortTypeConverter(); + static TypeConverter $converterfeedType = const ListingTypeConverter(); + static TypeConverter $converterfeedTypen = NullAwareTypeConverter.wrap($converterfeedType); +} + +class CustomSortTypeData extends DataClass implements Insertable { + final int id; + final SortType sortType; + final int accountId; + final int? communityId; + final ListingType? feedType; + const CustomSortTypeData({required this.id, required this.sortType, required this.accountId, this.communityId, this.feedType}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + { + map['sort_type'] = Variable($CustomSortTypeTable.$convertersortType.toSql(sortType)); + } + map['account_id'] = Variable(accountId); + if (!nullToAbsent || communityId != null) { + map['community_id'] = Variable(communityId); + } + if (!nullToAbsent || feedType != null) { + map['feed_type'] = Variable($CustomSortTypeTable.$converterfeedTypen.toSql(feedType)); + } + return map; + } + + CustomSortTypeCompanion toCompanion(bool nullToAbsent) { + return CustomSortTypeCompanion( + id: Value(id), + sortType: Value(sortType), + accountId: Value(accountId), + communityId: communityId == null && nullToAbsent ? const Value.absent() : Value(communityId), + feedType: feedType == null && nullToAbsent ? const Value.absent() : Value(feedType), + ); + } + + factory CustomSortTypeData.fromJson(Map json, {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return CustomSortTypeData( + id: serializer.fromJson(json['id']), + sortType: serializer.fromJson(json['sortType']), + accountId: serializer.fromJson(json['accountId']), + communityId: serializer.fromJson(json['communityId']), + feedType: serializer.fromJson(json['feedType']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'sortType': serializer.toJson(sortType), + 'accountId': serializer.toJson(accountId), + 'communityId': serializer.toJson(communityId), + 'feedType': serializer.toJson(feedType), + }; + } + + CustomSortTypeData copyWith({int? id, SortType? sortType, int? accountId, Value communityId = const Value.absent(), Value feedType = const Value.absent()}) => CustomSortTypeData( + id: id ?? this.id, + sortType: sortType ?? this.sortType, + accountId: accountId ?? this.accountId, + communityId: communityId.present ? communityId.value : this.communityId, + feedType: feedType.present ? feedType.value : this.feedType, + ); + @override + String toString() { + return (StringBuffer('CustomSortTypeData(') + ..write('id: $id, ') + ..write('sortType: $sortType, ') + ..write('accountId: $accountId, ') + ..write('communityId: $communityId, ') + ..write('feedType: $feedType') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, sortType, accountId, communityId, feedType); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is CustomSortTypeData && + other.id == this.id && + other.sortType == this.sortType && + other.accountId == this.accountId && + other.communityId == this.communityId && + other.feedType == this.feedType); +} + +class CustomSortTypeCompanion extends UpdateCompanion { + final Value id; + final Value sortType; + final Value accountId; + final Value communityId; + final Value feedType; + const CustomSortTypeCompanion({ + this.id = const Value.absent(), + this.sortType = const Value.absent(), + this.accountId = const Value.absent(), + this.communityId = const Value.absent(), + this.feedType = const Value.absent(), + }); + CustomSortTypeCompanion.insert({ + this.id = const Value.absent(), + required SortType sortType, + required int accountId, + this.communityId = const Value.absent(), + this.feedType = const Value.absent(), + }) : sortType = Value(sortType), + accountId = Value(accountId); + static Insertable custom({ + Expression? id, + Expression? sortType, + Expression? accountId, + Expression? communityId, + Expression? feedType, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (sortType != null) 'sort_type': sortType, + if (accountId != null) 'account_id': accountId, + if (communityId != null) 'community_id': communityId, + if (feedType != null) 'feed_type': feedType, + }); + } + + CustomSortTypeCompanion copyWith({Value? id, Value? sortType, Value? accountId, Value? communityId, Value? feedType}) { + return CustomSortTypeCompanion( + id: id ?? this.id, + sortType: sortType ?? this.sortType, + accountId: accountId ?? this.accountId, + communityId: communityId ?? this.communityId, + feedType: feedType ?? this.feedType, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (sortType.present) { + map['sort_type'] = Variable($CustomSortTypeTable.$convertersortType.toSql(sortType.value)); + } + if (accountId.present) { + map['account_id'] = Variable(accountId.value); + } + if (communityId.present) { + map['community_id'] = Variable(communityId.value); + } + if (feedType.present) { + map['feed_type'] = Variable($CustomSortTypeTable.$converterfeedTypen.toSql(feedType.value)); + } + return map; + } + + @override + String toString() { + return (StringBuffer('CustomSortTypeCompanion(') + ..write('id: $id, ') + ..write('sortType: $sortType, ') + ..write('accountId: $accountId, ') + ..write('communityId: $communityId, ') + ..write('feedType: $feedType') + ..write(')')) + .toString(); + } +} + abstract class _$AppDatabase extends GeneratedDatabase { _$AppDatabase(QueryExecutor e) : super(e); late final $AccountsTable accounts = $AccountsTable(this); @@ -1186,8 +1429,9 @@ abstract class _$AppDatabase extends GeneratedDatabase { late final $LocalSubscriptionsTable localSubscriptions = $LocalSubscriptionsTable(this); late final $UserLabelsTable userLabels = $UserLabelsTable(this); late final $DraftsTable drafts = $DraftsTable(this); + late final $CustomSortTypeTable customSortType = $CustomSortTypeTable(this); @override Iterable> get allTables => allSchemaEntities.whereType>(); @override - List get allSchemaEntities => [accounts, favorites, localSubscriptions, userLabels, drafts]; + List get allSchemaEntities => [accounts, favorites, localSubscriptions, userLabels, drafts, customSortType]; } diff --git a/lib/core/database/tables.dart b/lib/core/database/tables.dart index a9251b703..fd375f943 100644 --- a/lib/core/database/tables.dart +++ b/lib/core/database/tables.dart @@ -39,3 +39,11 @@ class Drafts extends Table { TextColumn get url => text().nullable()(); TextColumn get body => text().nullable()(); } + +class CustomSortType extends Table { + IntColumn get id => integer().autoIncrement()(); + TextColumn get sortType => text().map(const SortTypeConverter())(); + IntColumn get accountId => integer()(); + IntColumn get communityId => integer().nullable()(); + TextColumn get feedType => text().map(const ListingTypeConverter()).nullable()(); +} diff --git a/lib/core/database/type_converters.dart b/lib/core/database/type_converters.dart index b798c37b6..aa25a4a15 100644 --- a/lib/core/database/type_converters.dart +++ b/lib/core/database/type_converters.dart @@ -1,4 +1,7 @@ +import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; +import 'package:lemmy_api_client/v3.dart'; + import 'package:thunder/drafts/draft_type.dart'; class DraftTypeConverter extends TypeConverter { @@ -14,3 +17,33 @@ class DraftTypeConverter extends TypeConverter { return value.name; } } + +/// Converts [SortType] to be stored in the database and vice versa +class SortTypeConverter extends TypeConverter { + const SortTypeConverter(); + + @override + SortType fromSql(String fromDb) { + return SortType.values.firstWhereOrNull((element) => element.toString() == fromDb) ?? SortType.hot; + } + + @override + String toSql(SortType value) { + return value.toString(); + } +} + +/// Converts [ListingType] to be stored in the database and vice versa +class ListingTypeConverter extends TypeConverter { + const ListingTypeConverter(); + + @override + ListingType fromSql(String fromDb) { + return ListingType.values.byName(fromDb); + } + + @override + String toSql(ListingType value) { + return value.name; + } +} diff --git a/lib/core/enums/local_settings.dart b/lib/core/enums/local_settings.dart index 05baf1f18..5ba8207d2 100644 --- a/lib/core/enums/local_settings.dart +++ b/lib/core/enums/local_settings.dart @@ -69,9 +69,10 @@ enum LocalSettings { /// -------------------------- Feed Related Settings -------------------------- // Default Listing/Sort Settings - defaultFeedListingType(name: 'setting_general_default_listing_type', key: 'defaultFeedType', category: LocalSettingsCategories.general, subCategory: LocalSettingsSubCategories.feedTypeAndSorts), defaultFeedSortType(name: 'setting_general_default_sort_type', key: 'defaultFeedSortType', category: LocalSettingsCategories.general, subCategory: LocalSettingsSubCategories.feedTypeAndSorts), + rememberFeedSortType( + name: 'setting_general_remember_feed_sort_type', key: 'rememberFeedSortType', category: LocalSettingsCategories.general, subCategory: LocalSettingsSubCategories.feedTypeAndSorts), // NSFW Settings hideNsfwPosts(name: 'setting_general_hide_nsfw_posts', key: 'hideNsfwPostsFromFeed', category: LocalSettingsCategories.general, subCategory: LocalSettingsSubCategories.feed), @@ -81,7 +82,6 @@ enum LocalSettings { useTabletMode(name: 'setting_post_tablet_mode', key: 'tabletMode', category: LocalSettingsCategories.general, subCategory: LocalSettingsSubCategories.feed), // General Settings - scrapeMissingPreviews( name: 'setting_general_scrape_missing_previews', key: 'scrapeMissingLinkPreviews', category: LocalSettingsCategories.general, subCategory: LocalSettingsSubCategories.linksBehaviourSettings), // Deprecated, use browserMode @@ -314,9 +314,11 @@ enum LocalSettings { // This setting exists purely to save/load the user's selected advanced share options advancedShareOptions(name: 'advanced_share_options', key: ''), + // import export settings importExportSettings(name: 'import_export_settings', key: 'importExportSettings', category: LocalSettingsCategories.general, subCategory: LocalSettingsSubCategories.importExportSettings), importExportDatabase(name: 'import_export_database', key: 'importExportDatabase', category: LocalSettingsCategories.general, subCategory: LocalSettingsSubCategories.importExportSettings), + // video player videoAutoMute(name: 'auto_mute_videos', key: 'videoAutoMute', category: LocalSettingsCategories.videoPlayer, subCategory: LocalSettingsSubCategories.videoPlayer), videoDefaultPlaybackSpeed(name: 'video_default_playback_speed', key: 'videoDefaultPlaybackSpeed', category: LocalSettingsCategories.videoPlayer, subCategory: LocalSettingsSubCategories.videoPlayer), @@ -406,6 +408,7 @@ extension LocalizationExt on AppLocalizations { 'postBodyShowCommunityAvatar': postBodyShowCommunityAvatar, 'keywordFilters': keywordFilters, 'hideTopBarOnScroll': hideTopBarOnScroll, + 'rememberFeedSortType': rememberFeedSortType, 'compactPostCardMetadataItems': compactPostCardMetadataItems, 'cardPostCardMetadataItems': cardPostCardMetadataItems, 'userFormat': userFormat, diff --git a/lib/feed/bloc/feed_bloc.dart b/lib/feed/bloc/feed_bloc.dart index 6adb083ce..75e61b848 100644 --- a/lib/feed/bloc/feed_bloc.dart +++ b/lib/feed/bloc/feed_bloc.dart @@ -11,6 +11,7 @@ import 'package:thunder/core/singletons/lemmy_client.dart'; import 'package:thunder/feed/enums/feed_type_subview.dart'; import 'package:thunder/feed/utils/community.dart'; import 'package:thunder/feed/utils/post.dart'; +import 'package:thunder/feed/utils/utils.dart'; import 'package:thunder/feed/view/feed_page.dart'; import 'package:thunder/post/enums/post_action.dart'; import 'package:thunder/post/utils/post.dart'; @@ -432,10 +433,11 @@ class FeedBloc extends Bloc { /// Changes the current sort type of the feed, and refreshes the feed Future _onFeedChangeSortType(FeedChangeSortTypeEvent event, Emitter emit) async { + emit(state.copyWith(status: FeedStatus.switchingSortType, sortType: event.sortType)); + add(FeedFetchedEvent( feedType: state.feedType, postListingType: state.postListingType, - sortType: event.sortType, communityId: state.communityId, communityName: state.communityName, userId: state.userId, @@ -454,6 +456,10 @@ class FeedBloc extends Bloc { // Handle the initial fetch or reload of a feed if (event.reset) { + SortType? sortType; + if (state.status == FeedStatus.switchingSortType) sortType = state.sortType; + sortType ??= await getSortType(null, event.communityId, event.postListingType); + if (state.status != FeedStatus.initial) add(ResetFeedEvent()); GetCommunityResponse? fullCommunityView; @@ -502,7 +508,7 @@ class FeedBloc extends Bloc { Map feedItemResult = await fetchFeedItems( page: 1, postListingType: event.postListingType, - sortType: event.sortType, + sortType: sortType, communityId: event.communityId, communityName: event.communityName, userId: event.userId ?? fullPersonView?.personView.person.id, @@ -525,7 +531,7 @@ class FeedBloc extends Bloc { hasReachedCommentsEnd: hasReachedCommentsEnd, feedType: event.feedType, postListingType: event.postListingType, - sortType: event.sortType, + sortType: sortType, fullCommunityView: fullCommunityView, fullPersonView: fullPersonView, communityId: event.communityId, diff --git a/lib/feed/bloc/feed_event.dart b/lib/feed/bloc/feed_event.dart index c1bbf3c01..7a2bcbe29 100644 --- a/lib/feed/bloc/feed_event.dart +++ b/lib/feed/bloc/feed_event.dart @@ -17,9 +17,6 @@ final class FeedFetchedEvent extends FeedEvent { /// The type of general feed to display: all, local, subscribed. final ListingType? postListingType; - /// The sorting to be applied to the feed. - final SortType? sortType; - /// The id of the community to display posts for. final int? communityId; @@ -39,7 +36,6 @@ final class FeedFetchedEvent extends FeedEvent { this.feedType, this.feedTypeSubview = FeedTypeSubview.post, this.postListingType, - this.sortType, this.communityId, this.communityName, this.userId, diff --git a/lib/feed/bloc/feed_state.dart b/lib/feed/bloc/feed_state.dart index 4c2a28563..9fd6aab98 100644 --- a/lib/feed/bloc/feed_state.dart +++ b/lib/feed/bloc/feed_state.dart @@ -1,6 +1,6 @@ part of 'feed_bloc.dart'; -enum FeedStatus { initial, fetching, success, failure, failureLoadingCommunity, failureLoadingUser } +enum FeedStatus { initial, fetching, success, failure, failureLoadingCommunity, failureLoadingUser, switchingSortType } final class FeedState extends Equatable { const FeedState({ diff --git a/lib/feed/utils/utils.dart b/lib/feed/utils/utils.dart index 84766af39..5c8f32744 100644 --- a/lib/feed/utils/utils.dart +++ b/lib/feed/utils/utils.dart @@ -3,18 +3,26 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:lemmy_api_client/v3.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:swipeable_page_route/swipeable_page_route.dart'; import 'package:thunder/account/bloc/account_bloc.dart'; +import 'package:thunder/account/models/account.dart'; +import 'package:thunder/account/models/custom_sort_type.dart'; import 'package:thunder/community/bloc/anonymous_subscriptions_bloc.dart'; import 'package:thunder/community/bloc/community_bloc.dart'; import 'package:thunder/core/auth/bloc/auth_bloc.dart'; +import 'package:thunder/core/auth/helpers/fetch_account.dart'; +import 'package:thunder/core/enums/local_settings.dart'; +import 'package:thunder/core/singletons/preferences.dart'; import 'package:thunder/feed/feed.dart'; import 'package:thunder/instance/bloc/instance_bloc.dart'; import 'package:thunder/shared/pages/loading_page.dart'; import 'package:thunder/shared/sort_picker.dart'; import 'package:thunder/community/widgets/community_drawer.dart'; import 'package:thunder/thunder/bloc/thunder_bloc.dart'; +import 'package:thunder/utils/constants.dart'; +import 'package:thunder/utils/global_context.dart'; import 'package:thunder/utils/swipe.dart'; String getAppBarTitle(FeedState state) { @@ -55,6 +63,49 @@ IconData? getSortIcon(FeedState state) { return sortTypeItem?.icon; } +/// Gets the default sort type based on precedence. +/// +/// If the user is not logged in, it will fetch the app's default feed sort type. +/// If the user is logged in and [rememberFeedSortType] is false, it will fetch the user account'sdefault sort type. +/// If the user is logged in and [rememberFeedSortType] is true, it will fetch the custom sort type if available. If not, it will fetch the user account's default sort type. +/// If all else fails (should never happen), it will return [SortType.hot] +/// +/// Note that this does not take a [context] because it is called outside of a widget. +Future getSortType(SortType? sortType, int? communityId, ListingType? postListingType) async { + try { + Account? account = await fetchActiveProfileAccount(); + + SharedPreferences prefs = (await UserPreferences.instance).sharedPreferences; + bool rememberFeedSortType = prefs.getBool(LocalSettings.rememberFeedSortType.name) ?? false; + + SortType defaultSortType = SortType.values.byName(prefs.getString(LocalSettings.defaultFeedSortType.name) ?? DEFAULT_SORT_TYPE.name); + SortType? finalSortType = sortType; + + if (finalSortType == null) { + // Check to see if we have a remembered sort type. If so, then use that first.This will only apply to logged in users. + if (rememberFeedSortType && account != null) { + CustomSortType? customSortType = await CustomSortType.fetchCustomSortType( + account.userId!, + communityId, + postListingType, + ); + + if (customSortType != null) finalSortType = customSortType.sortType; + } + + // If there is no remembered sort type, then attempt to get the user's default sort type + finalSortType ??= GlobalContext.context.read().state.getSiteResponse?.myUser?.localUserView.localUser.defaultSortType; + + // Finally, use the app's default sort type + finalSortType ??= defaultSortType; + } + + return finalSortType; + } catch (e) { + return SortType.hot; + } +} + /// Navigates to a [FeedPage] with the given parameters /// /// [feedType] must be provided. @@ -89,7 +140,6 @@ Future navigateToFeedPage( FeedFetchedEvent( feedType: feedType, postListingType: postListingType, - sortType: sortType ?? authBloc.state.getSiteResponse?.myUser?.localUserView.localUser.defaultSortType ?? thunderBloc.state.sortTypeForInstance, communityId: communityId, communityName: communityName, userId: userId, @@ -121,7 +171,6 @@ Future navigateToFeedPage( child: Material( child: FeedPage( feedType: feedType, - sortType: sortType ?? authBloc.state.getSiteResponse?.myUser?.localUserView.localUser.defaultSortType ?? thunderBloc.state.sortTypeForInstance, communityName: communityName, communityId: communityId, userId: userId, @@ -137,17 +186,5 @@ Future navigateToFeedPage( Future triggerRefresh(BuildContext context) async { FeedState state = context.read().state; - - context.read().add( - FeedFetchedEvent( - feedType: state.feedType, - postListingType: state.postListingType, - sortType: state.sortType, - communityId: state.communityId, - communityName: state.communityName, - userId: state.userId, - username: state.username, - reset: true, - ), - ); + context.read().add(FeedChangeSortTypeEvent(state.sortType!)); } diff --git a/lib/feed/view/feed_page.dart b/lib/feed/view/feed_page.dart index 52dcb55f5..d94228799 100644 --- a/lib/feed/view/feed_page.dart +++ b/lib/feed/view/feed_page.dart @@ -49,7 +49,6 @@ class FeedPage extends StatefulWidget { this.useGlobalFeedBloc = false, required this.feedType, this.postListingType, - required this.sortType, this.communityId, this.communityName, this.userId, @@ -63,9 +62,6 @@ class FeedPage extends StatefulWidget { /// The type of general feed to display: all, local, subscribed. final ListingType? postListingType; - /// The sorting to be applied to the feed. - final SortType? sortType; - /// The id of the community to display posts for. final int? communityId; @@ -106,7 +102,6 @@ class _FeedPageState extends State with AutomaticKeepAliveClientMixin< bloc.add(FeedFetchedEvent( feedType: widget.feedType, postListingType: widget.postListingType, - sortType: widget.sortType, communityId: widget.communityId, communityName: widget.communityName, userId: widget.userId, @@ -139,7 +134,6 @@ class _FeedPageState extends State with AutomaticKeepAliveClientMixin< ..add(FeedFetchedEvent( feedType: widget.feedType, postListingType: widget.postListingType, - sortType: widget.sortType, communityId: widget.communityId, communityName: widget.communityName, userId: widget.userId, @@ -587,7 +581,6 @@ class _FeedViewState extends State { if (!canPop && (desiredListingType != currentListingType || communityMode)) { feedBloc.add( FeedFetchedEvent( - sortType: authBloc.state.getSiteResponse?.myUser?.localUserView.localUser.defaultSortType ?? thunderBloc.state.sortTypeForInstance, reset: true, postListingType: desiredListingType, feedType: FeedType.general, diff --git a/lib/feed/widgets/feed_page_app_bar.dart b/lib/feed/widgets/feed_page_app_bar.dart index 081ced48b..836771d73 100644 --- a/lib/feed/widgets/feed_page_app_bar.dart +++ b/lib/feed/widgets/feed_page_app_bar.dart @@ -9,6 +9,7 @@ import 'package:lemmy_api_client/v3.dart'; import 'package:swipeable_page_route/swipeable_page_route.dart'; import 'package:thunder/account/bloc/account_bloc.dart'; +import 'package:thunder/account/models/custom_sort_type.dart'; import 'package:thunder/community/bloc/anonymous_subscriptions_bloc.dart'; import 'package:thunder/community/bloc/community_bloc.dart'; import 'package:thunder/community/enums/community_action.dart'; @@ -161,8 +162,9 @@ class FeedAppBarCommunityActions extends StatelessWidget { Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; - final feedBloc = context.read(); final thunderBloc = context.read(); + final feedBloc = context.read(); + final authBloc = context.read(); return Row( children: [ @@ -206,7 +208,26 @@ class FeedAppBarCommunityActions extends StatelessWidget { isScrollControlled: true, builder: (builderContext) => SortPicker( title: l10n.sortOptions, - onSelect: (selected) async => feedBloc.add(FeedChangeSortTypeEvent(selected.payload)), + onSelect: (selected) async { + // Update the sort type in the db if rememberFeedSortType is enabled + bool rememberFeedSortType = thunderBloc.state.rememberFeedSortType; + bool isUserLoggedIn = authBloc.state.isLoggedIn; + + if (rememberFeedSortType && isUserLoggedIn) { + int? userId = authBloc.state.account?.userId; + assert(userId != null); + + CustomSortType customSortType = CustomSortType( + sortType: selected.payload, + accountId: userId!, + communityId: feedBloc.state.fullCommunityView?.communityView.community.id, + ); + + await CustomSortType.upsertCustomSortType(customSortType); + } + + feedBloc.add(FeedChangeSortTypeEvent(selected.payload)); + }, previouslySelected: feedBloc.state.sortType, minimumVersion: LemmyClient.instance.version, ), @@ -336,7 +357,10 @@ class FeedAppBarGeneralActions extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; + + final thunderBloc = context.read(); final feedBloc = context.read(); + final authBloc = context.read(); return Row( children: [ @@ -358,7 +382,26 @@ class FeedAppBarGeneralActions extends StatelessWidget { isScrollControlled: true, builder: (builderContext) => SortPicker( title: l10n.sortOptions, - onSelect: (selected) async => feedBloc.add(FeedChangeSortTypeEvent(selected.payload)), + onSelect: (selected) async { + // Update the sort type in the db if rememberFeedSortType is enabled + bool rememberFeedSortType = thunderBloc.state.rememberFeedSortType; + bool isUserLoggedIn = authBloc.state.isLoggedIn; + + if (rememberFeedSortType && isUserLoggedIn) { + int? userId = authBloc.state.account?.userId; + assert(userId != null); + + CustomSortType customSortType = CustomSortType( + sortType: selected.payload, + accountId: userId!, + feedType: feedBloc.state.postListingType, + ); + + await CustomSortType.upsertCustomSortType(customSortType); + } + + feedBloc.add(FeedChangeSortTypeEvent(selected.payload)); + }, previouslySelected: feedBloc.state.sortType, minimumVersion: LemmyClient.instance.version, ), diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index e699a89a8..2209d5d36 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1671,6 +1671,14 @@ "@refresh": {}, "refreshContent": "Refresh Content", "@refreshContent": {}, + "rememberFeedSortType": "Remember Feed Sort Type", + "@rememberFeedSortType": { + "description": "Setting for remembering feed sort type per community/feed" + }, + "rememberFeedSortTypeDescription": "Saves the current feed/community sort type", + "@rememberFeedSortTypeDescription": { + "description": "Description of remembering feed sort type per community/feed" + }, "removalReason": "Removal Reason", "@removalReason": { "description": "Title for the dialog for removing a post" diff --git a/lib/settings/pages/general_settings_page.dart b/lib/settings/pages/general_settings_page.dart index bd5420970..598581016 100644 --- a/lib/settings/pages/general_settings_page.dart +++ b/lib/settings/pages/general_settings_page.dart @@ -89,6 +89,9 @@ class _GeneralSettingsPageState extends State with SingleTi /// When enabled, the top bar will be hidden on scroll bool hideTopBarOnScroll = false; + /// When enabled, the sort type for a given feed/community will be remembered + bool rememberFeedSortType = false; + /// When enabled, an app update notification will be shown when an update is available bool showInAppUpdateNotification = false; @@ -194,6 +197,11 @@ class _GeneralSettingsPageState extends State with SingleTi await prefs.setBool(LocalSettings.hideTopBarOnScroll.name, value); setState(() => hideTopBarOnScroll = value); break; + case LocalSettings.rememberFeedSortType: + await prefs.setBool(LocalSettings.rememberFeedSortType.name, value); + setState(() => rememberFeedSortType = value); + break; + case LocalSettings.collapseParentCommentBodyOnGesture: await prefs.setBool(LocalSettings.collapseParentCommentBodyOnGesture.name, value); setState(() => collapseParentCommentOnGesture = value); @@ -278,6 +286,7 @@ class _GeneralSettingsPageState extends State with SingleTi markPostReadOnScroll = prefs.getBool(LocalSettings.markPostAsReadOnScroll.name) ?? false; tabletMode = prefs.getBool(LocalSettings.useTabletMode.name) ?? false; hideTopBarOnScroll = prefs.getBool(LocalSettings.hideTopBarOnScroll.name) ?? false; + rememberFeedSortType = prefs.getBool(LocalSettings.rememberFeedSortType.name) ?? false; collapseParentCommentOnGesture = prefs.getBool(LocalSettings.collapseParentCommentBodyOnGesture.name) ?? true; enableCommentNavigation = prefs.getBool(LocalSettings.enableCommentNavigation.name) ?? true; @@ -526,6 +535,19 @@ class _GeneralSettingsPageState extends State with SingleTi highlightedSetting: settingToHighlight, ), ), + SliverToBoxAdapter( + child: ToggleOption( + description: l10n.rememberFeedSortType, + value: rememberFeedSortType, + subtitle: l10n.rememberFeedSortTypeDescription, + iconEnabled: Icons.dynamic_feed_rounded, + iconDisabled: Icons.dynamic_feed_rounded, + onToggle: (bool value) => setPreferences(LocalSettings.rememberFeedSortType, value), + highlightKey: settingToHighlightKey, + setting: LocalSettings.rememberFeedSortType, + highlightedSetting: settingToHighlight, + ), + ), const SliverToBoxAdapter(child: SizedBox(height: 16.0)), SliverToBoxAdapter( child: Padding( diff --git a/lib/thunder/bloc/thunder_bloc.dart b/lib/thunder/bloc/thunder_bloc.dart index bfe9d919b..d3abcd82c 100644 --- a/lib/thunder/bloc/thunder_bloc.dart +++ b/lib/thunder/bloc/thunder_bloc.dart @@ -140,6 +140,7 @@ class ThunderBloc extends Bloc { ImageCachingMode imageCachingMode = ImageCachingMode.values.byName(prefs.getString(LocalSettings.imageCachingMode.name) ?? ImageCachingMode.relaxed.name); bool showNavigationLabels = prefs.getBool(LocalSettings.showNavigationLabels.name) ?? true; bool hideTopBarOnScroll = prefs.getBool(LocalSettings.hideTopBarOnScroll.name) ?? false; + bool rememberFeedSortType = prefs.getBool(LocalSettings.rememberFeedSortType.name) ?? false; BrowserMode browserMode = BrowserMode.values.byName(prefs.getString(LocalSettings.browserMode.name) ?? BrowserMode.customTabs.name); @@ -315,6 +316,7 @@ class ThunderBloc extends Bloc { imageCachingMode: imageCachingMode, showNavigationLabels: showNavigationLabels, hideTopBarOnScroll: hideTopBarOnScroll, + rememberFeedSortType: rememberFeedSortType, /// -------------------------- Feed Post Related Settings -------------------------- // Compact Related Settings diff --git a/lib/thunder/bloc/thunder_state.dart b/lib/thunder/bloc/thunder_state.dart index 7b85a7a3e..c5770ce7f 100644 --- a/lib/thunder/bloc/thunder_state.dart +++ b/lib/thunder/bloc/thunder_state.dart @@ -49,6 +49,7 @@ class ThunderState extends Equatable { this.imageCachingMode = ImageCachingMode.relaxed, this.showNavigationLabels = true, this.hideTopBarOnScroll = false, + this.rememberFeedSortType = false, /// -------------------------- Feed Post Related Settings -------------------------- // Compact Related Settings @@ -219,6 +220,7 @@ class ThunderState extends Equatable { final ImageCachingMode imageCachingMode; final bool showNavigationLabels; final bool hideTopBarOnScroll; + final bool rememberFeedSortType; /// -------------------------- Feed Post Related Settings -------------------------- /// Compact Related Settings @@ -396,6 +398,7 @@ class ThunderState extends Equatable { ImageCachingMode? imageCachingMode, bool? showNavigationLabels, bool? hideTopBarOnScroll, + bool? rememberFeedSortType, /// -------------------------- Feed Post Related Settings -------------------------- /// Compact Related Settings @@ -568,6 +571,7 @@ class ThunderState extends Equatable { imageCachingMode: imageCachingMode ?? this.imageCachingMode, showNavigationLabels: showNavigationLabels ?? this.showNavigationLabels, hideTopBarOnScroll: hideTopBarOnScroll ?? this.hideTopBarOnScroll, + rememberFeedSortType: rememberFeedSortType ?? this.rememberFeedSortType, /// -------------------------- Feed Post Related Settings -------------------------- // Compact Related Settings @@ -744,6 +748,8 @@ class ThunderState extends Equatable { communityFullNameInstanceNameColor, imageCachingMode, showNavigationLabels, + hideTopBarOnScroll, + rememberFeedSortType, /// -------------------------- Feed Post Related Settings -------------------------- /// Compact Related Settings diff --git a/lib/thunder/pages/thunder_page.dart b/lib/thunder/pages/thunder_page.dart index e0a858a78..5395a57dd 100644 --- a/lib/thunder/pages/thunder_page.dart +++ b/lib/thunder/pages/thunder_page.dart @@ -504,7 +504,6 @@ class _ThunderState extends State { FeedFetchedEvent( feedType: FeedType.general, postListingType: state.getSiteResponse?.myUser?.localUserView.localUser.defaultListingType ?? thunderBlocState.defaultListingType, - sortType: state.getSiteResponse?.myUser?.localUserView.localUser.defaultSortType ?? thunderBlocState.sortTypeForInstance, reset: true, ), ); @@ -637,7 +636,6 @@ class _ThunderState extends State { useGlobalFeedBloc: true, feedType: FeedType.general, postListingType: state.getSiteResponse?.myUser?.localUserView.localUser.defaultListingType ?? thunderBlocState.defaultListingType, - sortType: state.getSiteResponse?.myUser?.localUserView.localUser.defaultSortType ?? thunderBlocState.sortTypeForInstance, scaffoldStateKey: scaffoldStateKey, ), const SearchPage(), diff --git a/test/database/migration_test.dart b/test/database/migration_test.dart index 25caa0529..702d7df45 100644 --- a/test/database/migration_test.dart +++ b/test/database/migration_test.dart @@ -158,7 +158,7 @@ void main() { final tables = db.allTables.toList(); final tableNames = tables.map((e) => e.actualTableName).toList(); - expect(tables.length, 5); + expect(tables.length, 6); expect(tableNames, containsAll(['accounts', 'local_subscriptions', 'favorites', 'user_labels', 'drafts'])); // Expect correct number of accounts, and correct information