Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: fetch stories from web directly instead of firebase api. #448

Merged
merged 8 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion assets/remote-config-dev.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,13 @@
"commentTextSelector": "td > table > tbody > tr > td.default > div.comment > div.commtext",
"commentHeadSelector": "td > table > tbody > tr > td.default > div > span > a",
"commentAgeSelector": "td > table > tbody > tr > td.default > div > span > span.age",
"commentIndentSelector": "td > table > tbody > tr > td.ind"
"commentIndentSelector": "td > table > tbody > tr > td.ind",
"storySelector": "#hnmain > tbody > tr > td > table > tbody > .athing",
"subtextSelector": "#hnmain > tbody > tr > td > table > tbody > tr > .subtext",
"titlelineSelector": ".title > .titleline > a",
"pointSelector": ".subline > .score",
"userSelector": ".subline > .hnuser",
"ageSelector": ".subline > .age",
"cmtCountSelector": ".subline > a",
"moreLinkSelector": "#hnmain > tbody > tr:nth-child(3) > td > table > tbody > tr > td.title > a"
}
10 changes: 9 additions & 1 deletion assets/remote-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,13 @@
"commentTextSelector": "td > table > tbody > tr > td.default > div.comment > div.commtext",
"commentHeadSelector": "td > table > tbody > tr > td.default > div > span > a",
"commentAgeSelector": "td > table > tbody > tr > td.default > div > span > span.age",
"commentIndentSelector": "td > table > tbody > tr > td.ind"
"commentIndentSelector": "td > table > tbody > tr > td.ind",
"storySelector": "#hnmain > tbody > tr > td > table > tbody > .athing",
"subtextSelector": "#hnmain > tbody > tr > td > table > tbody > tr > .subtext",
"titlelineSelector": ".title > .titleline > a",
"pointSelector": ".subline > .score",
"userSelector": ".subline > .hnuser",
"ageSelector": ".subline > .age",
"cmtCountSelector": ".subline > a",
"moreLinkSelector": "#hnmain > tbody > tr:nth-child(3) > td > table > tbody > tr > td.title > a"
}
1 change: 1 addition & 0 deletions fastlane/metadata/android/en-US/changelogs/148.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- UX improvements.
2 changes: 1 addition & 1 deletion ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ SPEC CHECKSUMS:
synced_shared_preferences: f722742b06d65c7315b8e9f56b794c9fbd5597f7
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1
webview_flutter_wkwebview: be0f0d33777f1bfd0c9fdcb594786704dbf65f36
webview_flutter_wkwebview: 2a23822e9039b7b1bc52e5add778e5d89ad488d1
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6

