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 {
switch content.userInfo["type"] as? String {
case "messages.new":
content.categoryIdentifier = "REPLYABLE_MESSAGE"
try handleMessagingNotification(request: request, content: content)
default:
try handleDefaultNotification(content: content)
@@ -60,8 +61,6 @@ class NotificationService: UNNotificationServiceExtension {
let pfpIdentifier = meta["pfp"] as? String
content.categoryIdentifier = "REPLYABLE_MESSAGE"
let metaCopy = meta as? [String: Any] ?? [:]
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 _$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: 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();
}
return null;
}, [query.value]);
}, [query]);
// Clean up timer on dispose
useEffect(() {

View File

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

View File

@@ -27,6 +27,224 @@ import 'package:styled_widget/styled_widget.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
Future<SnPublisher> publisher(Ref ref, String uname) async {
final apiClient = ref.watch(apiClientProvider);
@@ -132,170 +350,6 @@ class PublisherProfileScreen extends HookConsumerWidget {
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(
data:
(data) => AppScaffold(
@@ -351,7 +405,9 @@ class PublisherProfileScreen extends HookConsumerWidget {
SliverGap(16),
SliverPostList(pubName: name, pinned: true),
SliverToBoxAdapter(
child: publisherCategoryTabWidget(),
child: _PublisherCategoryTabWidget(
categoryTabController: categoryTabController,
),
),
SliverPostList(
key: ValueKey(categoryTab.value),
@@ -377,10 +433,19 @@ class PublisherProfileScreen extends HookConsumerWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
publisherBasisWidget(data).padding(bottom: 8),
publisherBadgesWidget(data),
publisherVerificationWidget(data),
publisherBioWidget(data),
_PublisherBasisWidget(
data: data,
subStatus: subStatus,
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(
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(
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),
SliverToBoxAdapter(child: publisherCategoryTabWidget()),
SliverToBoxAdapter(
child: _PublisherCategoryTabWidget(
categoryTabController: categoryTabController,
),
),
SliverPostList(
key: ValueKey(categoryTab.value),
pubName: name,

View File

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

View File

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

View File

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