lemmur-app-android/lib/pages/home_tab.dart

349 lines
12 KiB
Dart
Raw Normal View History

import 'dart:math' show max;
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';
import '../hooks/infinite_scroll.dart';
2020-10-05 23:09:36 +02:00
import '../hooks/memo_future.dart';
import '../hooks/stores.dart';
2021-03-01 14:21:45 +01:00
import '../l10n/l10n.dart';
import '../stores/config_store.dart';
import '../util/goto.dart';
import '../widgets/bottom_modal.dart';
2021-10-21 14:40:28 +02:00
import '../widgets/cached_network_image.dart';
import '../widgets/infinite_scroll.dart';
2021-02-09 20:39:31 +01:00
import '../widgets/sortable_infinite_list.dart';
import 'inbox.dart';
import 'settings/add_account_page.dart';
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
class HomeTab extends HookWidget {
2021-01-03 19:43:39 +01:00
const HomeTab();
@override
Widget build(BuildContext context) {
final accStore = useAccountsStore();
2021-04-16 21:59:51 +02:00
final defaultListingType =
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));
final isc = useInfiniteScrollController();
final theme = Theme.of(context);
2020-10-05 23:09:36 +02:00
final instancesIcons = useMemoFuture(() async {
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 {
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
});
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(() {
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;
}, [
selectedList.value.instanceHost == null ||
accStore.isAnonymousFor(selectedList.value.instanceHost!),
2021-01-30 15:26:48 +01:00
accStore.hasNoAccount,
accStore.instances.length,
]);
handleListChange() async {
2021-02-09 15:12:13 +01:00
final val = await showBottomModal<_SelectedList>(
context: context,
builder: (context) {
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(
L10n.of(context).subscribed,
2021-02-09 15:12:13 +01:00
style: TextStyle(
color: accStore.hasNoAccount
? 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:
theme.textTheme.bodyText1?.color?.withOpacity(0.7)),
2021-02-09 15:12:13 +01:00
),
onTap: () => goToInstance(context, instance),
dense: true,
contentPadding: EdgeInsets.zero,
2021-02-09 15:12:13 +01:00
visualDensity: const VisualDensity(
vertical: VisualDensity.minimumDensity),
leading: (instancesIcons.hasData &&
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(
imageUrl: instancesIcons.data![instance]!,
2021-02-09 15:12:13 +01:00
height: 25,
width: 25,
),
),
)
: const SizedBox(width: 30),
),
2021-01-30 15:26:48 +01:00
ListTile(
title: Text(
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)
? 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)
? () => 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(
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(
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
);
},
);
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
final last = selectedList.value.instanceHost == null
2020-10-05 21:55:33 +02: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
);
}
return Scaffold(
2020-10-05 23:14:36 +02:00
// TODO: make appbar autohide when scrolling down
appBar: AppBar(
actions: [
IconButton(
2021-01-03 19:43:39 +01:00
icon: const Icon(Icons.notifications),
onPressed: () => goTo(context, (_) => const InboxPage()),
)
],
title: TextButton(
style: TextButton.styleFrom(
2021-01-03 19:43:39 +01:00
padding: const EdgeInsets.symmetric(horizontal: 15),
),
onPressed: handleListChange,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(
child: Text(
title,
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
),
),
2021-02-09 15:12:13 +01:00
const Icon(Icons.arrow_drop_down),
],
),
),
),
body: InfiniteHomeList(
controller: isc,
selectedList: selectedList.value,
),
);
}
}
2020-10-06 16:22:49 +02:00
/// Infinite list of posts
class InfiniteHomeList extends HookWidget {
final InfiniteScrollController controller;
final _SelectedList selectedList;
2021-01-03 18:21:56 +01:00
const InfiniteHomeList({
required this.selectedList,
required this.controller,
});
2020-10-06 16:28:38 +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(
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) {
return accStore.loggedInInstances;
}
2021-03-18 17:41:52 +01:00
return accStore.instances;
}();
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
return newPosts;
}
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
));
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(
selectedList.instanceHost!, selectedList.listingType),
controller: controller,
);
}
}
class _SelectedList {
2021-01-30 15:26:48 +01:00
/// when null it implies the 'EVERYTHING' mode
final String? instanceHost;
final PostListingType listingType;
2021-01-03 19:43:39 +01:00
const _SelectedList({
required this.listingType,
this.instanceHost,
});
@override
String toString() =>
2021-01-30 15:26:48 +01:00
'SelectedList(instanceHost: $instanceHost, listingType: $listingType)';
}