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

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