Skip to content

Commit

Permalink
WIP fetchMore again
Browse files Browse the repository at this point in the history
  • Loading branch information
felipebueno committed Jun 27, 2023
1 parent 3010593 commit ab19c16
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 107 deletions.
101 changes: 101 additions & 0 deletions lib/data/models/post_type.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import 'package:flutter/material.dart';

enum PostType {
top,
bitcoin,
nostr,
tech,
meta,
job;

String get endpoint {
if (this == PostType.top) {
return 'top/posts/day.json?when=day';
}

return '~/$name.json?sub=$name';
}

String get name {
switch (this) {
case PostType.top:
return 'top';

case PostType.bitcoin:
return 'bitcoin';

case PostType.nostr:
return 'nostr';

case PostType.tech:
return 'tech';

case PostType.meta:
return 'meta';

case PostType.job:
return 'jobs';

default:
return '';
}
}

String get title {
switch (this) {
case PostType.top:
return 'Top';

case PostType.bitcoin:
return 'Bitcoin';

case PostType.nostr:
return 'Nostr';

case PostType.tech:
return 'Tech';

case PostType.meta:
return 'Meta';

case PostType.job:
return 'Jobs';

default:
return '';
}
}

IconData get icon {
switch (this) {
case PostType.top:
return Icons.new_releases;

case PostType.bitcoin:
return Icons.monetization_on;

case PostType.nostr:
return Icons.chat_bubble_sharp;

case PostType.tech:
return Icons.computer;

case PostType.meta:
return Icons.info;

case PostType.job:
return Icons.work;

default:
return Icons.new_releases;
}
}

String getBody(String cursor) {
if (this == PostType.top) {
return '{"operationName":"topItems","variables":{"when":"day","cursor":"$cursor"},"query":"fragment ItemFields on Item {\\n id\\n parentId\\n createdAt\\n deletedAt\\n title\\n url\\n user {\\n name\\n streak\\n hideCowboyHat\\n id\\n __typename\\n }\\n fwdUserId\\n otsHash\\n position\\n sats\\n boost\\n bounty\\n bountyPaidTo\\n path\\n upvotes\\n meSats\\n meDontLike\\n meBookmark\\n meSubscription\\n outlawed\\n freebie\\n ncomments\\n commentSats\\n lastCommentAt\\n maxBid\\n isJob\\n company\\n location\\n remote\\n subName\\n pollCost\\n status\\n uploadId\\n mine\\n __typename\\n}\\n\\nquery topItems(\$sort: String, \$cursor: String, \$when: String) {\\n topItems(sort: \$sort, cursor: \$cursor, when: \$when) {\\n cursor\\n items {\\n ...ItemFields\\n __typename\\n }\\n pins {\\n ...ItemFields\\n __typename\\n }\\n __typename\\n }\\n}\\n"}';
}

return '{"operationName":"items","variables":{"sub":"$name","cursor":"$cursor"},"query":"fragment ItemFields on Item {\\n id\\n parentId\\n createdAt\\n deletedAt\\n title\\n url\\n user {\\n name\\n streak\\n hideCowboyHat\\n id\\n __typename\\n }\\n fwdUserId\\n otsHash\\n position\\n sats\\n boost\\n bounty\\n bountyPaidTo\\n path\\n upvotes\\n meSats\\n meDontLike\\n meBookmark\\n meSubscription\\n outlawed\\n freebie\\n ncomments\\n commentSats\\n lastCommentAt\\n maxBid\\n isJob\\n company\\n location\\n remote\\n subName\\n pollCost\\n status\\n uploadId\\n mine\\n __typename\\n}\\n\\nquery items(\$sub: String, \$sort: String, \$type: String, \$cursor: String, \$name: String, \$within: String) {\\n items(\\n sub: \$sub\\n sort: \$sort\\n type: \$type\\n cursor: \$cursor\\n name: \$name\\n within: \$within\\n ) {\\n cursor\\n items {\\n ...ItemFields\\n __typename\\n }\\n pins {\\n ...ItemFields\\n __typename\\n }\\n __typename\\n }\\n}\\n"}';
}
}
84 changes: 20 additions & 64 deletions lib/data/sn_api.dart
Original file line number Diff line number Diff line change
@@ -1,17 +1,9 @@
import 'package:dio/dio.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:stacker_news/data/models/item.dart';
import 'package:stacker_news/data/models/post_type.dart';
import 'package:stacker_news/data/models/user.dart';

