✨ Comment threading
👽 Fix chat notify level
This commit is contained in:
@@ -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"
|
||||||
}
|
}
|
||||||
|
@@ -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));
|
||||||
|
@@ -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,29 +718,128 @@ 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},
|
||||||
|
);
|
||||||
|
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: [
|
children: [
|
||||||
Text('repliesCount')
|
for (final post in posts.value)
|
||||||
.plural(parent.repliesCount)
|
Column(
|
||||||
.tr()
|
children: [
|
||||||
.fontSize(15)
|
InkWell(
|
||||||
.bold()
|
child: Row(
|
||||||
.padding(horizontal: 5),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
featuredReply.when(
|
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(
|
||||||
|
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:
|
data:
|
||||||
(value) => Row(
|
(value) => Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -747,9 +861,49 @@ class PostReplyPreview extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
error: (error, _) => Row(children: [const Icon(Symbols.close)]),
|
error:
|
||||||
loading: () => Row(children: [CircularProgressIndicator()]),
|
(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,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@@ -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,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
Reference in New Issue
Block a user