💄 Optimize post reaction sheet

This commit is contained in:
2025-10-12 22:21:15 +08:00
parent e8ff1bfd22
commit 1be33916af
2 changed files with 307 additions and 71 deletions

View File

@@ -2,17 +2,65 @@ import 'dart:math' as math;
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_popup_card/flutter_popup_card.dart'; import 'package:flutter_popup_card/flutter_popup_card.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart'; import 'package:island/models/post.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/services/time.dart'; import 'package:island/services/time.dart';
import 'package:island/widgets/account/account_pfc.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
part 'post_reaction_sheet.g.dart';
@riverpod
class ReactionListNotifier extends _$ReactionListNotifier
with CursorPagingNotifierMixin<SnPostReaction> {
static const int _pageSize = 20;
int? totalCount;
@override
Future<CursorPagingData<SnPostReaction>> build({
required String symbol,
required String postId,
}) {
return fetch(cursor: null);
}
@override
Future<CursorPagingData<SnPostReaction>> fetch({
required String? cursor,
}) async {
final client = ref.read(apiClientProvider);
final offset = cursor == null ? 0 : int.parse(cursor);
final response = await client.get(
'/sphere/posts/$postId/reactions',
queryParameters: {'symbol': symbol, 'offset': offset, 'take': _pageSize},
);
totalCount = int.tryParse(response.headers.value('x-total') ?? '0') ?? 0;
final List<dynamic> data = response.data;
final reactions =
data.map((json) => SnPostReaction.fromJson(json)).toList();
final hasMore = reactions.length == _pageSize;
final nextCursor = hasMore ? (offset + reactions.length).toString() : null;
return CursorPagingData(
items: reactions,
hasMore: hasMore,
nextCursor: nextCursor,
);
}
}
const kAvailableStickers = { const kAvailableStickers = {
'angry', 'angry',
'clap', 'clap',
@@ -49,6 +97,7 @@ class PostReactionSheet extends StatelessWidget {
final Function(String symbol, int attitude) onReact; final Function(String symbol, int attitude) onReact;
final String postId; final String postId;
const PostReactionSheet({ const PostReactionSheet({
super.key,
required this.reactionsCount, required this.reactionsCount,
required this.reactionsMade, required this.reactionsMade,
required this.onReact, required this.onReact,
@@ -162,6 +211,18 @@ class PostReactionSheet extends StatelessWidget {
symbol, symbol,
details.localPosition, details.localPosition,
postId, postId,
reactionsCount[symbol] ?? 0,
);
}
},
onSecondaryTapUp: (details) {
if (count > 0) {
showReactionDetailsPopup(
context,
symbol,
details.localPosition,
postId,
reactionsCount[symbol] ?? 0,
); );
} }
}, },
@@ -269,57 +330,21 @@ class PostReactionSheet extends StatelessWidget {
class ReactionDetailsPopup extends HookConsumerWidget { class ReactionDetailsPopup extends HookConsumerWidget {
final String symbol; final String symbol;
final String postId; final String postId;
final int totalCount;
const ReactionDetailsPopup({ const ReactionDetailsPopup({
super.key, super.key,
required this.symbol, required this.symbol,
required this.postId, required this.postId,
required this.totalCount,
}); });
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final reactions = useState<List<SnPostReaction>>([]); final provider = reactionListNotifierProvider(
final isLoading = useState(false); symbol: symbol,
final hasMore = useState(true); postId: postId,
final offset = useState(0);
Future<void> loadReactions() async {
if (isLoading.value || !hasMore.value) return;
isLoading.value = true;
try {
final client = ref.watch(apiClientProvider);
final response = await client.get(
'/sphere/posts/${postId}/reactions',
queryParameters: {
'symbol': symbol,
'offset': offset.value,
'take': 20,
},
); );
final newReactions =
(response.data as List)
.map((json) => SnPostReaction.fromJson(json))
.toList();
if (newReactions.length < 20) {
hasMore.value = false;
}
reactions.value = [...reactions.value, ...newReactions];
offset.value += newReactions.length;
} catch (err) {
// Handle error
} finally {
isLoading.value = false;
}
}
useEffect(() {
loadReactions();
return null;
}, []);
final width = math.min(MediaQuery.of(context).size.width * 0.8, 480.0); final width = math.min(MediaQuery.of(context).size.width * 0.8, 480.0);
return PopupCard( return PopupCard(
elevation: 8, elevation: 8,
@@ -340,34 +365,32 @@ class ReactionDetailsPopup extends HookConsumerWidget {
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
).tr(), ).tr(),
const Spacer(), const Spacer(),
Text('${reactions.value.length} reactions'.tr()), Text('reactions'.plural(totalCount)),
], ],
), ),
), ),
const Divider(height: 1), const Divider(height: 1),
Expanded( Expanded(
child: ListView.builder( child: PagingHelperView(
itemCount: reactions.value.length + (hasMore.value ? 1 : 0), provider: provider,
futureRefreshable: provider.future,
notifierRefreshable: provider.notifier,
contentBuilder:
(data, widgetCount, endItemView) => ListView.builder(
itemCount: widgetCount,
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (index == reactions.value.length) { if (index == widgetCount - 1) {
if (isLoading.value) { return endItemView;
return const Center(
child: Padding(
padding: EdgeInsets.all(16),
child: CircularProgressIndicator(),
),
);
} else {
loadReactions();
return const SizedBox.shrink();
}
} }
final reaction = reactions.value[index]; final reaction = data.items[index];
return ListTile( return ListTile(
leading: ProfilePictureWidget( leading: AccountPfcGestureDetector(
uname: reaction.account?.name ?? 'unknown',
child: ProfilePictureWidget(
file: reaction.account?.profile.picture, file: reaction.account?.profile.picture,
), ),
),
title: Text(reaction.account?.nick ?? 'unknown'.tr()), title: Text(reaction.account?.nick ?? 'unknown'.tr()),
subtitle: Text( subtitle: Text(
'${reaction.createdAt.formatRelative(context)} · ${reaction.createdAt.formatSystem()}', '${reaction.createdAt.formatRelative(context)} · ${reaction.createdAt.formatSystem()}',
@@ -376,6 +399,7 @@ class ReactionDetailsPopup extends HookConsumerWidget {
}, },
), ),
), ),
),
], ],
), ),
), ),
@@ -388,11 +412,17 @@ Future<void> showReactionDetailsPopup(
String symbol, String symbol,
Offset offset, Offset offset,
String postId, String postId,
int totalCount,
) async { ) async {
await showPopupCard<void>( await showPopupCard<void>(
offset: offset, offset: offset,
context: context, context: context,
builder: (context) => ReactionDetailsPopup(symbol: symbol, postId: postId), builder:
(context) => ReactionDetailsPopup(
symbol: symbol,
postId: postId,
totalCount: totalCount,
),
alignment: Alignment.center, alignment: Alignment.center,
dimBackground: true, dimBackground: true,
); );

View File

@@ -0,0 +1,206 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'post_reaction_sheet.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$reactionListNotifierHash() =>
r'92cf80d2461e46ca62cf6e6a37f8b16c239e7449';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
abstract class _$ReactionListNotifier
extends
BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnPostReaction>> {
late final String symbol;
late final String postId;
FutureOr<CursorPagingData<SnPostReaction>> build({
required String symbol,
required String postId,
});
}
/// See also [ReactionListNotifier].
@ProviderFor(ReactionListNotifier)
const reactionListNotifierProvider = ReactionListNotifierFamily();
/// See also [ReactionListNotifier].
class ReactionListNotifierFamily
extends Family<AsyncValue<CursorPagingData<SnPostReaction>>> {
/// See also [ReactionListNotifier].
const ReactionListNotifierFamily();
/// See also [ReactionListNotifier].
ReactionListNotifierProvider call({
required String symbol,
required String postId,
}) {
return ReactionListNotifierProvider(symbol: symbol, postId: postId);
}
@override
ReactionListNotifierProvider getProviderOverride(
covariant ReactionListNotifierProvider provider,
) {
return call(symbol: provider.symbol, postId: provider.postId);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'reactionListNotifierProvider';
}
/// See also [ReactionListNotifier].
class ReactionListNotifierProvider
extends
AutoDisposeAsyncNotifierProviderImpl<
ReactionListNotifier,
CursorPagingData<SnPostReaction>
> {
/// See also [ReactionListNotifier].
ReactionListNotifierProvider({required String symbol, required String postId})
: this._internal(
() =>
ReactionListNotifier()
..symbol = symbol
..postId = postId,
from: reactionListNotifierProvider,
name: r'reactionListNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$reactionListNotifierHash,
dependencies: ReactionListNotifierFamily._dependencies,
allTransitiveDependencies:
ReactionListNotifierFamily._allTransitiveDependencies,
symbol: symbol,
postId: postId,
);
ReactionListNotifierProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.symbol,
required this.postId,
}) : super.internal();
final String symbol;
final String postId;
@override
FutureOr<CursorPagingData<SnPostReaction>> runNotifierBuild(
covariant ReactionListNotifier notifier,
) {
return notifier.build(symbol: symbol, postId: postId);
}
@override
Override overrideWith(ReactionListNotifier Function() create) {
return ProviderOverride(
origin: this,
override: ReactionListNotifierProvider._internal(
() =>
create()
..symbol = symbol
..postId = postId,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
symbol: symbol,
postId: postId,
),
);
}
@override
AutoDisposeAsyncNotifierProviderElement<
ReactionListNotifier,
CursorPagingData<SnPostReaction>
>
createElement() {
return _ReactionListNotifierProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is ReactionListNotifierProvider &&
other.symbol == symbol &&
other.postId == postId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, symbol.hashCode);
hash = _SystemHash.combine(hash, postId.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin ReactionListNotifierRef
on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnPostReaction>> {
/// The parameter `symbol` of this provider.
String get symbol;
/// The parameter `postId` of this provider.
String get postId;
}
class _ReactionListNotifierProviderElement
extends
AutoDisposeAsyncNotifierProviderElement<
ReactionListNotifier,
CursorPagingData<SnPostReaction>
>
with ReactionListNotifierRef {
_ReactionListNotifierProviderElement(super.provider);
@override
String get symbol => (origin as ReactionListNotifierProvider).symbol;
@override
String get postId => (origin as ReactionListNotifierProvider).postId;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package