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

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