From 93d2670063a6f637d96ce19032dfe693cdce0c98 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Thu, 1 Jan 2026 02:29:27 +0800 Subject: [PATCH] :sparkles: Able to manage publisher actor --- assets/i18n/en-US.json | 18 ++ lib/screens/account/profile.g.dart | 2 +- lib/screens/creators/hub.dart | 182 +++++++++++++++++++++ lib/screens/creators/hub.g.dart | 78 +++++++++ lib/screens/posts/publisher_profile.g.dart | 2 +- lib/services/activitypub_service.dart | 17 ++ 6 files changed, 297 insertions(+), 2 deletions(-) diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index df5ee6bb..4b3faa36 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -255,6 +255,24 @@ "walletCurrencyShortGolds": "NSD", "retry": "Retry", "creatorHubUnselectedHint": "Pick / create a publisher to get started.", + "publisherFediverse": "Fediverse Actor", + "publisherFediverseDescription": "Configure your publisher's ActivityPub actor for federated social networking", + "publisherFediverseEnabled": "Enabled", + "publisherFediverseDisabled": "Disabled", + "publisherFediverseNotConfigured": "Not configured", + "publisherFediverseEnableHint": "Enable your publisher to interact with fediverse", + "publisherFediverseDisableHint": "Disable your publisher's fediverse actor", + "publisherFediverseEnableConfirm": "Enable fediverse actor?", + "publisherFediverseDisableConfirm": "Disable fediverse actor?", + "publisherFediverseEnabledSuccess": "Fediverse actor enabled successfully", + "publisherFediverseDisabledSuccess": "Fediverse actor disabled successfully", + "publisherFediverseFailedToEnable": "Failed to enable fediverse actor", + "publisherFediverseFailedToDisable": "Failed to disable fediverse actor", + "publisherFediverseWhatIs": "What is Fediverse?", + "publisherFediverseAbout": "The fediverse is a federated network of social platforms. Enabling this feature allows your publisher to interact with users across different ActivityPub-compatible services like Mastodon, PeerTube, and more.", + "publisherFediverseActorUri": "Actor URI", + "publisherFediverseFollowerCount": "Followers", + "publisherFediverseNoFollowers": "No followers yet", "relationships": "Relationships", "addFriend": "Send a Friend Request", "addFriendShort": "Add as Friend", diff --git a/lib/screens/account/profile.g.dart b/lib/screens/account/profile.g.dart index e4c432a9..4eb315e0 100644 --- a/lib/screens/account/profile.g.dart +++ b/lib/screens/account/profile.g.dart @@ -212,7 +212,7 @@ final class AccountAppbarForcegroundColorProvider } String _$accountAppbarForcegroundColorHash() => - r'127fcc7fd6ec6a41ac4a6975276b5271aa4fa7d0'; + r'59e0049a5158ea653f0afd724df9ff2312b90050'; final class AccountAppbarForcegroundColorFamily extends $Family with $FunctionalFamilyOverride, String> { diff --git a/lib/screens/creators/hub.dart b/lib/screens/creators/hub.dart index f8f9507f..eb01692f 100644 --- a/lib/screens/creators/hub.dart +++ b/lib/screens/creators/hub.dart @@ -6,6 +6,7 @@ import 'package:go_router/go_router.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/models/activitypub.dart'; import 'package:island/models/post.dart'; import 'package:island/models/publisher.dart'; import 'package:island/models/heatmap.dart'; @@ -15,6 +16,7 @@ import 'package:island/screens/creators/publishers_form.dart'; import 'package:island/services/responsive.dart'; import 'package:island/utils/text.dart'; import 'package:island/widgets/account/account_picker.dart'; +import 'package:island/widgets/activitypub/actor_profile.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/content/cloud_files.dart'; @@ -78,6 +80,19 @@ Future> publisherInvites(Ref ref) async { .toList(); } +@riverpod +Future publisherActorStatus( + Ref ref, + String? publisherName, +) async { + if (publisherName == null) throw Exception('Publisher name is required'); + final apiClient = ref.watch(apiClientProvider); + final response = await apiClient.get( + '/sphere/publishers/$publisherName/fediverse', + ); + return SnActorStatusResponse.fromJson(response.data); +} + final publisherMemberListNotifierProvider = AsyncNotifierProvider.family .autoDispose(PublisherMemberListNotifier.new); @@ -501,6 +516,24 @@ class CreatorHubScreen extends HookConsumerWidget { leading: const Icon(Symbols.edit), onTap: updatePublisher, ), + ListTile( + shape: RoundedRectangleBorder( + borderRadius: const BorderRadius.all(Radius.circular(8)), + ), + minTileHeight: 48, + title: Text('publisherFediverse').tr(), + trailing: Icon(Symbols.chevron_right), + leading: const Icon(Symbols.public), + onTap: () { + showModalBottomSheet( + isScrollControlled: true, + context: context, + builder: (context) => _PublisherFediverseSheet( + publisherUname: currentPublisher.value!.name, + ), + ); + }, + ), ListTile( shape: RoundedRectangleBorder( borderRadius: const BorderRadius.all(Radius.circular(8)), @@ -1126,3 +1159,152 @@ class _PublisherInviteSheet extends HookConsumerWidget { ); } } + +class _PublisherFediverseSheet extends HookConsumerWidget { + final String publisherUname; + + const _PublisherFediverseSheet({required this.publisherUname}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final actorStatus = ref.watch(publisherActorStatusProvider(publisherUname)); + final apiClient = ref.read(apiClientProvider); + final isLoading = useState(false); + + Future toggleActor() async { + final currentStatus = actorStatus.value; + if (currentStatus == null) return; + + final confirm = await showConfirmAlert( + currentStatus.enabled + ? 'publisherFediverseDisableConfirm'.tr() + : 'publisherFediverseEnableConfirm'.tr(), + currentStatus.enabled + ? 'publisherFediverseDisabled'.tr() + : 'publisherFediverseEnabled'.tr(), + isDanger: !currentStatus.enabled, + ); + if (confirm != true) return; + + try { + isLoading.value = true; + if (currentStatus.enabled) { + await apiClient.delete( + '/sphere/publishers/$publisherUname/fediverse', + ); + } else { + await apiClient.post('/sphere/publishers/$publisherUname/fediverse'); + } + ref.invalidate(publisherActorStatusProvider(publisherUname)); + if (context.mounted) { + Navigator.pop(context); + } + } catch (err) { + showErrorAlert(err); + } finally { + isLoading.value = false; + } + } + + return SheetScaffold( + titleText: 'publisherFediverse'.tr(), + child: actorStatus.when( + data: (status) => SingleChildScrollView( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Column( + spacing: 16, + children: [ + Card.outlined( + child: SwitchListTile( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + value: status.enabled, + onChanged: isLoading.value ? null : (_) => toggleActor(), + title: Text( + status.enabled + ? 'publisherFediverseEnabled'.tr() + : 'publisherFediverseDisabled'.tr(), + ), + subtitle: Text( + status.enabled + ? 'publisherFediverseDisableHint'.tr() + : 'publisherFediverseEnableHint'.tr(), + ), + secondary: isLoading.value + ? const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : Icon( + status.enabled + ? Icons.check_circle + : Icons.circle_outlined, + color: status.enabled ? Colors.green : Colors.grey, + ), + ), + ).padding(horizontal: 16), + if (status.enabled) ...[ + if (status.actor != null) ...[ + ListTile( + leading: ActorPictureWidget( + actor: status.actor!, + radius: 24, + ), + title: Text( + status.actor!.displayName ?? + status.actor!.username ?? + 'unknown'.tr(), + ), + subtitle: Text( + '@${status.actor!.username}@${status.actor!.instance.domain}', + ), + isThreeLine: true, + contentPadding: const EdgeInsets.symmetric(horizontal: 28), + ), + ListTile( + leading: const Icon(Symbols.link), + title: Text('publisherFediverseActorUri').tr(), + subtitle: Text(status.actorUri ?? 'N/A'), + contentPadding: const EdgeInsets.symmetric(horizontal: 32), + ), + ], + ListTile( + leading: const Icon(Symbols.group), + title: Text('publisherFediverseFollowerCount').tr(), + subtitle: Text( + status.followerCount > 0 + ? status.followerCount.toString() + : 'publisherFediverseNoFollowers'.tr(), + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 32), + ), + ], + ExpansionTile( + leading: const Icon(Symbols.info), + title: Text('publisherFediverseWhatIs').tr(), + tilePadding: const EdgeInsets.symmetric(horizontal: 32), + children: [ + Padding( + padding: const EdgeInsets.symmetric( + vertical: 16, + horizontal: 32, + ), + child: Text('publisherFediverseAbout').tr(), + ), + ], + ), + ], + ), + ), + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, _) => ResponseErrorWidget( + error: error, + onRetry: () => + ref.invalidate(publisherActorStatusProvider(publisherUname)), + ), + ), + ); + } +} diff --git a/lib/screens/creators/hub.g.dart b/lib/screens/creators/hub.g.dart index 237185a6..f953a389 100644 --- a/lib/screens/creators/hub.g.dart +++ b/lib/screens/creators/hub.g.dart @@ -354,3 +354,81 @@ final class PublisherInvitesProvider } String _$publisherInvitesHash() => r'93aafc2f02af0a7a055ec1770b3999363dfaabdc'; + +@ProviderFor(publisherActorStatus) +const publisherActorStatusProvider = PublisherActorStatusFamily._(); + +final class PublisherActorStatusProvider + extends + $FunctionalProvider< + AsyncValue, + SnActorStatusResponse, + FutureOr + > + with + $FutureModifier, + $FutureProvider { + const PublisherActorStatusProvider._({ + required PublisherActorStatusFamily super.from, + required String? super.argument, + }) : super( + retry: null, + name: r'publisherActorStatusProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$publisherActorStatusHash(); + + @override + String toString() { + return r'publisherActorStatusProvider' + '' + '($argument)'; + } + + @$internal + @override + $FutureProviderElement $createElement( + $ProviderPointer pointer, + ) => $FutureProviderElement(pointer); + + @override + FutureOr create(Ref ref) { + final argument = this.argument as String?; + return publisherActorStatus(ref, argument); + } + + @override + bool operator ==(Object other) { + return other is PublisherActorStatusProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$publisherActorStatusHash() => + r'406117cb99b2aef236945ef0ef59e857d8835029'; + +final class PublisherActorStatusFamily extends $Family + with $FunctionalFamilyOverride, String?> { + const PublisherActorStatusFamily._() + : super( + retry: null, + name: r'publisherActorStatusProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + PublisherActorStatusProvider call(String? publisherName) => + PublisherActorStatusProvider._(argument: publisherName, from: this); + + @override + String toString() => r'publisherActorStatusProvider'; +} diff --git a/lib/screens/posts/publisher_profile.g.dart b/lib/screens/posts/publisher_profile.g.dart index 1b3ab72d..ef94ede2 100644 --- a/lib/screens/posts/publisher_profile.g.dart +++ b/lib/screens/posts/publisher_profile.g.dart @@ -293,7 +293,7 @@ final class PublisherAppbarForcegroundColorProvider } String _$publisherAppbarForcegroundColorHash() => - r'cd9a9816177a6eecc2bc354acebbbd48892ffdd7'; + r'a7c9795c68a29beb611d2c258022c9a5640f2061'; final class PublisherAppbarForcegroundColorFamily extends $Family with $FunctionalFamilyOverride, String> { diff --git a/lib/services/activitypub_service.dart b/lib/services/activitypub_service.dart index 8b8c139d..63f7da18 100644 --- a/lib/services/activitypub_service.dart +++ b/lib/services/activitypub_service.dart @@ -70,4 +70,21 @@ class ActivityPubService { .toList(); return users; } + + Future getPublisherActorStatus( + String publisherName, + ) async { + final response = await _client.get( + '/sphere/publishers/$publisherName/fediverse', + ); + return SnActorStatusResponse.fromJson(response.data); + } + + Future enablePublisherActor(String publisherName) async { + await _client.post('/sphere/publishers/$publisherName/fediverse'); + } + + Future disablePublisherActor(String publisherName) async { + await _client.delete('/sphere/publishers/$publisherName/fediverse'); + } }