PODFILE CHECKSUM: f03c7c11cf2b623592c89c68c628682778bb78b4
Expand Down
156 changes: 80 additions & 76 deletions lib/blocs/stories/stories_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import 'dart:math';
import 'package:bloc/bloc.dart';
import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:equatable/equatable.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/config/locator.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/models/models.dart';
Expand All @@ -23,6 +22,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
required FilterCubit filterCubit,
OfflineRepository? offlineRepository,
HackerNewsRepository? hackerNewsRepository,
HackerNewsWebRepository? hackerNewsWebRepository,
PreferenceRepository? preferenceRepository,
Logger? logger,
}) : _preferenceCubit = preferenceCubit,
Expand All @@ -31,13 +31,15 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
offlineRepository ?? locator.get<OfflineRepository>(),
_hackerNewsRepository =
hackerNewsRepository ?? locator.get<HackerNewsRepository>(),
_hackerNewsWebRepository =
hackerNewsWebRepository ?? locator.get<HackerNewsWebRepository>(),
_preferenceRepository =
preferenceRepository ?? locator.get<PreferenceRepository>(),
_logger = logger ?? locator.get<Logger>(),
super(const StoriesState.init()) {
on<LoadStories>(
onLoadStories,
transformer: concurrent(),
transformer: sequential(),
);
on<StoriesInitialize>(onInitialize);
on<StoriesRefresh>(onRefresh);
Expand All @@ -61,39 +63,20 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
final FilterCubit _filterCubit;
final OfflineRepository _offlineRepository;
final HackerNewsRepository _hackerNewsRepository;
final HackerNewsWebRepository _hackerNewsWebRepository;
final PreferenceRepository _preferenceRepository;
final Logger _logger;
DeviceScreenType? deviceScreenType;
StreamSubscription<PreferenceState>? _preferenceSubscription;
static const int _smallPageSize = 10;
static const int _largePageSize = 20;
static const int _tabletSmallPageSize = 15;
static const int _tabletLargePageSize = 25;
static const String _logPrefix = '[StoriesBloc]';
static const int apiPageSize = 30;

Future<void> onInitialize(
StoriesInitialize event,
Emitter<StoriesState> emit,
) async {
_preferenceSubscription ??= _preferenceCubit.stream
.distinct((PreferenceState previous, PreferenceState next) {
return previous.isComplexStoryTileEnabled ==
next.isComplexStoryTileEnabled;
})
.debounceTime(AppDurations.oneSecond)
.listen((PreferenceState event) {
final bool isComplexTile = event.isComplexStoryTileEnabled;
final int pageSize = getPageSize(isComplexTile: isComplexTile);

if (pageSize != state.currentPageSize) {
add(StoriesPageSizeChanged(pageSize: pageSize));
}
});
final bool isComplexTile = _preferenceCubit.state.isComplexStoryTileEnabled;
final int pageSize = getPageSize(isComplexTile: isComplexTile);
emit(
const StoriesState.init().copyWith(
currentPageSize: pageSize,
downloadStatus: state.downloadStatus,
storiesDownloaded: state.storiesDownloaded,
storiesToBeDownloaded: state.storiesToBeDownloaded,
Expand Down Expand Up @@ -121,12 +104,10 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
.copyWithStatusUpdated(type: type, to: Status.inProgress),
);
_offlineRepository
.getCachedStoriesStream(
ids: ids.sublist(0, min(ids.length, state.currentPageSize)),
)
.getCachedStoriesStream(ids: ids)
.listen((Story story) => add(StoryLoaded(story: story, type: type)))
.onDone(() => add(StoryLoadingCompleted(type: type)));
} else {
} else if (event.useApi) {
final List<int> ids =
await _hackerNewsRepository.fetchStoryIds(type: type);
emit(
Expand All @@ -135,15 +116,37 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
.copyWithCurrentPageUpdated(type: type, to: 0)
.copyWithStatusUpdated(type: type, to: Status.inProgress),
);

await _hackerNewsRepository
.fetchStoriesStream(
ids: ids.sublist(0, state.currentPageSize),
ids: ids.sublist(0, apiPageSize),
sequential: _preferenceCubit.state.isComplexStoryTileEnabled ||
_preferenceCubit.state.isFaviconEnabled,
)
.listen((Story story) {
add(StoryLoaded(story: story, type: type));
}).asFuture<void>();
} else {
emit(
state
.copyWithCurrentPageUpdated(type: type, to: 0)
.copyWithStatusUpdated(type: type, to: Status.inProgress),
);

await _hackerNewsWebRepository
.fetchStoriesStream(event.type, page: 1)
.handleError((dynamic e) {
_logger.e('$_logPrefix error loading stories $e');

switch (e.runtimeType) {
case RateLimitedException:
case RateLimitedWithFallbackException:
case PossibleParsingException:
add(event.copyWith(useApi: true));
}
}).listen((Story story) {
add(StoryLoaded(story: story, type: type));
}).asFuture<void>();
add(StoryLoadingCompleted(type: type));
}
}
Expand Down Expand Up @@ -174,7 +177,10 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
}
}

