diff --git a/lib/features/alerts/presentation/alerts_screen.dart b/lib/features/alerts/presentation/alerts_screen.dart index f01f934..9d46244 100644 --- a/lib/features/alerts/presentation/alerts_screen.dart +++ b/lib/features/alerts/presentation/alerts_screen.dart @@ -1,8 +1,9 @@ -import 'package:flutter/material.dart'; import 'package:bluesky_poptart/chat/bsky/convo/defs.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:lazurite/core/l10n/l10n.dart'; +import 'package:lazurite/core/theme/theme_extensions.dart'; import 'package:lazurite/core/widgets/lazurite_app_bar.dart'; import 'package:lazurite/features/messages/bloc/convo_list_bloc.dart'; import 'package:lazurite/features/messages/presentation/widgets/convo_list_pane.dart'; @@ -11,7 +12,17 @@ import 'package:lazurite/features/notifications/cubit/unread_count_cubit.dart'; import 'package:lazurite/features/notifications/presentation/widgets/notifications_pane.dart'; import 'package:lazurite/shared/presentation/widgets/app_screen_entrance.dart'; -enum AlertsTab { notifications, messages, requests } +enum AlertsTab { + notifications, + messages, + requests; + + String get target => switch (this) { + AlertsTab.notifications => '/alerts', + AlertsTab.messages => '/alerts/messages', + AlertsTab.requests => '/alerts/requests', + }; +} class AlertsScreen extends StatefulWidget { const AlertsScreen({super.key, this.initialTab = AlertsTab.notifications}); @@ -63,7 +74,7 @@ class _AlertsScreenState extends State { var unread = 0; for (final convo in state.convos) { final status = convo.status; - final isRequest = status != null && status.isKnownValue && status.knownValue == KnownConvoViewStatus.request; + final isRequest = status != null && status.isKnownValue && status.knownValue == KnownConvoStatus.request; if (!isRequest) { unread += convo.unreadCount; } @@ -71,16 +82,11 @@ class _AlertsScreenState extends State { return unread; } - Widget _buildTab(AlertsTab tab) { - switch (tab) { - case AlertsTab.notifications: - return const NotificationsPane(); - case AlertsTab.messages: - return const ConvoListPane(tab: ConvoTab.primary); - case AlertsTab.requests: - return const ConvoListPane(tab: ConvoTab.requests); - } - } + Widget _buildTab(AlertsTab tab) => switch (tab) { + AlertsTab.notifications => const NotificationsPane(), + AlertsTab.messages => const ConvoListPane(tab: ConvoTab.primary), + AlertsTab.requests => const ConvoListPane(tab: ConvoTab.requests), + }; void _markAllRead(BuildContext context) { context.read().add(const NotificationsMarkedRead()); @@ -100,47 +106,44 @@ class _AlertsTabs extends StatelessWidget { final int messagesUnreadCount; @override - Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - border: Border(bottom: BorderSide(color: Theme.of(context).dividerColor)), - ), - child: Row( - children: [ - _AlertsTabButton( - tab: AlertsTab.notifications, - label: context.l10n.labelNotifications, - currentTab: currentTab, - unreadCount: notificationsUnreadCount, - ), - _AlertsTabButton( - tab: AlertsTab.messages, - label: context.l10n.labelMessages, - currentTab: currentTab, - unreadCount: messagesUnreadCount, - ), - _AlertsTabButton(tab: AlertsTab.requests, label: context.l10n.labelMessageRequests, currentTab: currentTab), - ], - ), - ); - } + Widget build(BuildContext context) => Container( + decoration: BoxDecoration( + border: Border(bottom: BorderSide(color: Theme.of(context).dividerColor)), + ), + child: Row( + children: [ + _AlertsTabButton( + tab: AlertsTab.notifications, + label: context.l10n.labelNotifications, + currentTab: currentTab, + unreadCount: notificationsUnreadCount, + ), + _AlertsTabButton( + tab: AlertsTab.messages, + label: context.l10n.labelMessages, + currentTab: currentTab, + unreadCount: messagesUnreadCount, + ), + _AlertsTabButton(tab: AlertsTab.requests, label: context.l10n.labelMessageRequests, currentTab: currentTab), + ], + ), + ); } class _AlertsTabButton extends StatelessWidget { const _AlertsTabButton({required this.tab, required this.label, required this.currentTab, this.unreadCount = 0}); final AlertsTab tab; - final String label; final AlertsTab currentTab; + final String label; final int unreadCount; @override Widget build(BuildContext context) { final isSelected = currentTab == tab; - final theme = Theme.of(context); - final textStyle = theme.textTheme.bodyLarge?.copyWith( + final textStyle = context.textTheme.bodyLarge?.copyWith( fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, - color: isSelected ? theme.colorScheme.onSurface : theme.colorScheme.onSurfaceVariant, + color: isSelected ? context.colorScheme.onSurface : context.colorScheme.onSurfaceVariant, ); return Expanded( @@ -150,7 +153,7 @@ class _AlertsTabButton extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 12), decoration: BoxDecoration( border: Border( - bottom: BorderSide(color: isSelected ? theme.colorScheme.primary : Colors.transparent, width: 2), + bottom: BorderSide(color: isSelected ? context.colorScheme.primary : Colors.transparent, width: 2), ), ), child: Row( @@ -164,12 +167,15 @@ class _AlertsTabButton extends StatelessWidget { key: ValueKey('alerts-tab-unread-${tab.name}'), constraints: const BoxConstraints(minWidth: 18), padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration(color: theme.colorScheme.primary, borderRadius: BorderRadius.circular(12)), + decoration: BoxDecoration( + color: context.colorScheme.primary, + borderRadius: BorderRadius.circular(12), + ), child: Text( unreadCount > 99 ? '99+' : unreadCount.toString(), textAlign: TextAlign.center, - style: theme.textTheme.labelSmall?.copyWith( - color: theme.colorScheme.onPrimary, + style: context.textTheme.labelSmall?.copyWith( + color: context.colorScheme.onPrimary, fontWeight: FontWeight.w700, ), ), @@ -183,14 +189,8 @@ class _AlertsTabButton extends StatelessWidget { } void _navigateToTab(BuildContext context) { - final target = switch (tab) { - AlertsTab.notifications => '/alerts', - AlertsTab.messages => '/alerts/messages', - AlertsTab.requests => '/alerts/requests', - }; - - if (GoRouterState.of(context).uri.path != target) { - context.go(target); + if (GoRouterState.of(context).uri.path != tab.target) { + context.go(tab.target); } } } diff --git a/lib/features/feed/presentation/post_thread_screen.dart b/lib/features/feed/presentation/post_thread_screen.dart index 13d1b85..cb8db87 100644 --- a/lib/features/feed/presentation/post_thread_screen.dart +++ b/lib/features/feed/presentation/post_thread_screen.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:bluesky_poptart/app/bsky/feed/defs.dart'; import 'package:bluesky_poptart/app/bsky/feed/post.dart'; -import 'package:lazurite/features/moderation/domain/moderation_models.dart' as bsky_moderation; +import 'package:poptart_bluesky_moderation/poptart_bluesky_moderation.dart' as bsky_moderation; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; diff --git a/lib/features/feed/presentation/widgets/compact_post_card.dart b/lib/features/feed/presentation/widgets/compact_post_card.dart index ae45b95..229e16f 100644 --- a/lib/features/feed/presentation/widgets/compact_post_card.dart +++ b/lib/features/feed/presentation/widgets/compact_post_card.dart @@ -7,7 +7,7 @@ import 'package:lazurite/features/feed/presentation/widgets/facet_text.dart'; import 'package:lazurite/features/feed/presentation/widgets/post_embed_view.dart'; import 'package:lazurite/features/feed/presentation/widgets/post_repost_context.dart'; import 'package:lazurite/features/feed/presentation/widgets/post_text_styles.dart'; -import 'package:lazurite/features/moderation/domain/moderation_models.dart' as bsky_moderation; +import 'package:poptart_bluesky_moderation/poptart_bluesky_moderation.dart' as bsky_moderation; import 'package:lazurite/features/moderation/presentation/moderation_ui_helpers.dart'; import 'package:lazurite/features/moderation/presentation/widgets/moderated_blur_overlay.dart'; import 'package:lazurite/features/moderation/presentation/widgets/moderation_badge_row.dart'; diff --git a/lib/features/feed/presentation/widgets/grid_post_card.dart b/lib/features/feed/presentation/widgets/grid_post_card.dart index 9114c36..aa4594e 100644 --- a/lib/features/feed/presentation/widgets/grid_post_card.dart +++ b/lib/features/feed/presentation/widgets/grid_post_card.dart @@ -1,7 +1,7 @@ import 'package:bluesky_poptart/app/bsky/actor/defs.dart'; import 'package:bluesky_poptart/app/bsky/embed/record_with_media.dart'; import 'package:bluesky_poptart/app/bsky/feed/defs.dart'; -import 'package:lazurite/features/moderation/domain/moderation_models.dart' as bsky_moderation; +import 'package:poptart_bluesky_moderation/poptart_bluesky_moderation.dart' as bsky_moderation; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:lazurite/core/cache/lazurite_image_cache.dart'; diff --git a/lib/features/feed/presentation/widgets/post_card.dart b/lib/features/feed/presentation/widgets/post_card.dart index 23dd169..22e8c23 100644 --- a/lib/features/feed/presentation/widgets/post_card.dart +++ b/lib/features/feed/presentation/widgets/post_card.dart @@ -1,6 +1,6 @@ import 'package:bluesky_poptart/app/bsky/actor/defs.dart'; import 'package:bluesky_poptart/app/bsky/feed/defs.dart'; -import 'package:lazurite/features/moderation/domain/moderation_models.dart' as bsky_moderation; +import 'package:poptart_bluesky_moderation/poptart_bluesky_moderation.dart' as bsky_moderation; import 'package:flutter/material.dart'; import 'package:lazurite/core/l10n/l10n.dart'; import 'package:lazurite/core/theme/theme_extensions.dart'; diff --git a/lib/features/feed/presentation/widgets/post_card_with_actions.dart b/lib/features/feed/presentation/widgets/post_card_with_actions.dart index fb633c1..a4e5965 100644 --- a/lib/features/feed/presentation/widgets/post_card_with_actions.dart +++ b/lib/features/feed/presentation/widgets/post_card_with_actions.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:bluesky_poptart/app/bsky/feed/defs.dart'; import 'package:bluesky_poptart/app/bsky/feed/post.dart'; -import 'package:lazurite/features/moderation/domain/moderation_models.dart' as bsky_moderation; +import 'package:poptart_bluesky_moderation/poptart_bluesky_moderation.dart' as bsky_moderation; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:lazurite/core/l10n/l10n.dart'; diff --git a/lib/features/feed/presentation/widgets/post_embed_view.dart b/lib/features/feed/presentation/widgets/post_embed_view.dart index 971be83..cbe20b4 100644 --- a/lib/features/feed/presentation/widgets/post_embed_view.dart +++ b/lib/features/feed/presentation/widgets/post_embed_view.dart @@ -15,7 +15,7 @@ import 'package:lazurite/features/feed/presentation/media/media_actions.dart'; import 'package:lazurite/features/feed/presentation/media/video_layout.dart'; import 'package:lazurite/features/feed/presentation/media/video_player_route_args.dart'; import 'package:lazurite/features/feed/presentation/widgets/post_record_embed.dart'; -import 'package:lazurite/features/moderation/domain/moderation_models.dart' as bsky_moderation; +import 'package:poptart_bluesky_moderation/poptart_bluesky_moderation.dart' as bsky_moderation; import 'package:lazurite/features/moderation/presentation/moderation_ui_helpers.dart'; import 'package:lazurite/features/moderation/presentation/widgets/moderated_blur_overlay.dart'; import 'package:lazurite/shared/presentation/widgets/external_link_preview_card.dart'; diff --git a/lib/features/lists/presentation/list_detail_screen.dart b/lib/features/lists/presentation/list_detail_screen.dart index dad4c81..a0ae19c 100644 --- a/lib/features/lists/presentation/list_detail_screen.dart +++ b/lib/features/lists/presentation/list_detail_screen.dart @@ -1,6 +1,6 @@ import 'package:poptart_core/poptart_core.dart' show AtUri; import 'package:bluesky_poptart/app/bsky/graph/defs.dart' as bsky_graph; -import 'package:lazurite/features/moderation/domain/moderation_models.dart' as bsky_moderation; +import 'package:poptart_bluesky_moderation/poptart_bluesky_moderation.dart' as bsky_moderation; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; diff --git a/lib/features/messages/bloc/message_bloc.dart b/lib/features/messages/bloc/message_bloc.dart index 3bfffdb..7870f8e 100644 --- a/lib/features/messages/bloc/message_bloc.dart +++ b/lib/features/messages/bloc/message_bloc.dart @@ -99,6 +99,7 @@ class MessageBloc extends Bloc { final isTarget = m.when( messageView: (data) => data.id == event.messageId, deletedMessageView: (_) => false, + systemMessageView: (_) => false, unknown: (_) => false, ); return isTarget ? deletedMessage : m; diff --git a/lib/features/messages/presentation/message_thread_screen.dart b/lib/features/messages/presentation/message_thread_screen.dart index 544751a..a554b14 100644 --- a/lib/features/messages/presentation/message_thread_screen.dart +++ b/lib/features/messages/presentation/message_thread_screen.dart @@ -60,7 +60,12 @@ class _MessageThreadScreenState extends State { void _copyAllMessages(List messages) { final lines = messages.reversed .map( - (m) => m.when(messageView: (data) => data.text, deletedMessageView: (_) => '[deleted]', unknown: (_) => ''), + (m) => m.when( + messageView: (data) => data.text, + deletedMessageView: (_) => '[deleted]', + systemMessageView: (_) => '', + unknown: (_) => '', + ), ) .where((t) => t.isNotEmpty) .join('\n'); @@ -129,6 +134,7 @@ class _MessageThreadScreenState extends State { messageView: (data) => MessageBubble(message: data, isCurrentUser: data.sender.did == currentUserDid), deletedMessageView: (data) => const DeletedMessageBubble(isCurrentUser: false), + systemMessageView: (_) => const SizedBox.shrink(), unknown: (_) => const SizedBox.shrink(), ); }, diff --git a/lib/features/messages/presentation/widgets/convo_list_item.dart b/lib/features/messages/presentation/widgets/convo_list_item.dart index 1f2b10c..ad19445 100644 --- a/lib/features/messages/presentation/widgets/convo_list_item.dart +++ b/lib/features/messages/presentation/widgets/convo_list_item.dart @@ -103,6 +103,7 @@ class ConvoListItem extends StatelessWidget { return lastMessage.when( messageView: (data) => data.text.isNotEmpty ? data.text : null, deletedMessageView: (_) => context.l10n.messageDeleted, + systemMessageView: (_) => null, unknown: (_) => null, ); } diff --git a/lib/features/moderation/data/moderation_service.dart b/lib/features/moderation/data/moderation_service.dart index 303a0cb..557e18e 100644 --- a/lib/features/moderation/data/moderation_service.dart +++ b/lib/features/moderation/data/moderation_service.dart @@ -5,6 +5,7 @@ import 'package:bluesky_poptart/app/bsky/feed/defs.dart'; import 'package:bluesky_poptart/app/bsky/labeler/defs.dart'; import 'package:bluesky_poptart/app/bsky/labeler/get_services.dart'; import 'package:bluesky_poptart/app/bsky/notification/list_notifications.dart' as notifications; +import 'package:bluesky_poptart/app/bsky/actor/get_preferences/output.dart'; import 'package:lazurite/core/cache/poptart_cache_codecs.dart'; import 'package:lazurite/core/database/app_database.dart'; import 'package:lazurite/core/logging/app_logger.dart'; @@ -13,13 +14,11 @@ import 'package:lazurite/core/network/poptart_client_adapter.dart'; import 'package:lazurite/core/network/unauthorized_recovery_runner.dart'; import 'package:lazurite/core/network/xrpc_client_factory.dart'; import 'package:lazurite/features/auth/data/models/auth_models.dart'; -import 'package:lazurite/features/moderation/domain/moderation_models.dart' as moderation; -import 'package:poptart_lex/com/atproto/label/defs.dart'; +import 'package:poptart_bluesky_moderation/poptart_bluesky_moderation.dart' as moderation; +import 'package:poptart_lex/com/atproto/label/defs.dart' show LabelValueDefinition, LabelValueDefinitionStrings; const _officialBlueskyLabelerDid = 'did:plc:ar7c4by46qjdydhdevvrndac'; const _maxCustomLabelers = 20; -const _noUnauthenticatedLabel = '!no-unauthenticated'; - class ModerationService { ModerationService({ required Bluesky bluesky, @@ -225,158 +224,51 @@ class ModerationService { moderation.ModerationDecision moderatePost(PostView post) { final opts = _opts; if (opts == null) { - return const moderation.ModerationDecision.empty(); + return moderation.ModerationDecision.init(); } - return moderation.ModerationDecision( - me: post.author.did == _resolvedUserDid, - causes: [ - if (_hasAuthenticatedViewer(opts) && opts.prefs.hiddenPosts.contains(post.uri.toString())) _hiddenCause(), - ..._viewerCauses(post.author.viewer, opts), - ..._labelCauses(post.labels ?? const [], moderation.LabelTarget.content, opts), - ..._labelCauses(post.author.labels ?? const [], moderation.LabelTarget.account, opts), - ], - ); + return moderation.moderatePost(moderation.ModerationSubjectPost.postView(data: post), opts); } moderation.ModerationDecision moderateProfile(ProfileView profile) { final opts = _opts; if (opts == null) { - return const moderation.ModerationDecision.empty(); + return moderation.ModerationDecision.init(); } - return _moderateActor(did: profile.did, viewer: profile.viewer, labels: profile.labels ?? const [], opts: opts); + return moderation.moderateProfile(moderation.ModerationSubjectProfile.profileView(data: profile), opts); } moderation.ModerationDecision moderateProfileBasic(ProfileViewBasic profile) { final opts = _opts; if (opts == null) { - return const moderation.ModerationDecision.empty(); + return moderation.ModerationDecision.init(); } - return _moderateActor(did: profile.did, viewer: profile.viewer, labels: profile.labels ?? const [], opts: opts); + return moderation.moderateProfile(moderation.ModerationSubjectProfile.profileViewBasic(data: profile), opts); } moderation.ModerationDecision moderateProfileDetailed(ProfileViewDetailed profile) { final opts = _opts; if (opts == null) { - return const moderation.ModerationDecision.empty(); + return moderation.ModerationDecision.init(); } - return _moderateActor(did: profile.did, viewer: profile.viewer, labels: profile.labels ?? const [], opts: opts); + return moderation.moderateProfile(moderation.ModerationSubjectProfile.profileViewDetailed(data: profile), opts); } moderation.ModerationDecision moderateNotification(notifications.Notification notification) { final opts = _opts; if (opts == null) { - return const moderation.ModerationDecision.empty(); + return moderation.ModerationDecision.init(); } - return moderation.ModerationDecision.merge([ - _moderateActor( - did: notification.author.did, - viewer: notification.author.viewer, - labels: notification.author.labels ?? const [], - opts: opts, - ), - moderation.ModerationDecision( - me: notification.author.did == _resolvedUserDid, - causes: _labelCauses(notification.labels ?? const [], moderation.LabelTarget.content, opts), - ), - ]); - } - - moderation.ModerationDecision _moderateActor({ - required String did, - required dynamic viewer, - required List