Compare commits

...

5 Commits

Author SHA1 Message Date
f4e2c0440d Publisher list on account 2025-09-07 01:02:40 +08:00
c9c0aa9fb5 ♻️ Refactor the profile and pub profile 2025-09-06 23:25:09 +08:00
baed28bef7 🐛 Fix some errors 2025-09-06 23:00:51 +08:00
7a6eecf628 🐛 Fix post shuffle 2025-09-06 23:00:26 +08:00
6a63bc235b 🐛 Fix iOS NSE 2025-09-06 22:52:05 +08:00
9 changed files with 1137 additions and 753 deletions

View File

@@ -47,6 +47,7 @@ class NotificationService: UNNotificationServiceExtension {
private func processNotification(request: UNNotificationRequest, content: UNMutableNotificationContent) throws { private func processNotification(request: UNNotificationRequest, content: UNMutableNotificationContent) throws {
switch content.userInfo["type"] as? String { switch content.userInfo["type"] as? String {
case "messages.new": case "messages.new":
content.categoryIdentifier = "REPLYABLE_MESSAGE"
try handleMessagingNotification(request: request, content: content) try handleMessagingNotification(request: request, content: content)
default: default:
try handleDefaultNotification(content: content) try handleDefaultNotification(content: content)
@@ -60,8 +61,6 @@ class NotificationService: UNNotificationServiceExtension {
let pfpIdentifier = meta["pfp"] as? String let pfpIdentifier = meta["pfp"] as? String
content.categoryIdentifier = "REPLYABLE_MESSAGE"
let metaCopy = meta as? [String: Any] ?? [:] let metaCopy = meta as? [String: Any] ?? [:]
let pfpUrl = pfpIdentifier != nil ? getAttachmentUrl(for: pfpIdentifier!) : nil let pfpUrl = pfpIdentifier != nil ? getAttachmentUrl(for: pfpIdentifier!) : nil

File diff suppressed because it is too large Load Diff

View File

@@ -762,5 +762,127 @@ class _AccountBotDeveloperProviderElement
String get uname => (origin as AccountBotDeveloperProvider).uname; String get uname => (origin as AccountBotDeveloperProvider).uname;
} }
String _$accountPublishersHash() => r'25f5695b4a5154163d77f1769876d826bf736609';
/// See also [accountPublishers].
@ProviderFor(accountPublishers)
const accountPublishersProvider = AccountPublishersFamily();
/// See also [accountPublishers].
class AccountPublishersFamily extends Family<AsyncValue<List<SnPublisher>>> {
/// See also [accountPublishers].
const AccountPublishersFamily();
/// See also [accountPublishers].
AccountPublishersProvider call(String id) {
return AccountPublishersProvider(id);
}
@override
AccountPublishersProvider getProviderOverride(
covariant AccountPublishersProvider provider,
) {
return call(provider.id);
}
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'accountPublishersProvider';
}
/// See also [accountPublishers].
class AccountPublishersProvider
extends AutoDisposeFutureProvider<List<SnPublisher>> {
/// See also [accountPublishers].
AccountPublishersProvider(String id)
: this._internal(
(ref) => accountPublishers(ref as AccountPublishersRef, id),
from: accountPublishersProvider,
name: r'accountPublishersProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$accountPublishersHash,
dependencies: AccountPublishersFamily._dependencies,
allTransitiveDependencies:
AccountPublishersFamily._allTransitiveDependencies,
id: id,
);
AccountPublishersProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.id,
}) : super.internal();
final String id;
@override
Override overrideWith(
FutureOr<List<SnPublisher>> Function(AccountPublishersRef provider) create,
) {
return ProviderOverride(
origin: this,
override: AccountPublishersProvider._internal(
(ref) => create(ref as AccountPublishersRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
id: id,
),
);
}
@override
AutoDisposeFutureProviderElement<List<SnPublisher>> createElement() {
return _AccountPublishersProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is AccountPublishersProvider && other.id == id;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, id.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin AccountPublishersRef on AutoDisposeFutureProviderRef<List<SnPublisher>> {
/// The parameter `id` of this provider.
String get id;
}
class _AccountPublishersProviderElement
extends AutoDisposeFutureProviderElement<List<SnPublisher>>
with AccountPublishersRef {
_AccountPublishersProviderElement(super.provider);
@override
String get id => (origin as AccountPublishersProvider).id;
}
// ignore_for_file: type=lint // 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 // 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

View File

@@ -72,7 +72,7 @@ class MarketplaceWebFeedsScreen extends HookConsumerWidget {
searchController.clear(); searchController.clear();
} }
return null; return null;
}, [query.value]); }, [query]);
// Clean up timer on dispose // Clean up timer on dispose
useEffect(() { useEffect(() {

View File

@@ -7,7 +7,7 @@ part of 'explore.dart';
// ************************************************************************** // **************************************************************************
String _$activityListNotifierHash() => String _$activityListNotifierHash() =>
r'a4968856ac34b59d47cfd4a7cbb39289aef2a1b1'; r'167021cada54da7c8d8437eef1ffb387a92ea2e3';
/// Copied from Dart SDK /// Copied from Dart SDK
class _SystemHash { class _SystemHash {

View File

@@ -27,6 +27,224 @@ import 'package:styled_widget/styled_widget.dart';
part 'pub_profile.g.dart'; part 'pub_profile.g.dart';
class _PublisherBasisWidget extends StatelessWidget {
final SnPublisher data;
final AsyncValue<SnSubscriptionStatus> subStatus;
final ValueNotifier<bool> subscribing;
final VoidCallback subscribe;
final VoidCallback unsubscribe;
const _PublisherBasisWidget({
required this.data,
required this.subStatus,
required this.subscribing,
required this.subscribe,
required this.unsubscribe,
});
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 20,
children: [
GestureDetector(
child: Badge(
isLabelVisible: data.type == 0,
padding: EdgeInsets.all(4),
label: Icon(
Symbols.launch,
size: 16,
color: Theme.of(context).colorScheme.onPrimary,
),
backgroundColor: Theme.of(context).colorScheme.primary,
offset: Offset(0, 48),
child: ProfilePictureWidget(
file: data.picture,
radius: 32,
borderRadius: data.type == 0 ? null : 12,
),
),
onTap: () {
if (data.account?.name != null) {
Navigator.pop(context, true);
context.pushNamed(
'accountProfile',
pathParameters: {'name': data.account!.name},
);
}
},
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
spacing: 6,
children: [
Text(data.nick).fontSize(20),
if (data.verification != null)
VerificationMark(mark: data.verification!),
Expanded(
child: Text(
'@${data.name}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
).fontSize(14).opacity(0.85),
),
],
),
if (data.type == 0 && data.account != null)
Row(
crossAxisAlignment: CrossAxisAlignment.center,
spacing: 6,
children: [
Icon(
data.type == 0 ? Symbols.person : Symbols.workspaces,
fill: 1,
size: 17,
),
Text(
'publisherBelongsTo'.tr(args: ['@${data.account!.name}']),
).fontSize(14),
],
).opacity(0.85),
const Gap(4),
if (data.type == 0 && data.account != null)
AccountStatusWidget(
uname: data.account!.name,
padding: EdgeInsets.zero,
),
subStatus
.when(
data:
(status) => FilledButton.icon(
onPressed:
subscribing.value
? null
: (status.isSubscribed
? unsubscribe
: subscribe),
icon: Icon(
status.isSubscribed
? Symbols.remove_circle
: Symbols.add_circle,
),
label:
Text(
status.isSubscribed
? 'unsubscribe'
: 'subscribe',
).tr(),
style: ButtonStyle(
visualDensity: VisualDensity(vertical: -2),
),
),
error: (_, _) => const SizedBox(),
loading:
() => const SizedBox(
height: 36,
child: Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
),
),
),
)
.padding(top: 8),
],
),
),
],
).padding(horizontal: 24, top: 24);
}
}
class _PublisherBadgesWidget extends StatelessWidget {
final SnPublisher data;
final AsyncValue<List<SnAccountBadge>> badges;
const _PublisherBadgesWidget({required this.data, required this.badges});
@override
Widget build(BuildContext context) {
return (badges.value?.isNotEmpty ?? false)
? Card(
child: BadgeList(
badges: badges.value!,
).padding(horizontal: 26, vertical: 20),
).padding(horizontal: 4)
: const SizedBox.shrink();
}
}
class _PublisherVerificationWidget extends StatelessWidget {
final SnPublisher data;
const _PublisherVerificationWidget({required this.data});
@override
Widget build(BuildContext context) {
return (data.verification != null)
? Card(
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: VerificationStatusCard(mark: data.verification!),
)
: const SizedBox.shrink();
}
}
class _PublisherBioWidget extends StatelessWidget {
final SnPublisher data;
const _PublisherBioWidget({required this.data});
@override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('bio').tr().bold().fontSize(15).padding(bottom: 8),
if (data.bio.isEmpty)
Text('descriptionNone').tr().italic()
else
MarkdownTextContent(
content: data.bio,
linesMargin: EdgeInsets.zero,
),
],
).padding(horizontal: 20, vertical: 16),
);
}
}
class _PublisherCategoryTabWidget extends StatelessWidget {
final TabController categoryTabController;
const _PublisherCategoryTabWidget({required this.categoryTabController});
@override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: TabBar(
controller: categoryTabController,
dividerColor: Colors.transparent,
splashBorderRadius: const BorderRadius.all(Radius.circular(8)),
tabs: [
Tab(text: 'all'.tr()),
Tab(text: 'postTypePost'.tr()),
Tab(text: 'postArticle'.tr()),
],
),
);
}
}
@riverpod @riverpod
Future<SnPublisher> publisher(Ref ref, String uname) async { Future<SnPublisher> publisher(Ref ref, String uname) async {
final apiClient = ref.watch(apiClientProvider); final apiClient = ref.watch(apiClientProvider);
@@ -132,170 +350,6 @@ class PublisherProfileScreen extends HookConsumerWidget {
offset: Offset(1.0, 1.0), offset: Offset(1.0, 1.0),
); );
Widget publisherBasisWidget(SnPublisher data) => Row(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 20,
children: [
GestureDetector(
child: Badge(
isLabelVisible: data.type == 0,
padding: EdgeInsets.all(4),
label: Icon(
Symbols.launch,
size: 16,
color: Theme.of(context).colorScheme.onPrimary,
),
backgroundColor: Theme.of(context).colorScheme.primary,
offset: Offset(0, 48),
child: ProfilePictureWidget(
file: data.picture,
radius: 32,
borderRadius: data.type == 0 ? null : 12,
),
),
onTap: () {
Navigator.pop(context, true);
if (data.account?.name != null) {
context.pushNamed(
'accountProfile',
pathParameters: {'name': data.account!.name},
);
}
},
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
spacing: 6,
children: [
Text(data.nick).fontSize(20),
if (data.verification != null)
VerificationMark(mark: data.verification!),
Expanded(
child: Text(
'@${data.name}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
).fontSize(14).opacity(0.85),
),
],
),
if (data.type == 0 && data.account != null)
Row(
crossAxisAlignment: CrossAxisAlignment.center,
spacing: 6,
children: [
Icon(
data.type == 0 ? Symbols.person : Symbols.workspaces,
fill: 1,
size: 17,
),
Text(
'publisherBelongsTo'.tr(args: ['@${data.account!.name}']),
).fontSize(14),
],
).opacity(0.85),
const Gap(4),
if (data.type == 0 && data.account != null)
AccountStatusWidget(
uname: data.account!.name,
padding: EdgeInsets.zero,
),
subStatus
.when(
data:
(status) => FilledButton.icon(
onPressed:
subscribing.value
? null
: (status.isSubscribed
? unsubscribe
: subscribe),
icon: Icon(
status.isSubscribed
? Symbols.remove_circle
: Symbols.add_circle,
),
label:
Text(
status.isSubscribed
? 'unsubscribe'
: 'subscribe',
).tr(),
style: ButtonStyle(
visualDensity: VisualDensity(vertical: -2),
),
),
error: (_, _) => const SizedBox(),
loading:
() => const SizedBox(
height: 36,
child: Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
),
),
),
)
.padding(top: 8),
],
),
),
],
).padding(horizontal: 24, top: 24);
Widget publisherBadgesWidget(SnPublisher data) =>
(badges.value?.isNotEmpty ?? false)
? Card(
child: BadgeList(
badges: badges.value!,
).padding(horizontal: 26, vertical: 20),
).padding(horizontal: 4)
: const SizedBox.shrink();
Widget publisherVerificationWidget(SnPublisher data) =>
(data.verification != null)
? Card(
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: VerificationStatusCard(mark: data.verification!),
)
: const SizedBox.shrink();
Widget publisherBioWidget(SnPublisher data) => Card(
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('bio').tr().bold().fontSize(15).padding(bottom: 8),
if (data.bio.isEmpty)
Text('descriptionNone').tr().italic()
else
MarkdownTextContent(
content: data.bio,
linesMargin: EdgeInsets.zero,
),
],
).padding(horizontal: 20, vertical: 16),
);
Widget publisherCategoryTabWidget() => Card(
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: TabBar(
controller: categoryTabController,
dividerColor: Colors.transparent,
splashBorderRadius: const BorderRadius.all(Radius.circular(8)),
tabs: [
Tab(text: 'all'.tr()),
Tab(text: 'postTypePost'.tr()),
Tab(text: 'postArticle'.tr()),
],
),
);
return publisher.when( return publisher.when(
data: data:
(data) => AppScaffold( (data) => AppScaffold(
@@ -351,7 +405,9 @@ class PublisherProfileScreen extends HookConsumerWidget {
SliverGap(16), SliverGap(16),
SliverPostList(pubName: name, pinned: true), SliverPostList(pubName: name, pinned: true),
SliverToBoxAdapter( SliverToBoxAdapter(
child: publisherCategoryTabWidget(), child: _PublisherCategoryTabWidget(
categoryTabController: categoryTabController,
),
), ),
SliverPostList( SliverPostList(
key: ValueKey(categoryTab.value), key: ValueKey(categoryTab.value),
@@ -377,10 +433,19 @@ class PublisherProfileScreen extends HookConsumerWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
publisherBasisWidget(data).padding(bottom: 8), _PublisherBasisWidget(
publisherBadgesWidget(data), data: data,
publisherVerificationWidget(data), subStatus: subStatus,
publisherBioWidget(data), subscribing: subscribing,
subscribe: subscribe,
unsubscribe: unsubscribe,
).padding(bottom: 8),
_PublisherBadgesWidget(
data: data,
badges: badges,
),
_PublisherVerificationWidget(data: data),
_PublisherBioWidget(data: data),
], ],
), ),
), ),
@@ -432,15 +497,32 @@ class PublisherProfileScreen extends HookConsumerWidget {
), ),
), ),
SliverToBoxAdapter( SliverToBoxAdapter(
child: publisherBasisWidget(data).padding(bottom: 8), child: _PublisherBasisWidget(
data: data,
subStatus: subStatus,
subscribing: subscribing,
subscribe: subscribe,
unsubscribe: unsubscribe,
).padding(bottom: 8),
), ),
SliverToBoxAdapter(child: publisherBadgesWidget(data)),
SliverToBoxAdapter( SliverToBoxAdapter(
child: publisherVerificationWidget(data), child: _PublisherBadgesWidget(
data: data,
badges: badges,
),
),
SliverToBoxAdapter(
child: _PublisherVerificationWidget(data: data),
),
SliverToBoxAdapter(
child: _PublisherBioWidget(data: data),
), ),
SliverToBoxAdapter(child: publisherBioWidget(data)),
SliverPostList(pubName: name, pinned: true), SliverPostList(pubName: name, pinned: true),
SliverToBoxAdapter(child: publisherCategoryTabWidget()), SliverToBoxAdapter(
child: _PublisherCategoryTabWidget(
categoryTabController: categoryTabController,
),
),
SliverPostList( SliverPostList(
key: ValueKey(categoryTab.value), key: ValueKey(categoryTab.value),
pubName: name, pubName: name,

View File

@@ -77,7 +77,7 @@ class MarketplaceStickersScreen extends HookConsumerWidget {
searchController.clear(); searchController.clear();
} }
return null; return null;
}, [query.value]); }, [query]);
// Clean up timer on dispose // Clean up timer on dispose
useEffect(() { useEffect(() {

View File

@@ -48,7 +48,7 @@ class PostFeaturedList extends HookConsumerWidget {
'PostFeaturedList: isCollapsed changed to ${isCollapsed.value}', 'PostFeaturedList: isCollapsed changed to ${isCollapsed.value}',
); );
return null; return null;
}, [isCollapsed.value]); }, [isCollapsed]);
useEffect(() { useEffect(() {
if (featuredPostsAsync.hasValue && featuredPostsAsync.value!.isNotEmpty) { if (featuredPostsAsync.hasValue && featuredPostsAsync.value!.isNotEmpty) {
@@ -93,7 +93,7 @@ class PostFeaturedList extends HookConsumerWidget {
); );
} }
return null; return null;
}, [featuredPostsAsync.value]); }, [featuredPostsAsync]);
return ClipRRect( return ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(Radius.circular(8)),

View File

@@ -36,46 +36,50 @@ class PostShuffleScreen extends HookConsumerWidget {
bottom: bottom:
kBottomControlHeight + MediaQuery.of(context).padding.bottom, kBottomControlHeight + MediaQuery.of(context).padding.bottom,
), ),
child: child: Builder(
(postListState.value?.items.length ?? 0) > 0 key: ValueKey(postListState.value?.items.length ?? 0),
? CardSwiper( builder: (context) {
controller: cardSwiperController, if ((postListState.value?.items.length ?? 0) > 0) {
cardsCount: postListState.value!.items.length, return CardSwiper(
isLoop: false, controller: cardSwiperController,
cardBuilder: ( cardsCount: postListState.value!.items.length,
context, isLoop: false,
index, cardBuilder: (
horizontalOffsetPercentage, context,
verticalOffsetPercentage, index,
) { horizontalOffsetPercentage,
return Center( verticalOffsetPercentage,
child: ConstrainedBox( ) {
constraints: BoxConstraints(maxWidth: 540), return Center(
child: SingleChildScrollView( child: ConstrainedBox(
child: Card( constraints: BoxConstraints(maxWidth: 540),
margin: EdgeInsets.zero, child: SingleChildScrollView(
child: ClipRRect( child: Card(
borderRadius: const BorderRadius.all( margin: EdgeInsets.zero,
Radius.circular(8), child: ClipRRect(
), borderRadius: const BorderRadius.all(
child: PostActionableItem( Radius.circular(8),
item: postListState.value!.items[index], ),
), child: PostActionableItem(
item: postListState.value!.items[index],
), ),
), ),
), ),
), ),
); ),
}, );
onEnd: () { },
if (postListState.value?.hasMore ?? true) { onEnd: () async {
postListNotifier.fetch( if (postListState.value?.hasMore ?? true) {
cursor: postListState.value?.nextCursor, postListNotifier.forceRefresh();
); }
} },
}, );
) } else {
: Center(child: CircularProgressIndicator()), return Center(child: CircularProgressIndicator());
}
},
),
), ),
Positioned( Positioned(
left: 0, left: 0,