Merge pull request #101 from krawieck/refresh
This commit is contained in:
commit
68b740e529
|
@ -0,0 +1,44 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
|
||||
import 'memo_future.dart';
|
||||
|
||||
class Refreshable<T> {
|
||||
const Refreshable({@required this.snapshot, @required this.refresh})
|
||||
: assert(snapshot != null),
|
||||
assert(refresh != null);
|
||||
|
||||
final AsyncSnapshot<T> snapshot;
|
||||
final AsyncCallback refresh;
|
||||
}
|
||||
|
||||
/// Similar to [useMemoFuture] but adds a `.refresh` method which
|
||||
/// allows to re-run the fetcher. Calling `.refresh` will not
|
||||
/// turn AsyncSnapshot into a loading state. Instead it will
|
||||
/// replace the ready state with the new data when available
|
||||
///
|
||||
/// `keys` will re-run the initial fetching thus yielding a
|
||||
/// loading state in the AsyncSnapshot
|
||||
Refreshable<T> useRefreshable<T>(AsyncValueGetter<T> fetcher,
|
||||
[List<Object> keys = const <dynamic>[]]) {
|
||||
final newData = useState<T>(null);
|
||||
final snapshot = useMemoFuture(() async {
|
||||
newData.value = null;
|
||||
return fetcher();
|
||||
}, keys);
|
||||
|
||||
final outSnapshot = () {
|
||||
if (newData.value != null) {
|
||||
return AsyncSnapshot.withData(ConnectionState.done, newData.value);
|
||||
}
|
||||
return snapshot;
|
||||
}();
|
||||
|
||||
return Refreshable(
|
||||
snapshot: outSnapshot,
|
||||
refresh: () async {
|
||||
newData.value = await fetcher();
|
||||
},
|
||||
);
|
||||
}
|
|
@ -2,12 +2,13 @@ import 'dart:async';
|
|||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:fuzzy/fuzzy.dart';
|
||||
import 'package:lemmy_api_client/lemmy_api_client.dart';
|
||||
|
||||
import '../hooks/delayed_loading.dart';
|
||||
import '../hooks/memo_future.dart';
|
||||
import '../hooks/refreshable.dart';
|
||||
import '../hooks/stores.dart';
|
||||
import '../util/extensions/api.dart';
|
||||
import '../util/extensions/iterators.dart';
|
||||
|
@ -29,8 +30,7 @@ class CommunitiesTab extends HookWidget {
|
|||
useMemoized(() => accountsStore.loggedInInstances.length);
|
||||
final isCollapsed = useState(List.filled(amountOfDisplayInstances, false));
|
||||
|
||||
// TODO: rebuild when instances/accounts change
|
||||
final instancesSnap = useMemoFuture(() {
|
||||
getInstances() {
|
||||
final futures = accountsStore.loggedInInstances
|
||||
.map(
|
||||
(instanceHost) =>
|
||||
|
@ -39,8 +39,9 @@ class CommunitiesTab extends HookWidget {
|
|||
.toList();
|
||||
|
||||
return Future.wait(futures);
|
||||
});
|
||||
final communitiesSnap = useMemoFuture(() {
|
||||
}
|
||||
|
||||
getCommunities() {
|
||||
final futures = accountsStore.loggedInInstances
|
||||
.map(
|
||||
(instanceHost) => LemmyApi(instanceHost)
|
||||
|
@ -56,9 +57,14 @@ class CommunitiesTab extends HookWidget {
|
|||
.toList();
|
||||
|
||||
return Future.wait(futures);
|
||||
});
|
||||
}
|
||||
|
||||
if (communitiesSnap.hasError || instancesSnap.hasError) {
|
||||
// TODO: rebuild when instances/accounts change
|
||||
final instancesRefreshable = useRefreshable(getInstances);
|
||||
final communitiesRefreshable = useRefreshable(getCommunities);
|
||||
|
||||
if (communitiesRefreshable.snapshot.hasError ||
|
||||
instancesRefreshable.snapshot.hasError) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(),
|
||||
body: Center(
|
||||
|
@ -68,15 +74,16 @@ class CommunitiesTab extends HookWidget {
|
|||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Text(
|
||||
communitiesSnap.error?.toString() ??
|
||||
instancesSnap.error?.toString(),
|
||||
communitiesRefreshable.snapshot.error?.toString() ??
|
||||
instancesRefreshable.snapshot.error?.toString(),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (!communitiesSnap.hasData || !instancesSnap.hasData) {
|
||||
} else if (!communitiesRefreshable.snapshot.hasData ||
|
||||
!instancesRefreshable.snapshot.hasData) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(),
|
||||
body: const Center(
|
||||
|
@ -85,8 +92,22 @@ class CommunitiesTab extends HookWidget {
|
|||
);
|
||||
}
|
||||
|
||||
final instances = instancesSnap.data;
|
||||
final communities = communitiesSnap.data
|
||||
refresh() async {
|
||||
await HapticFeedback.mediumImpact();
|
||||
try {
|
||||
await Future.wait([
|
||||
instancesRefreshable.refresh(),
|
||||
communitiesRefreshable.refresh(),
|
||||
]);
|
||||
// ignore: avoid_catches_without_on_clauses
|
||||
} catch (e) {
|
||||
Scaffold.of(context)
|
||||
.showSnackBar(SnackBar(content: Text(e.toString())));
|
||||
}
|
||||
}
|
||||
|
||||
final instances = instancesRefreshable.snapshot.data;
|
||||
final communities = communitiesRefreshable.snapshot.data
|
||||
..forEach(
|
||||
(e) => e.sort((a, b) => a.communityName.compareTo(b.communityName)));
|
||||
|
||||
|
@ -136,91 +157,95 @@ class CommunitiesTab extends HookWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
body: ListView(
|
||||
children: [
|
||||
for (var i = 0; i < amountOfDisplayInstances; i++)
|
||||
Column(
|
||||
children: [
|
||||
ListTile(
|
||||
onTap: () => goToInstance(
|
||||
context, accountsStore.loggedInInstances.elementAt(i)),
|
||||
onLongPress: () => toggleCollapse(i),
|
||||
leading: instances[i].icon != null
|
||||
? CachedNetworkImage(
|
||||
height: 50,
|
||||
width: 50,
|
||||
imageUrl: instances[i].icon,
|
||||
imageBuilder: (context, imageProvider) => Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
image: DecorationImage(
|
||||
fit: BoxFit.cover, image: imageProvider),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: refresh,
|
||||
child: ListView(
|
||||
children: [
|
||||
for (var i = 0; i < amountOfDisplayInstances; i++)
|
||||
Column(
|
||||
children: [
|
||||
ListTile(
|
||||
onTap: () => goToInstance(
|
||||
context, accountsStore.loggedInInstances.elementAt(i)),
|
||||
onLongPress: () => toggleCollapse(i),
|
||||
leading: instances[i].icon != null
|
||||
? CachedNetworkImage(
|
||||
height: 50,
|
||||
width: 50,
|
||||
imageUrl: instances[i].icon,
|
||||
imageBuilder: (context, imageProvider) => Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
image: DecorationImage(
|
||||
fit: BoxFit.cover, image: imageProvider),
|
||||
),
|
||||
),
|
||||
errorWidget: (_, __, ___) =>
|
||||
const SizedBox(width: 50),
|
||||
)
|
||||
: const SizedBox(width: 50),
|
||||
title: Text(
|
||||
instances[i].name,
|
||||
style: theme.textTheme.headline6,
|
||||
),
|
||||
trailing: IconButton(
|
||||
icon: Icon(isCollapsed.value[i]
|
||||
? Icons.keyboard_arrow_up
|
||||
: Icons.keyboard_arrow_down),
|
||||
onPressed: () => toggleCollapse(i),
|
||||
),
|
||||
),
|
||||
if (!isCollapsed.value[i])
|
||||
for (final comm in filterCommunities(communities[i]))
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 17),
|
||||
child: ListTile(
|
||||
onTap: () => goToCommunity.byId(
|
||||
context,
|
||||
accountsStore.loggedInInstances.elementAt(i),
|
||||
comm.communityId),
|
||||
dense: true,
|
||||
leading: VerticalDivider(
|
||||
color: theme.hintColor,
|
||||
),
|
||||
errorWidget: (_, __, ___) =>
|
||||
const SizedBox(width: 50),
|
||||
)
|
||||
: const SizedBox(width: 50),
|
||||
title: Text(
|
||||
instances[i].name,
|
||||
style: theme.textTheme.headline6,
|
||||
),
|
||||
trailing: IconButton(
|
||||
icon: Icon(isCollapsed.value[i]
|
||||
? Icons.keyboard_arrow_up
|
||||
: Icons.keyboard_arrow_down),
|
||||
onPressed: () => toggleCollapse(i),
|
||||
),
|
||||
),
|
||||
if (!isCollapsed.value[i])
|
||||
for (final comm in filterCommunities(communities[i]))
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 17),
|
||||
child: ListTile(
|
||||
onTap: () => goToCommunity.byId(
|
||||
context,
|
||||
accountsStore.loggedInInstances.elementAt(i),
|
||||
comm.communityId),
|
||||
dense: true,
|
||||
leading: VerticalDivider(
|
||||
color: theme.hintColor,
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
if (comm.communityIcon != null)
|
||||
CachedNetworkImage(
|
||||
height: 30,
|
||||
width: 30,
|
||||
imageUrl: comm.communityIcon,
|
||||
imageBuilder: (context, imageProvider) =>
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
image: DecorationImage(
|
||||
fit: BoxFit.cover,
|
||||
image: imageProvider),
|
||||
title: Row(
|
||||
children: [
|
||||
if (comm.communityIcon != null)
|
||||
CachedNetworkImage(
|
||||
height: 30,
|
||||
width: 30,
|
||||
imageUrl: comm.communityIcon,
|
||||
imageBuilder: (context, imageProvider) =>
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
image: DecorationImage(
|
||||
fit: BoxFit.cover,
|
||||
image: imageProvider),
|
||||
),
|
||||
),
|
||||
),
|
||||
errorWidget: (_, __, ___) =>
|
||||
const SizedBox(width: 30),
|
||||
)
|
||||
else
|
||||
const SizedBox(width: 30),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
'''!${comm.communityName}${comm.isLocal ? '' : '@${comm.originInstanceHost}'}''',
|
||||
),
|
||||
],
|
||||
errorWidget: (_, __, ___) =>
|
||||
const SizedBox(width: 30),
|
||||
)
|
||||
else
|
||||
const SizedBox(width: 30),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
'''!${comm.communityName}${comm.isLocal ? '' : '@${comm.originInstanceHost}'}''',
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: _CommunitySubscribeToggle(
|
||||
key: ValueKey(comm.communityId),
|
||||
instanceHost: comm.instanceHost,
|
||||
communityId: comm.communityId,
|
||||
),
|
||||
),
|
||||
trailing: _CommunitySubscribeToggle(
|
||||
instanceHost: comm.instanceHost,
|
||||
communityId: comm.communityId,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -231,9 +256,10 @@ class _CommunitySubscribeToggle extends HookWidget {
|
|||
final String instanceHost;
|
||||
|
||||
const _CommunitySubscribeToggle(
|
||||
{@required this.instanceHost, @required this.communityId})
|
||||
{@required this.instanceHost, @required this.communityId, Key key})
|
||||
: assert(instanceHost != null),
|
||||
assert(communityId != null);
|
||||
assert(communityId != null),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import 'package:esys_flutter_share/esys_flutter_share.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:lemmy_api_client/lemmy_api_client.dart';
|
||||
|
||||
import '../hooks/logged_in_action.dart';
|
||||
import '../hooks/memo_future.dart';
|
||||
import '../hooks/refreshable.dart';
|
||||
import '../hooks/stores.dart';
|
||||
import '../util/more_icon.dart';
|
||||
import '../widgets/comment_section.dart';
|
||||
|
@ -30,23 +31,22 @@ class FullPostPage extends HookWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final accStore = useAccountsStore();
|
||||
final fullPostSnap = useMemoFuture(() => LemmyApi(instanceHost)
|
||||
final fullPostRefreshable = useRefreshable(() => LemmyApi(instanceHost)
|
||||
.v1
|
||||
.getPost(id: id, auth: accStore.defaultTokenFor(instanceHost)?.raw));
|
||||
final loggedInAction = useLoggedInAction(instanceHost);
|
||||
final newComments = useState(const <CommentView>[]);
|
||||
|
||||
// FALLBACK VIEW
|
||||
|
||||
if (!fullPostSnap.hasData && this.post == null) {
|
||||
if (!fullPostRefreshable.snapshot.hasData && this.post == null) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (fullPostSnap.hasError)
|
||||
Text(fullPostSnap.error.toString())
|
||||
if (fullPostRefreshable.snapshot.hasError)
|
||||
Text(fullPostRefreshable.snapshot.error.toString())
|
||||
else
|
||||
const CircularProgressIndicator(),
|
||||
],
|
||||
|
@ -57,12 +57,27 @@ class FullPostPage extends HookWidget {
|
|||
|
||||
// VARIABLES
|
||||
|
||||
final post = fullPostSnap.hasData ? fullPostSnap.data.post : this.post;
|
||||
final post = fullPostRefreshable.snapshot.hasData
|
||||
? fullPostRefreshable.snapshot.data.post
|
||||
: this.post;
|
||||
|
||||
final fullPost = fullPostSnap.data;
|
||||
final fullPost = fullPostRefreshable.snapshot.data;
|
||||
|
||||
// FUNCTIONS
|
||||
|
||||
refresh() async {
|
||||
await HapticFeedback.mediumImpact();
|
||||
|
||||
try {
|
||||
await fullPostRefreshable.refresh();
|
||||
// ignore: avoid_catches_without_on_clauses
|
||||
} catch (e) {
|
||||
Scaffold.of(context).showSnackBar(SnackBar(
|
||||
content: Text(e.toString()),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
sharePost() => Share.text('Share post', post.apId, 'text/plain');
|
||||
|
||||
comment() async {
|
||||
|
@ -89,31 +104,34 @@ class FullPostPage extends HookWidget {
|
|||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: loggedInAction((_) => comment()),
|
||||
child: const Icon(Icons.comment)),
|
||||
body: ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
children: [
|
||||
Post(post, fullPost: true),
|
||||
if (fullPostSnap.hasData)
|
||||
CommentSection(
|
||||
newComments.value.followedBy(fullPost.comments).toList(),
|
||||
postCreatorId: fullPost.post.creatorId)
|
||||
else if (fullPostSnap.hasError)
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 10, vertical: 30),
|
||||
child: Column(
|
||||
children: [
|
||||
const Icon(Icons.error),
|
||||
Text('Error: ${fullPostSnap.error}')
|
||||
],
|
||||
body: RefreshIndicator(
|
||||
onRefresh: refresh,
|
||||
child: ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
children: [
|
||||
Post(post, fullPost: true),
|
||||
if (fullPostRefreshable.snapshot.hasData)
|
||||
CommentSection(
|
||||
newComments.value.followedBy(fullPost.comments).toList(),
|
||||
postCreatorId: fullPost.post.creatorId)
|
||||
else if (fullPostRefreshable.snapshot.hasError)
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 10, vertical: 30),
|
||||
child: Column(
|
||||
children: [
|
||||
const Icon(Icons.error),
|
||||
Text('Error: ${fullPostRefreshable.snapshot.error}')
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 40),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
)
|
||||
else
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 40),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
|
||||
import '../hooks/ref.dart';
|
||||
|
@ -61,42 +62,49 @@ class InfiniteScroll<T> extends HookWidget {
|
|||
|
||||
final page = data.value.length ~/ batchSize + 1;
|
||||
|
||||
return ListView.builder(
|
||||
padding: padding,
|
||||
// +2 for the loading widget and prepend widget
|
||||
itemCount: data.value.length + 2,
|
||||
itemBuilder: (_, i) {
|
||||
if (i == 0) {
|
||||
return prepend;
|
||||
}
|
||||
i -= 1;
|
||||
|
||||
// reached the bottom, fetch more
|
||||
if (i == data.value.length) {
|
||||
// if there are no more, skip
|
||||
if (!hasMore.current) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
// if it's already fetching more, skip
|
||||
if (!isFetching.current) {
|
||||
isFetching.current = true;
|
||||
fetchMore(page, batchSize).then((newData) {
|
||||
// if got less than the batchSize, mark the list as done
|
||||
if (newData.length < batchSize) {
|
||||
hasMore.current = false;
|
||||
}
|
||||
// append new data
|
||||
data.value = [...data.value, ...newData];
|
||||
}).whenComplete(() => isFetching.current = false);
|
||||
}
|
||||
|
||||
return loadingWidget;
|
||||
}
|
||||
|
||||
// not last element, render list item
|
||||
return builder(data.value[i]);
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
controller.clear();
|
||||
await HapticFeedback.mediumImpact();
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
},
|
||||
child: ListView.builder(
|
||||
padding: padding,
|
||||
// +2 for the loading widget and prepend widget
|
||||
itemCount: data.value.length + 2,
|
||||
itemBuilder: (_, i) {
|
||||
if (i == 0) {
|
||||
return prepend;
|
||||
}
|
||||
i -= 1;
|
||||
|
||||
// reached the bottom, fetch more
|
||||
if (i == data.value.length) {
|
||||
// if there are no more, skip
|
||||
if (!hasMore.current) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
// if it's already fetching more, skip
|
||||
if (!isFetching.current) {
|
||||
isFetching.current = true;
|
||||
fetchMore(page, batchSize).then((newData) {
|
||||
// if got less than the batchSize, mark the list as done
|
||||
if (newData.length < batchSize) {
|
||||
hasMore.current = false;
|
||||
}
|
||||
// append new data
|
||||
data.value = [...data.value, ...newData];
|
||||
}).whenComplete(() => isFetching.current = false);
|
||||
}
|
||||
|
||||
return loadingWidget;
|
||||
}
|
||||
|
||||
// not last element, render list item
|
||||
return builder(data.value[i]);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue