feat(discussions): questions on published studysets, answered by learners

A learner asks about a published studyset, a topic in it, or one card,
note or step of a release, and others answer, as on Stack Exchange
(ADR 0013): questions and answers in the app's editor without images,
sounds or videos, comments of up to 600 characters under the question
and each answer. There are no votes: the asker accepts an answer, which
comes first, then the oldest; the author's posts are marked. The
instance reads from git's file list which topics a question's target
sits in, so a topic lists what is asked within it and below it.

Posts live in two server-only tables, written through endpoints, so the
synced tables stay as they are and v0.17.0 keeps syncing. Everyone
signed in reads; writing needs an identity and a public profile; the
studyset's author hides posts and blocks people with feedback's blocks;
a withdrawn studyset takes no new questions; anyone writes at most 30
posts an hour. A post with others' replies under it is emptied when
deleted, else deleted; deletePerson deletes what a person wrote the same
way, and a published studyset's deletion takes its discussions with it.

The migration adds Serverpod's gen_random_uuid_v7(), discussion_post
and discussion_seen with their indexes and cascading foreign keys, and
changes nothing else.

In the app, discussions open from a copy's studyset or topic menu, a
studied card's "Ask about this", Explore's summary and the Publish
sheet; the Profile tab's inbox lists those the person follows, and its
count includes what is new in them.

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-15 23:52:40 +02:00
commit 5efa552489
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -6455,7 +6455,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get inboxIntro =>
'Reports and suggestions on your published studysets, and those you wrote.';
'Questions and answers, reports and suggestions on your published studysets, and those you wrote in.';
@override
String get inboxEmpty => 'Nothing yet.';
@ -6470,6 +6470,222 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get reportToAuthor => 'Report to the author';
@override
String get discussionsTitle => 'Discussions';
@override
String get discussionsSearch => 'Search questions';
@override
String get discussionsAsk => 'Ask a question';
@override
String get discussionsEmpty => 'No questions yet';
@override
String get discussionsEmptyHint =>
'Ask the first: about the studyset, a topic or one of its flashcards.';
@override
String get discussionsNoMatch => 'No question\'s title says that.';
@override
String get discussionsMore => 'Show more';
@override
String get discussionsUnreachable =>
'The discussions can\'t be reached right now.';
@override
String discussionsAnswers(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count answers',
one: '1 answer',
zero: 'No answers',
);
return '$_temp0';
}
@override
String discussionsBy(String name, String date) {
return '$name · $date';
}
@override
String get discussionsSomeone => 'Someone';
@override
String get discussionsDeletedQuestion => 'Deleted question';
@override
String get discussionsHidden => 'Hidden';
@override
String get discussionsAboutCard => 'About a flashcard';
@override
String get discussionsAboutNote => 'About a note';
@override
String get discussionsAboutStep => 'About a step';
@override
String get discussionsAccepted => 'Accepted';
@override
String discussionsOn(String title) {
return 'On $title';
}
@override
String get discussionRefusalNoIdentity =>
'To ask and answer, sign in with an identity in Profile: posts show who wrote them.';
@override
String get discussionRefusalNoProfile =>
'To ask and answer, make a public profile in Profile: posts show who wrote them.';
@override
String get discussionRefusalBlocked =>
'The author of this studyset keeps you from writing on their studysets.';
@override
String get discussionRefusalWithdrawn =>
'The author took this studyset off Explore: what is here stays, but nothing new is asked.';
@override
String get discussionRefusalNotInRelease =>
'That isn\'t in the studyset\'s release any more.';
@override
String get discussionRefusalEmpty => 'Write something first.';
@override
String get discussionRefusalTooLong => 'That\'s longer than allowed.';
@override
String get discussionRefusalTooMany =>
'You\'ve written a lot in the last hour. Try again a little later.';
@override
String get discussionRefusalWithFiles =>
'Images and sounds can\'t be posted: your files are yours alone, so nobody else would see them.';
@override
String get discussionFailed =>
'Couldn\'t reach the instance. Try again once you are online.';
@override
String get discussionTitle => 'Question';
@override
String get discussionMissing => 'This question isn\'t there any more.';
@override
String get discussionDeleted => 'This was deleted.';
@override
String get discussionEdited => 'edited';
@override
String get discussionWriteAnswer => 'Write an answer';
@override
String get discussionYourAnswer => 'Your answer';
@override
String get discussionEditHeading => 'Edit';
@override
String get discussionTitleField => 'Title';
@override
String get discussionTitleHint => 'What do you want to know?';
@override
String get discussionPost => 'Post';
@override
String get discussionComment => 'Add a comment';
@override
String get discussionCommentHint => 'A short remark';
@override
String get discussionAccept => 'Accept';
@override
String get discussionUnaccept => 'Take back';
@override
String get discussionMenu => 'More';
@override
String get discussionMenuEdit => 'Edit';
@override
String get discussionMenuDelete => 'Delete';
@override
String get discussionMenuHide => 'Hide';
@override
String get discussionMenuShow => 'Show again';
@override
String discussionMenuBlock(String name) {
return 'Block $name';
}
@override
String get discussionDeleteTitle => 'Delete this?';
@override
String get discussionDeleteBody =>
'What others replied under it stays, without your text and name.';
@override
String get discussionAnswered => 'Answer posted.';
@override
String get discussionCommented => 'Comment posted.';
@override
String get discussionSaved => 'Saved.';
@override
String get discussionDeletedDone => 'Deleted.';
@override
String get discussionAcceptedDone => 'Answer accepted.';
@override
String get discussionUnacceptedDone => 'No answer accepted now.';
@override
String get discussionHiddenDone => 'Hidden: only its writer and you see it.';
@override
String get discussionShownDone => 'Shown again.';
@override
String get discussionBlockedDone =>
'Blocked: what they write on your studysets shows to them alone.';
@override
String get discussionAskAbout => 'Ask about this';
@override
String get inboxDiscussions => 'Discussions';
@override
String get inboxFeedback => 'Reports and suggestions';
@override
String get inboxDiscussionsEmpty => 'No discussions yet.';
@override
String get cardReportToAuthor => 'Send to the author too';