diff --git a/lib/features/moderation/data/moderation_service.dart b/lib/features/moderation/data/moderation_service.dart index 8f60325..ffa41d0 100644 --- a/lib/features/moderation/data/moderation_service.dart +++ b/lib/features/moderation/data/moderation_service.dart @@ -15,6 +15,7 @@ import 'package:poptart_lex/com/atproto/label/defs.dart'; const _officialBlueskyLabelerDid = 'did:plc:ar7c4by46qjdydhdevvrndac'; const _maxCustomLabelers = 20; +const _noUnauthenticatedLabel = '!no-unauthenticated'; class ModerationService { ModerationService({ @@ -301,6 +302,10 @@ class ModerationService { } moderation.ModerationCause? _labelCause(Label label, moderation.LabelTarget target, moderation.ModerationOpts opts) { + if (label.val == _noUnauthenticatedLabel && _hasAuthenticatedViewer(opts)) { + return null; + } + final definition = _definitionForLabel(label, opts); if (definition == null) { return null; @@ -332,6 +337,8 @@ class ModerationService { ); } + bool _hasAuthenticatedViewer(moderation.ModerationOpts opts) => opts.userDid != null && opts.userDid!.isNotEmpty; + moderation.ModerationUI postUi(PostView post, moderation.ModerationBehaviorContext context) => moderatePost(post).getUI(context); diff --git a/test/features/moderation/data/moderation_service_test.dart b/test/features/moderation/data/moderation_service_test.dart index 880b546..8b9a7dd 100644 --- a/test/features/moderation/data/moderation_service_test.dart +++ b/test/features/moderation/data/moderation_service_test.dart @@ -13,6 +13,7 @@ import 'package:http/http.dart' as http; import 'package:lazurite/core/database/app_database.dart'; import 'package:lazurite/core/network/poptart_client_adapter.dart'; import 'package:lazurite/features/moderation/data/moderation_service.dart'; +import 'package:lazurite/features/moderation/domain/moderation_models.dart' as moderation; import '../../../helpers/test_bluesky_client.dart'; @@ -99,6 +100,52 @@ void main() { service.dispose(); }); + test('ignores unauthenticated-only labels when the viewer is authenticated', () async { + final service = ModerationService( + bluesky: _testBlueskyClient(), + database: database, + accountDid: _accountDid, + userDid: _accountDid, + ); + + await service.ensureInitialized(); + + final profileUi = service.profileDetailedUi( + _authGatedProfile(), + moderation.ModerationBehaviorContext.profileView, + ); + final avatarUi = service.profileDetailedUi(_authGatedProfile(), moderation.ModerationBehaviorContext.avatar); + + expect(profileUi.blur, isFalse); + expect(profileUi.noOverride, isFalse); + expect(avatarUi.blur, isFalse); + + service.dispose(); + }); + + test('applies unauthenticated-only labels when no viewer is authenticated', () async { + final service = ModerationService( + bluesky: _testBlueskyClient(anonymous: true), + database: database, + accountDid: 'anonymous', + ); + + await service.updatePreferences(preferences: const []); + + final profileUi = service.profileDetailedUi( + _authGatedProfile(), + moderation.ModerationBehaviorContext.profileView, + ); + final avatarUi = service.profileDetailedUi(_authGatedProfile(), moderation.ModerationBehaviorContext.avatar); + + expect(profileUi.blur, isTrue); + expect(profileUi.noOverride, isTrue); + expect(avatarUi.blur, isTrue); + expect(avatarUi.noOverride, isTrue); + + service.dispose(); + }); + test('falls back to cached preferences after a request failure', () async { final seededService = ModerationService( bluesky: _testBlueskyClient( @@ -322,14 +369,33 @@ void main() { }); } -Bluesky _testBlueskyClient({_FakeActorService? actor, _FakeLabelerService? labeler}) { +Bluesky _testBlueskyClient({_FakeActorService? actor, _FakeLabelerService? labeler, bool anonymous = false}) { final transport = _FakeModerationTransport( actor: actor ?? _FakeActorService(), labeler: labeler ?? const _FakeLabelerService(), ); + if (anonymous) { + return Bluesky.anonymous(getClient: transport.get, postClient: transport.post); + } return testBluesky(getClient: transport.get, postClient: transport.post); } +ProfileViewDetailed _authGatedProfile() { + return ProfileViewDetailed( + did: _accountDid, + handle: 'test.bsky.social', + indexedAt: DateTime.utc(2026, 5, 16), + labels: [ + Label( + src: 'did:plc:ar7c4by46qjdydhdevvrndac', + uri: 'at://$_accountDid/app.bsky.actor.profile/self', + val: '!no-unauthenticated', + cts: DateTime.utc(2026, 5, 16), + ), + ], + ); +} + class _FakeModerationTransport { _FakeModerationTransport({_FakeActorService? actor, _FakeLabelerService? labeler}) : actor = actor ?? _FakeActorService(),