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

@ -6518,7 +6518,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get inboxIntro =>
'Meldungen und Vorschläge zu deinen veröffentlichten Lernsets und die, die du geschrieben hast.';
'Fragen und Antworten, Meldungen und Vorschläge zu deinen veröffentlichten Lernsets und die, in denen du geschrieben hast.';
@override
String get inboxEmpty => 'Noch nichts.';
@ -6533,6 +6533,223 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get reportToAuthor => 'Fehler im Original melden';
@override
String get discussionsTitle => 'Diskussionen';
@override
String get discussionsSearch => 'Fragen durchsuchen';
@override
String get discussionsAsk => 'Frage stellen';
@override
String get discussionsEmpty => 'Noch keine Fragen';
@override
String get discussionsEmptyHint =>
'Stell die erste: zum Lernset, zu einem Thema oder zu einer seiner Karteikarten.';
@override
String get discussionsNoMatch => 'Kein Fragentitel sagt das.';
@override
String get discussionsMore => 'Mehr zeigen';
@override
String get discussionsUnreachable =>
'Die Diskussionen sind gerade nicht erreichbar.';
@override
String discussionsAnswers(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count Antworten',
one: '1 Antwort',
zero: 'Keine Antworten',
);
return '$_temp0';
}
@override
String discussionsBy(String name, String date) {
return '$name · $date';
}
@override
String get discussionsSomeone => 'Jemand';
@override
String get discussionsDeletedQuestion => 'Gelöschte Frage';
@override
String get discussionsHidden => 'Ausgeblendet';
@override
String get discussionsAboutCard => 'Zu einer Karteikarte';
@override
String get discussionsAboutNote => 'Zu einer Notiz';
@override
String get discussionsAboutStep => 'Zu einem Schritt';
@override
String get discussionsAccepted => 'Angenommen';
@override
String discussionsOn(String title) {
return 'Zu $title';
}
@override
String get discussionRefusalNoIdentity =>
'Um zu fragen und zu antworten, melde dich im Profil mit einer Identität an: Beiträge zeigen, wer sie geschrieben hat.';
@override
String get discussionRefusalNoProfile =>
'Um zu fragen und zu antworten, leg im Profil ein öffentliches Profil an: Beiträge zeigen, wer sie geschrieben hat.';
@override
String get discussionRefusalBlocked =>
'Wer dieses Lernset veröffentlicht hat, lässt dich zu seinen Lernsets nichts schreiben.';
@override
String get discussionRefusalWithdrawn =>
'Dieses Lernset wurde aus Entdecken genommen: Was hier steht, bleibt, aber Neues wird nicht gefragt.';
@override
String get discussionRefusalNotInRelease =>
'Das ist nicht mehr in der Veröffentlichung des Lernsets.';
@override
String get discussionRefusalEmpty => 'Schreib zuerst etwas.';
@override
String get discussionRefusalTooLong => 'Das ist länger als erlaubt.';
@override
String get discussionRefusalTooMany =>
'Du hast in der letzten Stunde viel geschrieben. Versuch es etwas später noch einmal.';
@override
String get discussionRefusalWithFiles =>
'Bilder und Töne lassen sich nicht posten: Deine Dateien gehören dir allein, also sähe sie niemand sonst.';
@override
String get discussionFailed =>
'Die Instanz ist nicht erreichbar. Versuch es noch einmal, sobald du online bist.';
@override
String get discussionTitle => 'Frage';
@override
String get discussionMissing => 'Diese Frage gibt es nicht mehr.';
@override
String get discussionDeleted => 'Das wurde gelöscht.';
@override
String get discussionEdited => 'bearbeitet';
@override
String get discussionWriteAnswer => 'Antwort schreiben';
@override
String get discussionYourAnswer => 'Deine Antwort';
@override
String get discussionEditHeading => 'Bearbeiten';
@override
String get discussionTitleField => 'Titel';
@override
String get discussionTitleHint => 'Was möchtest du wissen?';
@override
String get discussionPost => 'Posten';
@override
String get discussionComment => 'Kommentar hinzufügen';
@override
String get discussionCommentHint => 'Eine kurze Bemerkung';
@override
String get discussionAccept => 'Annehmen';
@override
String get discussionUnaccept => 'Zurücknehmen';
@override
String get discussionMenu => 'Mehr';
@override
String get discussionMenuEdit => 'Bearbeiten';
@override
String get discussionMenuDelete => 'Löschen';
@override
String get discussionMenuHide => 'Ausblenden';
@override
String get discussionMenuShow => 'Wieder zeigen';
@override
String discussionMenuBlock(String name) {
return '$name blockieren';
}
@override
String get discussionDeleteTitle => 'Das löschen?';
@override
String get discussionDeleteBody =>
'Was andere darunter geantwortet haben, bleibt, ohne deinen Text und Namen.';
@override
String get discussionAnswered => 'Antwort gepostet.';
@override
String get discussionCommented => 'Kommentar gepostet.';
@override
String get discussionSaved => 'Gespeichert.';
@override
String get discussionDeletedDone => 'Gelöscht.';
@override
String get discussionAcceptedDone => 'Antwort angenommen.';
@override
String get discussionUnacceptedDone => 'Jetzt ist keine Antwort angenommen.';
@override
String get discussionHiddenDone =>
'Ausgeblendet: Nur wer es geschrieben hat und du sehen es.';
@override
String get discussionShownDone => 'Wieder sichtbar.';
@override
String get discussionBlockedDone =>
'Blockiert: Was diese Person zu deinen Lernsets schreibt, sieht nur sie selbst.';
@override
String get discussionAskAbout => 'Dazu fragen';
@override
String get inboxDiscussions => 'Diskussionen';
@override
String get inboxFeedback => 'Meldungen und Vorschläge';
@override
String get inboxDiscussionsEmpty => 'Noch keine Diskussionen.';
@override
String get cardReportToAuthor => 'Auch im Original melden';