Comment threading

👽 Fix chat notify level
This commit is contained in:
2025-08-01 13:01:38 +08:00
parent 890a8a44cf
commit 4d489425fa
5 changed files with 225 additions and 55 deletions

View File

@@ -727,5 +727,6 @@
"selectMicrophone": "Select Microphone",
"selectCamera": "Select Camera",
"switchedTo": "Switched to {}",
"connecting": "Connecting"
"connecting": "Connecting",
"repliesLoadMore": "Load more replies"
}

View File

@@ -41,7 +41,7 @@ class ChatDetailScreen extends HookConsumerWidget {
try {
final client = ref.watch(apiClientProvider);
await client.patch(
'/chat/$id/members/me/notify',
'/sphere/chat/$id/members/me/notify',
data: {'notify_level': level},
);
ref.invalidate(chatroomIdentityProvider(id));
@@ -59,7 +59,7 @@ class ChatDetailScreen extends HookConsumerWidget {
try {
final client = ref.watch(apiClientProvider);
await client.patch(
'/chat/$id/members/me/notify',
'/sphere/chat/$id/members/me/notify',
data: {'break_until': until.toUtc().toIso8601String()},
);
ref.invalidate(chatroomProvider(id));

View File

@@ -1,5 +1,4 @@
import 'dart:math' as math;
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
@@ -49,18 +48,24 @@ class PostActionableItem extends HookConsumerWidget {
final EdgeInsets? padding;
final bool isFullPost;
final bool isShowReference;
final bool isEmbedReply;
final bool isEmbedOpenable;
final double? borderRadius;
final Function? onRefresh;
final VoidCallback? onRefresh;
final Function(SnPost)? onUpdate;
final VoidCallback? onOpen;
const PostActionableItem({
super.key,
required this.item,
this.padding,
this.isFullPost = false,
this.isShowReference = true,
this.isEmbedReply = true,
this.isEmbedOpenable = false,
this.borderRadius,
this.onRefresh,
this.onUpdate,
this.onOpen,
});
@override
@@ -82,11 +87,15 @@ class PostActionableItem extends HookConsumerWidget {
padding: padding,
isFullPost: isFullPost,
isShowReference: isShowReference,
isEmbedReply: isEmbedReply,
isEmbedOpenable: isEmbedOpenable,
isTextSelectable: false,
onRefresh: onRefresh,
onUpdate: onUpdate,
onOpen: onOpen,
),
onTap: () {
onOpen?.call();
context.pushNamed('postDetail', pathParameters: {'id': item.id});
},
);
@@ -207,9 +216,11 @@ class PostItem extends HookConsumerWidget {
final bool isFullPost;
final bool isShowReference;
final bool isEmbedReply;
final bool isEmbedOpenable;
final bool isTextSelectable;
final Function? onRefresh;
final VoidCallback? onRefresh;
final Function(SnPost)? onUpdate;
final VoidCallback? onOpen;
const PostItem({
super.key,
required this.item,
@@ -217,9 +228,11 @@ class PostItem extends HookConsumerWidget {
this.isFullPost = false,
this.isShowReference = true,
this.isEmbedReply = true,
this.isEmbedOpenable = false,
this.isTextSelectable = true,
this.onRefresh,
this.onUpdate,
this.onOpen,
});
@override
@@ -531,7 +544,9 @@ class PostItem extends HookConsumerWidget {
_buildReferencePost(context, item, renderingPadding),
if (item.repliesCount > 0 && isEmbedReply)
PostReplyPreview(
item,
parent: item,
isOpenable: isEmbedOpenable,
onOpen: onOpen,
).padding(horizontal: renderingPadding.horizontal, top: 8),
Gap(renderingPadding.vertical),
],
@@ -703,56 +718,195 @@ Widget _buildReferencePost(
class PostReplyPreview extends HookConsumerWidget {
final SnPost parent;
const PostReplyPreview(this.parent, {super.key});
final bool isOpenable;
final bool isCompact;
final bool isAutoload;
final VoidCallback? onOpen;
const PostReplyPreview({
super.key,
required this.parent,
this.isOpenable = false,
this.isCompact = false,
this.isAutoload = true,
this.onOpen,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final featuredReply = ref.watch(PostFeaturedReplyProvider(parent.id));
final contentWidget = Container(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerLow,
border: Border.all(color: Theme.of(context).dividerColor),
borderRadius: BorderRadius.all(Radius.circular(8)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
spacing: 4,
children: [
Text('repliesCount')
.plural(parent.repliesCount)
.tr()
.fontSize(15)
.bold()
.padding(horizontal: 5),
featuredReply.when(
data:
(value) => Row(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8,
children: [
ProfilePictureWidget(
file: value!.publisher.picture,
radius: 12,
).padding(top: 4),
if (value.content?.isNotEmpty ?? false)
Expanded(
child: MarkdownTextContent(content: value.content!),
)
else
Expanded(
child: Text(
'postHasAttachments',
).plural(value.attachments.length),
final posts = useState<List<SnPost>>([]);
final loading = useState(false);
Future<void> fetchMoreReplies({int pageSize = 1}) async {
final client = ref.read(apiClientProvider);
try {
loading.value = true;
final response = await client.get(
'/sphere/posts/${parent.id}/replies',
queryParameters: {'offset': posts.value.length, 'take': pageSize},
);
posts.value = [
...posts.value,
...response.data.map((e) => SnPost.fromJson(e)),
];
} catch (err) {
showErrorAlert(err);
} finally {
loading.value = false;
}
}
useEffect(() {
if (isAutoload) fetchMoreReplies();
return null;
}, [parent]);
final featuredReply =
isOpenable ? null : ref.watch(PostFeaturedReplyProvider(parent.id));
final itemWidget =
isOpenable
? Column(
children: [
for (final post in posts.value)
Column(
children: [
InkWell(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8,
children: [
ProfilePictureWidget(
file: post.publisher.picture,
radius: 12,
).padding(top: 4),
if (post.content?.isNotEmpty ?? false)
Expanded(
child: MarkdownTextContent(
content: post.content!,
).padding(top: 2),
)
else
Expanded(
child: Text(
'postHasAttachments',
).plural(post.attachments.length),
),
],
),
onTap: () {
onOpen?.call();
context.pushNamed(
'postDetail',
pathParameters: {'id': post.id},
);
},
),
],
),
error: (error, _) => Row(children: [const Icon(Symbols.close)]),
loading: () => Row(children: [CircularProgressIndicator()]),
),
],
),
);
if (post.repliesCount > 0)
PostReplyPreview(
parent: post,
isOpenable: true,
isCompact: true,
isAutoload: false,
onOpen: onOpen,
).padding(left: 24),
],
),
if (loading.value)
Row(
spacing: 8,
children: [
SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(),
),
Text('loading').tr(),
],
)
else if (posts.value.length < parent.repliesCount)
InkWell(
child: Row(
spacing: 8,
children: [
const Icon(Symbols.keyboard_arrow_down, size: 20),
Text('repliesLoadMore').tr(),
],
),
onTap: () {
fetchMoreReplies();
},
),
],
)
: featuredReply!.when(
data:
(value) => Row(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8,
children: [
ProfilePictureWidget(
file: value!.publisher.picture,
radius: 12,
).padding(top: 4),
if (value.content?.isNotEmpty ?? false)
Expanded(
child: MarkdownTextContent(content: value.content!),
)
else
Expanded(
child: Text(
'postHasAttachments',
).plural(value.attachments.length),
),
],
),
error:
(error, _) => Row(
spacing: 8,
children: [
const Icon(Symbols.close, size: 18),
Text(error.toString()),
],
),
loading:
() => Row(
spacing: 8,
children: [
SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(),
),
Text('loading').tr(),
],
),
);
final contentWidget =
isCompact
? itemWidget
: Container(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerLow,
border: Border.all(color: Theme.of(context).dividerColor),
borderRadius: BorderRadius.all(Radius.circular(8)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
spacing: 4,
children: [
Text('repliesCount')
.plural(parent.repliesCount)
.tr()
.fontSize(15)
.bold()
.padding(horizontal: 5),
itemWidget,
],
),
);
return InkWell(
borderRadius: const BorderRadius.all(Radius.circular(8)),

View File

@@ -56,7 +56,13 @@ class PostRepliesNotifier extends _$PostRepliesNotifier
class PostRepliesList extends HookConsumerWidget {
final String postId;
final double? maxWidth;
const PostRepliesList({super.key, required this.postId, this.maxWidth});
final VoidCallback? onOpen;
const PostRepliesList({
super.key,
required this.postId,
this.maxWidth,
this.onOpen,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -92,6 +98,8 @@ class PostRepliesList extends HookConsumerWidget {
borderRadius: 8,
item: data.items[index],
isShowReference: false,
isEmbedOpenable: true,
onOpen: onOpen,
),
);

View File

@@ -24,7 +24,14 @@ class PostRepliesSheet extends HookConsumerWidget {
// Replies list
Expanded(
child: CustomScrollView(
slivers: [PostRepliesList(postId: post.id.toString())],
slivers: [
PostRepliesList(
postId: post.id.toString(),
onOpen: () {
Navigator.pop(context);
},
),
],
),
),
// Quick reply section