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", "selectMicrophone": "Select Microphone",
"selectCamera": "Select Camera", "selectCamera": "Select Camera",
"switchedTo": "Switched to {}", "switchedTo": "Switched to {}",
"connecting": "Connecting" "connecting": "Connecting",
"repliesLoadMore": "Load more replies"
} }

View File

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

View File

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

View File

@@ -56,7 +56,13 @@ class PostRepliesNotifier extends _$PostRepliesNotifier
class PostRepliesList extends HookConsumerWidget { class PostRepliesList extends HookConsumerWidget {
final String postId; final String postId;
final double? maxWidth; 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 @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@@ -92,6 +98,8 @@ class PostRepliesList extends HookConsumerWidget {
borderRadius: 8, borderRadius: 8,
item: data.items[index], item: data.items[index],
isShowReference: false, isShowReference: false,
isEmbedOpenable: true,
onOpen: onOpen,
), ),
); );

View File

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