feat(studyset): a studyset's marker says what the instance sees of it (ADR 0019)

The developer's decision of 21 Oct, on the library's cards and the
studyset's Share in its action row alike:
- a lock: private, end-to-end encrypted;
- an open lock: readable by the instance, not published;
- a globe: published.

The lock and the open lock open one dialog saying what that means and
changing it (Make non-private; Make private, or Publish…, which opens the
Publish sheet, while it isn't private); a topic's dialog says its privacy
is its tree's. The globe opens the Publish sheet, whose status for a
studyset not published now says so ("Not published") with an open lock,
as "Private" means something else now. Texts in en and de.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-21 23:07:31 +02:00
commit 9fb1ab2f6c
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
2 changed files with 425 additions and 48 deletions

View file

@ -90,19 +90,30 @@ void main() {
await tester.pumpAndSettle();
}
/// Opens the Publish sheet from Share: at once once published, else
/// through what its privacy means, which leads there while it isn't
/// private (ADR 0019).
Future<void> openSheet(WidgetTester tester) async {
await tester.tap(find.text('Share'));
await tester.pumpAndSettle();
if (find.text('Publish…').evaluate().isNotEmpty) {
await tester.tap(find.text('Publish…'));
await tester.pumpAndSettle();
}
}
testWidgets('a private studyset is published under a name made from its '
"title, at its author's handle", (tester) async {
testWidgets('a studyset not published is published under a name made '
"from its title, at its author's handle", (tester) async {
stored('japanese', 'Japanese', license: 'CC-BY-SA-4.0');
await showStudyset(tester, 'japanese');
expect(find.byIcon(LucideIcons.lock), findsOneWidget);
// Readable by the instance and not published: an open lock.
expect(find.byIcon(LucideIcons.lockOpen), findsOneWidget);
await openSheet(tester);
expect(find.text('Private · only you can see it'), findsOneWidget);
expect(
find.text('Not published · only you see it in Learn'),
findsOneWidget,
);
expect(
find.text('Found at halkyon.eu/@raisondetredev/japanese'),
findsOneWidget,
@ -362,16 +373,20 @@ void main() {
expect(publishing.publishes, hasLength(1));
});
testWidgets('on a phone it is an icon that says the studyset is private', (
tester,
) async {
testWidgets('on a phone it is an icon that says the studyset is not '
'private, leading to publishing it', (tester) async {
stored('japanese', 'Japanese', license: 'CC-BY-4.0');
await showStudyset(tester, 'japanese', width: 400);
expect(find.byTooltip('Share: private'), findsOneWidget);
await tester.tap(find.byTooltip('Share: private'));
expect(find.byTooltip('Share: not private'), findsOneWidget);
await tester.tap(find.byTooltip('Share: not private'));
await tester.pumpAndSettle();
expect(find.text('Private · only you can see it'), findsOneWidget);
await tester.tap(find.text('Publish…'));
await tester.pumpAndSettle();
expect(
find.text('Not published · only you see it in Learn'),
findsOneWidget,
);
});
testWidgets('an anonymous person is told how to publish, and the instance '

View file

@ -16,6 +16,7 @@ import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_conten
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/deleted_studyset_card.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_card.dart';
import 'package:halkyon_ui/halkyon_ui.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:material_ui/material_ui.dart';
import '../../../support/fake_card_gateway.dart';
@ -85,6 +86,7 @@ void main() {
DateTime? archivedAt,
DateTime? starredAt,
String? publishedAt,
bool private = false,
}) => gateway.receiveRemoteChange(
id,
encodeStudysetPayload(
@ -100,6 +102,7 @@ void main() {
),
archivedAt: archivedAt,
starredAt: starredAt,
isPrivate: private,
);
/// [finder] within the card of the studyset titled [title].
@ -796,11 +799,13 @@ void main() {
expect(onCard('Kanji', find.text('By @alice')), findsOneWidget);
});
testWidgets('a lock while private, a globe once out on Explore, as the '
'instance says', (tester) async {
testWidgets('a lock while private, an open lock while the instance can '
'read it and it is not published, a globe once out on Explore, as '
'the instance says', (tester) async {
stored('japanese', 'Japanese');
stored('kanji', 'Kanji');
stored('kana', 'Kana');
stored('diary', 'Diary', private: true);
final store = MemoryPublishedStore();
await showLibrary(
@ -815,9 +820,25 @@ void main() {
publishedStore: store,
);
expect(
onCard('Japanese', find.byIcon(LucideIcons.globe)),
findsOneWidget,
);
expect(onCard('Japanese', find.byTooltip('Public')), findsOneWidget);
expect(onCard('Kanji', find.byTooltip('Private')), findsOneWidget);
expect(onCard('Kana', find.byTooltip('Private')), findsOneWidget);
expect(
onCard('Kanji', find.byIcon(LucideIcons.lockOpen)),
findsOneWidget,
);
expect(
onCard('Kanji', find.byTooltip('Not private, not published')),
findsOneWidget,
);
expect(
onCard('Kana', find.byTooltip('Not private, not published')),
findsOneWidget,
);
expect(onCard('Diary', find.byIcon(LucideIcons.lock)), findsOneWidget);
expect(onCard('Diary', find.byTooltip('Private')), findsOneWidget);
// Remembered for when the instance can't be reached.
expect(store.kept, {'japanese'});
});
@ -835,21 +856,74 @@ void main() {
);
expect(onCard('Japanese', find.byTooltip('Public')), findsOneWidget);
expect(onCard('Kanji', find.byTooltip('Private')), findsOneWidget);
expect(
onCard('Kanji', find.byTooltip('Not private, not published')),
findsOneWidget,
);
});
testWidgets('the lock opens the Publish sheet', (tester) async {
testWidgets('the open lock says the instance can read it, and leads to '
'the Publish sheet or makes it private', (tester) async {
stored('japanese', 'Japanese');
gateway.canMakePrivate = true;
await showLibrary(tester);
await tester.tap(onCard('Japanese', find.byTooltip('Private')));
await tester.tap(
onCard('Japanese', find.byTooltip('Not private, not published')),
);
await tester.pumpAndSettle();
expect(find.text("“Japanese” isn't private"), findsOneWidget);
await tester.tap(find.text('Publish…'));
await tester.pumpAndSettle();
expect(find.text('Publish'), findsOneWidget);
expect(
find.text('Sign in to have a public profile and publish.'),
findsOneWidget,
);
await tester.tapAt(Offset.zero);
await tester.pumpAndSettle();
await tester.tap(
onCard('Japanese', find.byTooltip('Not private, not published')),
);
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(FilledButton, 'Make private'));
await tester.pump();
await tester.pump();
expect(gateway.privacyChanges, [('japanese', true)]);
});
testWidgets('the lock says it is private, offers no publishing, and makes '
'it non-private', (tester) async {
stored('diary', 'Diary', private: true);
await showLibrary(tester);
await tester.tap(onCard('Diary', find.byTooltip('Private')));
await tester.pumpAndSettle();
expect(find.text('“Diary” is private'), findsOneWidget);
expect(find.text('Publish…'), findsNothing);
await tester.tap(find.widgetWithText(FilledButton, 'Make non-private'));
await tester.pump();
await tester.pump();
expect(gateway.privacyChanges, [('diary', false)]);
});
testWidgets('the globe opens the Publish sheet', (tester) async {
stored('japanese', 'Japanese');
await showLibrary(
tester,
identified: true,
publishing: FakePublishGateway(
publications: {'japanese': publication()},
),
publishedStore: MemoryPublishedStore({'japanese'}),
);
await tester.tap(onCard('Japanese', find.byTooltip('Public')));
await tester.pumpAndSettle();
expect(find.text('Publish'), findsOneWidget);
expect(find.textContaining('halkyon.eu/@raisondetredev'), findsWidgets);
});
});
}