From db7fef4a728b6390c8e8ecbc47966c39bc932455 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 4 May 2025 23:36:36 +0800 Subject: [PATCH] :sparkles: Realm invites --- lib/screens/realm/realms.dart | 161 +++++++++++++++++++++++++++++++- lib/screens/realm/realms.g.dart | 19 ++++ 2 files changed, 179 insertions(+), 1 deletion(-) diff --git a/lib/screens/realm/realms.dart b/lib/screens/realm/realms.dart index 4c6a801..df2b518 100644 --- a/lib/screens/realm/realms.dart +++ b/lib/screens/realm/realms.dart @@ -3,6 +3,7 @@ import 'package:dio/dio.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:image_picker/image_picker.dart'; import 'package:island/models/file.dart'; @@ -36,7 +37,22 @@ class RealmListScreen extends HookConsumerWidget { final realms = ref.watch(realmsJoinedProvider); return AppScaffold( - appBar: AppBar(title: const Text('realms').tr()), + appBar: AppBar( + title: const Text('realms').tr(), + actions: [ + IconButton( + icon: const Icon(Symbols.email), + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (_) => _RealmInviteSheet(), + ); + }, + ), + const Gap(8), + ], + ), floatingActionButton: FloatingActionButton( heroTag: Key("realms-page-fab"), child: const Icon(Symbols.add), @@ -300,3 +316,146 @@ class EditRealmScreen extends HookConsumerWidget { ); } } + +@riverpod +Future> realmInvites(Ref ref) async { + final client = ref.watch(apiClientProvider); + final resp = await client.get('/realms/invites'); + return resp.data + .map((e) => SnRealmMember.fromJson(e)) + .cast() + .toList(); +} + +class _RealmInviteSheet extends HookConsumerWidget { + const _RealmInviteSheet(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final invites = ref.watch(realmInvitesProvider); + + Future acceptInvite(SnRealmMember invite) async { + try { + final client = ref.read(apiClientProvider); + await client.post('/realms/invites/${invite.realm!.id}/accept'); + ref.invalidate(realmInvitesProvider); + ref.invalidate(realmsJoinedProvider); + } catch (err) { + showErrorAlert(err); + } + } + + Future declineInvite(SnRealmMember invite) async { + try { + final client = ref.read(apiClientProvider); + await client.post('/realms/invites/${invite.realm!.id}/decline'); + ref.invalidate(realmInvitesProvider); + } catch (err) { + showErrorAlert(err); + } + } + + return Container( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.8, + ), + child: Material( + color: Colors.transparent, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: EdgeInsets.only( + top: 16, + left: 20, + right: 16, + bottom: 12, + ), + child: Row( + children: [ + Text( + 'invites'.tr(), + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w600, + letterSpacing: -0.5, + ), + ), + const Spacer(), + IconButton( + icon: const Icon(Symbols.refresh), + style: IconButton.styleFrom( + minimumSize: const Size(36, 36), + ), + onPressed: () { + ref.invalidate(realmInvitesProvider); + }, + ), + IconButton( + icon: const Icon(Symbols.close), + onPressed: () => Navigator.pop(context), + style: IconButton.styleFrom( + minimumSize: const Size(36, 36), + ), + ), + ], + ), + ), + const Divider(height: 1), + Expanded( + child: invites.when( + data: + (items) => + items.isEmpty + ? Center( + child: + Text( + 'invitesEmpty', + textAlign: TextAlign.center, + ).tr(), + ) + : ListView.builder( + shrinkWrap: true, + itemCount: items.length, + itemBuilder: (context, index) { + final invite = items[index]; + return ListTile( + leading: ProfilePictureWidget( + fileId: invite.realm!.pictureId, + radius: 24, + fallbackIcon: Symbols.group, + ), + title: Text(invite.realm!.name), + subtitle: + Text( + invite.role >= 100 + ? 'permissionOwner' + : invite.role >= 50 + ? 'permissionModerator' + : 'permissionMember', + ).tr(), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Symbols.check), + onPressed: () => acceptInvite(invite), + ), + IconButton( + icon: const Icon(Symbols.close), + onPressed: () => declineInvite(invite), + ), + ], + ), + ); + }, + ), + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stack) => Center(child: Text('Error: $error')), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/screens/realm/realms.g.dart b/lib/screens/realm/realms.g.dart index b3cdc84..fad4bb0 100644 --- a/lib/screens/realm/realms.g.dart +++ b/lib/screens/realm/realms.g.dart @@ -156,5 +156,24 @@ class _RealmProviderElement extends AutoDisposeFutureProviderElement String? get identifier => (origin as RealmProvider).identifier; } +String _$realmInvitesHash() => r'e265999a03932f8077fb95a619fd8849a215375a'; + +/// See also [realmInvites]. +@ProviderFor(realmInvites) +final realmInvitesProvider = + AutoDisposeFutureProvider>.internal( + realmInvites, + name: r'realmInvitesProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$realmInvitesHash, + dependencies: null, + allTransitiveDependencies: null, + ); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef RealmInvitesRef = AutoDisposeFutureProviderRef>; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package