✨ Post details
This commit is contained in:
@ -1,5 +1,7 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:island/models/post.dart';
|
||||
import 'package:island/route.gr.dart';
|
||||
import 'package:island/widgets/content/cloud_file_collection.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:island/widgets/content/markdown.dart';
|
||||
@ -8,7 +10,13 @@ import 'package:styled_widget/styled_widget.dart';
|
||||
class PostItem extends StatelessWidget {
|
||||
final SnPost item;
|
||||
final EdgeInsets? padding;
|
||||
const PostItem({super.key, required this.item, this.padding});
|
||||
final bool isOpenable;
|
||||
const PostItem({
|
||||
super.key,
|
||||
required this.item,
|
||||
this.padding,
|
||||
this.isOpenable = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -26,13 +34,20 @@ class PostItem extends StatelessWidget {
|
||||
children: [
|
||||
ProfilePictureWidget(item: item.publisher.picture),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(item.publisher.nick).bold(),
|
||||
if (item.content.isNotEmpty)
|
||||
MarkdownTextContent(content: item.content),
|
||||
],
|
||||
child: GestureDetector(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(item.publisher.nick).bold(),
|
||||
if (item.content.isNotEmpty)
|
||||
MarkdownTextContent(content: item.content),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
if (isOpenable) {
|
||||
context.router.push(PostDetailRoute(id: item.id));
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
|
107
lib/widgets/post/post_quick_reply.dart
Normal file
107
lib/widgets/post/post_quick_reply.dart
Normal file
@ -0,0 +1,107 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/post.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/screens/account/me/publishers.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
class PostQuickReply extends HookConsumerWidget {
|
||||
final SnPost parent;
|
||||
final Function? onPosted;
|
||||
const PostQuickReply({super.key, required this.parent, this.onPosted});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final publishers = ref.watch(publishersManagedProvider);
|
||||
|
||||
final currentPublisher = useState<SnPublisher?>(null);
|
||||
|
||||
useEffect(() {
|
||||
if (publishers.value?.isNotEmpty ?? false) {
|
||||
currentPublisher.value = publishers.value!.first;
|
||||
}
|
||||
return null;
|
||||
}, [publishers]);
|
||||
|
||||
final submitting = useState(false);
|
||||
|
||||
final contentController = useTextEditingController();
|
||||
|
||||
Future<void> performAction() async {
|
||||
if (!contentController.text.isNotEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
await client.post(
|
||||
'/posts',
|
||||
data: {
|
||||
'content': contentController.text,
|
||||
'replied_post_id': parent.id,
|
||||
},
|
||||
options: Options(headers: {'X-Pub': currentPublisher.value?.name}),
|
||||
);
|
||||
contentController.clear();
|
||||
onPosted?.call();
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return publishers.when(
|
||||
data:
|
||||
(data) => Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
ProfilePictureWidget(
|
||||
item: currentPublisher.value?.picture,
|
||||
radius: 16,
|
||||
).padding(right: 4),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: contentController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Post your reply',
|
||||
border: const OutlineInputBorder(),
|
||||
isDense: true,
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
),
|
||||
),
|
||||
style: TextStyle(fontSize: 14),
|
||||
maxLines: null,
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
icon:
|
||||
submitting.value
|
||||
? SizedBox(
|
||||
width: 28,
|
||||
height: 28,
|
||||
child: CircularProgressIndicator(strokeWidth: 3),
|
||||
)
|
||||
: Icon(LucideIcons.send, size: 20),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
onPressed: submitting.value ? null : performAction,
|
||||
),
|
||||
],
|
||||
),
|
||||
loading: () => const SizedBox.shrink(),
|
||||
error: (e, _) => const SizedBox.shrink(),
|
||||
);
|
||||
}
|
||||
}
|
116
lib/widgets/post/post_replies.dart
Normal file
116
lib/widgets/post/post_replies.dart
Normal file
@ -0,0 +1,116 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/post.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/widgets/post/post_item.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
class PostRepliesList extends HookConsumerWidget {
|
||||
final int postId;
|
||||
const PostRepliesList({super.key, required this.postId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final postAsync = ref.watch(postRepliesProvider(postId));
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh:
|
||||
() => Future.sync((() {
|
||||
ref.invalidate(postRepliesProvider(postId));
|
||||
})),
|
||||
child: postAsync.when(
|
||||
data:
|
||||
(controller) => RefreshIndicator(
|
||||
onRefresh:
|
||||
() => Future.sync((() {
|
||||
ref.invalidate(postRepliesProvider(postId));
|
||||
})),
|
||||
child: InfiniteList(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).padding.bottom,
|
||||
),
|
||||
itemCount: controller.posts.length,
|
||||
isLoading: controller.isLoading,
|
||||
hasReachedMax: controller.hasReachedMax,
|
||||
onFetchData: controller.fetchMore,
|
||||
itemBuilder: (context, index) {
|
||||
final post = controller.posts[index];
|
||||
return PostItem(item: post);
|
||||
},
|
||||
separatorBuilder: (_, __) => const Divider(height: 1),
|
||||
emptyBuilder: (context) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
'No replies',
|
||||
textAlign: TextAlign.center,
|
||||
).fontSize(18).bold(),
|
||||
Text('Why not start a discussion?'),
|
||||
],
|
||||
).padding(vertical: 16);
|
||||
},
|
||||
),
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error:
|
||||
(e, _) => GestureDetector(
|
||||
child: Center(
|
||||
child: Text('Error: $e', textAlign: TextAlign.center),
|
||||
),
|
||||
onTap: () {
|
||||
ref.invalidate(postRepliesProvider(postId));
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final postRepliesProvider = FutureProviderFamily<_PostRepliesController, int>((
|
||||
ref,
|
||||
postId,
|
||||
) async {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
final controller = _PostRepliesController(client, postId);
|
||||
await controller.fetchMore();
|
||||
return controller;
|
||||
});
|
||||
|
||||
class _PostRepliesController {
|
||||
_PostRepliesController(this._dio, this.parentId);
|
||||
|
||||
final Dio _dio;
|
||||
final int parentId;
|
||||
final List<SnPost> posts = [];
|
||||
bool isLoading = false;
|
||||
bool hasReachedMax = false;
|
||||
int offset = 0;
|
||||
final int take = 20;
|
||||
int total = 0;
|
||||
|
||||
Future<void> fetchMore() async {
|
||||
if (isLoading || hasReachedMax) return;
|
||||
isLoading = true;
|
||||
|
||||
final response = await _dio.get(
|
||||
'/posts/$parentId/replies',
|
||||
queryParameters: {'offset': offset, 'take': take},
|
||||
);
|
||||
|
||||
final List<SnPost> fetched =
|
||||
(response.data as List)
|
||||
.map((e) => SnPost.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
final headerTotal = int.tryParse(response.headers['x-total']?.first ?? '');
|
||||
if (headerTotal != null) total = headerTotal;
|
||||
|
||||
posts.addAll(fetched);
|
||||
offset += fetched.length;
|
||||
if (posts.length >= total) hasReachedMax = true;
|
||||
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user