diff --git a/components/synced_shared_preferences/ios/Classes/SwiftSyncedSharedPreferencesPlugin.swift b/components/synced_shared_preferences/ios/Classes/SwiftSyncedSharedPreferencesPlugin.swift index 2d5a14c..25f67ce 100644 --- a/components/synced_shared_preferences/ios/Classes/SwiftSyncedSharedPreferencesPlugin.swift +++ b/components/synced_shared_preferences/ios/Classes/SwiftSyncedSharedPreferencesPlugin.swift @@ -76,6 +76,15 @@ final class SharedPrefsCore { return true } + + fileprivate func remove(key: String?) -> Bool{ + if let key = key { + let keyStore = NSUbiquitousKeyValueStore() + keyStore.removeObject(forKey: key) + } + + return true + } } public class SwiftSyncedSharedPreferencesPlugin: NSObject, FlutterPlugin { @@ -87,6 +96,14 @@ public class SwiftSyncedSharedPreferencesPlugin: NSObject, FlutterPlugin { public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { switch call.method { + case "remove": + if let params = call.arguments as? [String: Any] { + let key = params[keyKey] as? String + + let res = SharedPrefsCore.shared.remove(key: key) + result(res) + } + case "setBool": if let params = call.arguments as? [String: Any] { let val = params[valKey] as? Bool diff --git a/components/synced_shared_preferences/lib/synced_shared_preferences.dart b/components/synced_shared_preferences/lib/synced_shared_preferences.dart index dd5ad59..0a05ed5 100644 --- a/components/synced_shared_preferences/lib/synced_shared_preferences.dart +++ b/components/synced_shared_preferences/lib/synced_shared_preferences.dart @@ -15,6 +15,14 @@ class SyncedSharedPreferences { const MethodChannel(channel), ); + Future remove({ + required String key, + }) async { + return _channel.invokeMethod('remove', { + 'key': key, + }); + } + Future setBool({ required String key, required bool val, diff --git a/lib/blocs/auth/auth_bloc.dart b/lib/blocs/auth/auth_bloc.dart index 0b7723c..ad79acb 100644 --- a/lib/blocs/auth/auth_bloc.dart +++ b/lib/blocs/auth/auth_bloc.dart @@ -11,13 +11,13 @@ class AuthBloc extends Bloc { AuthBloc({ AuthRepository? authRepository, PreferenceRepository? preferenceRepository, - StoriesRepository? storiesRepository, + HackerNewsRepository? hackerNewsRepository, SembastRepository? sembastRepository, }) : _authRepository = authRepository ?? locator.get(), _preferenceRepository = preferenceRepository ?? locator.get(), - _storiesRepository = - storiesRepository ?? locator.get(), + _hackerNewsRepository = + hackerNewsRepository ?? locator.get(), _sembastRepository = sembastRepository ?? locator.get(), super(const AuthState.init()) { @@ -31,7 +31,7 @@ class AuthBloc extends Bloc { final AuthRepository _authRepository; final PreferenceRepository _preferenceRepository; - final StoriesRepository _storiesRepository; + final HackerNewsRepository _hackerNewsRepository; final SembastRepository _sembastRepository; Future onInitialize( @@ -41,7 +41,7 @@ class AuthBloc extends Bloc { await _authRepository.loggedIn.then((bool loggedIn) async { if (loggedIn) { final String? username = await _authRepository.username; - User? user = await _storiesRepository.fetchUser(id: username!); + User? user = await _hackerNewsRepository.fetchUser(id: username!); /// According to Hacker News' API documentation, /// if user has no public activity (posting a comment or story), @@ -89,7 +89,8 @@ class AuthBloc extends Bloc { ); if (successful) { - final User? user = await _storiesRepository.fetchUser(id: event.username); + final User? user = + await _hackerNewsRepository.fetchUser(id: event.username); emit( state.copyWith( user: user ?? User.emptyWithId(event.username), diff --git a/lib/blocs/stories/stories_bloc.dart b/lib/blocs/stories/stories_bloc.dart index 4db8522..1ce0c84 100644 --- a/lib/blocs/stories/stories_bloc.dart +++ b/lib/blocs/stories/stories_bloc.dart @@ -19,15 +19,15 @@ class StoriesBloc extends Bloc { required PreferenceCubit preferenceCubit, required FilterCubit filterCubit, OfflineRepository? offlineRepository, - StoriesRepository? storiesRepository, + HackerNewsRepository? hackerNewsRepository, PreferenceRepository? preferenceRepository, Logger? logger, }) : _preferenceCubit = preferenceCubit, _filterCubit = filterCubit, _offlineRepository = offlineRepository ?? locator.get(), - _storiesRepository = - storiesRepository ?? locator.get(), + _hackerNewsRepository = + hackerNewsRepository ?? locator.get(), _preferenceRepository = preferenceRepository ?? locator.get(), _logger = logger ?? locator.get(), @@ -37,6 +37,7 @@ class StoriesBloc extends Bloc { on(onLoadMore); on(onStoryLoaded); on(onStoryRead); + on(onStoryUnread); on(onStoriesLoaded); on(onDownload); on(onCancelDownload); @@ -49,7 +50,7 @@ class StoriesBloc extends Bloc { final PreferenceCubit _preferenceCubit; final FilterCubit _filterCubit; final OfflineRepository _offlineRepository; - final StoriesRepository _storiesRepository; + final HackerNewsRepository _hackerNewsRepository; final PreferenceRepository _preferenceRepository; final Logger _logger; DeviceScreenType? deviceScreenType; @@ -113,13 +114,14 @@ class StoriesBloc extends Bloc { add(StoriesLoaded(type: type)); }); } else { - final List ids = await _storiesRepository.fetchStoryIds(type: type); + final List ids = + await _hackerNewsRepository.fetchStoryIds(type: type); emit( state .copyWithStoryIdsUpdated(type: type, to: ids) .copyWithCurrentPageUpdated(type: type, to: 0), ); - _storiesRepository + _hackerNewsRepository .fetchStoriesStream(ids: ids.sublist(0, state.currentPageSize)) .listen((Story story) { add(StoryLoaded(story: story, type: type)); @@ -196,7 +198,7 @@ class StoriesBloc extends Bloc { add(StoriesLoaded(type: event.type)); }); } else { - _storiesRepository + _hackerNewsRepository .fetchStoriesStream( ids: state.storyIdsByType[event.type]!.sublist( lower, @@ -273,7 +275,8 @@ class StoriesBloc extends Bloc { ..remove(StoryType.latest); for (final StoryType type in prioritizedTypes) { - final List ids = await _storiesRepository.fetchStoryIds(type: type); + final List ids = + await _hackerNewsRepository.fetchStoryIds(type: type); await _offlineRepository.cacheStoryIds(type: type, ids: ids); prioritizedIds.addAll(ids); } @@ -293,7 +296,7 @@ class StoriesBloc extends Bloc { ); final Set latestIds = {}; - final List ids = await _storiesRepository.fetchStoryIds( + final List ids = await _hackerNewsRepository.fetchStoryIds( type: StoryType.latest, ); await _offlineRepository.cacheStoryIds(type: StoryType.latest, ids: ids); @@ -347,7 +350,7 @@ class StoriesBloc extends Bloc { } _logger.d('fetching story $id'); - final Story? story = await _storiesRepository.fetchStory(id: id); + final Story? story = await _hackerNewsRepository.fetchStory(id: id); if (story == null) { if (isPrioritized) { @@ -377,7 +380,7 @@ class StoriesBloc extends Bloc { /// In other words, we are prioritizing the story itself instead of /// the comments in the story. late final StreamSubscription? downloadStream; - downloadStream = _storiesRepository + downloadStream = _hackerNewsRepository .fetchAllChildrenComments(ids: story.kids) .whereType() .listen( @@ -460,7 +463,7 @@ class StoriesBloc extends Bloc { StoryRead event, Emitter emit, ) async { - unawaited(_preferenceRepository.updateHasRead(event.story.id)); + unawaited(_preferenceRepository.addHasRead(event.story.id)); emit( state.copyWith( @@ -469,6 +472,19 @@ class StoriesBloc extends Bloc { ); } + Future onStoryUnread( + StoryUnread event, + Emitter emit, + ) async { + unawaited(_preferenceRepository.removeHasRead(event.story.id)); + + emit( + state.copyWith( + readStoriesIds: {...state.readStoriesIds}..remove(event.story.id), + ), + ); + } + Future onClearAllReadStories( ClearAllReadStories event, Emitter emit, diff --git a/lib/blocs/stories/stories_event.dart b/lib/blocs/stories/stories_event.dart index 24ed8e4..0736027 100644 --- a/lib/blocs/stories/stories_event.dart +++ b/lib/blocs/stories/stories_event.dart @@ -95,6 +95,15 @@ class StoryRead extends StoriesEvent { List get props => [story]; } +class StoryUnread extends StoriesEvent { + StoryUnread({required this.story}); + + final Story story; + + @override + List get props => [story]; +} + class ClearAllReadStories extends StoriesEvent { @override List get props => []; diff --git a/lib/config/locator.dart b/lib/config/locator.dart index c99af23..b0352ec 100644 --- a/lib/config/locator.dart +++ b/lib/config/locator.dart @@ -24,7 +24,7 @@ Future setUpLocator() async { ), ) ..registerSingleton(SembastRepository()) - ..registerSingleton(StoriesRepository()) + ..registerSingleton(HackerNewsRepository()) ..registerSingleton(PreferenceRepository()) ..registerSingleton(SearchRepository()) ..registerSingleton(AuthRepository()) diff --git a/lib/cubits/comments/comments_cubit.dart b/lib/cubits/comments/comments_cubit.dart index 333b5bd..18737b4 100644 --- a/lib/cubits/comments/comments_cubit.dart +++ b/lib/cubits/comments/comments_cubit.dart @@ -32,7 +32,7 @@ class CommentsCubit extends Cubit { required CommentsOrder defaultCommentsOrder, CommentCache? commentCache, OfflineRepository? offlineRepository, - StoriesRepository? storiesRepository, + HackerNewsRepository? hackerNewsRepository, SembastRepository? sembastRepository, Logger? logger, }) : _filterCubit = filterCubit, @@ -40,8 +40,8 @@ class CommentsCubit extends Cubit { _commentCache = commentCache ?? locator.get(), _offlineRepository = offlineRepository ?? locator.get(), - _storiesRepository = - storiesRepository ?? locator.get(), + _hackerNewsRepository = + hackerNewsRepository ?? locator.get(), _sembastRepository = sembastRepository ?? locator.get(), _logger = logger ?? locator.get(), @@ -58,7 +58,7 @@ class CommentsCubit extends Cubit { final CollapseCache _collapseCache; final CommentCache _commentCache; final OfflineRepository _offlineRepository; - final StoriesRepository _storiesRepository; + final HackerNewsRepository _hackerNewsRepository; final SembastRepository _sembastRepository; final Logger _logger; @@ -96,7 +96,7 @@ class CommentsCubit extends Cubit { ), ); - _streamSubscription = _storiesRepository + _streamSubscription = _hackerNewsRepository .fetchAllCommentsRecursivelyStream( ids: targetAncestors!.last.kids, level: targetAncestors.last.level + 1, @@ -122,7 +122,7 @@ class CommentsCubit extends Cubit { final Item item = state.item; final Item updatedItem = state.isOfflineReading ? item - : await _storiesRepository + : await _hackerNewsRepository .fetchItem(id: item.id) .then(_toBuildable) .onError((_, __) => item) ?? @@ -138,12 +138,13 @@ class CommentsCubit extends Cubit { } else { switch (state.fetchMode) { case FetchMode.lazy: - commentStream = _storiesRepository.fetchCommentsStream( + commentStream = _hackerNewsRepository.fetchCommentsStream( ids: kids, getFromCache: useCommentCache ? _commentCache.getComment : null, ); case FetchMode.eager: - commentStream = _storiesRepository.fetchAllCommentsRecursivelyStream( + commentStream = + _hackerNewsRepository.fetchAllCommentsRecursivelyStream( ids: kids, getFromCache: useCommentCache ? _commentCache.getComment : null, ); @@ -190,16 +191,16 @@ class CommentsCubit extends Cubit { final Item item = state.item; final Item updatedItem = - await _storiesRepository.fetchItem(id: item.id) ?? item; + await _hackerNewsRepository.fetchItem(id: item.id) ?? item; final List kids = _sortKids(updatedItem.kids); late final Stream commentStream; if (state.fetchMode == FetchMode.lazy) { - commentStream = _storiesRepository.fetchCommentsStream( + commentStream = _hackerNewsRepository.fetchCommentsStream( ids: kids, ); } else { - commentStream = _storiesRepository.fetchAllCommentsRecursivelyStream( + commentStream = _hackerNewsRepository.fetchAllCommentsRecursivelyStream( ids: kids, ); } @@ -248,7 +249,7 @@ class CommentsCubit extends Cubit { /// Ignoring because the subscription will be cancelled in close() // ignore: cancel_subscriptions final StreamSubscription streamSubscription = - _storiesRepository + _hackerNewsRepository .fetchCommentsStream(ids: comment.kids) .asyncMap(_toBuildableComment) .whereNotNull() @@ -297,7 +298,7 @@ class CommentsCubit extends Cubit { HapticFeedbackUtil.light(); emit(state.copyWith(fetchParentStatus: CommentsStatus.inProgress)); final Item? parent = - await _storiesRepository.fetchItem(id: state.item.parent); + await _hackerNewsRepository.fetchItem(id: state.item.parent); if (parent == null) { return; @@ -318,7 +319,7 @@ class CommentsCubit extends Cubit { Future loadRootThread() async { HapticFeedbackUtil.light(); emit(state.copyWith(fetchRootStatus: CommentsStatus.inProgress)); - final Story? parent = await _storiesRepository + final Story? parent = await _hackerNewsRepository .fetchParentStory(id: state.item.id) .then(_toBuildableStory); diff --git a/lib/cubits/fav/fav_cubit.dart b/lib/cubits/fav/fav_cubit.dart index 30e7858..5042364 100644 --- a/lib/cubits/fav/fav_cubit.dart +++ b/lib/cubits/fav/fav_cubit.dart @@ -12,13 +12,13 @@ class FavCubit extends Cubit { required AuthBloc authBloc, AuthRepository? authRepository, PreferenceRepository? preferenceRepository, - StoriesRepository? storiesRepository, + HackerNewsRepository? hackerNewsRepository, }) : _authBloc = authBloc, _authRepository = authRepository ?? locator.get(), _preferenceRepository = preferenceRepository ?? locator.get(), - _storiesRepository = - storiesRepository ?? locator.get(), + _hackerNewsRepository = + hackerNewsRepository ?? locator.get(), super(FavState.init()) { init(); } @@ -26,7 +26,7 @@ class FavCubit extends Cubit { final AuthBloc _authBloc; final AuthRepository _authRepository; final PreferenceRepository _preferenceRepository; - final StoriesRepository _storiesRepository; + final HackerNewsRepository _hackerNewsRepository; static const int _pageSize = 20; String? _username; @@ -43,7 +43,7 @@ class FavCubit extends Cubit { currentPage: 0, ), ); - _storiesRepository + _hackerNewsRepository .fetchItemsStream( ids: favIds.sublist(0, _pageSize.clamp(0, favIds.length)), ) @@ -73,7 +73,7 @@ class FavCubit extends Cubit { ), ); - final Item? item = await _storiesRepository.fetchItem(id: id); + final Item? item = await _hackerNewsRepository.fetchItem(id: id); if (item == null) return; @@ -119,7 +119,7 @@ class FavCubit extends Cubit { upper = len; } - _storiesRepository + _hackerNewsRepository .fetchItemsStream( ids: state.favIds.sublist( lower, @@ -149,7 +149,7 @@ class FavCubit extends Cubit { _preferenceRepository.favList(of: username).then((List favIds) { emit(state.copyWith(favIds: favIds)); - _storiesRepository + _hackerNewsRepository .fetchItemsStream( ids: favIds.sublist(0, _pageSize.clamp(0, favIds.length)), ) diff --git a/lib/cubits/history/history_cubit.dart b/lib/cubits/history/history_cubit.dart index d74e451..eafcdaf 100644 --- a/lib/cubits/history/history_cubit.dart +++ b/lib/cubits/history/history_cubit.dart @@ -10,16 +10,16 @@ part 'history_state.dart'; class HistoryCubit extends Cubit { HistoryCubit({ required AuthBloc authBloc, - StoriesRepository? storiesRepository, + HackerNewsRepository? hackerNewsRepository, }) : _authBloc = authBloc, - _storiesRepository = - storiesRepository ?? locator.get(), + _hackerNewsRepository = + hackerNewsRepository ?? locator.get(), super(HistoryState.init()) { init(); } final AuthBloc _authBloc; - final StoriesRepository _storiesRepository; + final HackerNewsRepository _hackerNewsRepository; static const int _pageSize = 20; void init() { @@ -27,7 +27,7 @@ class HistoryCubit extends Cubit { if (authState.isLoggedIn) { final String username = authState.username; - _storiesRepository + _hackerNewsRepository .fetchSubmitted(userId: username) .then((List? submittedIds) { emit( @@ -38,7 +38,7 @@ class HistoryCubit extends Cubit { ), ); if (submittedIds != null) { - _storiesRepository + _hackerNewsRepository .fetchItemsStream( ids: submittedIds.sublist( 0, @@ -66,7 +66,7 @@ class HistoryCubit extends Cubit { upper = len; } - _storiesRepository + _hackerNewsRepository .fetchItemsStream( ids: state.submittedIds.sublist( lower, @@ -93,12 +93,12 @@ class HistoryCubit extends Cubit { ), ); - _storiesRepository + _hackerNewsRepository .fetchSubmitted(userId: username) .then((List? submittedIds) { emit(state.copyWith(submittedIds: submittedIds)); if (submittedIds != null) { - _storiesRepository + _hackerNewsRepository .fetchItemsStream( ids: submittedIds.sublist( 0, diff --git a/lib/cubits/notification/notification_cubit.dart b/lib/cubits/notification/notification_cubit.dart index 89017c2..ea82da5 100644 --- a/lib/cubits/notification/notification_cubit.dart +++ b/lib/cubits/notification/notification_cubit.dart @@ -16,13 +16,13 @@ class NotificationCubit extends Cubit { NotificationCubit({ required AuthBloc authBloc, required PreferenceCubit preferenceCubit, - StoriesRepository? storiesRepository, + HackerNewsRepository? hackerNewsRepository, PreferenceRepository? preferenceRepository, SembastRepository? sembastRepository, }) : _authBloc = authBloc, _preferenceCubit = preferenceCubit, - _storiesRepository = - storiesRepository ?? locator.get(), + _hackerNewsRepository = + hackerNewsRepository ?? locator.get(), _preferenceRepository = preferenceRepository ?? locator.get(), _sembastRepository = @@ -54,7 +54,7 @@ class NotificationCubit extends Cubit { final AuthBloc _authBloc; final PreferenceCubit _preferenceCubit; - final StoriesRepository _storiesRepository; + final HackerNewsRepository _hackerNewsRepository; final PreferenceRepository _preferenceRepository; final SembastRepository _sembastRepository; String? _username; @@ -82,7 +82,7 @@ class NotificationCubit extends Cubit { for (final int id in commentsToBeLoaded) { Comment? comment = await _sembastRepository.getComment(id: id); - comment ??= await _storiesRepository.fetchComment(id: id); + comment ??= await _hackerNewsRepository.fetchComment(id: id); if (comment != null) { emit( state.copyWith( @@ -160,7 +160,7 @@ class NotificationCubit extends Cubit { for (final int id in commentsToBeLoaded) { Comment? comment = await _sembastRepository.getComment(id: id); - comment ??= await _storiesRepository.fetchComment(id: id); + comment ??= await _hackerNewsRepository.fetchComment(id: id); if (comment != null) { emit(state.copyWith(comments: [...state.comments, comment])); } @@ -184,7 +184,7 @@ class NotificationCubit extends Cubit { } Future _fetchReplies() { - return _storiesRepository + return _hackerNewsRepository .fetchSubmitted(userId: _authBloc.state.username) .then((List? submittedItems) async { if (submittedItems != null) { @@ -194,7 +194,9 @@ class NotificationCubit extends Cubit { ); for (final int id in subscribedItems) { - await _storiesRepository.fetchItem(id: id).then((Item? item) async { + await _hackerNewsRepository + .fetchItem(id: id) + .then((Item? item) async { final List kids = item?.kids ?? []; final List previousKids = (await _sembastRepository.kids(of: id)) ?? []; @@ -216,7 +218,7 @@ class NotificationCubit extends Cubit { ...state.unreadCommentsIds, ]..sort((int lhs, int rhs) => rhs.compareTo(lhs)), ); - await _storiesRepository + await _hackerNewsRepository .fetchComment(id: newCommentId) .then((Comment? comment) { if (comment != null && !comment.dead && !comment.deleted) { diff --git a/lib/cubits/pin/pin_cubit.dart b/lib/cubits/pin/pin_cubit.dart index 992fa0c..44b5d0c 100644 --- a/lib/cubits/pin/pin_cubit.dart +++ b/lib/cubits/pin/pin_cubit.dart @@ -10,24 +10,26 @@ part 'pin_state.dart'; class PinCubit extends Cubit { PinCubit({ PreferenceRepository? preferenceRepository, - StoriesRepository? storiesRepository, + HackerNewsRepository? hackerNewsRepository, }) : _preferenceRepository = preferenceRepository ?? locator.get(), - _storiesRepository = - storiesRepository ?? locator.get(), + _hackerNewsRepository = + hackerNewsRepository ?? locator.get(), super(PinState.init()) { init(); } final PreferenceRepository _preferenceRepository; - final StoriesRepository _storiesRepository; + final HackerNewsRepository _hackerNewsRepository; void init() { emit(PinState.init()); _preferenceRepository.pinnedStoriesIds.then((List ids) { emit(state.copyWith(pinnedStoriesIds: ids)); - _storiesRepository.fetchStoriesStream(ids: ids).listen(_onStoryFetched); + _hackerNewsRepository + .fetchStoriesStream(ids: ids) + .listen(_onStoryFetched); }).whenComplete(() => emit(state.copyWith(status: Status.success))); } diff --git a/lib/cubits/poll/poll_cubit.dart b/lib/cubits/poll/poll_cubit.dart index e86710e..be04b30 100644 --- a/lib/cubits/poll/poll_cubit.dart +++ b/lib/cubits/poll/poll_cubit.dart @@ -11,13 +11,13 @@ part 'poll_state.dart'; class PollCubit extends Cubit { PollCubit({ required Story story, - StoriesRepository? storiesRepository, + HackerNewsRepository? hackerNewsRepository, }) : _story = story, - _storiesRepository = - storiesRepository ?? locator.get(), + _hackerNewsRepository = + hackerNewsRepository ?? locator.get(), super(PollState.init()); - final StoriesRepository _storiesRepository; + final HackerNewsRepository _hackerNewsRepository; final Story _story; Future init({ @@ -33,7 +33,7 @@ class PollCubit extends Cubit { if (pollOptionsIds.isEmpty || refresh) { final Story? updatedStory = - await _storiesRepository.fetchStory(id: _story.id); + await _hackerNewsRepository.fetchStory(id: _story.id); if (updatedStory != null) { pollOptionsIds = updatedStory.parts; @@ -47,7 +47,7 @@ class PollCubit extends Cubit { } if (pollOptionsIds.isNotEmpty) { - final List pollOptions = (await _storiesRepository + final List pollOptions = (await _hackerNewsRepository .fetchPollOptionsStream(ids: pollOptionsIds) .toSet()) .toList(); diff --git a/lib/cubits/user/user_cubit.dart b/lib/cubits/user/user_cubit.dart index c6b3a87..004a4bd 100644 --- a/lib/cubits/user/user_cubit.dart +++ b/lib/cubits/user/user_cubit.dart @@ -7,16 +7,16 @@ import 'package:hacki/repositories/repositories.dart'; part 'user_state.dart'; class UserCubit extends Cubit { - UserCubit({StoriesRepository? storiesRepository}) - : _storiesRepository = - storiesRepository ?? locator.get(), + UserCubit({HackerNewsRepository? hackerNewsRepository}) + : _hackerNewsRepository = + hackerNewsRepository ?? locator.get(), super(const UserState.init()); - final StoriesRepository _storiesRepository; + final HackerNewsRepository _hackerNewsRepository; void init({required String userId}) { emit(state.copyWith(status: Status.inProgress)); - _storiesRepository.fetchUser(id: userId).then((User? user) { + _hackerNewsRepository.fetchUser(id: userId).then((User? user) { emit( state.copyWith( user: user ?? User.emptyWithId(userId), diff --git a/lib/repositories/stories_repository.dart b/lib/repositories/hacker_news_repository.dart similarity index 96% rename from lib/repositories/stories_repository.dart rename to lib/repositories/hacker_news_repository.dart index 1355040..32c1c0e 100644 --- a/lib/repositories/stories_repository.dart +++ b/lib/repositories/hacker_news_repository.dart @@ -6,13 +6,13 @@ import 'package:hacki/services/services.dart'; import 'package:hacki/utils/utils.dart'; import 'package:logger/logger.dart'; -/// [StoriesRepository] is for fetching +/// [HackerNewsRepository] is for fetching /// [Item] such as [Story], [PollOption], [Comment] or [User]. /// /// You can learn more about the Hacker News API at /// https://github.com/HackerNews/API. -class StoriesRepository { - StoriesRepository({ +class HackerNewsRepository { + HackerNewsRepository({ FirebaseClient? firebaseClient, SembastRepository? sembastRepository, Logger? logger, @@ -239,7 +239,9 @@ class StoriesRepository { return comment; }).onError((Object? error, StackTrace stackTrace) { _logger.e(error, stackTrace: stackTrace); - return _sembastRepository.getCachedComment(id: id); + return _sembastRepository + .getCachedComment(id: id) + .then((Comment? value) => value?.copyWith(level: level)); }); if (comment != null) { @@ -267,7 +269,9 @@ class StoriesRepository { return comment; }).onError((Object? error, StackTrace stackTrace) { _logger.e(error, stackTrace: stackTrace); - return _sembastRepository.getCachedComment(id: id); + return _sembastRepository + .getCachedComment(id: id) + .then((Comment? value) => value?.copyWith(level: level)); }); if (comment != null) { diff --git a/lib/repositories/preference_repository.dart b/lib/repositories/preference_repository.dart index cac2188..f22c8b5 100644 --- a/lib/repositories/preference_repository.dart +++ b/lib/repositories/preference_repository.dart @@ -384,7 +384,7 @@ class PreferenceRepository { ); } - Future updateHasRead(int storyId) async { + Future addHasRead(int storyId) async { final String key = _getHasReadKey(storyId); if (Platform.isIOS) { await _syncedPrefs.setBool(key: key, val: true); @@ -398,6 +398,17 @@ class PreferenceRepository { } } + Future removeHasRead(int storyId) async { + final String key = _getHasReadKey(storyId); + if (Platform.isIOS) { + await _syncedPrefs.remove(key: key); + } else { + final SharedPreferences prefs = await _prefs; + + await prefs.remove(_getHasReadKey(storyId)); + } + } + Future clearAllReadStories() async { if (Platform.isIOS) { await _syncedPrefs.clearAll(); diff --git a/lib/repositories/repositories.dart b/lib/repositories/repositories.dart index 34358be..c3d1f19 100644 --- a/lib/repositories/repositories.dart +++ b/lib/repositories/repositories.dart @@ -1,7 +1,7 @@ export 'auth_repository.dart'; +export 'hacker_news_repository.dart'; export 'offline_repository.dart'; export 'post_repository.dart'; export 'preference_repository.dart'; export 'search_repository.dart'; export 'sembast_repository.dart'; -export 'stories_repository.dart'; diff --git a/lib/screens/home/home_screen.dart b/lib/screens/home/home_screen.dart index 1e173b6..5112375 100644 --- a/lib/screens/home/home_screen.dart +++ b/lib/screens/home/home_screen.dart @@ -253,7 +253,7 @@ class _HomeScreenState extends State final int? id = event.itemId; if (id != null) { - locator.get().fetchItem(id: id).then((Item? item) { + locator.get().fetchItem(id: id).then((Item? item) { if (mounted) { if (item != null) { goToItemScreen( @@ -272,7 +272,7 @@ class _HomeScreenState extends State if (storyId == null) return; await locator - .get() + .get() .fetchStory(id: storyId) .then((Story? story) { if (story == null) { @@ -297,7 +297,7 @@ class _HomeScreenState extends State context.read().markAsRead(commentId); await locator - .get() + .get() .fetchStory(id: storyId) .then((Story? story) { if (story == null) { diff --git a/lib/screens/item/item_screen.dart b/lib/screens/item/item_screen.dart index 17d7253..caed7ea 100644 --- a/lib/screens/item/item_screen.dart +++ b/lib/screens/item/item_screen.dart @@ -33,8 +33,8 @@ class ItemScreenArgs extends Equatable { final List? targetComments; /// when the user is trying to view a sub-thread from a main thread, we don't - /// need to fetch comments from [StoriesRepository] since we have some, if not - /// all, comments cached in [CommentCache]. + /// need to fetch comments from [HackerNewsRepository] since we have some, + /// if not all, comments cached in [CommentCache]. final bool useCommentCache; @override diff --git a/lib/screens/profile/profile_screen.dart b/lib/screens/profile/profile_screen.dart index ae34852..f81bff6 100644 --- a/lib/screens/profile/profile_screen.dart +++ b/lib/screens/profile/profile_screen.dart @@ -378,7 +378,7 @@ class _ProfileScreenState extends State void onCommentTapped(Comment comment, {VoidCallback? then}) { throttle.run(() { locator - .get() + .get() .fetchParentStoryWithComments(id: comment.parent) .then(((Story, List)? res) { if (res != null && mounted) { diff --git a/lib/screens/search/search_screen.dart b/lib/screens/search/search_screen.dart index eb03bea..23cc34c 100644 --- a/lib/screens/search/search_screen.dart +++ b/lib/screens/search/search_screen.dart @@ -317,6 +317,9 @@ class _SearchScreenState extends State with ItemActionMixin { label: filter.query, ), ], + const SizedBox( + width: Dimens.pt8, + ), ], ), ), diff --git a/lib/screens/widgets/countdown_reminder.dart b/lib/screens/widgets/countdown_reminder.dart index 2ce53cc..061012f 100644 --- a/lib/screens/widgets/countdown_reminder.dart +++ b/lib/screens/widgets/countdown_reminder.dart @@ -107,7 +107,7 @@ class _CountDownReminderState extends State onTap: () { if (state.storyId != null) { locator - .get() + .get() .fetchStory(id: state.storyId!) .then((Story? story) { if (story == null) { diff --git a/lib/screens/widgets/custom_linkify/linkifiers/code_linkifier.dart b/lib/screens/widgets/custom_linkify/linkifiers/code_linkifier.dart index b2e0b38..397692f 100644 --- a/lib/screens/widgets/custom_linkify/linkifiers/code_linkifier.dart +++ b/lib/screens/widgets/custom_linkify/linkifiers/code_linkifier.dart @@ -27,26 +27,35 @@ class CodeLinkifier extends Linkifier { list.add(element); } else { final String matchedText = match.group(0)!; - final num pos = element.text.indexOf(matchedText); final List splitTexts = element.text.split(matchedText); - int curPos = 0; - bool added = false; + final String preceding = splitTexts[0]; - for (final String text in splitTexts) { - list.addAll(parse([TextElement(text)], options)); + list.addAll( + parse( + [ + TextElement(preceding == '\n\n' ? '' : preceding), + ], + options, + ), + ); - curPos += text.length; + String trimmedText = matchedText + .replaceFirst(_openTag, '') + .replaceFirst(_closeTag, '') + .replaceAll('\n\n', '\n'); + trimmedText = '$trimmedText\n\n'; - if (!added && curPos >= pos) { - added = true; - final String trimmedText = matchedText - .replaceFirst(_openTag, '') - .replaceFirst(_closeTag, '') - .replaceAll('\n\n', '\n'); - list.add(CodeElement(trimmedText)); - } - } + list + ..add(CodeElement(trimmedText)) + ..addAll( + parse( + [ + TextElement(splitTexts[1]), + ], + options, + ), + ); } } else { list.add(element); diff --git a/lib/screens/widgets/stories_list_view.dart b/lib/screens/widgets/stories_list_view.dart index 51c111a..1d2d36a 100644 --- a/lib/screens/widgets/stories_list_view.dart +++ b/lib/screens/widgets/stories_list_view.dart @@ -146,6 +146,7 @@ class _StoriesListViewState extends State onMoreTapped: onMoreTapped, itemBuilder: (Widget child, Story story) { return Slidable( + key: ValueKey(story), enabled: !preferenceState.swipeGestureEnabled, startActionPane: ActionPane( motion: const BehindMotion(), @@ -179,6 +180,31 @@ class _StoriesListViewState extends State ), ], ), + endActionPane: ActionPane( + motion: const BehindMotion(), + dismissible: DismissiblePane( + closeOnCancel: true, + confirmDismiss: () async { + mark(story); + return false; + }, + onDismissed: () {}, + ), + children: [ + SlidableAction( + onPressed: (_) { + HapticFeedbackUtil.light(); + mark(story); + }, + backgroundColor: Theme.of(context).primaryColor, + foregroundColor: + Theme.of(context).colorScheme.onPrimary, + icon: state.readStoriesIds.contains(story.id) + ? Icons.visibility_off + : Icons.visibility, + ), + ], + ), child: OptionalWrapper( enabled: context .read() @@ -212,6 +238,15 @@ class _StoriesListViewState extends State ); } + void mark(Story story) { + final StoriesBloc storiesBloc = context.read(); + if (storiesBloc.state.readStoriesIds.contains(story.id)) { + context.read().add(StoryUnread(story: story)); + } else { + context.read().add(StoryRead(story: story)); + } + } + void loadMoreStories() => context.read().add(StoriesLoadMore(type: widget.storyType)); } diff --git a/lib/services/fetcher.dart b/lib/services/fetcher.dart index 7a8d2be..0c43eb4 100644 --- a/lib/services/fetcher.dart +++ b/lib/services/fetcher.dart @@ -44,7 +44,7 @@ abstract class Fetcher { logger: logger, ); - final StoriesRepository storiesRepository = StoriesRepository(); + final HackerNewsRepository hackerNewsRepository = HackerNewsRepository(); final SembastRepository sembastRepository = SembastRepository(); final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = @@ -57,7 +57,7 @@ abstract class Fetcher { Comment? newReply; - await storiesRepository + await hackerNewsRepository .fetchSubmitted(userId: username) .then((List? submittedItems) async { if (submittedItems != null) { @@ -67,7 +67,9 @@ abstract class Fetcher { ); for (final int id in subscribedItems) { - await storiesRepository.fetchRawItem(id: id).then((Item? item) async { + await hackerNewsRepository + .fetchRawItem(id: id) + .then((Item? item) async { final List kids = item?.kids ?? []; final List previousKids = (await sembastRepository.kids(of: id)) ?? []; @@ -81,7 +83,7 @@ abstract class Fetcher { for (final int newCommentId in diff) { if (unreadIds.contains(newCommentId)) continue; - await storiesRepository + await hackerNewsRepository .fetchRawComment(id: newCommentId) .then((Comment? comment) async { final bool hasPushedBefore = @@ -113,7 +115,7 @@ abstract class Fetcher { // pushed before. if (newReply != null) { final Story? story = - await storiesRepository.fetchRawParentStory(id: newReply!.id); + await hackerNewsRepository.fetchRawParentStory(id: newReply!.id); final String text = HtmlUtil.parseHtml(newReply!.text); if (story != null) { diff --git a/lib/services/web_analyzer.dart b/lib/services/web_analyzer.dart index eaa7e9e..fad6130 100644 --- a/lib/services/web_analyzer.dart +++ b/lib/services/web_analyzer.dart @@ -288,14 +288,14 @@ class WebAnalyzer { } static Future _fetchInfoFromStory(List meta) async { - final StoriesRepository storiesRepository = StoriesRepository(); + final HackerNewsRepository hackerNewsRepository = HackerNewsRepository(); final int storyId = meta.first; List kids = meta.sublist(1, meta.length); // Kids of stories from search results are always empty, so here we try // to fetch the story itself first and see if the kids are still empty. if (kids.isEmpty) { - final Story? story = await storiesRepository.fetchStory(id: storyId); + final Story? story = await hackerNewsRepository.fetchStory(id: storyId); if (story == null) return null; @@ -305,7 +305,7 @@ class WebAnalyzer { } final Comment? comment = - await storiesRepository.fetchComment(id: kids.first); + await hackerNewsRepository.fetchComment(id: kids.first); return comment != null ? '${comment.by}: ${comment.text}' : null; } diff --git a/lib/utils/html_util.dart b/lib/utils/html_util.dart index f43d04b..5de0d81 100644 --- a/lib/utils/html_util.dart +++ b/lib/utils/html_util.dart @@ -34,7 +34,11 @@ abstract class HtmlUtil { static String parseHtml(String text) { return HtmlUnescape() .convert(text) - .replaceAll('

', '\n') + .replaceAll('\n', '') + .replaceAllMapped( + RegExp(r'\(.*?)\', dotAll: true), + (Match match) => '\n${match[1]?.replaceAll('\n', ' ')}\n', + ) .replaceAllMapped( RegExp(r'\(.*?)\<\/i\>'), (Match match) => '*${match[1]}*', @@ -43,6 +47,7 @@ abstract class HtmlUtil { RegExp(r'\.*?\<\/a\>'), (Match match) => match[1] ?? '', ) - .replaceAll('\n', '\n\n'); + .replaceAll('\n', '\n\n') + .replaceAll('

', '\n\n'); } } diff --git a/lib/utils/link_util.dart b/lib/utils/link_util.dart index 1cbe672..b21b560 100644 --- a/lib/utils/link_util.dart +++ b/lib/utils/link_util.dart @@ -52,7 +52,10 @@ abstract class LinkUtil { if (useHackiForHnLink && link.isStoryLink) { final int? id = link.itemId; if (id != null) { - locator.get().fetchItem(id: id).then((Item? item) { + locator + .get() + .fetchItem(id: id) + .then((Item? item) { if (item != null) { router.push( '/${ItemScreen.routeName}', diff --git a/pubspec.yaml b/pubspec.yaml index db930d0..4fd423f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: hacki description: A Hacker News reader. -version: 2.3.2+131 +version: 2.4.0+131 publish_to: none environment: diff --git a/test/blocs/auth/auth_bloc_test.dart b/test/blocs/auth/auth_bloc_test.dart index 3b318f2..22f7495 100644 --- a/test/blocs/auth/auth_bloc_test.dart +++ b/test/blocs/auth/auth_bloc_test.dart @@ -9,7 +9,7 @@ class MockAuthRepository extends Mock implements AuthRepository {} class MockPreferenceRepository extends Mock implements PreferenceRepository {} -class MockStoriesRepository extends Mock implements StoriesRepository {} +class MockHackerNewsRepository extends Mock implements HackerNewsRepository {} class MockSembastRepository extends Mock implements SembastRepository {} @@ -17,7 +17,8 @@ void main() { final MockAuthRepository mockAuthRepository = MockAuthRepository(); final MockPreferenceRepository mockPreferenceRepository = MockPreferenceRepository(); - final MockStoriesRepository mockStoriesRepository = MockStoriesRepository(); + final MockHackerNewsRepository mockHackerNewsRepository = + MockHackerNewsRepository(); final MockSembastRepository mockSembastRepository = MockSembastRepository(); const int created = 0; @@ -49,7 +50,7 @@ void main() { AuthBloc( authRepository: mockAuthRepository, preferenceRepository: mockPreferenceRepository, - storiesRepository: mockStoriesRepository, + hackerNewsRepository: mockHackerNewsRepository, sembastRepository: mockSembastRepository, ).state, equals(const AuthState.init()), @@ -67,7 +68,7 @@ void main() { .thenAnswer((_) => Future.value(username)); when(() => mockAuthRepository.password) .thenAnswer((_) => Future.value(password)); - when(() => mockStoriesRepository.fetchUser(id: username)) + when(() => mockHackerNewsRepository.fetchUser(id: username)) .thenAnswer((_) => Future.value(tUser)); when(() => mockAuthRepository.loggedIn) .thenAnswer((_) => Future.value(false)); @@ -79,7 +80,7 @@ void main() { return AuthBloc( authRepository: mockAuthRepository, preferenceRepository: mockPreferenceRepository, - storiesRepository: mockStoriesRepository, + hackerNewsRepository: mockHackerNewsRepository, sembastRepository: mockSembastRepository, ); }, @@ -91,7 +92,7 @@ void main() { verify: (_) { verify(() => mockAuthRepository.loggedIn).called(2); verifyNever(() => mockAuthRepository.username); - verifyNever(() => mockStoriesRepository.fetchUser(id: username)); + verifyNever(() => mockHackerNewsRepository.fetchUser(id: username)); }, ); @@ -107,7 +108,7 @@ void main() { return AuthBloc( authRepository: mockAuthRepository, preferenceRepository: mockPreferenceRepository, - storiesRepository: mockStoriesRepository, + hackerNewsRepository: mockHackerNewsRepository, sembastRepository: mockSembastRepository, ); }, @@ -154,7 +155,8 @@ void main() { password: password, ), ).called(1); - verify(() => mockStoriesRepository.fetchUser(id: username)).called(1); + verify(() => mockHackerNewsRepository.fetchUser(id: username)) + .called(1); }, ); });