Able to render fediverse posts

This commit is contained in:
2026-01-01 01:47:09 +08:00
parent b3ae4ab36f
commit adb231278c
15 changed files with 814 additions and 136 deletions

View File

@@ -6,10 +6,13 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:html2md/html2md.dart' as html2md;
import 'package:island/models/account.dart';
import 'package:island/models/post.dart';
import 'package:island/pods/network.dart';
import 'package:island/services/time.dart';
import 'package:island/widgets/account/account_name.dart';
import 'package:island/widgets/activitypub/actor_profile.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/cloud_file_collection.dart';
import 'package:island/widgets/content/cloud_files.dart';
@@ -24,6 +27,14 @@ part 'post_shared.g.dart';
const kMessageEnableEmbedTypes = ['text', 'messages.new'];
/// Converts HTML content to markdown if contentType indicates HTML (contentType == 1)
String _convertContentToMarkdown(SnPost post) {
if (post.contentType == 1 && post.content != null) {
return html2md.convert(post.content!);
}
return post.content ?? '';
}
class RepliesState {
final List<SnPost> posts;
final bool loading;
@@ -120,6 +131,27 @@ class PostReplyPreview extends HookConsumerWidget {
this.onOpen,
});
Widget _buildProfilePicture(
BuildContext context,
SnPost post, {
double radius = 16,
}) {
// Handle publisher case
if (post.publisher != null) {
return ProfilePictureWidget(
file:
post.publisher!.picture ?? post.publisher!.account?.profile.picture,
radius: radius,
);
}
// Handle actor case
if (post.actor != null) {
return ActorAvatarWidget(actor: post.actor!, radius: radius);
}
// Fallback
return ProfilePictureWidget(fileId: null, radius: radius);
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final repliesState = ref.watch(repliesProvider(parent.id));
@@ -157,16 +189,15 @@ class PostReplyPreview extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8,
children: [
ProfilePictureWidget(
file:
post.publisher.picture ??
post.publisher.account?.profile.picture,
_buildProfilePicture(
context,
post,
radius: 12,
).padding(top: 4),
if (post.content?.isNotEmpty ?? false)
Expanded(
child: MarkdownTextContent(
content: post.content!,
content: _convertContentToMarkdown(post),
attachments: post.attachments,
).padding(top: 2),
)
@@ -244,16 +275,15 @@ class PostReplyPreview extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8,
children: [
ProfilePictureWidget(
file:
data.value?.publisher.picture ??
data.value?.publisher.account?.profile.picture,
_buildProfilePicture(
context,
data.value!,
radius: 12,
).padding(top: 4),
if (data.value?.content?.isNotEmpty ?? false)
Expanded(
child: MarkdownTextContent(
content: data.value!.content!,
content: _convertContentToMarkdown(data.value!),
attachments: data.value!.attachments,
),
)
@@ -408,6 +438,38 @@ class ReferencedPostWidget extends StatelessWidget {
this.renderingPadding = EdgeInsets.zero,
});
Widget _buildProfilePicture(
BuildContext context,
SnPost post, {
double radius = 16,
}) {
// Handle publisher case
if (post.publisher != null) {
return ProfilePictureWidget(
fileId: post.publisher!.picture?.id,
radius: radius,
);
}
// Handle actor case
if (post.actor != null) {
return ActorAvatarWidget(actor: post.actor!, radius: radius);
}
// Fallback
return ProfilePictureWidget(fileId: null, radius: radius);
}
String _getDisplayName(SnPost post) {
// Handle publisher case
if (post.publisher != null) {
return post.publisher!.nick;
}
// Handle actor case
if (post.actor != null) {
return post.actor!.displayName ?? post.actor!.username ?? 'Unknown';
}
return 'Unknown';
}
@override
Widget build(BuildContext context) {
final referencePost = item.repliedPost ?? item.forwardedPost;
@@ -479,17 +541,14 @@ class ReferencedPostWidget extends StatelessWidget {
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ProfilePictureWidget(
fileId: referencePost!.publisher.picture?.id,
radius: 16,
),
_buildProfilePicture(context, referencePost!, radius: 16),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
referencePost.publisher.nick,
_getDisplayName(referencePost),
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
@@ -541,7 +600,7 @@ class ReferencedPostWidget extends StatelessWidget {
).padding(bottom: 2),
if (referencePost.content?.isNotEmpty ?? false)
MarkdownTextContent(
content: referencePost.content!,
content: _convertContentToMarkdown(referencePost),
textStyle: const TextStyle(fontSize: 14),
isSelectable: false,
linesMargin: referencePost.type == 0
@@ -620,6 +679,72 @@ class PostHeader extends StatelessWidget {
this.hideOverlay = false,
});
Widget _buildProfilePicture(
BuildContext context,
SnPost post, {
double radius = 16,
}) {
// Handle publisher case
if (post.publisher != null) {
return ProfilePictureWidget(
file:
post.publisher!.picture ?? post.publisher!.account?.profile.picture,
radius: radius,
borderRadius: post.publisher!.type == 0 ? null : 6,
);
}
// Handle actor case
if (post.actor != null) {
return ActorAvatarWidget(actor: post.actor!, radius: radius);
}
// Fallback
return ProfilePictureWidget(fileId: null, radius: radius);
}
String _getDisplayName(SnPost post) {
// Handle publisher case
if (post.publisher != null) {
return post.publisher!.nick;
}
// Handle actor case
if (post.actor != null) {
return post.actor!.displayName ?? post.actor!.username ?? 'unknown'.tr();
}
return 'unknown'.tr();
}
String? _getPublisherName(SnPost post) {
// Handle publisher case
if (post.publisher != null) {
return post.publisher!.name;
}
// Handle actor case
if (post.actor != null) {
return '${post.actor!.username}@${post.actor!.instance.domain}';
}
return null;
}
int _getPublisherType(SnPost post) {
// Handle publisher case
if (post.publisher != null) {
return post.publisher!.type;
}
return 0; // Default to user type
}
bool _hasAccount(SnPost post) {
return post.publisher?.account != null;
}
SnAccount? _getAccount(SnPost post) {
return post.publisher?.account;
}
SnVerificationMark? _getVerification(SnPost post) {
return post.publisher?.verification;
}
@override
Widget build(BuildContext context) {
return Column(
@@ -629,21 +754,17 @@ class PostHeader extends StatelessWidget {
spacing: 12,
children: [
GestureDetector(
onTap: isInteractive
onTap: isInteractive && _getPublisherName(item) != null
? () {
context.pushNamed(
'publisherProfile',
pathParameters: {'name': item.publisher.name},
pathParameters: {
'name': _getPublisherName(item) as String,
},
);
}
: null,
child: ProfilePictureWidget(
file:
item.publisher.picture ??
item.publisher.account?.profile.picture,
radius: 16,
borderRadius: item.publisher.type == 0 ? null : 6,
),
child: _buildProfilePicture(context, item, radius: 16),
),
Expanded(
child: Column(
@@ -656,24 +777,23 @@ class PostHeader extends StatelessWidget {
children: [
Flexible(
child:
(item.publisher.account != null &&
item.publisher.type == 0)
(_hasAccount(item) && _getPublisherType(item) == 0)
? AccountName(
hideOverlay: hideOverlay,
account: item.publisher.account!,
textOverride: item.publisher.nick,
account: _getAccount(item)!,
textOverride: _getDisplayName(item),
style: TextStyle(fontWeight: FontWeight.bold),
hideVerificationMark: true,
)
: Text(
item.publisher.nick,
_getDisplayName(item),
maxLines: 1,
overflow: TextOverflow.ellipsis,
).bold(),
),
if (item.publisher.verification != null)
if (_getVerification(item) != null)
VerificationMark(
mark: item.publisher.verification!,
mark: _getVerification(item)!,
hideOverlay: hideOverlay,
),
if (item.realm == null)
@@ -681,7 +801,7 @@ class PostHeader extends StatelessWidget {
child: isCompact
? const SizedBox.shrink()
: Text(
'@${item.publisher.name}',
'@${_getPublisherName(item) ?? 'unknown'}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
).fontSize(11),
@@ -891,6 +1011,17 @@ class PostBody extends ConsumerWidget {
),
);
}
if (item.fediverseUri != null) {
metadataChildren.add(
Row(
spacing: 8,
children: [
const Icon(Symbols.globe, size: 16),
Text('fediversePostDescribe'.tr()).fontSize(13),
],
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -937,7 +1068,7 @@ class PostBody extends ConsumerWidget {
)
else
MarkdownTextContent(
content: '${item.content!}...',
content: '${_convertContentToMarkdown(item)}...',
attachments: item.attachments,
),
],
@@ -974,8 +1105,8 @@ class PostBody extends ConsumerWidget {
).padding(bottom: 4),
MarkdownTextContent(
content: item.isTruncated
? '${item.content!}...'
: item.content ?? '',
? '${_convertContentToMarkdown(item)}...'
: _convertContentToMarkdown(item),
isSelectable: isTextSelectable,
attachments: item.attachments,
),