✨ Existing friends management
This commit is contained in:
parent
0230ea5c79
commit
5346224f1e
@ -10,6 +10,7 @@
|
||||
"signInRiskDetected": "Risk detected, click Next to open a webpage and signin through it to pass security check.",
|
||||
"signUp": "Sign Up",
|
||||
"signUpCaption": "Create an account on Solarpass and then get the access of entire Solar Networks!",
|
||||
"signOut": "Sign Out",
|
||||
"poweredBy": "Powered by Project Hydrogen",
|
||||
"copyright": "Copyright © 2024 Solsynth LLC",
|
||||
"confirmation": "Confirmation",
|
||||
@ -30,6 +31,11 @@
|
||||
"notifyDone": "You're done!",
|
||||
"notifyDoneCaption": "There are no notifications unread for you.",
|
||||
"notifyListHint": "Pull to refresh, swipe to dismiss",
|
||||
"friend": "Friend",
|
||||
"friendPending": "Pending",
|
||||
"friendActive": "Active",
|
||||
"friendBlocked": "Blocked",
|
||||
"friendListHint": "Swipe left to decline, swipe right to approve",
|
||||
"reaction": "Reaction",
|
||||
"reactVerb": "React",
|
||||
"post": "Post",
|
||||
|
@ -4,12 +4,13 @@
|
||||
"chat": "聊天",
|
||||
"account": "账号",
|
||||
"riskDetection": "风险监测",
|
||||
"signIn": "登陆",
|
||||
"signInCaption": "登陆以发表帖子、文章、创建领域、和你的朋友聊天,以及获取更多功能!",
|
||||
"signInRequired": "请先登陆",
|
||||
"signInRiskDetected": "检测到风险,点击下一步按钮来打开一个网页,并通过在其上面登陆来通过安全检查。",
|
||||
"signIn": "登录",
|
||||
"signInCaption": "登录以发表帖子、文章、创建领域、和你的朋友聊天,以及获取更多功能!",
|
||||
"signInRequired": "请先登录",
|
||||
"signInRiskDetected": "检测到风险,点击下一步按钮来打开一个网页,并通过在其上面登录来通过安全检查。",
|
||||
"signUp": "注册",
|
||||
"signUpCaption": "在 Solarpass 注册一个账号以获得整个 Solar Networks 的存取权!",
|
||||
"signOut": "登出",
|
||||
"poweredBy": "由 Project Hydrogen 强力驱动",
|
||||
"copyright": "2024 Solsynth LLC © 版权所有",
|
||||
"confirmation": "确认",
|
||||
@ -30,6 +31,11 @@
|
||||
"notifyDone": "所有通知已读!",
|
||||
"notifyDoneCaption": "这里没有什么东西可以给你看的了~",
|
||||
"notifyListHint": "下拉以刷新,左滑来已读",
|
||||
"friend": "好友",
|
||||
"friendPending": "请求中",
|
||||
"friendActive": "活跃的好友",
|
||||
"friendBlocked": "封锁中",
|
||||
"friendListHint": "左滑来拒绝,右滑来接受",
|
||||
"reaction": "反应",
|
||||
"reactVerb": "作出反应",
|
||||
"post": "帖子",
|
||||
|
@ -8,9 +8,9 @@ class Account {
|
||||
String avatar;
|
||||
String banner;
|
||||
String description;
|
||||
String emailAddress;
|
||||
String? emailAddress;
|
||||
int powerLevel;
|
||||
int externalId;
|
||||
int? externalId;
|
||||
|
||||
Account({
|
||||
required this.id,
|
||||
@ -22,9 +22,9 @@ class Account {
|
||||
required this.avatar,
|
||||
required this.banner,
|
||||
required this.description,
|
||||
required this.emailAddress,
|
||||
this.emailAddress,
|
||||
required this.powerLevel,
|
||||
required this.externalId,
|
||||
this.externalId,
|
||||
});
|
||||
|
||||
factory Account.fromJson(Map<String, dynamic> json) => Account(
|
||||
|
53
lib/models/friendship.dart
Normal file
53
lib/models/friendship.dart
Normal file
@ -0,0 +1,53 @@
|
||||
import 'package:solian/models/account.dart';
|
||||
|
||||
class Friendship {
|
||||
int id;
|
||||
DateTime createdAt;
|
||||
DateTime updatedAt;
|
||||
DateTime? deletedAt;
|
||||
int accountId;
|
||||
int relatedId;
|
||||
int? blockedBy;
|
||||
Account account;
|
||||
Account related;
|
||||
int status;
|
||||
|
||||
Friendship({
|
||||
required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
this.deletedAt,
|
||||
required this.accountId,
|
||||
required this.relatedId,
|
||||
this.blockedBy,
|
||||
required this.account,
|
||||
required this.related,
|
||||
required this.status,
|
||||
});
|
||||
|
||||
factory Friendship.fromJson(Map<String, dynamic> json) => Friendship(
|
||||
id: json["id"],
|
||||
createdAt: DateTime.parse(json["created_at"]),
|
||||
updatedAt: DateTime.parse(json["updated_at"]),
|
||||
deletedAt: json["deleted_at"],
|
||||
accountId: json["account_id"],
|
||||
relatedId: json["related_id"],
|
||||
blockedBy: json["blocked_by"],
|
||||
account: Account.fromJson(json["account"]),
|
||||
related: Account.fromJson(json["related"]),
|
||||
status: json["status"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"id": id,
|
||||
"created_at": createdAt.toIso8601String(),
|
||||
"updated_at": updatedAt.toIso8601String(),
|
||||
"deleted_at": deletedAt,
|
||||
"account_id": accountId,
|
||||
"related_id": relatedId,
|
||||
"blocked_by": blockedBy,
|
||||
"account": account.toJson(),
|
||||
"related": related.toJson(),
|
||||
"status": status,
|
||||
};
|
||||
}
|
@ -50,6 +50,11 @@ class NotifyProvider extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void clearAt(int index) {
|
||||
notifications.removeAt(index);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void clearNonRealtime() {
|
||||
notifications = notifications.where((x) => !x.isRealtime).toList();
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:solian/models/channel.dart';
|
||||
import 'package:solian/models/post.dart';
|
||||
import 'package:solian/screens/account.dart';
|
||||
import 'package:solian/screens/account/friend.dart';
|
||||
import 'package:solian/screens/chat/chat.dart';
|
||||
import 'package:solian/screens/chat/index.dart';
|
||||
import 'package:solian/screens/chat/manage.dart';
|
||||
@ -20,6 +21,11 @@ final router = GoRouter(
|
||||
name: 'explore',
|
||||
builder: (context, state) => const ExploreScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/notification',
|
||||
name: 'notification',
|
||||
builder: (context, state) => const NotificationScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/chat',
|
||||
name: 'chat',
|
||||
@ -66,15 +72,15 @@ final router = GoRouter(
|
||||
dataset: state.pathParameters['dataset'] as String,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/notification',
|
||||
name: 'notification',
|
||||
builder: (context, state) => const NotificationScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/auth/sign-in',
|
||||
name: 'auth.sign-in',
|
||||
builder: (context, state) => SignInScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account/friend',
|
||||
name: 'account.friend',
|
||||
builder: (context, state) => const FriendScreen(),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
@ -1,11 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/utils/service_url.dart';
|
||||
import 'package:solian/widgets/common_wrapper.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
class AccountScreen extends StatefulWidget {
|
||||
const AccountScreen({super.key});
|
||||
@ -40,21 +40,25 @@ class _AccountScreenState extends State<AccountScreen> {
|
||||
padding: EdgeInsets.symmetric(vertical: 8, horizontal: 24),
|
||||
child: NameCard(),
|
||||
),
|
||||
InkWell(
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 18),
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.logout),
|
||||
title: Text("Sign out"),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
|
||||
leading: const Icon(Icons.diversity_1),
|
||||
title: Text(AppLocalizations.of(context)!.friend),
|
||||
onTap: () {
|
||||
router.goNamed('account.friend');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
|
||||
leading: const Icon(Icons.logout),
|
||||
title: Text(AppLocalizations.of(context)!.signOut),
|
||||
onTap: () {
|
||||
auth.signoff();
|
||||
setState(() {
|
||||
isAuthorized = false;
|
||||
});
|
||||
},
|
||||
)
|
||||
),
|
||||
],
|
||||
)
|
||||
: Center(
|
||||
@ -126,8 +130,7 @@ class NameCard extends StatelessWidget {
|
||||
children: [
|
||||
FutureBuilder(
|
||||
future: renderAvatar(context),
|
||||
builder:
|
||||
(BuildContext context, AsyncSnapshot<Widget> snapshot) {
|
||||
builder: (BuildContext context, AsyncSnapshot<Widget> snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return snapshot.data!;
|
||||
} else {
|
||||
@ -138,8 +141,7 @@ class NameCard extends StatelessWidget {
|
||||
const SizedBox(width: 20),
|
||||
FutureBuilder(
|
||||
future: renderLabel(context),
|
||||
builder:
|
||||
(BuildContext context, AsyncSnapshot<Column> snapshot) {
|
||||
builder: (BuildContext context, AsyncSnapshot<Column> snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return snapshot.data!;
|
||||
} else {
|
||||
@ -161,12 +163,7 @@ class ActionCard extends StatelessWidget {
|
||||
final String caption;
|
||||
final Function onTap;
|
||||
|
||||
const ActionCard(
|
||||
{super.key,
|
||||
required this.onTap,
|
||||
required this.title,
|
||||
required this.caption,
|
||||
required this.icon});
|
||||
const ActionCard({super.key, required this.onTap, required this.title, required this.caption, required this.icon});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
218
lib/screens/account/friend.dart
Normal file
218
lib/screens/account/friend.dart
Normal file
@ -0,0 +1,218 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:solian/models/account.dart';
|
||||
import 'package:solian/models/friendship.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/utils/service_url.dart';
|
||||
import 'package:solian/widgets/indent_wrapper.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
class FriendScreen extends StatefulWidget {
|
||||
const FriendScreen({super.key});
|
||||
|
||||
@override
|
||||
State<FriendScreen> createState() => _FriendScreenState();
|
||||
}
|
||||
|
||||
class _FriendScreenState extends State<FriendScreen> {
|
||||
bool _isSubmitting = false;
|
||||
|
||||
int _currentSideId = 0;
|
||||
List<Friendship> _friendships = List.empty();
|
||||
|
||||
Future<void> fetchFriendships() async {
|
||||
final auth = context.read<AuthProvider>();
|
||||
final prof = await auth.getProfiles();
|
||||
if (!await auth.isAuthorized()) return;
|
||||
|
||||
_currentSideId = prof['id'];
|
||||
|
||||
var uri = getRequestUri('passport', '/api/users/me/friends');
|
||||
|
||||
var res = await auth.client!.get(uri);
|
||||
if (res.statusCode == 200) {
|
||||
final result = jsonDecode(utf8.decode(res.bodyBytes)) as List<dynamic>;
|
||||
setState(() {
|
||||
_friendships = result.map((x) => Friendship.fromJson(x)).toList();
|
||||
});
|
||||
} else {
|
||||
var message = utf8.decode(res.bodyBytes);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Something went wrong... $message")),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateFriendship(Friendship relation, int status) async {
|
||||
setState(() => _isSubmitting = true);
|
||||
|
||||
final otherside = getOtherside(relation);
|
||||
|
||||
final auth = context.read<AuthProvider>();
|
||||
if (!await auth.isAuthorized()) {
|
||||
setState(() => _isSubmitting = false);
|
||||
return;
|
||||
}
|
||||
|
||||
var res = await auth.client!.put(
|
||||
getRequestUri('passport', '/api/users/me/friends/${otherside.id}'),
|
||||
headers: <String, String>{
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: jsonEncode(<String, dynamic>{
|
||||
'status': status,
|
||||
}),
|
||||
);
|
||||
if (res.statusCode == 200) {
|
||||
await fetchFriendships();
|
||||
} else {
|
||||
var message = utf8.decode(res.bodyBytes);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Something went wrong... $message")),
|
||||
);
|
||||
}
|
||||
|
||||
setState(() => _isSubmitting = false);
|
||||
}
|
||||
|
||||
List<Friendship> filterWithStatus(int status) {
|
||||
return _friendships.where((x) => x.status == status).toList();
|
||||
}
|
||||
|
||||
DismissDirection getDismissDirection(Friendship relation) {
|
||||
if (relation.status == 2) return DismissDirection.endToStart;
|
||||
if (relation.status == 1) return DismissDirection.startToEnd;
|
||||
if (relation.status == 0 && relation.relatedId != _currentSideId) return DismissDirection.startToEnd;
|
||||
return DismissDirection.horizontal;
|
||||
}
|
||||
|
||||
Account getOtherside(Friendship relation) {
|
||||
if (relation.accountId != _currentSideId) {
|
||||
return relation.account;
|
||||
} else {
|
||||
return relation.related;
|
||||
}
|
||||
}
|
||||
|
||||
String getAvatarUrl(String uuid) {
|
||||
return getRequestUri('passport', '/api/avatar/$uuid').toString();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
Future.delayed(Duration.zero, () {
|
||||
fetchFriendships();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget friendshipTileBuilder(context, index, status) {
|
||||
final element = filterWithStatus(status)[index];
|
||||
final otherside = getOtherside(element);
|
||||
|
||||
final randomId = DateTime.now().microsecondsSinceEpoch >> 10;
|
||||
|
||||
return Dismissible(
|
||||
key: Key(randomId.toString()),
|
||||
background: Container(
|
||||
color: Colors.red,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
alignment: Alignment.centerLeft,
|
||||
child: const Icon(Icons.close, color: Colors.white),
|
||||
),
|
||||
secondaryBackground: Container(
|
||||
color: Colors.green,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
alignment: Alignment.centerRight,
|
||||
child: const Icon(Icons.check, color: Colors.white),
|
||||
),
|
||||
direction: getDismissDirection(element),
|
||||
child: ListTile(
|
||||
title: Text(otherside.nick),
|
||||
subtitle: Text(otherside.name),
|
||||
leading: CircleAvatar(
|
||||
backgroundImage: NetworkImage(getAvatarUrl(otherside.avatar)),
|
||||
),
|
||||
),
|
||||
onDismissed: (direction) {
|
||||
if (direction == DismissDirection.startToEnd) {
|
||||
updateFriendship(element, 2);
|
||||
}
|
||||
if (direction == DismissDirection.endToStart) {
|
||||
updateFriendship(element, 1);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return IndentWrapper(
|
||||
title: AppLocalizations.of(context)!.friend,
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () => fetchFriendships(),
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: _isSubmitting ? const LinearProgressIndicator().animate().scaleX() : Container(),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
|
||||
color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.8),
|
||||
child: Text(AppLocalizations.of(context)!.friendPending),
|
||||
),
|
||||
),
|
||||
SliverList.builder(
|
||||
itemCount: filterWithStatus(0).length,
|
||||
itemBuilder: (_, __) => friendshipTileBuilder(_, __, 0),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
|
||||
color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.8),
|
||||
child: Text(AppLocalizations.of(context)!.friendActive),
|
||||
),
|
||||
),
|
||||
SliverList.builder(
|
||||
itemCount: filterWithStatus(1).length,
|
||||
itemBuilder: (_, __) => friendshipTileBuilder(_, __, 1),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
|
||||
color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.8),
|
||||
child: Text(AppLocalizations.of(context)!.friendBlocked),
|
||||
),
|
||||
),
|
||||
SliverList.builder(
|
||||
itemCount: filterWithStatus(2).length,
|
||||
itemBuilder: (_, __) => friendshipTileBuilder(_, __, 2),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.8),
|
||||
width: 0.3,
|
||||
)),
|
||||
),
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.friendListHint,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -83,26 +83,21 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
||||
onRefresh: () => Future.sync(
|
||||
() => _pagingController.refresh(),
|
||||
),
|
||||
child: Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
child: PagedListView<int, Post>(
|
||||
pagingController: _pagingController,
|
||||
builderDelegate: PagedChildBuilderDelegate<Post>(
|
||||
itemBuilder: (context, item, index) => PostItem(
|
||||
item: item,
|
||||
onUpdate: () => _pagingController.refresh(),
|
||||
onTap: () {
|
||||
router.pushNamed(
|
||||
'posts.screen',
|
||||
pathParameters: {
|
||||
'alias': item.alias,
|
||||
'dataset': '${item.modelType}s',
|
||||
},
|
||||
);
|
||||
child: PagedListView<int, Post>(
|
||||
pagingController: _pagingController,
|
||||
builderDelegate: PagedChildBuilderDelegate<Post>(
|
||||
itemBuilder: (context, item, index) => PostItem(
|
||||
item: item,
|
||||
onUpdate: () => _pagingController.refresh(),
|
||||
onTap: () {
|
||||
router.pushNamed(
|
||||
'posts.screen',
|
||||
pathParameters: {
|
||||
'alias': item.alias,
|
||||
'dataset': '${item.modelType}s',
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -49,7 +49,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
index: index,
|
||||
item: element,
|
||||
onDismiss: () => setState(() {
|
||||
nty.notifications.removeAt(index);
|
||||
nty.clearAt(index);
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
@ -23,12 +23,10 @@ class PostScreen extends StatefulWidget {
|
||||
class _PostScreenState extends State<PostScreen> {
|
||||
final _client = http.Client();
|
||||
|
||||
final PagingController<int, Post> _commentPagingController =
|
||||
PagingController(firstPageKey: 0);
|
||||
final PagingController<int, Post> _commentPagingController = PagingController(firstPageKey: 0);
|
||||
|
||||
Future<Post?> fetchPost(BuildContext context) async {
|
||||
final uri = getRequestUri(
|
||||
'interactive', '/api/p/${widget.dataset}/${widget.alias}');
|
||||
final uri = getRequestUri('interactive', '/api/p/${widget.dataset}/${widget.alias}');
|
||||
final res = await _client.get(uri);
|
||||
if (res.statusCode != 200) {
|
||||
final err = utf8.decode(res.bodyBytes);
|
||||
@ -51,32 +49,27 @@ class _PostScreenState extends State<PostScreen> {
|
||||
future: fetchPost(context),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData && snapshot.data != null) {
|
||||
return Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: PostItem(
|
||||
item: snapshot.data!,
|
||||
brief: false,
|
||||
ripple: false,
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: CommentListHeader(
|
||||
related: snapshot.data!,
|
||||
paging: _commentPagingController,
|
||||
),
|
||||
),
|
||||
CommentList(
|
||||
related: snapshot.data!,
|
||||
dataset: widget.dataset,
|
||||
paging: _commentPagingController,
|
||||
),
|
||||
],
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: PostItem(
|
||||
item: snapshot.data!,
|
||||
brief: false,
|
||||
ripple: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: CommentListHeader(
|
||||
related: snapshot.data!,
|
||||
paging: _commentPagingController,
|
||||
),
|
||||
),
|
||||
CommentList(
|
||||
related: snapshot.data!,
|
||||
dataset: widget.dataset,
|
||||
paging: _commentPagingController,
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return const Center(
|
||||
|
Loading…
Reference in New Issue
Block a user