import 'dart:async'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:fuzzy/fuzzy.dart'; import 'package:lemmy_api_client/v3.dart'; import '../hooks/delayed_loading.dart'; import '../hooks/logged_in_action.dart'; import '../hooks/refreshable.dart'; import '../hooks/stores.dart'; import '../util/extensions/api.dart'; import '../util/extensions/iterators.dart'; import '../util/goto.dart'; import '../util/text_color.dart'; import '../widgets/avatar.dart'; import '../widgets/pull_to_refresh.dart'; import 'instance/instance.dart'; /// List of subscribed communities per instance class CommunitiesTab extends HookWidget { const CommunitiesTab(); @override Widget build(BuildContext context) { final theme = Theme.of(context); final filterController = useListenable(useTextEditingController()); final accountsStore = useAccountsStore(); final amountOfDisplayInstances = accountsStore.loggedInInstances.length; final isCollapsed = useState(List.filled(amountOfDisplayInstances, false)); if (amountOfDisplayInstances != isCollapsed.value.length) { isCollapsed.value = List.filled(amountOfDisplayInstances, false); } getInstances() { final futures = accountsStore.loggedInInstances .map( (instanceHost) => LemmyApiV3(instanceHost) .run(const GetSite()) .then((e) => e.siteView!.site), ) .toList(); return Future.wait(futures); } getCommunities() { final futures = accountsStore.loggedInInstances .map( (instanceHost) => LemmyApiV3(instanceHost) .run(GetSite( auth: accountsStore.defaultUserDataFor(instanceHost)!.jwt.raw, )) .then((e) => e.myUser!.follows), ) .toList(); return Future.wait(futures); } final loggedInAccounts = accountsStore.loggedInInstances .map((instanceHost) => '$instanceHost${accountsStore.defaultUsernameFor(instanceHost)}') .toList(); final instancesRefreshable = useRefreshable(getInstances, loggedInAccounts); final communitiesRefreshable = useRefreshable(getCommunities, loggedInAccounts); if (communitiesRefreshable.snapshot.hasError || instancesRefreshable.snapshot.hasError) { return Scaffold( appBar: AppBar(), body: Center( child: Row( children: [ const Icon(Icons.error), Padding( padding: const EdgeInsets.all(8), child: Text( communitiesRefreshable.snapshot.error?.toString() ?? instancesRefreshable.snapshot.error!.toString(), ), ) ], ), ), ); } else if (!communitiesRefreshable.snapshot.hasData || !instancesRefreshable.snapshot.hasData) { return Scaffold( appBar: AppBar(), body: const Center( child: CircularProgressIndicator.adaptive(), ), ); } refresh() async { try { await Future.wait([ instancesRefreshable.refresh(), communitiesRefreshable.refresh(), ]); } catch (e) { ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text(e.toString()))); } } final instances = instancesRefreshable.snapshot.data!; final communities = communitiesRefreshable.snapshot.data! .map( (e) => e.sorted((a, b) => a.community.name.compareTo(b.community.name)), ) .toList(); final filterIcon = () { if (filterController.text.isEmpty) { return const Icon(Icons.filter_list); } return IconButton( onPressed: () { filterController.clear(); primaryFocus?.unfocus(); }, icon: const Icon(Icons.clear), ); }(); filterCommunities(List comm) { final matches = Fuzzy( comm.map((e) => e.community.name).toList(), options: FuzzyOptions(threshold: 0.5), ).search(filterController.text).map((e) => e.item); return matches .map((match) => comm.firstWhere((e) => e.community.name == match)); } toggleCollapse(int i) => isCollapsed.value = isCollapsed.value.mapWithIndex((e, j) => j == i ? !e : e).toList(); return Scaffold( appBar: AppBar( actions: [ IconButton( icon: const Icon(Icons.style), onPressed: () {}, // TODO: change styles? ), ], title: TextField( controller: filterController, textAlign: TextAlign.center, decoration: InputDecoration( suffixIcon: filterIcon, hintText: 'Filter', // TODO: hint with an filter icon ), ), ), body: PullToRefresh( onRefresh: refresh, child: amountOfDisplayInstances == 0 ? const Center( child: Text('You are not logged in to any instances'), ) : ListView( children: [ for (var i = 0; i < amountOfDisplayInstances; i++) Column( children: [ ListTile( onTap: () => Navigator.of(context).push( InstancePage.route( accountsStore.loggedInInstances.elementAt(i), ), ), onLongPress: () => toggleCollapse(i), leading: Avatar( url: instances[i].icon, alwaysShow: true, ), 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.community.id), dense: true, leading: VerticalDivider( color: theme.hintColor, ), title: Row( children: [ Avatar( radius: 15, url: comm.community.icon, alwaysShow: true, ), const SizedBox(width: 10), Text(comm.community.originPreferredName), ], ), trailing: _CommunitySubscribeToggle( key: ValueKey(comm.community.id), instanceHost: comm.instanceHost, communityId: comm.community.id, ), ), ) ], ), ], ), ), ); } } class _CommunitySubscribeToggle extends HookWidget { final int communityId; final String instanceHost; const _CommunitySubscribeToggle({ required this.instanceHost, required this.communityId, super.key, }); @override Widget build(BuildContext context) { final theme = Theme.of(context); final subbed = useState(true); final delayed = useDelayedLoading(); final loggedInAction = useLoggedInAction(instanceHost); handleTap(Jwt token) async { delayed.start(); try { await LemmyApiV3(instanceHost).run(FollowCommunity( communityId: communityId, follow: !subbed.value, auth: token.raw, )); subbed.value = !subbed.value; } on Exception catch (err) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text('Failed to ${subbed.value ? 'un' : ''}follow: $err'), )); } delayed.cancel(); } return InkWell( onTap: delayed.pending ? () {} : loggedInAction(handleTap), child: Container( decoration: delayed.loading ? null : BoxDecoration( color: subbed.value ? theme.colorScheme.secondary : null, border: Border.all(color: theme.colorScheme.secondary), borderRadius: BorderRadius.circular(7), ), child: delayed.loading ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator.adaptive()) : Icon( subbed.value ? Icons.done : Icons.add, color: subbed.value ? textColorBasedOnBackground(theme.colorScheme.secondary) : theme.colorScheme.secondary, size: 20, ), ), ); } }