feat(studyset): deleted studysets wait 14 days in Recently deleted

Deleting a studyset blanked it with everything inside it once a
four-second Undo was over. It now only marks it (`studyset.deletedAt`, a
column as `archivedAt`): it leaves every list, count and session with
what is inside it and waits in the library's Recently deleted for 14
days, saying how many are left, with Restore and Delete now. Deleting no
longer asks first and offers Undo; Delete now asks. Once the 14 days are
over, whichever of the person's devices looks next (as their data opens,
after every sync) deletes it for good, blanking first as before; the
server can't blank a payload and never writes a person's space (ADR
0007). The connector, publishing and the learner count leave deleted
studysets out too.

The migration adds one nullable column: app versions before it stop
syncing until they update.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-12 18:50:10 +02:00
commit 4644125cc4
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -19,7 +19,6 @@ import 'package:halkyon_learn_flutter/src/feature/study/model/rating.dart';
import 'package:halkyon_learn_flutter/src/feature/study/model/study_progress.dart';
import 'package:halkyon_learn_flutter/src/feature/study/model/studyset_study_options.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/bloc/studyset_list_bloc.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_deletions.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_payload.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_content.dart';
@ -43,7 +42,6 @@ void main() {
late CardRepository cards;
late NoteRepository notes;
late LearningPathRepository paths;
late StudysetDeletions deletions;
late ReviewRepository reviews;
late StudysetListBloc bloc;
late FakeSettingsGateway settingsGateway;
@ -73,7 +71,6 @@ void main() {
});
tearDown(() async {
await bloc.close();
await deletions.dispose();
await studysets.dispose();
await cards.dispose();
await notes.dispose();
@ -82,19 +79,8 @@ void main() {
await pdfs.dispose();
});
/// Opens the library, where a deletion can be undone for [undoWindow].
void start({
Duration undoWindow = const Duration(milliseconds: 1),
String? parentId,
}) {
deletions = StudysetDeletions(
studysets,
cards,
notes,
paths,
pdfs,
undoWindow: undoWindow,
);
/// Opens the library, or inside the studyset [parentId] its topics.
void start({String? parentId}) {
bloc = StudysetListBloc(
studysets,
cards,
@ -102,7 +88,6 @@ void main() {
paths,
pdfs,
reviews,
deletions,
settings,
parentId: parentId,
)..add(const StudysetListEventStarted());
@ -446,17 +431,34 @@ void main() {
);
});
test('leaves out a studyset whose deletion waits, and shows it again '
'when that is undone', () async {
test('a deleted studyset leaves the list for Recently deleted, and '
'comes back restored', () async {
final id = await studysets.create(StudysetContent.titled('Japanese'));
start(undoWindow: const Duration(hours: 1));
start();
await listing(['Japanese']);
deletions.delete(id);
await listing([]);
deletions.undo(id);
await studysets.markDeleted(id);
final waiting = await bloc.stream.firstWhere(
(state) =>
state is StudysetListStateLoaded &&
state.studysets.isEmpty &&
state.deleted.isNotEmpty,
);
expect(
(waiting as StudysetListStateLoaded).deleted.single.content.title,
'Japanese',
);
bloc.add(StudysetListEventRestoreRequested(id));
await listing(['Japanese']);
expect(
bloc.state,
isA<StudysetListStateLoaded>().having(
(state) => state.deleted,
'deleted',
isEmpty,
),
);
expect(gateway.deleted, isEmpty);
});