2020-10-06 00:19:34 +02:00
|
|
|
import 'dart:math' show max;
|
|
|
|
|
2020-10-04 21:50:40 +02:00
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
2021-04-05 20:14:39 +02:00
|
|
|
import 'package:lemmy_api_client/v3.dart';
|
2020-10-04 21:50:40 +02:00
|
|
|
|
|
|
|
import '../hooks/infinite_scroll.dart';
|
2020-10-05 23:09:36 +02:00
|
|
|
import '../hooks/memo_future.dart';
|
2020-10-04 21:50:40 +02:00
|
|
|
import '../hooks/stores.dart';
|
2021-03-01 14:21:45 +01:00
|
|
|
import '../l10n/l10n.dart';
|
2021-10-31 12:52:58 +01:00
|
|
|
import '../stores/config_store.dart';
|
2020-10-04 21:50:40 +02:00
|
|
|
import '../util/goto.dart';
|
|
|
|
import '../widgets/bottom_modal.dart';
|
2021-10-21 14:40:28 +02:00
|
|
|
import '../widgets/cached_network_image.dart';
|
2020-10-04 21:50:40 +02:00
|
|
|
import '../widgets/infinite_scroll.dart';
|
2021-02-09 20:39:31 +01:00
|
|
|
import '../widgets/sortable_infinite_list.dart';
|
2020-10-04 21:50:40 +02:00
|
|
|
import 'inbox.dart';
|
2022-01-20 11:50:24 +01:00
|
|
|
import 'instance/instance.dart';
|
2021-11-12 17:01:17 +01:00
|
|
|
import 'settings/add_account_page.dart';
|
2020-10-04 21:50:40 +02:00
|
|
|
|
2020-10-06 16:22:49 +02:00
|
|
|
/// First thing users sees when opening the app
|
|
|
|
/// Shows list of posts from all or just specific instances
|
2020-10-04 21:50:40 +02:00
|
|
|
class HomeTab extends HookWidget {
|
2021-01-03 19:43:39 +01:00
|
|
|
const HomeTab();
|
|
|
|
|
2020-10-04 21:50:40 +02:00
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
final accStore = useAccountsStore();
|
2021-04-16 21:59:51 +02:00
|
|
|
final defaultListingType =
|
2021-10-31 12:52:58 +01:00
|
|
|
useStore((ConfigStore store) => store.defaultListingType);
|
2020-12-03 22:46:25 +01:00
|
|
|
final selectedList = useState(_SelectedList(
|
2021-04-16 21:59:51 +02:00
|
|
|
listingType: accStore.hasNoAccount &&
|
|
|
|
defaultListingType == PostListingType.subscribed
|
2020-12-03 22:46:25 +01:00
|
|
|
? PostListingType.all
|
2021-04-16 21:59:51 +02:00
|
|
|
: defaultListingType));
|
2020-10-04 21:50:40 +02:00
|
|
|
final isc = useInfiniteScrollController();
|
|
|
|
final theme = Theme.of(context);
|
2020-10-05 23:09:36 +02:00
|
|
|
final instancesIcons = useMemoFuture(() async {
|
2021-04-09 00:11:44 +02:00
|
|
|
final sites = await Future.wait(accStore.instances.map((e) =>
|
|
|
|
LemmyApiV3(e)
|
|
|
|
.run<FullSiteView?>(const GetSite())
|
|
|
|
.catchError((e) => null)));
|
2020-10-05 23:09:36 +02:00
|
|
|
|
2021-01-03 21:25:05 +01:00
|
|
|
return {
|
2021-04-09 00:11:44 +02:00
|
|
|
for (final site in sites)
|
|
|
|
if (site != null) site.instanceHost: site.siteView?.site.icon
|
2021-01-03 21:25:05 +01:00
|
|
|
};
|
2020-10-05 23:09:36 +02:00
|
|
|
});
|
2020-10-04 21:50:40 +02:00
|
|
|
|
2021-01-30 15:26:48 +01:00
|
|
|
// if the current SelectedList points to something that no longer exists
|
|
|
|
// switch it to something else
|
|
|
|
// cases include:
|
|
|
|
// - listingType == subscribed on an instance that has no longer a logged in account
|
|
|
|
// - instanceHost of a removed instance
|
|
|
|
useEffect(() {
|
2021-04-09 00:11:44 +02:00
|
|
|
if ((selectedList.value.instanceHost == null ||
|
|
|
|
accStore.isAnonymousFor(selectedList.value.instanceHost!)) &&
|
2021-01-30 15:26:48 +01:00
|
|
|
selectedList.value.listingType == PostListingType.subscribed ||
|
|
|
|
!accStore.instances.contains(selectedList.value.instanceHost)) {
|
|
|
|
selectedList.value = _SelectedList(
|
2021-04-16 21:59:51 +02:00
|
|
|
listingType: accStore.hasNoAccount &&
|
|
|
|
defaultListingType == PostListingType.subscribed
|
2021-01-30 15:26:48 +01:00
|
|
|
? PostListingType.all
|
2021-04-16 21:59:51 +02:00
|
|
|
: defaultListingType,
|
2021-01-30 15:26:48 +01:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}, [
|
2021-04-09 00:11:44 +02:00
|
|
|
selectedList.value.instanceHost == null ||
|
|
|
|
accStore.isAnonymousFor(selectedList.value.instanceHost!),
|
2021-01-30 15:26:48 +01:00
|
|
|
accStore.hasNoAccount,
|
|
|
|
accStore.instances.length,
|
|
|
|
]);
|
|
|
|
|
2020-10-04 21:50:40 +02:00
|
|
|
handleListChange() async {
|
2021-02-09 15:12:13 +01:00
|
|
|
final val = await showBottomModal<_SelectedList>(
|
2020-10-04 21:50:40 +02:00
|
|
|
context: context,
|
|
|
|
builder: (context) {
|
2020-10-06 17:28:11 +02:00
|
|
|
pop(_SelectedList thing) => Navigator.of(context).pop(thing);
|
2021-01-30 15:26:48 +01:00
|
|
|
|
2021-02-09 15:12:13 +01:00
|
|
|
return Column(
|
|
|
|
children: [
|
|
|
|
const SizedBox(height: 5),
|
|
|
|
const ListTile(
|
|
|
|
title: Text('EVERYTHING'),
|
|
|
|
dense: true,
|
|
|
|
contentPadding: EdgeInsets.zero,
|
|
|
|
visualDensity:
|
|
|
|
VisualDensity(vertical: VisualDensity.minimumDensity),
|
|
|
|
leading: SizedBox.shrink(),
|
|
|
|
),
|
|
|
|
ListTile(
|
|
|
|
title: Text(
|
2021-11-05 21:37:27 +01:00
|
|
|
L10n.of(context).subscribed,
|
2021-02-09 15:12:13 +01:00
|
|
|
style: TextStyle(
|
|
|
|
color: accStore.hasNoAccount
|
2021-04-09 00:11:44 +02:00
|
|
|
? theme.textTheme.bodyText1?.color?.withOpacity(0.4)
|
2021-02-09 15:12:13 +01:00
|
|
|
: null,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
onTap: accStore.hasNoAccount
|
|
|
|
? null
|
|
|
|
: () => pop(
|
|
|
|
const _SelectedList(
|
|
|
|
listingType: PostListingType.subscribed,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
leading: const SizedBox(width: 20),
|
|
|
|
),
|
|
|
|
for (final listingType in [
|
|
|
|
PostListingType.local,
|
|
|
|
PostListingType.all,
|
|
|
|
])
|
|
|
|
ListTile(
|
|
|
|
title: Text(listingType.value),
|
|
|
|
leading: const SizedBox(width: 20, height: 20),
|
|
|
|
onTap: () => pop(_SelectedList(listingType: listingType)),
|
|
|
|
),
|
|
|
|
for (final instance in accStore.instances) ...[
|
|
|
|
const Padding(
|
|
|
|
padding: EdgeInsets.symmetric(horizontal: 10),
|
|
|
|
child: Divider(),
|
|
|
|
),
|
|
|
|
ListTile(
|
|
|
|
title: Text(
|
|
|
|
instance.toUpperCase(),
|
|
|
|
style: TextStyle(
|
|
|
|
color:
|
2021-04-09 00:11:44 +02:00
|
|
|
theme.textTheme.bodyText1?.color?.withOpacity(0.7)),
|
2021-02-09 15:12:13 +01:00
|
|
|
),
|
2022-01-20 11:50:24 +01:00
|
|
|
onTap: () => Navigator.of(context).push(
|
|
|
|
InstancePage.route(instance),
|
|
|
|
),
|
2020-10-04 21:50:40 +02:00
|
|
|
dense: true,
|
|
|
|
contentPadding: EdgeInsets.zero,
|
2021-02-09 15:12:13 +01:00
|
|
|
visualDensity: const VisualDensity(
|
|
|
|
vertical: VisualDensity.minimumDensity),
|
|
|
|
leading: (instancesIcons.hasData &&
|
2021-04-09 00:11:44 +02:00
|
|
|
instancesIcons.data![instance] != null)
|
2021-02-09 15:12:13 +01:00
|
|
|
? Padding(
|
|
|
|
padding: const EdgeInsets.only(left: 20),
|
|
|
|
child: SizedBox(
|
|
|
|
width: 25,
|
|
|
|
height: 25,
|
|
|
|
child: CachedNetworkImage(
|
2021-04-09 00:11:44 +02:00
|
|
|
imageUrl: instancesIcons.data![instance]!,
|
2021-02-09 15:12:13 +01:00
|
|
|
height: 25,
|
|
|
|
width: 25,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
: const SizedBox(width: 30),
|
2020-10-04 21:50:40 +02:00
|
|
|
),
|
2021-01-30 15:26:48 +01:00
|
|
|
ListTile(
|
|
|
|
title: Text(
|
2021-11-05 21:37:27 +01:00
|
|
|
L10n.of(context).subscribed,
|
2021-01-30 15:26:48 +01:00
|
|
|
style: TextStyle(
|
2021-02-09 15:12:13 +01:00
|
|
|
color: accStore.isAnonymousFor(instance)
|
2021-04-09 00:11:44 +02:00
|
|
|
? theme.textTheme.bodyText1?.color?.withOpacity(0.4)
|
2021-02-09 15:12:13 +01:00
|
|
|
: null),
|
2021-01-30 15:26:48 +01:00
|
|
|
),
|
2021-02-09 15:12:13 +01:00
|
|
|
onTap: accStore.isAnonymousFor(instance)
|
2021-09-30 12:55:23 +02:00
|
|
|
? () => Navigator.of(context)
|
|
|
|
.push(AddAccountPage.route(instance))
|
2021-02-09 15:12:13 +01:00
|
|
|
: () => pop(_SelectedList(
|
|
|
|
listingType: PostListingType.subscribed,
|
|
|
|
instanceHost: instance,
|
|
|
|
)),
|
|
|
|
leading: const SizedBox(width: 20),
|
|
|
|
),
|
|
|
|
ListTile(
|
2021-11-05 21:37:27 +01:00
|
|
|
title: Text(L10n.of(context).local),
|
2021-02-09 15:12:13 +01:00
|
|
|
onTap: () => pop(_SelectedList(
|
|
|
|
listingType: PostListingType.local,
|
|
|
|
instanceHost: instance,
|
|
|
|
)),
|
|
|
|
leading: const SizedBox(width: 20),
|
|
|
|
),
|
|
|
|
ListTile(
|
2021-11-05 21:37:27 +01:00
|
|
|
title: Text(L10n.of(context).all),
|
2021-02-09 15:12:13 +01:00
|
|
|
onTap: () => pop(_SelectedList(
|
|
|
|
listingType: PostListingType.all,
|
|
|
|
instanceHost: instance,
|
|
|
|
)),
|
2021-01-30 15:26:48 +01:00
|
|
|
leading: const SizedBox(width: 20),
|
|
|
|
),
|
2020-10-06 16:23:29 +02:00
|
|
|
],
|
2021-02-09 15:12:13 +01:00
|
|
|
],
|
2020-10-06 16:23:29 +02:00
|
|
|
);
|
2020-10-04 21:50:40 +02:00
|
|
|
},
|
|
|
|
);
|
|
|
|
if (val != null) {
|
|
|
|
selectedList.value = val;
|
|
|
|
isc.clear();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-05 21:55:33 +02:00
|
|
|
final title = () {
|
2021-03-09 08:51:08 +01:00
|
|
|
final first = selectedList.value.listingType.tr(context);
|
2021-01-16 14:48:15 +01:00
|
|
|
|
2020-12-31 14:58:23 +01:00
|
|
|
final last = selectedList.value.instanceHost == null
|
2020-10-05 21:55:33 +02:00
|
|
|
? ''
|
2020-12-31 14:58:23 +01:00
|
|
|
: '@${selectedList.value.instanceHost}';
|
2020-10-05 21:55:33 +02:00
|
|
|
return '$first$last';
|
|
|
|
}();
|
|
|
|
|
2020-12-03 22:46:25 +01:00
|
|
|
if (accStore.instances.isEmpty) {
|
|
|
|
return Scaffold(
|
2020-12-04 13:58:17 +01:00
|
|
|
body: Column(
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
2021-01-03 18:21:56 +01:00
|
|
|
children: const [
|
2020-12-04 13:58:17 +01:00
|
|
|
Center(child: Text('there needs to be at least one instance')),
|
|
|
|
],
|
|
|
|
),
|
2020-12-03 22:46:25 +01:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-10-04 21:50:40 +02:00
|
|
|
return Scaffold(
|
2020-10-05 23:14:36 +02:00
|
|
|
// TODO: make appbar autohide when scrolling down
|
2020-10-04 21:50:40 +02:00
|
|
|
appBar: AppBar(
|
|
|
|
actions: [
|
|
|
|
IconButton(
|
2021-01-03 19:43:39 +01:00
|
|
|
icon: const Icon(Icons.notifications),
|
|
|
|
onPressed: () => goTo(context, (_) => const InboxPage()),
|
2020-10-04 21:50:40 +02:00
|
|
|
)
|
|
|
|
],
|
|
|
|
title: TextButton(
|
2020-10-06 00:54:17 +02:00
|
|
|
style: TextButton.styleFrom(
|
2021-01-03 19:43:39 +01:00
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 15),
|
2020-10-06 00:54:17 +02:00
|
|
|
),
|
|
|
|
onPressed: handleListChange,
|
|
|
|
child: Row(
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
children: [
|
|
|
|
Flexible(
|
|
|
|
child: Text(
|
|
|
|
title,
|
2021-09-12 22:37:07 +02:00
|
|
|
style: theme.appBarTheme.titleTextStyle,
|
2021-02-24 20:52:18 +01:00
|
|
|
overflow: TextOverflow.fade,
|
|
|
|
softWrap: false,
|
2020-10-05 21:55:33 +02:00
|
|
|
),
|
2020-10-06 00:54:17 +02:00
|
|
|
),
|
2021-02-09 15:12:13 +01:00
|
|
|
const Icon(Icons.arrow_drop_down),
|
2020-10-06 00:54:17 +02:00
|
|
|
],
|
|
|
|
),
|
|
|
|
),
|
2020-10-04 21:50:40 +02:00
|
|
|
),
|
2021-01-26 23:38:23 +01:00
|
|
|
body: InfiniteHomeList(
|
|
|
|
controller: isc,
|
|
|
|
selectedList: selectedList.value,
|
2020-10-04 21:50:40 +02:00
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-06 16:22:49 +02:00
|
|
|
/// Infinite list of posts
|
2020-10-04 21:50:40 +02:00
|
|
|
class InfiniteHomeList extends HookWidget {
|
|
|
|
final InfiniteScrollController controller;
|
2020-10-06 17:28:11 +02:00
|
|
|
final _SelectedList selectedList;
|
2021-01-03 18:21:56 +01:00
|
|
|
|
|
|
|
const InfiniteHomeList({
|
2021-04-09 00:11:44 +02:00
|
|
|
required this.selectedList,
|
|
|
|
required this.controller,
|
|
|
|
});
|
2020-10-06 16:28:38 +02:00
|
|
|
|
2020-10-04 21:50:40 +02:00
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
final accStore = useAccountsStore();
|
|
|
|
|
2020-10-06 16:30:52 +02:00
|
|
|
/// fetches post from many instances at once and combines them into a single
|
|
|
|
/// list
|
|
|
|
///
|
|
|
|
/// Process of combining them works sort of like zip function in python
|
|
|
|
Future<List<PostView>> generalFetcher(
|
2020-10-04 21:50:40 +02:00
|
|
|
int page,
|
|
|
|
int limit,
|
|
|
|
SortType sort,
|
|
|
|
PostListingType listingType,
|
|
|
|
) async {
|
|
|
|
assert(
|
|
|
|
listingType != PostListingType.community, 'only subscribed or all');
|
|
|
|
|
|
|
|
final instances = () {
|
2021-03-18 17:41:52 +01:00
|
|
|
if (listingType == PostListingType.subscribed) {
|
2020-10-04 21:50:40 +02:00
|
|
|
return accStore.loggedInInstances;
|
|
|
|
}
|
2021-03-18 17:41:52 +01:00
|
|
|
|
|
|
|
return accStore.instances;
|
2020-10-04 21:50:40 +02:00
|
|
|
}();
|
|
|
|
|
2021-03-18 17:41:52 +01:00
|
|
|
final futures = [
|
|
|
|
for (final instanceHost in instances)
|
2021-04-05 20:14:39 +02:00
|
|
|
LemmyApiV3(instanceHost).run(GetPosts(
|
2021-03-18 17:41:52 +01:00
|
|
|
type: listingType,
|
|
|
|
sort: sort,
|
|
|
|
page: page,
|
|
|
|
limit: limit,
|
2021-04-05 20:14:39 +02:00
|
|
|
savedOnly: false,
|
2021-04-11 18:27:22 +02:00
|
|
|
auth: accStore.defaultUserDataFor(instanceHost)?.jwt.raw,
|
2021-03-18 17:41:52 +01:00
|
|
|
))
|
|
|
|
];
|
|
|
|
final instancePosts = await Future.wait(futures);
|
|
|
|
final longest = instancePosts.map((e) => e.length).reduce(max);
|
2021-01-03 21:25:05 +01:00
|
|
|
|
2021-03-18 17:41:52 +01:00
|
|
|
final newPosts = [
|
|
|
|
for (var i = 0; i < longest; i++)
|
|
|
|
for (final posts in instancePosts)
|
|
|
|
if (i < posts.length) posts[i]
|
|
|
|
];
|
2021-01-03 21:25:05 +01:00
|
|
|
|
2020-10-06 00:19:34 +02:00
|
|
|
return newPosts;
|
2020-10-04 21:50:40 +02:00
|
|
|
}
|
|
|
|
|
2021-02-09 20:39:31 +01:00
|
|
|
FetcherWithSorting<PostView> fetcherFromInstance(
|
|
|
|
String instanceHost, PostListingType listingType) =>
|
2021-04-05 20:14:39 +02:00
|
|
|
(page, batchSize, sort) => LemmyApiV3(instanceHost).run(GetPosts(
|
2020-10-06 12:06:29 +02:00
|
|
|
type: listingType,
|
|
|
|
sort: sort,
|
|
|
|
page: page,
|
|
|
|
limit: batchSize,
|
2021-04-05 20:14:39 +02:00
|
|
|
savedOnly: false,
|
2021-04-11 18:27:22 +02:00
|
|
|
auth: accStore.defaultUserDataFor(instanceHost)?.jwt.raw,
|
2021-01-24 20:01:55 +01:00
|
|
|
));
|
2020-10-04 21:50:40 +02:00
|
|
|
|
2021-02-09 20:39:31 +01:00
|
|
|
return InfinitePostList(
|
|
|
|
fetcher: selectedList.instanceHost == null
|
|
|
|
? (page, limit, sort) =>
|
|
|
|
generalFetcher(page, limit, sort, selectedList.listingType)
|
2020-10-06 16:30:52 +02:00
|
|
|
: fetcherFromInstance(
|
2021-04-09 00:11:44 +02:00
|
|
|
selectedList.instanceHost!, selectedList.listingType),
|
2020-10-04 21:50:40 +02:00
|
|
|
controller: controller,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-06 17:28:11 +02:00
|
|
|
class _SelectedList {
|
2021-01-30 15:26:48 +01:00
|
|
|
/// when null it implies the 'EVERYTHING' mode
|
2021-04-09 00:11:44 +02:00
|
|
|
final String? instanceHost;
|
2020-10-04 21:50:40 +02:00
|
|
|
final PostListingType listingType;
|
2021-01-03 19:43:39 +01:00
|
|
|
|
|
|
|
const _SelectedList({
|
2021-04-09 00:11:44 +02:00
|
|
|
required this.listingType,
|
2020-12-31 14:58:23 +01:00
|
|
|
this.instanceHost,
|
2021-04-09 00:11:44 +02:00
|
|
|
});
|
2020-10-04 21:50:40 +02:00
|
|
|
|
2021-12-04 18:03:54 +01:00
|
|
|
@override
|
2020-10-04 21:50:40 +02:00
|
|
|
String toString() =>
|
2021-01-30 15:26:48 +01:00
|
|
|
'SelectedList(instanceHost: $instanceHost, listingType: $listingType)';
|
2020-10-04 21:50:40 +02:00
|
|
|
}
|