import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:surface/providers/relationship.dart'; import 'package:surface/providers/sn_network.dart'; import 'package:surface/types/account.dart'; import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/app_bar_leading.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/loading_indicator.dart'; import '../providers/userinfo.dart'; import '../widgets/unauthorized_hint.dart'; const kFriendStatus = { 0: 'friendStatusPending', 1: 'friendStatusActive', 2: 'friendStatusBlocked', 3: 'friendStatusWaiting', }; class FriendScreen extends StatefulWidget { const FriendScreen({super.key}); @override State createState() => _FriendScreenState(); } class _FriendScreenState extends State { bool _isBusy = false; List _requests = List.empty(); List _relations = List.empty(); List _blocks = List.empty(); Future _fetchRelations() async { final ua = context.read(); if (!ua.isAuthorized) return; setState(() => _isBusy = true); try { final sn = context.read(); final resp = await sn.client.get('/cgi/id/users/me/relations?status=1'); _relations = List.from( resp.data?.map((e) => SnRelationship.fromJson(e)) ?? [], ); } catch (err) { if (!mounted) return; context.showErrorDialog(err); } finally { setState(() => _isBusy = false); } } Future _fetchRequests() async { final ua = context.read(); if (!ua.isAuthorized) return; setState(() => _isBusy = true); try { final sn = context.read(); final resp = await sn.client.get('/cgi/id/users/me/relations?status=0,3'); _requests = List.from( resp.data?.map((e) => SnRelationship.fromJson(e)) ?? [], ); } catch (err) { if (!mounted) return; context.showErrorDialog(err); } finally { setState(() => _isBusy = false); } } Future _fetchBlocks() async { final ua = context.read(); if (!ua.isAuthorized) return; setState(() => _isBusy = true); try { final sn = context.read(); final resp = await sn.client.get('/cgi/id/users/me/relations?status=2'); _blocks = List.from( resp.data?.map((e) => SnRelationship.fromJson(e)) ?? [], ); } catch (err) { if (!mounted) return; context.showErrorDialog(err); } finally { setState(() => _isBusy = false); } } bool _isUpdating = false; Future _changeRelation(SnRelationship relation, int dstStatus) async { setState(() => _isUpdating = true); try { final rel = context.read(); await rel.updateRelationship( relation.relatedId, dstStatus, relation.permNodes, ); if (!mounted) return; _fetchRelations(); } catch (err) { if (!mounted) return; context.showErrorDialog(err); } finally { setState(() => _isUpdating = false); } } Future _deleteRelation(SnRelationship relation) async { final confirm = await context.showConfirmDialog( 'friendDelete'.tr(args: [relation.related?.nick ?? 'unknown'.tr()]), 'friendDeleteDescription'.tr(args: [ relation.related?.nick ?? 'unknown'.tr(), ]), ); if (!confirm) return; if (!mounted) return; setState(() => _isUpdating = true); try { final rel = context.read(); await rel.deleteRelationship(relation.relatedId); if (!mounted) return; _fetchRelations(); } catch (err) { if (!mounted) return; context.showErrorDialog(err); } finally { setState(() => _isUpdating = false); } } void _showRequests() { showModalBottomSheet( context: context, builder: (context) => _FriendshipListWidget(relations: _requests), ).then((value) { if (value != null) { _fetchRequests(); _fetchRelations(); } }); } void _showBlocks() { showModalBottomSheet( context: context, builder: (context) => _FriendshipListWidget(relations: _blocks), ).then((value) { if (value != null) { _fetchBlocks(); _fetchRelations(); } }); } @override void initState() { super.initState(); _fetchRelations(); _fetchRequests(); _fetchBlocks(); } @override Widget build(BuildContext context) { final ua = context.read(); if (!ua.isAuthorized) { return Scaffold( appBar: AppBar( leading: AutoAppBarLeading(), title: Text('screenFriend').tr(), ), body: Center( child: UnauthorizedHint(), ), ); } return Scaffold( appBar: AppBar( leading: AutoAppBarLeading(), title: Text('screenFriend').tr(), ), floatingActionButton: FloatingActionButton( child: const Icon(Symbols.add), onPressed: () { showModalBottomSheet( context: context, builder: (context) => _NewFriendWidget(), ); }, ), body: Column( children: [ LoadingIndicator(isActive: _isBusy || _isUpdating), if (_requests.isNotEmpty) ListTile( title: Text('friendRequests').tr(), subtitle: Text( 'friendRequestsDescription', ).plural(_requests.length), contentPadding: const EdgeInsets.symmetric(horizontal: 24), leading: const Icon(Symbols.group_add), trailing: const Icon(Symbols.chevron_right), onTap: _showRequests, ), if (_blocks.isNotEmpty) ListTile( title: Text('friendBlocklist').tr(), subtitle: Text( 'friendBlocklistDescription', ).plural(_blocks.length), contentPadding: const EdgeInsets.symmetric(horizontal: 24), leading: const Icon(Symbols.block), trailing: const Icon(Symbols.chevron_right), onTap: _showBlocks, ), if (_requests.isNotEmpty || _blocks.isNotEmpty) const Divider(height: 1), Expanded( child: RefreshIndicator( onRefresh: () => Future.wait([ _fetchRelations(), _fetchRequests(), ]), child: ListView.builder( itemCount: _relations.length, itemBuilder: (context, index) { final relation = _relations[index]; final other = relation.related; return ListTile( contentPadding: const EdgeInsets.only(right: 24, left: 16), leading: AccountImage(content: other?.avatar), title: Text(other?.nick ?? 'unknown'), subtitle: Text(other?.nick ?? 'unknown'), trailing: SizedBox( height: 48, width: 120, child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end, children: [ Row( mainAxisAlignment: MainAxisAlignment.end, children: [ InkWell( onTap: _isUpdating ? null : () => _changeRelation(relation, 2), child: Text('friendBlock').tr(), ), const Gap(8), InkWell( onTap: _isUpdating ? null : () => _deleteRelation(relation), child: Text('friendDeleteAction').tr(), ), ], ), ], ), ), ); }, ), ), ), ], ), ); } } class _NewFriendWidget extends StatefulWidget { const _NewFriendWidget({super.key}); @override State<_NewFriendWidget> createState() => _NewFriendWidgetState(); } class _NewFriendWidgetState extends State<_NewFriendWidget> { bool _isBusy = false; final TextEditingController _relatedController = TextEditingController(); Future _sendRequest() async { if (_relatedController.text.isEmpty) return; setState(() => _isBusy = true); try { final sn = context.read(); await sn.client.post('/cgi/id/users/me/relations', data: { 'related': _relatedController.text, }); if (!mounted) return; Navigator.pop(context, true); context.showSnackbar('friendRequestSent'.tr()); } catch (err) { if (!mounted) return; context.showErrorDialog(err); } finally { setState(() => _isBusy = false); } } @override void dispose() { super.dispose(); _relatedController.dispose(); } @override Widget build(BuildContext context) { return StyledWidget(Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'friendNew', style: Theme.of(context).textTheme.titleLarge, ).tr(), const Gap(12), TextField( controller: _relatedController, readOnly: _isBusy, autocorrect: false, autofocus: true, textCapitalization: TextCapitalization.none, decoration: InputDecoration( labelText: 'fieldFriendRelatedName'.tr(), suffix: SizedBox( height: 24, child: IconButton( onPressed: _isBusy ? null : () => _sendRequest(), icon: Icon(Symbols.send), visualDensity: const VisualDensity(horizontal: -4, vertical: -4), padding: EdgeInsets.zero, ), ), ), onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), ) ], )).padding(all: 24); } } class _FriendshipListWidget extends StatefulWidget { final List relations; const _FriendshipListWidget({super.key, required this.relations}); @override State<_FriendshipListWidget> createState() => _FriendshipListWidgetState(); } class _FriendshipListWidgetState extends State<_FriendshipListWidget> { bool _isBusy = false; Future _acceptRequest(SnRelationship relation) async { setState(() => _isBusy = true); try { final rel = context.read(); await rel.acceptFriendRequest(relation.relatedId); if (!mounted) return; Navigator.pop(context, true); } catch (err) { if (!mounted) return; context.showErrorDialog(err); } finally { setState(() => _isBusy = false); } } Future _declineRequest(SnRelationship relation) async { setState(() => _isBusy = true); try { final rel = context.read(); await rel.declineFriendRequest(relation.relatedId); if (!mounted) return; Navigator.pop(context, true); } catch (err) { if (!mounted) return; context.showErrorDialog(err); } finally { setState(() => _isBusy = false); } } Future _changeRelation(SnRelationship relation, int dstStatus) async { setState(() => _isBusy = true); try { final rel = context.read(); await rel.updateRelationship( relation.relatedId, dstStatus, relation.permNodes, ); if (!mounted) return; Navigator.pop(context, true); } catch (err) { if (!mounted) return; context.showErrorDialog(err); } finally { setState(() => _isBusy = false); } } Future _deleteRelation(SnRelationship relation) async { final confirm = await context.showConfirmDialog( 'friendDelete'.tr(args: [relation.related?.nick ?? 'unknown'.tr()]), 'friendDeleteDescription'.tr(args: [ relation.related?.nick ?? 'unknown'.tr(), ]), ); if (!confirm) return; if (!mounted) return; setState(() => _isBusy = true); try { final rel = context.read(); await rel.deleteRelationship(relation.relatedId); if (!mounted) return; Navigator.pop(context, true); } catch (err) { if (!mounted) return; context.showErrorDialog(err); } finally { setState(() => _isBusy = false); } } @override Widget build(BuildContext context) { return ListView.builder( itemCount: widget.relations.length, itemBuilder: (context, index) { final relation = widget.relations[index]; final other = relation.related; return ListTile( contentPadding: const EdgeInsets.only(right: 24, left: 16), leading: AccountImage(content: other?.avatar), title: Text(other?.nick ?? 'unknown'.tr()), subtitle: Text(other?.nick ?? 'unknown'.tr()), trailing: SizedBox( height: 48, width: 120, child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end, children: [ Text(kFriendStatus[relation.status] ?? 'unknown') .tr() .opacity(0.75), if (relation.status == 0) Row( mainAxisAlignment: MainAxisAlignment.end, children: [ InkWell( onTap: _isBusy ? null : () => _acceptRequest(relation), child: Text('friendRequestAccept').tr(), ), const Gap(8), InkWell( onTap: _isBusy ? null : () => _declineRequest(relation), child: Text('friendRequestDecline').tr(), ), ], ) else if (relation.status == 2) Row( mainAxisAlignment: MainAxisAlignment.end, children: [ InkWell( onTap: _isBusy ? null : () => _changeRelation(relation, 1), child: Text('friendUnblock').tr(), ), const Gap(8), InkWell( onTap: _isBusy ? null : () => _deleteRelation(relation), child: Text('friendDeleteAction').tr(), ), ], ), ], ), ), ); }, ); } }