✨ ActivityPub service impl basis
This commit is contained in:
1
lib/widgets/activitypub/activitypub.dart
Normal file
1
lib/widgets/activitypub/activitypub.dart
Normal file
@@ -0,0 +1 @@
|
||||
export 'user_list_item.dart';
|
||||
172
lib/widgets/activitypub/actor_list_item.dart
Normal file
172
lib/widgets/activitypub/actor_list_item.dart
Normal file
@@ -0,0 +1,172 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:island/models/activitypub.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
class ApActorListItem extends StatelessWidget {
|
||||
final SnActivityPubActor actor;
|
||||
final bool isFollowing;
|
||||
final bool isLoading;
|
||||
final VoidCallback? onFollow;
|
||||
final VoidCallback? onUnfollow;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const ApActorListItem({
|
||||
super.key,
|
||||
required this.actor,
|
||||
this.isFollowing = false,
|
||||
this.isLoading = false,
|
||||
this.onFollow,
|
||||
this.onUnfollow,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
String _getDisplayName() {
|
||||
if (actor.displayName?.isNotEmpty ?? false) {
|
||||
return actor.displayName!;
|
||||
}
|
||||
if (actor.username?.isNotEmpty ?? false) {
|
||||
return actor.username!;
|
||||
}
|
||||
return actor.id.split('@').lastOrNull ?? 'Unknown';
|
||||
}
|
||||
|
||||
String _getUsername() {
|
||||
if (actor.username?.isNotEmpty ?? false) {
|
||||
return '@${actor.username}';
|
||||
}
|
||||
return actor.id;
|
||||
}
|
||||
|
||||
String _getInstanceDomain() {
|
||||
final parts = actor.id.split('@');
|
||||
if (parts.length >= 3) {
|
||||
return parts[2];
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
bool _isLocal() {
|
||||
// For now, assume all searched actors are remote
|
||||
// This could be determined by checking if the domain matches local instance
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final displayName = _getDisplayName();
|
||||
final username = _getUsername();
|
||||
final instanceDomain = _getInstanceDomain();
|
||||
final isLocal = _isLocal();
|
||||
|
||||
return ListTile(
|
||||
contentPadding: const EdgeInsets.only(left: 16, right: 12),
|
||||
leading: Stack(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
backgroundImage: actor.icon != null
|
||||
? CachedNetworkImageProvider(actor.icon!)
|
||||
: null,
|
||||
radius: 24,
|
||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: actor.icon == null
|
||||
? Icon(
|
||||
Symbols.person,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
if (!isLocal)
|
||||
Positioned(
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Symbols.public,
|
||||
size: 12,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Flexible(child: Text(displayName)),
|
||||
if (!isLocal && instanceDomain.isNotEmpty) const SizedBox(width: 6),
|
||||
if (!isLocal && instanceDomain.isNotEmpty)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
instanceDomain,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(username),
|
||||
if (actor.summary?.isNotEmpty ?? false)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: Text(
|
||||
actor.summary!,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
if (actor.type.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: Text(
|
||||
actor.type,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: isLoading
|
||||
? const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: isFollowing
|
||||
? OutlinedButton(
|
||||
onPressed: onUnfollow,
|
||||
style: OutlinedButton.styleFrom(
|
||||
minimumSize: const Size(88, 36),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
),
|
||||
child: const Text('Unfollow'),
|
||||
)
|
||||
: FilledButton(
|
||||
onPressed: onFollow,
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize: const Size(88, 36),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
),
|
||||
child: const Text('Follow'),
|
||||
),
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
}
|
||||
128
lib/widgets/activitypub/user_list_item.dart
Normal file
128
lib/widgets/activitypub/user_list_item.dart
Normal file
@@ -0,0 +1,128 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:island/models/activitypub.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:relative_time/relative_time.dart';
|
||||
|
||||
class ActivityPubUserListItem extends StatelessWidget {
|
||||
final SnActivityPubUser user;
|
||||
final bool isFollowing;
|
||||
final bool isLoading;
|
||||
final VoidCallback? onFollow;
|
||||
final VoidCallback? onUnfollow;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const ActivityPubUserListItem({
|
||||
super.key,
|
||||
required this.user,
|
||||
this.isFollowing = false,
|
||||
this.isLoading = false,
|
||||
this.onFollow,
|
||||
this.onUnfollow,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
contentPadding: const EdgeInsets.only(left: 16, right: 12),
|
||||
leading: Stack(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
backgroundImage: CachedNetworkImageProvider(user.avatarUrl),
|
||||
radius: 24,
|
||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
),
|
||||
if (!user.isLocal)
|
||||
Positioned(
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Symbols.public,
|
||||
size: 12,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Flexible(child: Text(user.displayName)),
|
||||
if (!user.isLocal) const SizedBox(width: 6),
|
||||
if (!user.isLocal)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
user.instanceDomain,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('@${user.username}'),
|
||||
if (user.bio.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: Text(
|
||||
user.bio,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: Text(
|
||||
'Followed ${RelativeTime(context).format(user.followedAt)}',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: isLoading
|
||||
? const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: isFollowing
|
||||
? OutlinedButton(
|
||||
onPressed: onUnfollow,
|
||||
style: OutlinedButton.styleFrom(
|
||||
minimumSize: const Size(88, 36),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
),
|
||||
child: const Text('Unfollow'),
|
||||
)
|
||||
: FilledButton(
|
||||
onPressed: onFollow,
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize: const Size(88, 36),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
),
|
||||
child: const Text('Follow'),
|
||||
),
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user