Able to manage publisher actor

This commit is contained in:
2026-01-01 02:29:27 +08:00
parent 788165ac5b
commit 93d2670063
6 changed files with 297 additions and 2 deletions

View File

@@ -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",

View File

@@ -212,7 +212,7 @@ final class AccountAppbarForcegroundColorProvider
}
String _$accountAppbarForcegroundColorHash() =>
r'127fcc7fd6ec6a41ac4a6975276b5271aa4fa7d0';
r'59e0049a5158ea653f0afd724df9ff2312b90050';
final class AccountAppbarForcegroundColorFamily extends $Family
with $FunctionalFamilyOverride<FutureOr<Color?>, String> {

View File

@@ -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<List<SnPublisherMember>> publisherInvites(Ref ref) async {
.toList();
}
@riverpod
Future<SnActorStatusResponse> 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<void> 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)),
),
),
);
}
}

View File

@@ -354,3 +354,81 @@ final class PublisherInvitesProvider
}
String _$publisherInvitesHash() => r'93aafc2f02af0a7a055ec1770b3999363dfaabdc';
@ProviderFor(publisherActorStatus)
const publisherActorStatusProvider = PublisherActorStatusFamily._();
final class PublisherActorStatusProvider
extends
$FunctionalProvider<
AsyncValue<SnActorStatusResponse>,
SnActorStatusResponse,
FutureOr<SnActorStatusResponse>
>
with
$FutureModifier<SnActorStatusResponse>,
$FutureProvider<SnActorStatusResponse> {
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<SnActorStatusResponse> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<SnActorStatusResponse> 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<FutureOr<SnActorStatusResponse>, 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';
}

View File

@@ -293,7 +293,7 @@ final class PublisherAppbarForcegroundColorProvider
}
String _$publisherAppbarForcegroundColorHash() =>
r'cd9a9816177a6eecc2bc354acebbbd48892ffdd7';
r'a7c9795c68a29beb611d2c258022c9a5640f2061';
final class PublisherAppbarForcegroundColorFamily extends $Family
with $FunctionalFamilyOverride<FutureOr<Color?>, String> {

View File

@@ -70,4 +70,21 @@ class ActivityPubService {
.toList();
return users;
}
Future<SnActorStatusResponse> getPublisherActorStatus(
String publisherName,
) async {
final response = await _client.get(
'/sphere/publishers/$publisherName/fediverse',
);
return SnActorStatusResponse.fromJson(response.data);
}
Future<void> enablePublisherActor(String publisherName) async {
await _client.post('/sphere/publishers/$publisherName/fediverse');
}
Future<void> disablePublisherActor(String publisherName) async {
await _client.delete('/sphere/publishers/$publisherName/fediverse');
}
}