enum PostType {
top,
bitcoin,
nostr,
tech,
meta,
job,
}

final class Api {
final Dio _dio = Dio(
BaseOptions(
Expand All @@ -38,56 +30,25 @@ final class Api {
}
// START Posts / Items
Future<List<Item>> fetchPosts(PostType postType) async {
String stories = '';

switch (postType) {
case PostType.top:
stories = 'top/posts/day.json?when=day';

break;

case PostType.bitcoin:
stories = '~/bitcoin.json?sub=bitcoin';

break;

case PostType.nostr:
stories = '~/nostr.json?sub=bitcoin';

break;

case PostType.tech:
stories = '~/tech.json?sub=tech';

case PostType.meta:
stories = '~/meta.json?sub=meta';

case PostType.job:
stories = '~/jobs.json?sub=jobs';

break;

default:
break;
}
String endpoint = postType.endpoint;

String? currCommit = await _getCurrBuildId();

final response = await _dio.get('/$currCommit/$stories');
final response = await _dio.get('/$currCommit/$endpoint');

if (response.statusCode == 200) {
return await _parseItems(response.data);
return await _parseItems(response.data, postType);
}

if (response.statusCode == 404) {
await _fetchAndSaveCurrBuildId();

currCommit = await _getCurrBuildId();

final retryResponse = await _dio.get('/$currCommit/$stories');
final retryResponse = await _dio.get('/$currCommit/$endpoint');

if (retryResponse.statusCode == 200) {
return await _parseItems(retryResponse.data);
return await _parseItems(retryResponse.data, postType);
} else {
throw Exception('Error fetching posts');
}
Expand All @@ -96,32 +57,23 @@ final class Api {
}
}

Future<List<Item>> _parseItems(dynamic responseData) async {
Future<List<Item>> _parseItems(
dynamic responseData,
PostType postType,
) async {
final data = (responseData['pageProps'] ?? responseData)['data'];
final itemsMap = (data['items'] ?? data['topItems']);
final List items = itemsMap['items'];

// Get cursor from list
final cursor = itemsMap['cursor'];
// Save cursor to shared prefs
if (cursor != null) {
await _saveCursor(cursor);
final prefs = await SharedPreferences.getInstance();
await prefs.setString('${postType.name}-cursor', cursor);
}

return items.map((item) => Item.fromJson(item)).toList();
}

Future<void> _saveCursor(String cursor) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('cursor', cursor);
}

Future<String?> _getCursor() async {
final prefs = await SharedPreferences.getInstance();

return prefs.getString('cursor');
}

Future<void> _fetchAndSaveCurrBuildId() async {
final response = await _dio.get('https://stacker.news');

Expand Down Expand Up @@ -152,16 +104,20 @@ final class Api {
}

Future<List<Item>> fetchMorePosts(PostType postType) async {
final cursor = await _getCursor();
final prefs = await SharedPreferences.getInstance();
final cursor = prefs.getString('${postType.name}-cursor');

if (cursor == null) {
throw Exception('Error fetching more');
}

final response = await _dio.post(
'https://stacker.news/api/graphql',
data:
'{\"operationName\":\"topItems\",\"variables\":{\"when\":\"day\",\"cursor\":\"$cursor\"},\"query\":\"fragment ItemFields on Item {\\n id\\n parentId\\n createdAt\\n deletedAt\\n title\\n url\\n user {\\n name\\n streak\\n hideCowboyHat\\n id\\n __typename\\n }\\n fwdUserId\\n otsHash\\n position\\n sats\\n boost\\n bounty\\n bountyPaidTo\\n path\\n upvotes\\n meSats\\n meDontLike\\n meBookmark\\n meSubscription\\n outlawed\\n freebie\\n ncomments\\n commentSats\\n lastCommentAt\\n maxBid\\n isJob\\n company\\n location\\n remote\\n subName\\n pollCost\\n status\\n uploadId\\n mine\\n __typename\\n}\\n\\nquery topItems(\$sort: String, \$cursor: String, \$when: String) {\\n topItems(sort: \$sort, cursor: \$cursor, when: \$when) {\\n cursor\\n items {\\n ...ItemFields\\n __typename\\n }\\n pins {\\n ...ItemFields\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}',
data: postType.getBody(cursor),
);

if (response.statusCode == 200) {
return await _parseItems(response.data);
return await _parseItems(response.data, postType);
}

throw Exception('Error fetching more');
Expand Down
51 changes: 13 additions & 38 deletions lib/views/pages/home_page.dart
Original file line number Diff line number Diff line change
@@ -1,53 +1,28 @@
import 'package:flutter/material.dart';
import 'package:stacker_news/data/sn_api.dart';
import 'package:stacker_news/data/models/post_type.dart';
import 'package:stacker_news/views/widgets/base_tab.dart';
import 'package:stacker_news/views/widgets/generic_page_scaffold.dart';
import 'package:stacker_news/views/widgets/sn_logo.dart';

class HomePage extends StatelessWidget {
static const String id = 'home';

// TODO: Clean up / Refactor this
final List<Tab> tabs = const [
Tab(
icon: Icon(Icons.new_releases),
child: SizedBox(width: 64, child: Center(child: Text('Top'))),
),
Tab(
icon: Icon(Icons.attach_money),
child: SizedBox(width: 64, child: Center(child: Text('Bitcoin'))),
),
Tab(
icon: Icon(Icons.chat_bubble_sharp),
child: SizedBox(width: 64, child: Center(child: Text('Nostr'))),
),
Tab(
icon: Icon(Icons.abc),
child: SizedBox(width: 64, child: Center(child: Text('Tech'))),
),
Tab(
icon: Icon(Icons.merge_type),
child: SizedBox(width: 64, child: Center(child: Text('Meta'))),
),
Tab(
icon: Icon(Icons.handshake_sharp),
child: SizedBox(width: 64, child: Center(child: Text('Jobs'))),
),
];

final List<Widget> tabViews = const [
BaseTab(postType: PostType.top),
BaseTab(postType: PostType.bitcoin),
BaseTab(postType: PostType.nostr),
BaseTab(postType: PostType.tech),
BaseTab(postType: PostType.meta),
BaseTab(postType: PostType.job),
];

const HomePage({super.key});

@override
Widget build(BuildContext context) {
final tabs = PostType.values
.map((t) => Tab(
icon: Icon(t.icon),
child: SizedBox(
width: 64,
child: Center(child: Text(t.title)),
),
))
.toList();

final tabViews = PostType.values.map((t) => BaseTab(postType: t)).toList();

return DefaultTabController(
length: tabs.length,
child: GenericPageScaffold(
Expand Down
13 changes: 8 additions & 5 deletions lib/views/widgets/base_tab.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:stacker_news/data/models/item.dart';
import 'package:stacker_news/data/models/post_type.dart';
import 'package:stacker_news/data/sn_api.dart';
import 'package:stacker_news/utils.dart';
import 'package:stacker_news/views/widgets/post_item.dart';
Expand All @@ -23,15 +24,17 @@ class _BaseTabState extends State<BaseTab> with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;

Future<List<Item>> _fetchPosts() async =>
await Api().fetchPosts(widget.postType);
final Api _api = Api();

Future<List<Item>> _fetchInitialPosts() async =>
await _api.fetchPosts(widget.postType);

@override
Widget build(BuildContext context) {
super.build(context);

return FutureBuilder(
future: _fetchPosts(),
future: _fetchInitialPosts(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
Expand All @@ -49,7 +52,7 @@ class _BaseTabState extends State<BaseTab> with AutomaticKeepAliveClientMixin {
children: [
Expanded(
child: RefreshIndicator(
onRefresh: _fetchPosts,
onRefresh: _fetchInitialPosts,
child: posts.isEmpty
? const Center(child: Text('No posts found'))
: ListView.separated(
Expand Down Expand Up @@ -82,7 +85,7 @@ class _BaseTabState extends State<BaseTab> with AutomaticKeepAliveClientMixin {
icon: const Icon(Icons.add),
label: const Text('MORE'),
onPressed: () async {
final items = await Api().fetchMorePosts(widget.postType);
final items = await _api.fetchMorePosts(widget.postType);

print(items);
},
Expand Down

0 comments on commit ab19c16

Please sign in to comment.