feat(explore): your own studyset is yours, not one to add
Explore offered Add on the author's own published studyset and made a whole second copy of it beside the original. The instance now names the studyset an entry was published from to its author alone (`yourStudysetId`, never to anyone else), and Explore shows it as Yours, opening the original, while it is in the library. The summary still adds a copy anyway, to see it as learners get it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
fd69719652
commit
d3ece5c528
17 changed files with 263 additions and 17 deletions
File diff suppressed because one or more lines are too long
|
|
@ -41,6 +41,7 @@ abstract class ExploreEntry
|
|||
required this.pathCount,
|
||||
required this.pdfCount,
|
||||
this.learners,
|
||||
this.yourStudysetId,
|
||||
});
|
||||
|
||||
factory ExploreEntry({
|
||||
|
|
@ -68,6 +69,7 @@ abstract class ExploreEntry
|
|||
required int pathCount,
|
||||
required int pdfCount,
|
||||
int? learners,
|
||||
_isc.UuidValue? yourStudysetId,
|
||||
}) = _ExploreEntryImpl;
|
||||
|
||||
factory ExploreEntry.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
|
|
@ -100,6 +102,11 @@ abstract class ExploreEntry
|
|||
pathCount: jsonSerialization['pathCount'] as int,
|
||||
pdfCount: jsonSerialization['pdfCount'] as int,
|
||||
learners: jsonSerialization['learners'] as int?,
|
||||
yourStudysetId: jsonSerialization['yourStudysetId'] == null
|
||||
? null
|
||||
: _isc.UuidValueJsonExtension.fromJson(
|
||||
jsonSerialization['yourStudysetId'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -163,6 +170,11 @@ abstract class ExploreEntry
|
|||
/// null for fewer than 5, so that nobody can be told apart.
|
||||
int? learners;
|
||||
|
||||
/// For its author asking, their studyset it was published from (decided
|
||||
/// 2026-10-16: Explore opens it rather than adding a copy of it); null for
|
||||
/// everyone else.
|
||||
_isc.UuidValue? yourStudysetId;
|
||||
|
||||
/// Returns a shallow copy of this [ExploreEntry]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_isc.useResult
|
||||
|
|
@ -191,6 +203,7 @@ abstract class ExploreEntry
|
|||
int? pathCount,
|
||||
int? pdfCount,
|
||||
int? learners,
|
||||
_isc.UuidValue? yourStudysetId,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
|
|
@ -220,6 +233,7 @@ abstract class ExploreEntry
|
|||
'pathCount': pathCount,
|
||||
'pdfCount': pdfCount,
|
||||
if (learners != null) 'learners': learners,
|
||||
if (yourStudysetId != null) 'yourStudysetId': yourStudysetId?.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -251,6 +265,7 @@ abstract class ExploreEntry
|
|||
'pathCount': pathCount,
|
||||
'pdfCount': pdfCount,
|
||||
if (learners != null) 'learners': learners,
|
||||
if (yourStudysetId != null) 'yourStudysetId': yourStudysetId?.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -288,6 +303,7 @@ class _ExploreEntryImpl extends ExploreEntry {
|
|||
required int pathCount,
|
||||
required int pdfCount,
|
||||
int? learners,
|
||||
_isc.UuidValue? yourStudysetId,
|
||||
}) : super._(
|
||||
publicId: publicId,
|
||||
location: location,
|
||||
|
|
@ -313,6 +329,7 @@ class _ExploreEntryImpl extends ExploreEntry {
|
|||
pathCount: pathCount,
|
||||
pdfCount: pdfCount,
|
||||
learners: learners,
|
||||
yourStudysetId: yourStudysetId,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [ExploreEntry]
|
||||
|
|
@ -344,6 +361,7 @@ class _ExploreEntryImpl extends ExploreEntry {
|
|||
int? pathCount,
|
||||
int? pdfCount,
|
||||
Object? learners = _Undefined,
|
||||
Object? yourStudysetId = _Undefined,
|
||||
}) {
|
||||
return ExploreEntry(
|
||||
publicId: publicId ?? this.publicId,
|
||||
|
|
@ -376,6 +394,9 @@ class _ExploreEntryImpl extends ExploreEntry {
|
|||
pathCount: pathCount ?? this.pathCount,
|
||||
pdfCount: pdfCount ?? this.pdfCount,
|
||||
learners: learners is int? ? learners : this.learners,
|
||||
yourStudysetId: yourStudysetId is _isc.UuidValue?
|
||||
? yourStudysetId
|
||||
: this.yourStudysetId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1697,6 +1697,8 @@
|
|||
"exploreAdd": "Hinzufügen",
|
||||
"exploreInLibraryShort": "In Bibliothek",
|
||||
"exploreInLibrary": "In deiner Bibliothek",
|
||||
"exploreYours": "Deins",
|
||||
"exploreAddCopyAnyway": "Trotzdem eine Kopie hinzufügen",
|
||||
"explorePercent": "{percent} %",
|
||||
"exploreAdded": "Zu deiner Bibliothek hinzugefügt.",
|
||||
"exploreOpen": "Öffnen",
|
||||
|
|
|
|||
|
|
@ -1997,6 +1997,8 @@
|
|||
"exploreAdd": "Add",
|
||||
"exploreInLibraryShort": "In library",
|
||||
"exploreInLibrary": "In your library",
|
||||
"exploreYours": "Yours",
|
||||
"exploreAddCopyAnyway": "Add a copy anyway",
|
||||
"explorePercent": "{percent} %",
|
||||
"@explorePercent": {"placeholders": {"percent": {"type": "int"}}},
|
||||
"exploreAdded": "Added to your library.",
|
||||
|
|
|
|||
|
|
@ -10047,6 +10047,18 @@ abstract class AppLocalizations {
|
|||
/// **'In your library'**
|
||||
String get exploreInLibrary;
|
||||
|
||||
/// No description provided for @exploreYours.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Yours'**
|
||||
String get exploreYours;
|
||||
|
||||
/// No description provided for @exploreAddCopyAnyway.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Add a copy anyway'**
|
||||
String get exploreAddCopyAnyway;
|
||||
|
||||
/// No description provided for @explorePercent.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -6333,6 +6333,12 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get exploreInLibrary => 'In deiner Bibliothek';
|
||||
|
||||
@override
|
||||
String get exploreYours => 'Deins';
|
||||
|
||||
@override
|
||||
String get exploreAddCopyAnyway => 'Trotzdem eine Kopie hinzufügen';
|
||||
|
||||
@override
|
||||
String explorePercent(int percent) {
|
||||
return '$percent %';
|
||||
|
|
|
|||
|
|
@ -6276,6 +6276,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get exploreInLibrary => 'In your library';
|
||||
|
||||
@override
|
||||
String get exploreYours => 'Yours';
|
||||
|
||||
@override
|
||||
String get exploreAddCopyAnyway => 'Add a copy anyway';
|
||||
|
||||
@override
|
||||
String explorePercent(int percent) {
|
||||
return '$percent %';
|
||||
|
|
|
|||
|
|
@ -35,8 +35,9 @@ class ExploreBloc(final ExploreRepository _explore)
|
|||
),
|
||||
);
|
||||
try {
|
||||
final (copies, (:page, :hasMore)) = await (
|
||||
final (copies, held, (:page, :hasMore)) = await (
|
||||
_explore.copies(),
|
||||
_explore.held(),
|
||||
_explore.list(event.filter),
|
||||
).wait;
|
||||
if (query != _query) {
|
||||
|
|
@ -48,6 +49,7 @@ class ExploreBloc(final ExploreRepository _explore)
|
|||
studysets: page,
|
||||
hasMore: hasMore,
|
||||
copies: copies,
|
||||
held: held,
|
||||
),
|
||||
);
|
||||
} on Object catch (error) {
|
||||
|
|
@ -178,7 +180,9 @@ final class const ExploreNotWritten() extends ExploreOutcome;
|
|||
|
||||
/// Explore as its screen shows it: the [filter] and what answers it, a
|
||||
/// page at a time, which of them the person has a copy of ([copies], by
|
||||
/// public id, the copy's top studyset), which are being added and how far
|
||||
/// public id, the copy's top studyset), which studysets are in their
|
||||
/// library ([held]: one of theirs Explore lists opens rather than being
|
||||
/// added again), which are being added and how far
|
||||
/// ([adding], null before the first step), and what the last request came
|
||||
/// to ([outcome], cleared by the next state).
|
||||
final class const ExploreState({
|
||||
|
|
@ -188,9 +192,17 @@ final class const ExploreState({
|
|||
final bool hasMore = false,
|
||||
final bool loadingMore = false,
|
||||
final Map<String, String> copies = const {},
|
||||
final Set<String> held = const {},
|
||||
final Map<String, double?> adding = const {},
|
||||
final ExploreOutcome? outcome,
|
||||
}) with Equatable {
|
||||
/// The person's own studyset [studyset] was published from, while it is
|
||||
/// in their library; null for anyone else's.
|
||||
String? yoursOf(ExploreStudyset studyset) => switch (studyset.yours) {
|
||||
final id? when held.contains(id) => id,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
/// This state with what is given changed, and no outcome unless given.
|
||||
ExploreState copyWith({
|
||||
ExploreStatus? status,
|
||||
|
|
@ -199,6 +211,7 @@ final class const ExploreState({
|
|||
bool? hasMore,
|
||||
bool? loadingMore,
|
||||
Map<String, String>? copies,
|
||||
Set<String>? held,
|
||||
Map<String, double?>? adding,
|
||||
ExploreOutcome? outcome,
|
||||
}) => ExploreState(
|
||||
|
|
@ -208,6 +221,7 @@ final class const ExploreState({
|
|||
hasMore: hasMore ?? this.hasMore,
|
||||
loadingMore: loadingMore ?? this.loadingMore,
|
||||
copies: copies ?? this.copies,
|
||||
held: held ?? this.held,
|
||||
adding: adding ?? this.adding,
|
||||
outcome: outcome,
|
||||
);
|
||||
|
|
@ -220,6 +234,7 @@ final class const ExploreState({
|
|||
hasMore,
|
||||
loadingMore,
|
||||
copies,
|
||||
held,
|
||||
adding,
|
||||
outcome,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -46,6 +46,12 @@ class ExploreRepository(
|
|||
publication: studyset.id,
|
||||
};
|
||||
|
||||
/// The ids of the person's studysets in their library, those deleted
|
||||
/// left out: whether one Explore names as theirs is still there.
|
||||
Future<Set<String>> held() async => {
|
||||
for (final studyset in await _studysets.all()) studyset.id,
|
||||
};
|
||||
|
||||
/// What the latest release of the published studyset the person's copy
|
||||
/// [rootId] (its top studyset) came from would bring it; null while the
|
||||
/// copy holds the latest, and for a studyset its author withdrew, whose
|
||||
|
|
|
|||
|
|
@ -188,6 +188,7 @@ class ServerpodExploreGateway(final protocol.Client _client)
|
|||
paths: entry.pathCount,
|
||||
pdfs: entry.pdfCount,
|
||||
learners: entry.learners,
|
||||
yours: entry.yourStudysetId?.uuid,
|
||||
);
|
||||
|
||||
/// A description as the instance keeps it, its rich text's JSON; empty
|
||||
|
|
|
|||
|
|
@ -52,6 +52,11 @@ final class const ExploreStudyset({
|
|||
/// How many people learn it here, rounded (5, 10, 20, 50, …); null for
|
||||
/// fewer than 5 (ADR 0005: shown, never ranked).
|
||||
final int? learners,
|
||||
|
||||
/// For its author, their studyset it was published from (decided
|
||||
/// 2026-10-16: Explore opens it rather than adding a copy); null for
|
||||
/// everyone else, and from instances before v0.18.
|
||||
final String? yours,
|
||||
}) with Equatable {
|
||||
/// Its author as they are shown: their display name, else their handle.
|
||||
String get author => displayName.isEmpty ? '@$handle' : displayName;
|
||||
|
|
@ -82,6 +87,7 @@ final class const ExploreStudyset({
|
|||
paths,
|
||||
pdfs,
|
||||
learners,
|
||||
yours,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -626,9 +626,11 @@ class const LicenseChip({required final String license, super.key})
|
|||
}
|
||||
|
||||
/// Add to library for [studyset]: its progress while it is being added, In
|
||||
/// your library (opening the copy) once there is a copy. [wide] in the
|
||||
/// summary sheet, which either closes; [compact] on a card, as Add and In
|
||||
/// library, leaving the card's line its room.
|
||||
/// your library (opening the copy) once there is a copy, and Yours (opening
|
||||
/// it) for the person's own (decided 2026-10-16), the summary also adding a
|
||||
/// copy anyway, to see it as learners get it. [wide] in the summary sheet,
|
||||
/// which either closes; [compact] on a card, as Add and In library, leaving
|
||||
/// the card's line its room.
|
||||
class const ExploreAddButton({
|
||||
required final ExploreStudyset studyset,
|
||||
required final ExploreState state,
|
||||
|
|
@ -647,6 +649,48 @@ class const ExploreAddButton({
|
|||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
)
|
||||
: null;
|
||||
void add() {
|
||||
final bloc = context.read<ExploreBloc>();
|
||||
// From the summary, it goes on in the list: its progress shows on
|
||||
// the studyset's card, and what came of it below the list.
|
||||
if (wide) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
bloc.add(ExploreEventAddRequested(studyset));
|
||||
}
|
||||
|
||||
if (state.yoursOf(studyset) case final own?) {
|
||||
final open = TextButton.icon(
|
||||
style: wide
|
||||
? TextButton.styleFrom(minimumSize: const Size.fromHeight(48))
|
||||
: compact
|
||||
? TextButton.styleFrom(visualDensity: VisualDensity.compact)
|
||||
: null,
|
||||
onPressed: () {
|
||||
final router = GoRouter.of(context);
|
||||
if (wide) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
router.go(RoutePaths.studyset(own));
|
||||
},
|
||||
icon: const Icon(LucideIcons.user),
|
||||
label: Text(l10n.exploreYours),
|
||||
);
|
||||
if (!wide || state.copies.containsKey(studyset.publicId)) {
|
||||
return open;
|
||||
}
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
open,
|
||||
if (state.adding.containsKey(studyset.publicId))
|
||||
const Center(child: ButtonLoadingIndicator())
|
||||
else
|
||||
TextButton(onPressed: add, child: Text(l10n.exploreAddCopyAnyway)),
|
||||
],
|
||||
);
|
||||
}
|
||||
if (state.copies[studyset.publicId] case final copy?) {
|
||||
return TextButton.icon(
|
||||
style: wide
|
||||
|
|
@ -679,15 +723,7 @@ class const ExploreAddButton({
|
|||
}
|
||||
return FilledButton.tonalIcon(
|
||||
style: style,
|
||||
onPressed: () {
|
||||
final bloc = context.read<ExploreBloc>();
|
||||
// From the summary, it goes on in the list: its progress shows on
|
||||
// the studyset's card, and what came of it below the list.
|
||||
if (wide) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
bloc.add(ExploreEventAddRequested(studyset));
|
||||
},
|
||||
onPressed: add,
|
||||
icon: const Icon(LucideIcons.plus),
|
||||
label: Text(compact ? l10n.exploreAdd : l10n.exploreAddToLibrary),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ ExploreStudyset published(
|
|||
int paths = 0,
|
||||
int? learners,
|
||||
String? basedOn,
|
||||
String? yours,
|
||||
}) => ExploreStudyset(
|
||||
publicId: publicId,
|
||||
location: 'halkyon.eu/@$handle/$publicId',
|
||||
|
|
@ -53,6 +54,7 @@ ExploreStudyset published(
|
|||
paths: paths,
|
||||
learners: learners,
|
||||
basedOn: basedOn,
|
||||
yours: yours,
|
||||
);
|
||||
|
||||
/// The JSON a synced [payload] holds.
|
||||
|
|
@ -366,6 +368,64 @@ void main() {
|
|||
expect(find.text('Add'), findsNothing);
|
||||
});
|
||||
|
||||
group("the person's own studyset", () {
|
||||
setUp(
|
||||
() => studysets.receiveRemoteChange(
|
||||
'mine',
|
||||
encodeStudysetPayload(StudysetContent.titled('Japanese')),
|
||||
),
|
||||
);
|
||||
|
||||
testWidgets('shows as theirs and opens, adding no copy; the summary adds '
|
||||
'a copy anyway when asked', (tester) async {
|
||||
await showExplore(
|
||||
tester,
|
||||
FakeExploreGateway(
|
||||
published: [published('Japanese', yours: 'mine')],
|
||||
releases: [release('japanese', 'Japanese')],
|
||||
),
|
||||
);
|
||||
expect(find.text('Add'), findsNothing);
|
||||
|
||||
await tester.tap(find.text('Yours'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(StudysetScreen), findsOneWidget);
|
||||
expect(await StudysetRepository(studysets).all(), hasLength(1));
|
||||
|
||||
await openTab(tester, 'Explore');
|
||||
await tester.tap(find.text('Japanese').first);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Add a copy anyway'));
|
||||
await tester.pumpAndSettle();
|
||||
final all = await StudysetRepository(studysets).all();
|
||||
expect(all, hasLength(2));
|
||||
expect(
|
||||
all.where((studyset) => studyset.content.publishedAt != null),
|
||||
hasLength(1),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('deleted from the library, it can be added again', (
|
||||
tester,
|
||||
) async {
|
||||
studysets.receiveRemoteChange(
|
||||
'mine',
|
||||
encodeStudysetPayload(StudysetContent.titled('Japanese')),
|
||||
deletedAt: DateTime.utc(2026, 10, 15),
|
||||
);
|
||||
await showExplore(
|
||||
tester,
|
||||
FakeExploreGateway(
|
||||
published: [published('Japanese', yours: 'mine')],
|
||||
releases: [release('japanese', 'Japanese')],
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('Yours'), findsNothing);
|
||||
expect(find.text('Add'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('a release in a schema this app does not know is not added, '
|
||||
'and says why', (tester) async {
|
||||
final newer = release('japanese', 'Japanese');
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import 'package:halkyon_learn_server/src/generated/protocol.dart';
|
|||
import 'package:halkyon_learn_server/src/profile/profile_rules.dart';
|
||||
import 'package:halkyon_learn_server/src/publish/release_repository.dart';
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_auth_idp_server/core.dart';
|
||||
|
||||
/// How many studysets a page of Explore lists.
|
||||
const explorePageSize = 30;
|
||||
|
|
@ -263,7 +264,10 @@ class ExploreEndpoint extends Endpoint {
|
|||
final rows = await session.db.unsafeQuery(
|
||||
'SELECT * FROM ('
|
||||
'SELECT DISTINCT ON (r."publishedStudysetId") r.*, p."publicId", '
|
||||
'p."slug", p."learners", f."handle", f."displayName", f."avatarHash" '
|
||||
'p."slug", p."learners", f."handle", f."displayName", f."avatarHash", '
|
||||
// The author's own studyset, for the author alone.
|
||||
'CASE WHEN p."authUserId"::text = @me THEN p."studysetId"::text '
|
||||
'END AS "yourStudysetId" '
|
||||
'FROM "studyset_release" r '
|
||||
'JOIN "published_studyset" p ON p."id" = r."publishedStudysetId" '
|
||||
'JOIN "public_profile" f ON f."authUserId" = p."authUserId" '
|
||||
|
|
@ -273,7 +277,10 @@ class ExploreEndpoint extends Endpoint {
|
|||
'${where.isEmpty ? '' : 'WHERE ${where.join(' AND ')} '}'
|
||||
'ORDER BY latest."releasedAt" DESC, latest."id" DESC'
|
||||
'${paged ? ' LIMIT @limit OFFSET @offset' : ''}',
|
||||
parameters: QueryParameters.named(parameters),
|
||||
parameters: QueryParameters.named({
|
||||
...parameters,
|
||||
'me': session.authenticated!.authUserId.uuid,
|
||||
}),
|
||||
);
|
||||
final domain = accountDomain(session.serverpod);
|
||||
return [
|
||||
|
|
@ -310,6 +317,10 @@ class ExploreEndpoint extends Endpoint {
|
|||
pathCount: latest['pathCount'] as int,
|
||||
pdfCount: latest['pdfCount'] as int,
|
||||
learners: learnersShown(latest['learners'] as int),
|
||||
yourStudysetId: switch (latest['yourStudysetId']) {
|
||||
final String id => UuidValue.withValidation(id),
|
||||
_ => null,
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,3 +38,7 @@ fields:
|
|||
### How many people learn it here, rounded down (5, 10, 20, 50, 100, …);
|
||||
### null for fewer than 5, so that nobody can be told apart.
|
||||
learners: int?
|
||||
### For its author asking, their studyset it was published from (decided
|
||||
### 2026-10-16: Explore opens it rather than adding a copy of it); null for
|
||||
### everyone else.
|
||||
yourStudysetId: UuidValue?
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ abstract class ExploreEntry
|
|||
required this.pathCount,
|
||||
required this.pdfCount,
|
||||
this.learners,
|
||||
this.yourStudysetId,
|
||||
});
|
||||
|
||||
factory ExploreEntry({
|
||||
|
|
@ -68,6 +69,7 @@ abstract class ExploreEntry
|
|||
required int pathCount,
|
||||
required int pdfCount,
|
||||
int? learners,
|
||||
_is.UuidValue? yourStudysetId,
|
||||
}) = _ExploreEntryImpl;
|
||||
|
||||
factory ExploreEntry.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
|
|
@ -100,6 +102,11 @@ abstract class ExploreEntry
|
|||
pathCount: jsonSerialization['pathCount'] as int,
|
||||
pdfCount: jsonSerialization['pdfCount'] as int,
|
||||
learners: jsonSerialization['learners'] as int?,
|
||||
yourStudysetId: jsonSerialization['yourStudysetId'] == null
|
||||
? null
|
||||
: _is.UuidValueJsonExtension.fromJson(
|
||||
jsonSerialization['yourStudysetId'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -163,6 +170,11 @@ abstract class ExploreEntry
|
|||
/// null for fewer than 5, so that nobody can be told apart.
|
||||
int? learners;
|
||||
|
||||
/// For its author asking, their studyset it was published from (decided
|
||||
/// 2026-10-16: Explore opens it rather than adding a copy of it); null for
|
||||
/// everyone else.
|
||||
_is.UuidValue? yourStudysetId;
|
||||
|
||||
/// Returns a shallow copy of this [ExploreEntry]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
|
|
@ -191,6 +203,7 @@ abstract class ExploreEntry
|
|||
int? pathCount,
|
||||
int? pdfCount,
|
||||
int? learners,
|
||||
_is.UuidValue? yourStudysetId,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
|
|
@ -220,6 +233,7 @@ abstract class ExploreEntry
|
|||
'pathCount': pathCount,
|
||||
'pdfCount': pdfCount,
|
||||
if (learners != null) 'learners': learners,
|
||||
if (yourStudysetId != null) 'yourStudysetId': yourStudysetId?.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -251,6 +265,7 @@ abstract class ExploreEntry
|
|||
'pathCount': pathCount,
|
||||
'pdfCount': pdfCount,
|
||||
if (learners != null) 'learners': learners,
|
||||
if (yourStudysetId != null) 'yourStudysetId': yourStudysetId?.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -288,6 +303,7 @@ class _ExploreEntryImpl extends ExploreEntry {
|
|||
required int pathCount,
|
||||
required int pdfCount,
|
||||
int? learners,
|
||||
_is.UuidValue? yourStudysetId,
|
||||
}) : super._(
|
||||
publicId: publicId,
|
||||
location: location,
|
||||
|
|
@ -313,6 +329,7 @@ class _ExploreEntryImpl extends ExploreEntry {
|
|||
pathCount: pathCount,
|
||||
pdfCount: pdfCount,
|
||||
learners: learners,
|
||||
yourStudysetId: yourStudysetId,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [ExploreEntry]
|
||||
|
|
@ -344,6 +361,7 @@ class _ExploreEntryImpl extends ExploreEntry {
|
|||
int? pathCount,
|
||||
int? pdfCount,
|
||||
Object? learners = _Undefined,
|
||||
Object? yourStudysetId = _Undefined,
|
||||
}) {
|
||||
return ExploreEntry(
|
||||
publicId: publicId ?? this.publicId,
|
||||
|
|
@ -376,6 +394,9 @@ class _ExploreEntryImpl extends ExploreEntry {
|
|||
pathCount: pathCount ?? this.pathCount,
|
||||
pdfCount: pdfCount ?? this.pdfCount,
|
||||
learners: learners is int? ? learners : this.learners,
|
||||
yourStudysetId: yourStudysetId is _is.UuidValue?
|
||||
? yourStudysetId
|
||||
: this.yourStudysetId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -430,6 +430,43 @@ void main() {
|
|||
}
|
||||
});
|
||||
|
||||
test('tells its author which of their studysets each of theirs was '
|
||||
'published from, and nobody else', () async {
|
||||
final japanese = await studyset(alice, 'Japanese');
|
||||
final published = await publish(alice, japanese, 'japanese');
|
||||
|
||||
UuidValue? yoursIn(List<ExploreEntry> entries) => entries
|
||||
.firstWhere((entry) => entry.publicId == published.publicId)
|
||||
.yourStudysetId;
|
||||
expect(
|
||||
yoursIn(
|
||||
await endpoints.explore.list(signedIn(alice), ExploreQuery()),
|
||||
),
|
||||
japanese,
|
||||
);
|
||||
expect(
|
||||
yoursIn(
|
||||
await endpoints.explore.entries(signedIn(alice), [
|
||||
published.publicId,
|
||||
]),
|
||||
),
|
||||
japanese,
|
||||
);
|
||||
expect(
|
||||
yoursIn(await endpoints.explore.list(signedIn(bob), ExploreQuery())),
|
||||
isNull,
|
||||
);
|
||||
expect(yoursIn(await list()), isNull);
|
||||
expect(
|
||||
yoursIn(
|
||||
await endpoints.explore.entries(signedIn(bob), [
|
||||
published.publicId,
|
||||
]),
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
});
|
||||
|
||||
test('says which of the studysets asked for are out, and their '
|
||||
'latest version', () async {
|
||||
final published = await publish(
|
||||
|
|
|
|||
Loading…
Reference in a new issue