diff --git a/lib/business_logic/follow_request/followrequest.dart b/lib/business_logic/follow_request/followrequest.dart new file mode 100644 index 0000000..f7d2694 --- /dev/null +++ b/lib/business_logic/follow_request/followrequest.dart @@ -0,0 +1,58 @@ +import 'dart:convert'; + +import 'package:loris/business_logic/account/account.dart'; +import 'package:loris/global.dart' as global; +import 'package:http/http.dart' as http; + +Future> _getFollowRequest(String identityName) async { + final identity = global.settings!.identities[identityName]!; + final headers = {...identity.getAuthHeaders(), ...global.defaultHeaders}; + final Uri uri = Uri( + scheme: "https", + host: identity.instanceUrl, + path: "/api/v1/follow_requests", + ); + final result = await http.get(uri, headers: headers); + if (result.statusCode != 200) return []; + + List models = []; + for (var m in jsonDecode(result.body)) { + models.add(AccountModel.fromJson(m, identityName)); + } + return models; +} + +Future> getFollowRequests() async { + List>> pending = []; + global.settings!.identities.forEach((key, value) { + pending.add(_getFollowRequest(key)); + }); + + List finished = []; + for (var m in pending) { + finished.addAll(await m); + } + return finished; +} + +// accept follow request or deny +// request is accepted unless deny is true +Future handleFollowRequest(AccountModel account, + {deny = false}) async { + final identity = global.settings!.identities[account.identity]!; + final headers = {...identity.getAuthHeaders(), ...global.defaultHeaders}; + final Uri uri = Uri( + scheme: "https", + host: identity.instanceUrl, + path: + "/api/v1/follow_requests/${account.id}/${deny ? "reject" : "authorize"}", + ); + + final result = await http.post(uri, headers: headers); + print(uri); + print(result.body); + if (result.statusCode != 200) { + return null; + } + return RelationshipModel.fromJson(jsonDecode(result.body), account.identity); +} diff --git a/lib/dialogues/followreqs.dart b/lib/dialogues/followreqs.dart new file mode 100644 index 0000000..bce44ab --- /dev/null +++ b/lib/dialogues/followreqs.dart @@ -0,0 +1,146 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:localization/localization.dart'; +import 'package:loris/business_logic/account/account.dart'; +import 'package:loris/business_logic/follow_request/followrequest.dart'; +import 'package:loris/global.dart' as global; +import 'package:loris/partials/post.dart'; +import 'package:loris/themes/themes.dart' as themes; + +class FollowReqScreen extends StatefulWidget { + const FollowReqScreen({super.key}); + + @override + State createState() => _FollowReqScreenState(); +} + +class _FollowReqScreenState extends State { + List requests = []; + bool loading = true; + + Future fetchRequests() async { + final results = await getFollowRequests(); + if (mounted) { + setState(() { + requests = results; + loading = false; + }); + } + } + + @override + void initState() { + fetchRequests(); + super.initState(); + } + + void handleFailure(RelationshipModel? model) {} + + void updateList(AccountModel model) { + if (mounted) { + setState(() { + requests.remove(model); + }); + } + } + + @override + Widget build(BuildContext context) { + return BackdropFilter( + filter: + ImageFilter.blur(sigmaX: 10, sigmaY: 10, tileMode: TileMode.mirror), + child: SimpleDialog( + titlePadding: EdgeInsets.fromLTRB(0, themes.defaultRadius.x, 0, 0), + title: loading ? const LinearProgressIndicator() : null, + contentPadding: const EdgeInsets.all(0), + children: [ + Container( + width: global.getWidth(context), + height: MediaQuery.of(context).size.height * 2 / 3, + constraints: global.getConstraints(context), + child: ListView.separated( + itemBuilder: (context, index) { + final account = requests[index]; + return FollowRequestDisplay( + account: account, + accept: () async { + if (mounted) { + setState(() { + loading = true; + }); + } + final result = await handleFollowRequest(account); + handleFailure(result); + if (result != null) { + updateList(account); + } + if (mounted) { + setState(() { + loading = false; + }); + } + }, + deny: (() async { + if (mounted) { + setState(() { + loading = true; + }); + } + final result = + await handleFollowRequest(account, deny: true); + handleFailure(result); + if (result != null) { + updateList(account); + } + if (mounted) { + setState(() { + loading = false; + }); + } + }), + ); + }, + separatorBuilder: (context, index) => + Divider(color: Theme.of(context).hintColor), + itemCount: requests.length), + ) + ], + )); + } +} + +class FollowRequestDisplay extends StatelessWidget { + const FollowRequestDisplay( + {super.key, + required this.deny, + required this.accept, + required this.account}); + final void Function()? deny; + final void Function()? accept; + final AccountModel account; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + DisplayName(account: account), + SelectableText("${"requested-to-follow".i18n()} ${account.identity}"), + Wrap(alignment: WrapAlignment.spaceBetween, children: [ + TextButton.icon( + onPressed: deny, + icon: const Icon(Icons.do_not_disturb_alt_outlined), + label: Text("deny".i18n())), + TextButton.icon( + onPressed: accept, + icon: const Icon(Icons.check), + label: Text("accept".i18n())) + ]), + ], + ), + ); + } +} diff --git a/lib/i18n/en_US.json b/lib/i18n/en_US.json index 029f973..4b208c9 100644 --- a/lib/i18n/en_US.json +++ b/lib/i18n/en_US.json @@ -87,6 +87,8 @@ "sending-file": "sending file", "open-media-in-browser": "open media in browser", "unread": "unread", - "expand": "expand" + "expand": "expand", + "requested-to-follow": "has requested to follow:", + "follow-requests": "follow requests" } \ No newline at end of file diff --git a/lib/pages/settings/account.dart b/lib/pages/settings/account.dart index 8d34ef9..8d96a0e 100644 --- a/lib/pages/settings/account.dart +++ b/lib/pages/settings/account.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:localization/localization.dart'; +import 'package:loris/dialogues/followreqs.dart'; import '../../global.dart' as global; class AccountSettings extends StatelessWidget { @@ -13,6 +14,7 @@ class AccountSettings extends StatelessWidget { LogoutButton(identity: global.settings!.identities.keys.toList()[i])); } children.add(const NewAccountButton()); + children.add(const FollowreqButton()); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: children, @@ -73,3 +75,19 @@ Future logout(context, String identity) async { (Navigator.of(context).pushReplacementNamed("/")); } } + +class FollowreqButton extends StatelessWidget { + const FollowreqButton({super.key}); + + @override + Widget build(BuildContext context) { + return TextButton.icon( + onPressed: (() => showDialog( + barrierColor: Colors.transparent, + context: context, + builder: (context) => const FollowReqScreen(), + )), + icon: const Icon(Icons.check), + label: Text("follow-requests".i18n())); + } +} diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index ba45a3f..c42f990 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -3,6 +3,8 @@ import 'package:localization/localization.dart'; import './account.dart' as account; import './about.dart' as about; import './app.dart' as app; +import 'package:loris/global.dart' as global; +import 'package:loris/themes/themes.dart' as themes; Widget settings(context) { final List categories = [ @@ -42,8 +44,11 @@ class SettingsPanel extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.all(24.0), + padding: const EdgeInsets.symmetric( + horizontal: themes.defaultSeperatorHeight * 2, + vertical: themes.defaultSeperatorHeight), child: Container( + constraints: global.getConstraints(context), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, border: Border.all( @@ -56,9 +61,9 @@ class SettingsPanel extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( + SelectableText( title, - style: Theme.of(context).textTheme.headlineMedium, + style: Theme.of(context).textTheme.displayMedium, ), content, ],