import 'dart:async'; 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/v2.dart'; import '../hooks/delayed_loading.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'; /// 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) => LemmyApiV2(instanceHost) .run(const GetSite()) .then((e) => e.siteView.site), ) .toList(); return Future.wait(futures); } getCommunities() { final futures = accountsStore.loggedInInstances .map( (instanceHost) => LemmyApiV2(instanceHost) .run(GetUserDetails( sort: SortType.active, savedOnly: false, userId: accountsStore.defaultTokenFor(instanceHost).payload.id, auth: accountsStore.defaultTokenFor(instanceHost).raw, )) .then((e) => e.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(), ), ); } 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.community.name.compareTo(b.community.name))); 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: RefreshIndicator( 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: () => goToInstance(context, accountsStore.loggedInInstances.elementAt(i)), onLongPress: () => toggleCollapse(i), leading: Avatar(url: instances[i].icon), 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, ), const SizedBox(width: 10), Text(comm.community.originDisplayName), ], ), 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, Key key}) : assert(instanceHost != null), assert(communityId != null), super(key: key); @override Widget build(BuildContext context) { final theme = Theme.of(context); final subbed = useState(true); final delayed = useDelayedLoading(); final accountsStore = useAccountsStore(); handleTap() async { delayed.start(); try { await LemmyApiV2(instanceHost).run(FollowCommunity( communityId: communityId, follow: !subbed.value, auth: accountsStore.defaultTokenFor(instanceHost).raw, )); subbed.value = !subbed.value; } on Exception catch (err) { Scaffold.of(context).showSnackBar(SnackBar( content: Text('Failed to ${subbed.value ? 'un' : ''}follow: $err'), )); } delayed.cancel(); } return InkWell( onTap: delayed.pending ? () {} : handleTap, child: Container( decoration: delayed.loading ? null : BoxDecoration( color: subbed.value ? theme.accentColor : null, border: Border.all(color: theme.accentColor), borderRadius: BorderRadius.circular(7), ), child: delayed.loading ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator()) : Icon( subbed.value ? Icons.done : Icons.add, color: subbed.value ? textColorBasedOnBackground(theme.accentColor) : theme.accentColor, size: 20, ), ), ); } }