🐛 Fix some bugs in creator hub

This commit is contained in:
2025-12-06 21:26:00 +08:00
parent 7516e197fe
commit 9c370647dd
5 changed files with 430 additions and 439 deletions

View File

@@ -5,10 +5,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:island/models/webfeed.dart'; import 'package:island/models/webfeed.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
final webFeedListProvider = FutureProvider.family<List<SnWebFeed>, String>(( final webFeedListProvider = FutureProvider.autoDispose
ref, .family<List<SnWebFeed>, String>((ref, pubName) async {
pubName,
) async {
final client = ref.watch(apiClientProvider); final client = ref.watch(apiClientProvider);
final response = await client.get('/sphere/publishers/$pubName/feeds'); final response = await client.get('/sphere/publishers/$pubName/feeds');
return (response.data as List) return (response.data as List)
@@ -51,8 +49,7 @@ class WebFeedNotifier extends AsyncNotifier<SnWebFeed> {
final client = ref.read(apiClientProvider); final client = ref.read(apiClientProvider);
final url = '/sphere/publishers/${feed.publisherId}/feeds'; final url = '/sphere/publishers/${feed.publisherId}/feeds';
final response = final response = feed.id.isEmpty
feed.id.isEmpty
? await client.post(url, data: feed.toJson()) ? await client.post(url, data: feed.toJson())
: await client.patch('$url/${feed.id}', data: feed.toJson()); : await client.patch('$url/${feed.id}', data: feed.toJson());

View File

@@ -20,6 +20,7 @@ import 'package:island/widgets/navigation/fab_menu.dart';
import 'package:island/widgets/response.dart'; import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:relative_time/relative_time.dart'; import 'package:relative_time/relative_time.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:super_sliver_list/super_sliver_list.dart'; import 'package:super_sliver_list/super_sliver_list.dart';
import 'package:island/pods/chat/chat_room.dart'; import 'package:island/pods/chat/chat_room.dart';
@@ -50,8 +51,7 @@ class ChatRoomListTile extends HookConsumerWidget {
if (validMembers.isNotEmpty) { if (validMembers.isNotEmpty) {
final userInfo = ref.watch(userInfoProvider); final userInfo = ref.watch(userInfoProvider);
if (userInfo.value != null) { if (userInfo.value != null) {
validMembers = validMembers = validMembers
validMembers
.where((e) => e.accountId != userInfo.value!.id) .where((e) => e.accountId != userInfo.value!.id)
.toList(); .toList();
} }
@@ -60,37 +60,60 @@ class ChatRoomListTile extends HookConsumerWidget {
Widget buildSubtitle() { Widget buildSubtitle() {
if (subtitle != null) return subtitle!; if (subtitle != null) return subtitle!;
return summary.when( return AnimatedSwitcher(
data: (data) { duration: const Duration(milliseconds: 300),
if (data == null) { layoutBuilder: (currentChild, previousChildren) => Stack(
return isDirect && room.description == null alignment: Alignment.centerLeft,
children: [
...previousChildren,
if (currentChild != null) currentChild,
],
),
child: summary.when(
data: (data) => Container(
key: const ValueKey('data'),
child: data == null
? isDirect && room.description == null
? Text( ? Text(
validMembers.map((e) => '@${e.account.name}').join(', '), validMembers
.map((e) => '@${e.account.name}')
.join(', '),
maxLines: 1, maxLines: 1,
) )
: Text(room.description ?? 'descriptionNone'.tr(), maxLines: 1); : Text(
} room.description ?? 'descriptionNone'.tr(),
maxLines: 1,
return Column( )
: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
if (data.unreadCount > 0) if (data.unreadCount > 0)
Text( Text(
'unreadMessages'.plural(data.unreadCount), 'unreadMessages'.plural(data.unreadCount),
style: Theme.of(context).textTheme.bodySmall?.copyWith( style: Theme.of(context).textTheme.bodySmall
?.copyWith(
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
), ),
), ),
if (data.lastMessage == null) if (data.lastMessage == null)
Text(room.description ?? 'descriptionNone'.tr(), maxLines: 1) Text(
room.description ?? 'descriptionNone'.tr(),
maxLines: 1,
)
else else
Row( Row(
spacing: 4, spacing: 4,
children: [ children: [
Badge( Badge(
label: Text(data.lastMessage!.sender.account.nick), label: Text(
textColor: Theme.of(context).colorScheme.onPrimary, data.lastMessage!.sender.account.nick,
backgroundColor: Theme.of(context).colorScheme.primary, ),
textColor: Theme.of(
context,
).colorScheme.onPrimary,
backgroundColor: Theme.of(
context,
).colorScheme.primary,
), ),
Expanded( Expanded(
child: Text( child: Text(
@@ -114,19 +137,37 @@ class ChatRoomListTile extends HookConsumerWidget {
], ],
), ),
], ],
),
),
loading: () => Container(
key: const ValueKey('loading'),
child: Builder(
builder: (context) {
final seed = DateTime.now().microsecondsSinceEpoch;
final len = 4 + (seed % 17); // 4..20 inclusive
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
var s = seed;
final buffer = StringBuffer();
for (var i = 0; i < len; i++) {
s = (s * 1103515245 + 12345) & 0x7fffffff;
buffer.write(chars[s % chars.length]);
}
return Skeletonizer(
enabled: true,
child: Text(buffer.toString()),
); );
}, },
loading: () => const SizedBox.shrink(), ),
error: ),
(_, _) => error: (_, _) => Container(
isDirect && room.description == null key: const ValueKey('error'),
child: isDirect && room.description == null
? Text( ? Text(
validMembers.map((e) => '@${e.account.name}').join(', '), validMembers.map((e) => '@${e.account.name}').join(', '),
maxLines: 1, maxLines: 1,
) )
: Text( : Text(room.description ?? 'descriptionNone'.tr(), maxLines: 1),
room.description ?? 'descriptionNone'.tr(), ),
maxLines: 1,
), ),
); );
} }
@@ -149,11 +190,9 @@ class ChatRoomListTile extends HookConsumerWidget {
loading: () => false, loading: () => false,
error: (_, _) => false, error: (_, _) => false,
), ),
child: child: (isDirect && room.picture?.id == null)
(isDirect && room.picture?.id == null)
? SplitAvatarWidget( ? SplitAvatarWidget(
filesId: filesId: validMembers
validMembers
.map((e) => e.account.profile.picture?.id) .map((e) => e.account.profile.picture?.id)
.toList(), .toList(),
) )
@@ -199,8 +238,7 @@ class ChatListBodyWidget extends HookConsumerWidget {
builder: (context, ref, _) { builder: (context, ref, _) {
final summaryState = ref.watch(chatSummaryProvider); final summaryState = ref.watch(chatSummaryProvider);
return summaryState.maybeWhen( return summaryState.maybeWhen(
loading: loading: () => const LinearProgressIndicator(
() => const LinearProgressIndicator(
minHeight: 2, minHeight: 2,
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.zero,
), ),
@@ -210,16 +248,13 @@ class ChatListBodyWidget extends HookConsumerWidget {
), ),
Expanded( Expanded(
child: chats.when( child: chats.when(
data: data: (items) => RefreshIndicator(
(items) => RefreshIndicator( onRefresh: () => Future.sync(() {
onRefresh:
() => Future.sync(() {
ref.invalidate(chatRoomJoinedProvider); ref.invalidate(chatRoomJoinedProvider);
}), }),
child: SuperListView.builder( child: SuperListView.builder(
padding: EdgeInsets.only(bottom: 96), padding: EdgeInsets.only(bottom: 96),
itemCount: itemCount: items
items
.where( .where(
(item) => (item) =>
selectedTab.value == 0 || selectedTab.value == 0 ||
@@ -228,13 +263,11 @@ class ChatListBodyWidget extends HookConsumerWidget {
) )
.length, .length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final filteredItems = final filteredItems = items
items
.where( .where(
(item) => (item) =>
selectedTab.value == 0 || selectedTab.value == 0 ||
(selectedTab.value == 1 && (selectedTab.value == 1 && item.type == 1) ||
item.type == 1) ||
(selectedTab.value == 2 && item.type != 1), (selectedTab.value == 2 && item.type != 1),
) )
.toList(); .toList();
@@ -260,8 +293,7 @@ class ChatListBodyWidget extends HookConsumerWidget {
), ),
), ),
loading: () => const Center(child: CircularProgressIndicator()), loading: () => const Center(child: CircularProgressIndicator()),
error: error: (error, stack) => ResponseErrorWidget(
(error, stack) => ResponseErrorWidget(
error: error, error: error,
onRetry: () { onRetry: () {
ref.invalidate(chatRoomJoinedProvider); ref.invalidate(chatRoomJoinedProvider);
@@ -552,15 +584,9 @@ class _ChatInvitesSheet extends HookConsumerWidget {
), ),
], ],
child: invites.when( child: invites.when(
data: data: (items) => items.isEmpty
(items) =>
items.isEmpty
? Center( ? Center(
child: child: Text('invitesEmpty', textAlign: TextAlign.center).tr(),
Text(
'invitesEmpty',
textAlign: TextAlign.center,
).tr(),
) )
: ListView.builder( : ListView.builder(
shrinkWrap: true, shrinkWrap: true,
@@ -576,10 +602,10 @@ class _ChatInvitesSheet extends HookConsumerWidget {
if (invite.chatRoom!.type == 1) if (invite.chatRoom!.type == 1)
Badge( Badge(
label: const Text('directMessage').tr(), label: const Text('directMessage').tr(),
backgroundColor: backgroundColor: Theme.of(
Theme.of(context).colorScheme.primary, context,
textColor: ).colorScheme.primary,
Theme.of(context).colorScheme.onPrimary, textColor: Theme.of(context).colorScheme.onPrimary,
), ),
], ],
), ),

View File

@@ -44,8 +44,7 @@ class PollListNotifier extends AsyncNotifier<List<SnPollWithStats>>
queryParameters: queryParams, queryParameters: queryParams,
); );
totalCount = int.parse(response.headers.value('X-Total') ?? '0'); totalCount = int.parse(response.headers.value('X-Total') ?? '0');
final items = final items = response.data
response.data
.map((json) => SnPollWithStats.fromJson(json)) .map((json) => SnPollWithStats.fromJson(json))
.cast<SnPollWithStats>() .cast<SnPollWithStats>()
.toList(); .toList();
@@ -91,6 +90,7 @@ class CreatorPollListScreen extends HookConsumerWidget {
body: ExtendedRefreshIndicator( body: ExtendedRefreshIndicator(
onRefresh: () => ref.refresh(pollListNotifierProvider(pubName).future), onRefresh: () => ref.refresh(pollListNotifierProvider(pubName).future),
child: PaginationList( child: PaginationList(
footerSkeletonMaxWidth: 640,
provider: pollListNotifierProvider(pubName), provider: pollListNotifierProvider(pubName),
notifier: pollListNotifierProvider(pubName).notifier, notifier: pollListNotifierProvider(pubName).notifier,
padding: const EdgeInsets.only(top: 12), padding: const EdgeInsets.only(top: 12),
@@ -119,8 +119,7 @@ class _CreatorPollItem extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context); final theme = Theme.of(context);
final ended = pollWithStats.endedAt; final ended = pollWithStats.endedAt;
final endedText = final endedText = ended == null
ended == null
? 'No end' ? 'No end'
: MaterialLocalizations.of(context).formatFullDate(ended); : MaterialLocalizations.of(context).formatFullDate(ended);
@@ -152,8 +151,7 @@ class _CreatorPollItem extends HookConsumerWidget {
], ],
), ),
trailing: PopupMenuButton<String>( trailing: PopupMenuButton<String>(
itemBuilder: itemBuilder: (context) => [
(context) => [
PopupMenuItem( PopupMenuItem(
child: Row( child: Row(
children: [ children: [
@@ -167,8 +165,7 @@ class _CreatorPollItem extends HookConsumerWidget {
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
isDismissible: false, isDismissible: false,
builder: builder: (context) => PollEditorScreen(
(context) => PollEditorScreen(
initialPublisher: pubName, initialPublisher: pubName,
initialPollId: pollWithStats.id, initialPollId: pollWithStats.id,
), ),
@@ -189,21 +186,16 @@ class _CreatorPollItem extends HookConsumerWidget {
onTap: () async { onTap: () async {
final confirmed = await showDialog<bool>( final confirmed = await showDialog<bool>(
context: context, context: context,
builder: builder: (context) => AlertDialog(
(context) => AlertDialog(
title: Text('Delete Poll'), title: Text('Delete Poll'),
content: Text( content: Text('Are you sure you want to delete this poll?'),
'Are you sure you want to delete this poll?',
),
actions: [ actions: [
TextButton( TextButton(
onPressed: onPressed: () => Navigator.of(context).pop(false),
() => Navigator.of(context).pop(false),
child: Text('Cancel'), child: Text('Cancel'),
), ),
TextButton( TextButton(
onPressed: onPressed: () => Navigator.of(context).pop(true),
() => Navigator.of(context).pop(true),
child: Text('Delete'), child: Text('Delete'),
), ),
], ],
@@ -212,9 +204,7 @@ class _CreatorPollItem extends HookConsumerWidget {
if (confirmed == true) { if (confirmed == true) {
try { try {
final client = ref.read(apiClientProvider); final client = ref.read(apiClientProvider);
await client.delete( await client.delete('/sphere/polls/${pollWithStats.id}');
'/sphere/polls/${pollWithStats.id}',
);
ref.invalidate(pollListNotifierProvider(pubName)); ref.invalidate(pollListNotifierProvider(pubName));
showSnackBar('Poll deleted successfully'); showSnackBar('Poll deleted successfully');
} catch (e) { } catch (e) {

View File

@@ -12,7 +12,6 @@ import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/paging/pagination_list.dart'; import 'package:island/widgets/paging/pagination_list.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:island/widgets/extended_refresh_indicator.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
final siteListNotifierProvider = AsyncNotifierProvider.family.autoDispose( final siteListNotifierProvider = AsyncNotifierProvider.family.autoDispose(
@@ -38,8 +37,7 @@ class SiteListNotifier extends AsyncNotifier<List<SnPublicationSite>>
queryParameters: queryParams, queryParameters: queryParams,
); );
totalCount = int.parse(response.headers.value('X-Total') ?? '0'); totalCount = int.parse(response.headers.value('X-Total') ?? '0');
final items = final items = response.data
response.data
.map((json) => SnPublicationSite.fromJson(json)) .map((json) => SnPublicationSite.fromJson(json))
.cast<SnPublicationSite>() .cast<SnPublicationSite>()
.toList(); .toList();
@@ -70,14 +68,11 @@ class CreatorSiteListScreen extends HookConsumerWidget {
onPressed: () => _createSite(context), onPressed: () => _createSite(context),
child: Icon(Icons.add), child: Icon(Icons.add),
), ),
body: ExtendedRefreshIndicator( body: PaginationList(
onRefresh: () => ref.refresh(siteListNotifierProvider(pubName).future), footerSkeletonMaxWidth: 640,
child: CustomScrollView(
slivers: [
const SliverGap(8),
PaginationList(
provider: siteListNotifierProvider(pubName), provider: siteListNotifierProvider(pubName),
notifier: siteListNotifierProvider(pubName).notifier, notifier: siteListNotifierProvider(pubName).notifier,
padding: const EdgeInsets.only(top: 12),
itemBuilder: (context, index, site) { itemBuilder: (context, index, site) {
return ConstrainedBox( return ConstrainedBox(
constraints: BoxConstraints(maxWidth: 640), constraints: BoxConstraints(maxWidth: 640),
@@ -85,9 +80,6 @@ class CreatorSiteListScreen extends HookConsumerWidget {
).center(); ).center();
}, },
), ),
],
),
),
); );
} }
} }
@@ -148,8 +140,7 @@ class _CreatorSiteItem extends HookConsumerWidget {
), ),
), ),
PopupMenuButton<String>( PopupMenuButton<String>(
itemBuilder: itemBuilder: (context) => [
(context) => [
PopupMenuItem( PopupMenuItem(
child: Row( child: Row(
children: [ children: [
@@ -162,11 +153,8 @@ class _CreatorSiteItem extends HookConsumerWidget {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: builder: (context) =>
(context) => SiteForm( SiteForm(pubName: pubName, siteSlug: site.slug),
pubName: pubName,
siteSlug: site.slug,
),
); );
}, },
), ),
@@ -181,20 +169,16 @@ class _CreatorSiteItem extends HookConsumerWidget {
onTap: () async { onTap: () async {
final confirmed = await showDialog<bool>( final confirmed = await showDialog<bool>(
context: context, context: context,
builder: builder: (context) => AlertDialog(
(context) => AlertDialog(
title: Text('deleteSite'.tr()), title: Text('deleteSite'.tr()),
content: Text('deleteSiteConfirm'.tr()), content: Text('deleteSiteConfirm'.tr()),
actions: [ actions: [
TextButton( TextButton(
onPressed: onPressed: () => Navigator.of(context).pop(false),
() =>
Navigator.of(context).pop(false),
child: Text('cancel'.tr()), child: Text('cancel'.tr()),
), ),
TextButton( TextButton(
onPressed: onPressed: () => Navigator.of(context).pop(true),
() => Navigator.of(context).pop(true),
child: Text('delete'.tr()), child: Text('delete'.tr()),
), ),
], ],

View File

@@ -41,8 +41,7 @@ class StickersScreen extends HookConsumerWidget {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: builder: (context) => SheetScaffold(
(context) => SheetScaffold(
titleText: 'createStickerPack'.tr(), titleText: 'createStickerPack'.tr(),
child: StickerPackForm(pubName: pubName), child: StickerPackForm(pubName: pubName),
), ),
@@ -54,8 +53,7 @@ class StickersScreen extends HookConsumerWidget {
}, },
child: const Icon(Symbols.add), child: const Icon(Symbols.add),
), ),
body: body: isWideScreen(context)
isWideScreen(context)
? Center( ? Center(
child: ConstrainedBox( child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 640), constraints: BoxConstraints(maxWidth: 640),
@@ -83,6 +81,7 @@ class SliverStickerPacksList extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return PaginationList( return PaginationList(
padding: EdgeInsets.zero,
provider: stickerPacksProvider(pubName), provider: stickerPacksProvider(pubName),
notifier: stickerPacksProvider(pubName).notifier, notifier: stickerPacksProvider(pubName).notifier,
itemBuilder: (context, index, sticker) { itemBuilder: (context, index, sticker) {
@@ -97,8 +96,7 @@ class SliverStickerPacksList extends HookConsumerWidget {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: builder: (context) => SheetScaffold(
(context) => SheetScaffold(
titleText: sticker.name, titleText: sticker.name,
actions: [ actions: [
IconButton( IconButton(
@@ -108,8 +106,7 @@ class SliverStickerPacksList extends HookConsumerWidget {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: builder: (context) => SheetScaffold(
(context) => SheetScaffold(
titleText: 'createSticker'.tr(), titleText: 'createSticker'.tr(),
child: StickerForm(packId: id), child: StickerForm(packId: id),
), ),
@@ -165,8 +162,7 @@ class StickerPacksNotifier extends AsyncNotifier<List<SnStickerPack>>
); );
totalCount = int.parse(response.headers.value('X-Total') ?? '0'); totalCount = int.parse(response.headers.value('X-Total') ?? '0');
final stickers = final stickers = response.data
response.data
.map((e) => SnStickerPack.fromJson(e)) .map((e) => SnStickerPack.fromJson(e))
.cast<SnStickerPack>() .cast<SnStickerPack>()
.toList(); .toList();
@@ -262,8 +258,7 @@ class StickerPackForm extends HookConsumerWidget {
color: Theme.of(context).colorScheme.surfaceContainer, color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.all(Radius.circular(8)), borderRadius: BorderRadius.all(Radius.circular(8)),
), ),
child: child: (icon.value?.isEmpty ?? true)
(icon.value?.isEmpty ?? true)
? const SizedBox.shrink() ? const SizedBox.shrink()
: CloudImageWidget(fileId: icon.value!), : CloudImageWidget(fileId: icon.value!),
), ),
@@ -273,8 +268,7 @@ class StickerPackForm extends HookConsumerWidget {
onPressed: () { onPressed: () {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
builder: builder: (context) => CloudFilePicker(
(context) => CloudFilePicker(
allowedTypes: {UniversalFileType.image}, allowedTypes: {UniversalFileType.image},
), ),
).then((value) { ).then((value) {
@@ -300,8 +294,8 @@ class StickerPackForm extends HookConsumerWidget {
} }
return null; return null;
}, },
onTapOutside: onTapOutside: (_) =>
(_) => FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
), ),
TextFormField( TextFormField(
controller: descriptionController, controller: descriptionController,
@@ -314,8 +308,8 @@ class StickerPackForm extends HookConsumerWidget {
), ),
minLines: 3, minLines: 3,
maxLines: null, maxLines: null,
onTapOutside: onTapOutside: (_) =>
(_) => FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
), ),
TextFormField( TextFormField(
controller: prefixController, controller: prefixController,
@@ -332,8 +326,8 @@ class StickerPackForm extends HookConsumerWidget {
} }
return null; return null;
}, },
onTapOutside: onTapOutside: (_) =>
(_) => FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
), ),
], ],
), ),