void onLoadMore(StoriesLoadMore event, Emitter<StoriesState> emit) {
Future<void> onLoadMore(
StoriesLoadMore event,
Emitter<StoriesState> emit,
) async {
if (state.statusByType[event.type] == Status.inProgress) return;

emit(
Expand All @@ -184,52 +190,60 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
),
);

final int currentPage = state.currentPageByType[event.type]!;
final int len = state.storyIdsByType[event.type]!.length;
final int currentPage = state.currentPageByType[event.type]! + 1;
emit(
state.copyWithCurrentPageUpdated(type: event.type, to: currentPage + 1),
state.copyWithCurrentPageUpdated(type: event.type, to: currentPage),
);
final int currentPageSize = state.currentPageSize;
final int lower = currentPageSize * (currentPage + 1);
int upper = currentPageSize + lower;

if (len > lower) {
if (len < upper) {
upper = len;
}

if (state.isOfflineReading) {
_offlineRepository
.getCachedStoriesStream(
ids: state.storyIdsByType[event.type]!.sublist(
lower,
upper,
),
)
.listen(
(Story story) => add(StoryLoaded(story: story, type: event.type)),
)
.onDone(() => add(StoryLoadingCompleted(type: event.type)));
} else {
_hackerNewsRepository
.fetchStoriesStream(
ids: state.storyIdsByType[event.type]!.sublist(
lower,
upper,
),
)
.listen(
(Story story) => add(StoryLoaded(story: story, type: event.type)),
)
.onDone(() => add(StoryLoadingCompleted(type: event.type)));
}
} else {
if (state.isOfflineReading) {
emit(
state.copyWithStatusUpdated(
type: event.type,
to: Status.success,
),
);
} else if (event.useApi) {
late final int length;
final List<int>? ids = state.storyIdsByType[event.type];

if (ids?.isEmpty ?? true) {
final List<int> ids =
await _hackerNewsRepository.fetchStoryIds(type: event.type);
length = ids.length;
} else {
length = ids!.length;
}

final int lower = apiPageSize * currentPage;
final int upper = min(length, lower + apiPageSize);
_hackerNewsRepository
.fetchStoriesStream(
ids: state.storyIdsByType[event.type]!.sublist(
lower,
upper,
),
)
.listen(
(Story story) => add(StoryLoaded(story: story, type: event.type)),
)
.onDone(() => add(StoryLoadingCompleted(type: event.type)));
} else {
_hackerNewsWebRepository
.fetchStoriesStream(event.type, page: currentPage)
.handleError((dynamic e) {
_logger.e('$_logPrefix error loading more stories $e');

switch (e.runtimeType) {
case RateLimitedException:
case RateLimitedWithFallbackException:
case PossibleParsingException:
add(event.copyWith(useApi: true));
}
})
.listen(
(Story story) => add(StoryLoaded(story: story, type: event.type)),
)
.onDone(() => add(StoryLoadingCompleted(type: event.type)));
}
}

Expand Down Expand Up @@ -520,16 +534,6 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {

bool hasRead(Story story) => state.readStoriesIds.contains(story.id);

int getPageSize({required bool isComplexTile}) {
int pageSize = isComplexTile ? _smallPageSize : _largePageSize;

if (deviceScreenType != DeviceScreenType.mobile) {
pageSize = isComplexTile ? _tabletSmallPageSize : _tabletLargePageSize;
}

return pageSize;
}

@override
Future<void> close() async {
await _preferenceSubscription?.cancel();
Expand Down
28 changes: 25 additions & 3 deletions lib/blocs/stories/stories_event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,25 @@ abstract class StoriesEvent extends Equatable {
}

class LoadStories extends StoriesEvent {
LoadStories({required this.type, this.isRefreshing = false});
LoadStories({
required this.type,
this.isRefreshing = false,
this.useApi = false,
});

final StoryType type;
final bool isRefreshing;
final bool useApi;

LoadStories copyWith({required bool useApi}) {
return LoadStories(type: type, isRefreshing: isRefreshing, useApi: useApi);
}

@override
List<Object?> get props => <Object?>[
type,
isRefreshing,
useApi,
];
}

Expand All @@ -33,12 +43,24 @@ class StoriesRefresh extends StoriesEvent {
}

class StoriesLoadMore extends StoriesEvent {
StoriesLoadMore({required this.type});
StoriesLoadMore({
required this.type,
this.useApi = false,
});

final StoryType type;

final bool useApi;

StoriesLoadMore copyWith({required bool useApi}) {
return StoriesLoadMore(type: type, useApi: useApi);
}

@override
List<Object?> get props => <Object?>[type];
List<Object?> get props => <Object?>[
type,
useApi,
];
}

class StoriesDownload extends StoriesEvent {
Expand Down
6 changes: 0 additions & 6 deletions lib/blocs/stories/stories_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ class StoriesState extends Equatable {
required this.readStoriesIds,
required this.isOfflineReading,
required this.downloadStatus,
required this.currentPageSize,
required this.storiesDownloaded,
required this.storiesToBeDownloaded,
});
Expand Down Expand Up @@ -53,7 +52,6 @@ class StoriesState extends Equatable {
},
}) : isOfflineReading = false,
downloadStatus = StoriesDownloadStatus.idle,
currentPageSize = 0,
readStoriesIds = const <int>{},
storiesDownloaded = 0,
storiesToBeDownloaded = 0;
Expand All @@ -65,7 +63,6 @@ class StoriesState extends Equatable {
final Set<int> readStoriesIds;
final StoriesDownloadStatus downloadStatus;
final bool isOfflineReading;
final int currentPageSize;
final int storiesDownloaded;
final int storiesToBeDownloaded;

Expand All @@ -77,7 +74,6 @@ class StoriesState extends Equatable {
Set<int>? readStoriesIds,
StoriesDownloadStatus? downloadStatus,
bool? isOfflineReading,
int? currentPageSize,
int? storiesDownloaded,
int? storiesToBeDownloaded,
}) {
Expand All @@ -89,7 +85,6 @@ class StoriesState extends Equatable {
readStoriesIds: readStoriesIds ?? this.readStoriesIds,
isOfflineReading: isOfflineReading ?? this.isOfflineReading,
downloadStatus: downloadStatus ?? this.downloadStatus,
currentPageSize: currentPageSize ?? this.currentPageSize,
storiesDownloaded: storiesDownloaded ?? this.storiesDownloaded,
storiesToBeDownloaded:
storiesToBeDownloaded ?? this.storiesToBeDownloaded,
Expand Down Expand Up @@ -179,7 +174,6 @@ class StoriesState extends Equatable {
readStoriesIds,
isOfflineReading,
downloadStatus,
currentPageSize,
storiesDownloaded,
storiesToBeDownloaded,
];
Expand Down
Loading
Loading