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
80 changed files with 28214 additions and 27 deletions

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,78 @@
# ADR 0013: Discussions on published studysets
- **Status:** accepted
- **Date:** 2026-10-15
## Context
A published studyset has reports and suggestions (ADR 0005, rule 11): a line between one
writer and its author, written as rows in each writer's own synced space and gathered by
the instance as they are read. Learners also have questions about what they learn, and
other learners can answer them, as on Stack Exchange; the developer's example was
"Meaning of 青い衝撃 in this situation?" in Japanese › Grammar. The working notes (11 Oct)
planned these as discussions "built on the feedback threads", the step before federation.
Feedback's storage doesn't fit them well: many people answer, the asker accepts one,
posts are edited, and lists are ordered and paged by activity. Rows spread across
writers' spaces could only encode acceptance in decision rows, and could never be edited
(synced content is only ever blanked). New synced tables would make every app older than
the change stop syncing (the sync engine refuses an app whose synced tables differ).
## Decision
The developer decided (2026-10-15), each as recommended:
1. **Server tables, not synced.** `discussion_post` holds questions, answers and
comments, `discussion_seen` when each person last read a discussion; both are
server-only, written through endpoints, online only. Counting, ordering, accepting and
editing are plain queries, posts are ready to federate later (ADR 0005, stages 5 and
6), and the synced tables stay as they are, so older apps keep syncing.
2. **Questions, answers and comments**, as Stack Exchange has them: comments go under the
question and under each answer, never under a comment; questions and answers are rich
text in the app's editor (without images, sounds or videos: the person's files are
theirs alone, and nobody else could load them), comments plain text of up to 600
characters.
3. **No votes.** The accepted answer first, then the oldest first; the author's posts
marked. Votes cost nothing to fake (rule 8 of ADR 0005), so they count for nothing.
The asker accepts an answer, and can take it back.
4. **About the studyset, a topic or an item.** A question names what it is about as the
release names it: the published studyset or a topic in it, a card, a note or a step,
by its id in a release. The instance reads where that sits in the release (git's file
list alone) and keeps the topics it is within, so a topic lists what is asked within
it and in the topics below it. A copy's items know their ids and releases, so the app
asks about them directly; Explore asks about the studyset as a whole, naming no item.
5. **Deleting keeps others' replies.** A post nobody else replied under goes for good; a
post with others' replies under it stays for them, its title, text and writer gone
("Deleted question"). Deleting a person deletes what they wrote the same way, in
`deletePerson`'s transaction; deleting a published studyset deletes its discussions.
6. **The feedback rules stay**: everyone signed in reads; writing needs an identity and a
public profile (posts name their writer); the studyset's author hides posts (then
only their writer and the author see them) and blocks people (the same blocks as
feedback's: what a blocked person writes shows to them alone). A withdrawn studyset
keeps its discussions readable and answerable, but takes no new questions. A person
writes at most 30 posts an hour.
7. **Where it shows**: a copy's menu opens the discussions within that studyset or topic;
a studied card from a release offers "Ask about this"; Explore's summary and the
author's Publish sheet open the whole studyset's; the person's inbox (Profile) lists
the discussions they follow (on their studysets, or asked, answered or commented in),
what others wrote since they last read one marked and counted on the Profile tab.
## Consequences
- The synced tables don't change: v0.17.0 keeps syncing with an instance that has
discussions.
- Writing needs the instance; a question being written stays in the composer meanwhile.
- Moderation is the author's alone; an instance's operator has no tools for it yet.
- Federation (Fedify, ADR 0005 rule 7) can announce posts as the instance holds them.
## Alternatives considered
- **Rows in each writer's synced space, as feedback**: offline writing, one pattern for
both; but no edits, and acceptance and ordering pieced together from many spaces.
- **New synced tables**: offline and clean, but older apps stop syncing until updated.
- **Votes ordering answers**, or shown without ordering: the familiar way, but votes are
free to fake, and Explore already refuses to rank by them.
- **Flat replies** (a forum thread): simpler, but no accepted answer, and short remarks mix
with answers.
- **Everything goes with a deleted question**: simpler, but people's answers would vanish
with someone else's choice.

View file

@ -32,6 +32,14 @@ import 'package:halkyon_learn_client/src/protocol/dictionary/dictionary_key_hit.
as _ii0vb6vg;
import 'package:halkyon_learn_client/src/protocol/dictionary/dictionary_source.dart'
as _i8vj64sp;
import 'package:halkyon_learn_client/src/protocol/discussion/discussion_refusal.dart'
as _ikbvm73t;
import 'package:halkyon_learn_client/src/protocol/discussion/discussion_summary.dart'
as _i7lxdcg2;
import 'package:halkyon_learn_client/src/protocol/discussion/discussion_target.dart'
as _iy0rtcfg;
import 'package:halkyon_learn_client/src/protocol/discussion/discussion_view.dart'
as _i31gsvwz;
import 'package:halkyon_learn_client/src/protocol/explore/explore_entry.dart'
as _iyigb6vc;
import 'package:halkyon_learn_client/src/protocol/explore/explore_query.dart'
@ -508,6 +516,214 @@ class EndpointDictionary extends _isc.EndpointRef {
);
}
/// A published studyset's discussions (ADR 0013), for everyone signed in to
/// read: questions about it, a topic in it or one of its items, with their
/// answers and comments; but for what its author hid, and what people the
/// author blocked wrote.
/// {@category Endpoint}
class EndpointDiscussions extends _isc.EndpointRef {
EndpointDiscussions(_isc.EndpointCaller caller) : super(caller);
@override
String get name => 'discussions';
/// The questions on the published studyset [publicId], those last active
/// first, [discussionPage] to a [page] (the first without): within [topic] (a studyset of
/// the release, by its id: questions about it and what is inside it),
/// about [item] alone, or with [query] in their titles. A page may hold
/// fewer than it could, when the caller may not see some. Throws
/// [NotFoundException] for a studyset never published.
_ida.Future<List<_i7lxdcg2.DiscussionSummary>> list(
_isc.UuidValue publicId, {
String? topic,
_iy0rtcfg.DiscussionTarget? item,
String? query,
int? page,
}) => caller.callServerEndpoint<List<_i7lxdcg2.DiscussionSummary>>(
'discussions',
'list',
{
'publicId': publicId,
'topic': topic,
'item': item,
'query': query,
'page': page,
},
);
/// The question [questionId] with its answers and comments, as the caller
/// may see them. Throws [NotFoundException] for one they may not see.
_ida.Future<_i31gsvwz.DiscussionView> read(_isc.UuidValue questionId) =>
caller.callServerEndpoint<_i31gsvwz.DiscussionView>(
'discussions',
'read',
{'questionId': questionId},
);
/// Why the caller may not ask about the published studyset [publicId];
/// null when they may. Throws [NotFoundException] for one never
/// published.
_ida.Future<_ikbvm73t.DiscussionRefusal?> askRefusal(
_isc.UuidValue publicId,
) => caller.callServerEndpoint<_ikbvm73t.DiscussionRefusal?>(
'discussions',
'askRefusal',
{'publicId': publicId},
);
}
/// Writing in published studysets' discussions (ADR 0013), and the inbox
/// of those a person follows. For people with an identity and a public
/// profile, whom the studyset's author hasn't blocked: posts name their
/// writer.
/// {@category Endpoint}
class EndpointDiscussion extends _isc.EndpointRef {
EndpointDiscussion(_isc.EndpointCaller caller) : super(caller);
@override
String get name => 'discussion';
/// Asks [title], with [body] (rich text), about [target] of the published
/// studyset [publicId] (the studyset as a whole when it names no item);
/// returns the question's id. Throws
/// [DiscussionRefusedException] when the caller may not, the text won't
/// do, the release lacks the target or the studyset was withdrawn, and
/// [NotFoundException] for a studyset never published.
_ida.Future<_isc.UuidValue> ask(
_isc.UuidValue publicId,
_iy0rtcfg.DiscussionTarget target,
String title,
String body,
) => caller.callServerEndpoint<_isc.UuidValue>(
'discussion',
'ask',
{
'publicId': publicId,
'target': target,
'title': title,
'body': body,
},
);
/// Answers the question [questionId] with [body] (rich text); returns the
/// answer's id. Throws [NotFoundException] for a question the caller
/// can't see, or one deleted, and [DiscussionRefusedException] when they
/// may not write or the text won't do.
_ida.Future<_isc.UuidValue> answer(
_isc.UuidValue questionId,
String body,
) => caller.callServerEndpoint<_isc.UuidValue>(
'discussion',
'answer',
{
'questionId': questionId,
'body': body,
},
);
/// Comments [text] (plain text) on [onId], a question or an answer;
/// returns the comment's id. Throws [NotFoundException] for something the
/// caller can't see, a comment, or something deleted, and
/// [DiscussionRefusedException] when they may not write or the text won't
/// do.
_ida.Future<_isc.UuidValue> comment(
_isc.UuidValue onId,
String text,
) => caller.callServerEndpoint<_isc.UuidValue>(
'discussion',
'comment',
{
'onId': onId,
'text': text,
},
);
/// Changes what the caller's post [postId] says to [body] (rich text, a
/// comment's plain text), a question's title to [title] where given.
/// Throws [NotFoundException] for a post that isn't theirs, or one
/// deleted, and [DiscussionRefusedException] when the text won't do.
_ida.Future<void> edit(
_isc.UuidValue postId,
String body, {
String? title,
}) => caller.callServerEndpoint<void>(
'discussion',
'edit',
{
'postId': postId,
'body': body,
'title': title,
},
);
/// Deletes the caller's post [postId]: for good when nobody else replied
/// under it, else its title, text and writer go and it stays for the
/// replies. Throws [NotFoundException] for a post that isn't theirs.
_ida.Future<void> delete(_isc.UuidValue postId) =>
caller.callServerEndpoint<void>(
'discussion',
'delete',
{'postId': postId},
);
/// Accepts [answerId] as the answer to the caller's question
/// [questionId], in place of any before; null accepts none. Throws
/// [NotFoundException] for a question that isn't theirs, or an answer
/// that isn't one of its own.
_ida.Future<void> accept(
_isc.UuidValue questionId,
_isc.UuidValue? answerId,
) => caller.callServerEndpoint<void>(
'discussion',
'accept',
{
'questionId': questionId,
'answerId': answerId,
},
);
/// Hides the post [postId] on one of the caller's published studysets
/// ([hidden]), or shows it again: hidden, only its writer and the author
/// see it. Throws [NotFoundException] for a post on anyone else's.
_ida.Future<void> hide(
_isc.UuidValue postId,
bool hidden,
) => caller.callServerEndpoint<void>(
'discussion',
'hide',
{
'postId': postId,
'hidden': hidden,
},
);
/// The discussions the caller follows, those last active first: those on
/// their published studysets, and those they asked, answered or commented
/// in; each saying whether others wrote in it since they last read it.
_ida.Future<List<_i7lxdcg2.DiscussionSummary>> inbox() =>
caller.callServerEndpoint<List<_i7lxdcg2.DiscussionSummary>>(
'discussion',
'inbox',
{},
);
/// How many discussions of the caller's inbox have something new.
_ida.Future<int> unseen() => caller.callServerEndpoint<int>(
'discussion',
'unseen',
{},
);
/// Records that the caller has read the discussion [questionId] as it is
/// now. Throws [NotFoundException] for a question there isn't.
_ida.Future<void> markSeen(_isc.UuidValue questionId) =>
caller.callServerEndpoint<void>(
'discussion',
'markSeen',
{'questionId': questionId},
);
}
/// Explore (ADR 0005, rule 4): the studysets published on this instance,
/// found by fit (search, languages, kinds of content), never by popularity,
/// the newest release first; and their releases, to learn from as a copy.
@ -934,6 +1150,8 @@ class Client extends _isc.ServerpodClientShared {
gitHubSignIn = EndpointGitHubSignIn(this);
gitHubUpgrade = EndpointGitHubUpgrade(this);
dictionary = EndpointDictionary(this);
discussions = EndpointDiscussions(this);
discussion = EndpointDiscussion(this);
explore = EndpointExplore(this);
feedback = EndpointFeedback(this);
inbox = EndpointInbox(this);
@ -964,6 +1182,10 @@ class Client extends _isc.ServerpodClientShared {
late final EndpointDictionary dictionary;
late final EndpointDiscussions discussions;
late final EndpointDiscussion discussion;
late final EndpointExplore explore;
late final EndpointFeedback feedback;
@ -992,6 +1214,8 @@ class Client extends _isc.ServerpodClientShared {
'gitHubSignIn': gitHubSignIn,
'gitHubUpgrade': gitHubUpgrade,
'dictionary': dictionary,
'discussions': discussions,
'discussion': discussion,
'explore': explore,
'feedback': feedback,
'inbox': inbox,

View file

@ -0,0 +1,239 @@
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
/* To generate run: "serverpod generate" */
// ignore_for_file: implementation_imports
// ignore_for_file: library_private_types_in_public_api
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: public_member_api_docs
// ignore_for_file: type_literal_in_constant_pattern
// ignore_for_file: use_super_parameters
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:halkyon_learn_client/src/protocol/protocol.dart' as _itx5j3f5;
import 'package:serverpod_client/serverpod_client.dart' as _isc;
import '../feedback/feedback_person.dart' as _i4q3oto5;
/// A question, an answer or a comment as the app shows it.
abstract class DiscussionPostView
implements _isc.SerializableModel, _isc.ProtocolSerialization {
DiscussionPostView._({
required this.id,
required this.kind,
this.parentId,
this.by,
required this.fromAuthor,
this.title,
required this.body,
required this.createdAt,
this.editedAt,
required this.deleted,
required this.hidden,
required this.mine,
});
factory DiscussionPostView({
required _isc.UuidValue id,
required String kind,
_isc.UuidValue? parentId,
_i4q3oto5.FeedbackPerson? by,
required bool fromAuthor,
String? title,
required String body,
required DateTime createdAt,
DateTime? editedAt,
required bool deleted,
required bool hidden,
required bool mine,
}) = _DiscussionPostViewImpl;
factory DiscussionPostView.fromJson(Map<String, dynamic> jsonSerialization) {
return DiscussionPostView(
id: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['id']),
kind: jsonSerialization['kind'] as String,
parentId: jsonSerialization['parentId'] == null
? null
: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['parentId']),
by: jsonSerialization['by'] == null
? null
: _itx5j3f5.Protocol().deserialize<_i4q3oto5.FeedbackPerson>(
jsonSerialization['by'],
),
fromAuthor: _isc.BoolJsonExtension.fromJson(
jsonSerialization['fromAuthor'],
),
title: jsonSerialization['title'] as String?,
body: jsonSerialization['body'] as String,
createdAt: _isc.DateTimeJsonExtension.fromJson(
jsonSerialization['createdAt'],
),
editedAt: jsonSerialization['editedAt'] == null
? null
: _isc.DateTimeJsonExtension.fromJson(jsonSerialization['editedAt']),
deleted: _isc.BoolJsonExtension.fromJson(jsonSerialization['deleted']),
hidden: _isc.BoolJsonExtension.fromJson(jsonSerialization['hidden']),
mine: _isc.BoolJsonExtension.fromJson(jsonSerialization['mine']),
);
}
_isc.UuidValue id;
/// 'question', 'answer' or 'comment'.
String kind;
/// What a comment is on, its question or an answer; null otherwise.
_isc.UuidValue? parentId;
/// Who wrote it, as their public profile shows them; null once deleted,
/// or when its writer has no public profile any more.
_i4q3oto5.FeedbackPerson? by;
/// Whether its writer is the published studyset's author.
bool fromAuthor;
/// A question's title.
String? title;
/// What it says: rich text (the document's JSON), a comment's plain text;
/// empty once deleted.
String body;
/// When it was written and last edited, in UTC.
DateTime createdAt;
DateTime? editedAt;
bool deleted;
/// Whether the author hid it: then only its writer and the author see it.
bool hidden;
/// Whether the viewer wrote it.
bool mine;
/// Returns a shallow copy of this [DiscussionPostView]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
DiscussionPostView copyWith({
_isc.UuidValue? id,
String? kind,
_isc.UuidValue? parentId,
_i4q3oto5.FeedbackPerson? by,
bool? fromAuthor,
String? title,
String? body,
DateTime? createdAt,
DateTime? editedAt,
bool? deleted,
bool? hidden,
bool? mine,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'DiscussionPostView',
'id': id.toJson(),
'kind': kind,
if (parentId != null) 'parentId': parentId?.toJson(),
if (by != null) 'by': by?.toJson(),
'fromAuthor': fromAuthor,
if (title != null) 'title': title,
'body': body,
'createdAt': createdAt.toJson(),
if (editedAt != null) 'editedAt': editedAt?.toJson(),
'deleted': deleted,
'hidden': hidden,
'mine': mine,
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'DiscussionPostView',
'id': id.toJson(),
'kind': kind,
if (parentId != null) 'parentId': parentId?.toJson(),
if (by != null) 'by': by?.toJsonForProtocol(),
'fromAuthor': fromAuthor,
if (title != null) 'title': title,
'body': body,
'createdAt': createdAt.toJson(),
if (editedAt != null) 'editedAt': editedAt?.toJson(),
'deleted': deleted,
'hidden': hidden,
'mine': mine,
};
}
@override
String toString() {
return _isc.SerializationManager.encode(this);
}
}
class _Undefined {}
class _DiscussionPostViewImpl extends DiscussionPostView {
_DiscussionPostViewImpl({
required _isc.UuidValue id,
required String kind,
_isc.UuidValue? parentId,
_i4q3oto5.FeedbackPerson? by,
required bool fromAuthor,
String? title,
required String body,
required DateTime createdAt,
DateTime? editedAt,
required bool deleted,
required bool hidden,
required bool mine,
}) : super._(
id: id,
kind: kind,
parentId: parentId,
by: by,
fromAuthor: fromAuthor,
title: title,
body: body,
createdAt: createdAt,
editedAt: editedAt,
deleted: deleted,
hidden: hidden,
mine: mine,
);
/// Returns a shallow copy of this [DiscussionPostView]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
@override
DiscussionPostView copyWith({
_isc.UuidValue? id,
String? kind,
Object? parentId = _Undefined,
Object? by = _Undefined,
bool? fromAuthor,
Object? title = _Undefined,
String? body,
DateTime? createdAt,
Object? editedAt = _Undefined,
bool? deleted,
bool? hidden,
bool? mine,
}) {
return DiscussionPostView(
id: id ?? this.id,
kind: kind ?? this.kind,
parentId: parentId is _isc.UuidValue? ? parentId : this.parentId,
by: by is _i4q3oto5.FeedbackPerson? ? by : this.by?.copyWith(),
fromAuthor: fromAuthor ?? this.fromAuthor,
title: title is String? ? title : this.title,
body: body ?? this.body,
createdAt: createdAt ?? this.createdAt,
editedAt: editedAt is DateTime? ? editedAt : this.editedAt,
deleted: deleted ?? this.deleted,
hidden: hidden ?? this.hidden,
mine: mine ?? this.mine,
);
}
}

View file

@ -0,0 +1,74 @@
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
/* To generate run: "serverpod generate" */
// ignore_for_file: implementation_imports
// ignore_for_file: library_private_types_in_public_api
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: public_member_api_docs
// ignore_for_file: type_literal_in_constant_pattern
// ignore_for_file: use_super_parameters
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:serverpod_client/serverpod_client.dart' as _isc;
/// Why writing in a discussion was refused; nothing changed.
enum DiscussionRefusal implements _isc.SerializableModel {
/// The person signed in without an identity: anything involving other
/// people needs one.
noIdentity,
/// The person has no public profile: posts name their writer.
noProfile,
/// The studyset's author keeps the person from writing on their
/// studysets.
blocked,
/// The author took the studyset off Explore: what is there stays
/// readable, but nobody asks anything new.
withdrawn,
/// The release doesn't hold what the question would be about.
notInRelease,
/// A title or text left empty.
empty,
/// A title, text or comment longer than allowed.
tooLong,
/// More posts in the last hour than a person may write.
tooMany;
static DiscussionRefusal fromJson(String name) {
switch (name) {
case 'noIdentity':
return DiscussionRefusal.noIdentity;
case 'noProfile':
return DiscussionRefusal.noProfile;
case 'blocked':
return DiscussionRefusal.blocked;
case 'withdrawn':
return DiscussionRefusal.withdrawn;
case 'notInRelease':
return DiscussionRefusal.notInRelease;
case 'empty':
return DiscussionRefusal.empty;
case 'tooLong':
return DiscussionRefusal.tooLong;
case 'tooMany':
return DiscussionRefusal.tooMany;
default:
throw ArgumentError(
'Value "$name" cannot be converted to "DiscussionRefusal"',
);
}
}
@override
String toJson() => name;
@override
String toString() => name;
}

View file

@ -0,0 +1,78 @@
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
/* To generate run: "serverpod generate" */
// ignore_for_file: implementation_imports
// ignore_for_file: library_private_types_in_public_api
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: public_member_api_docs
// ignore_for_file: type_literal_in_constant_pattern
// ignore_for_file: use_super_parameters
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:serverpod_client/serverpod_client.dart' as _isc;
import '../discussion/discussion_refusal.dart' as _i5ff9exj;
/// Writing in a discussion was refused; nothing changed.
abstract class DiscussionRefusedException
implements
_isc.SerializableException,
_isc.SerializableModel,
_isc.ProtocolSerialization {
DiscussionRefusedException._({required this.reason});
factory DiscussionRefusedException({
required _i5ff9exj.DiscussionRefusal reason,
}) = _DiscussionRefusedExceptionImpl;
factory DiscussionRefusedException.fromJson(
Map<String, dynamic> jsonSerialization,
) {
return DiscussionRefusedException(
reason: _i5ff9exj.DiscussionRefusal.fromJson(
(jsonSerialization['reason'] as String),
),
);
}
/// Why it was refused.
_i5ff9exj.DiscussionRefusal reason;
/// Returns a shallow copy of this [DiscussionRefusedException]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
DiscussionRefusedException copyWith({_i5ff9exj.DiscussionRefusal? reason});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'DiscussionRefusedException',
'reason': reason.toJson(),
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'DiscussionRefusedException',
'reason': reason.toJson(),
};
}
@override
String toString() {
return 'DiscussionRefusedException(reason: $reason)';
}
}
class _DiscussionRefusedExceptionImpl extends DiscussionRefusedException {
_DiscussionRefusedExceptionImpl({required _i5ff9exj.DiscussionRefusal reason})
: super._(reason: reason);
/// Returns a shallow copy of this [DiscussionRefusedException]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
@override
DiscussionRefusedException copyWith({_i5ff9exj.DiscussionRefusal? reason}) {
return DiscussionRefusedException(reason: reason ?? this.reason);
}
}

View file

@ -0,0 +1,281 @@
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
/* To generate run: "serverpod generate" */
// ignore_for_file: implementation_imports
// ignore_for_file: library_private_types_in_public_api
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: public_member_api_docs
// ignore_for_file: type_literal_in_constant_pattern
// ignore_for_file: use_super_parameters
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:halkyon_learn_client/src/protocol/protocol.dart' as _itx5j3f5;
import 'package:serverpod_client/serverpod_client.dart' as _isc;
import '../discussion/discussion_target.dart' as _iqswgn91;
import '../feedback/feedback_person.dart' as _i4q3oto5;
/// A question as a list shows it.
abstract class DiscussionSummary
implements _isc.SerializableModel, _isc.ProtocolSerialization {
DiscussionSummary._({
required this.questionId,
required this.publicId,
required this.studysetTitle,
required this.title,
this.asker,
required this.askerIsAuthor,
required this.target,
required this.createdAt,
required this.activeAt,
required this.answers,
required this.accepted,
required this.deleted,
required this.hidden,
required this.mine,
required this.unseen,
});
factory DiscussionSummary({
required _isc.UuidValue questionId,
required _isc.UuidValue publicId,
required String studysetTitle,
required String title,
_i4q3oto5.FeedbackPerson? asker,
required bool askerIsAuthor,
required _iqswgn91.DiscussionTarget target,
required DateTime createdAt,
required DateTime activeAt,
required int answers,
required bool accepted,
required bool deleted,
required bool hidden,
required bool mine,
required bool unseen,
}) = _DiscussionSummaryImpl;
factory DiscussionSummary.fromJson(Map<String, dynamic> jsonSerialization) {
return DiscussionSummary(
questionId: _isc.UuidValueJsonExtension.fromJson(
jsonSerialization['questionId'],
),
publicId: _isc.UuidValueJsonExtension.fromJson(
jsonSerialization['publicId'],
),
studysetTitle: jsonSerialization['studysetTitle'] as String,
title: jsonSerialization['title'] as String,
asker: jsonSerialization['asker'] == null
? null
: _itx5j3f5.Protocol().deserialize<_i4q3oto5.FeedbackPerson>(
jsonSerialization['asker'],
),
askerIsAuthor: _isc.BoolJsonExtension.fromJson(
jsonSerialization['askerIsAuthor'],
),
target: _itx5j3f5.Protocol().deserialize<_iqswgn91.DiscussionTarget>(
jsonSerialization['target'],
),
createdAt: _isc.DateTimeJsonExtension.fromJson(
jsonSerialization['createdAt'],
),
activeAt: _isc.DateTimeJsonExtension.fromJson(
jsonSerialization['activeAt'],
),
answers: jsonSerialization['answers'] as int,
accepted: _isc.BoolJsonExtension.fromJson(jsonSerialization['accepted']),
deleted: _isc.BoolJsonExtension.fromJson(jsonSerialization['deleted']),
hidden: _isc.BoolJsonExtension.fromJson(jsonSerialization['hidden']),
mine: _isc.BoolJsonExtension.fromJson(jsonSerialization['mine']),
unseen: _isc.BoolJsonExtension.fromJson(jsonSerialization['unseen']),
);
}
_isc.UuidValue questionId;
/// The published studyset it is on, and its title.
_isc.UuidValue publicId;
String studysetTitle;
/// Its title; empty once deleted.
String title;
/// Who asked it; null once deleted, or without a public profile.
_i4q3oto5.FeedbackPerson? asker;
bool askerIsAuthor;
/// What it is about.
_iqswgn91.DiscussionTarget target;
/// When it was asked, and when anything last happened in it, in UTC.
DateTime createdAt;
DateTime activeAt;
/// How many answers the viewer sees, and whether one is accepted.
int answers;
bool accepted;
bool deleted;
bool hidden;
/// Whether the viewer asked it.
bool mine;
/// Whether others wrote in it since the viewer last read it, where it is
/// theirs to follow: they asked, answered or commented, or it is on their
/// studyset.
bool unseen;
/// Returns a shallow copy of this [DiscussionSummary]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
DiscussionSummary copyWith({
_isc.UuidValue? questionId,
_isc.UuidValue? publicId,
String? studysetTitle,
String? title,
_i4q3oto5.FeedbackPerson? asker,
bool? askerIsAuthor,
_iqswgn91.DiscussionTarget? target,
DateTime? createdAt,
DateTime? activeAt,
int? answers,
bool? accepted,
bool? deleted,
bool? hidden,
bool? mine,
bool? unseen,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'DiscussionSummary',
'questionId': questionId.toJson(),
'publicId': publicId.toJson(),
'studysetTitle': studysetTitle,
'title': title,
if (asker != null) 'asker': asker?.toJson(),
'askerIsAuthor': askerIsAuthor,
'target': target.toJson(),
'createdAt': createdAt.toJson(),
'activeAt': activeAt.toJson(),
'answers': answers,
'accepted': accepted,
'deleted': deleted,
'hidden': hidden,
'mine': mine,
'unseen': unseen,
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'DiscussionSummary',
'questionId': questionId.toJson(),
'publicId': publicId.toJson(),
'studysetTitle': studysetTitle,
'title': title,
if (asker != null) 'asker': asker?.toJsonForProtocol(),
'askerIsAuthor': askerIsAuthor,
'target': target.toJsonForProtocol(),
'createdAt': createdAt.toJson(),
'activeAt': activeAt.toJson(),
'answers': answers,
'accepted': accepted,
'deleted': deleted,
'hidden': hidden,
'mine': mine,
'unseen': unseen,
};
}
@override
String toString() {
return _isc.SerializationManager.encode(this);
}
}
class _Undefined {}
class _DiscussionSummaryImpl extends DiscussionSummary {
_DiscussionSummaryImpl({
required _isc.UuidValue questionId,
required _isc.UuidValue publicId,
required String studysetTitle,
required String title,
_i4q3oto5.FeedbackPerson? asker,
required bool askerIsAuthor,
required _iqswgn91.DiscussionTarget target,
required DateTime createdAt,
required DateTime activeAt,
required int answers,
required bool accepted,
required bool deleted,
required bool hidden,
required bool mine,
required bool unseen,
}) : super._(
questionId: questionId,
publicId: publicId,
studysetTitle: studysetTitle,
title: title,
asker: asker,
askerIsAuthor: askerIsAuthor,
target: target,
createdAt: createdAt,
activeAt: activeAt,
answers: answers,
accepted: accepted,
deleted: deleted,
hidden: hidden,
mine: mine,
unseen: unseen,
);
/// Returns a shallow copy of this [DiscussionSummary]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
@override
DiscussionSummary copyWith({
_isc.UuidValue? questionId,
_isc.UuidValue? publicId,
String? studysetTitle,
String? title,
Object? asker = _Undefined,
bool? askerIsAuthor,
_iqswgn91.DiscussionTarget? target,
DateTime? createdAt,
DateTime? activeAt,
int? answers,
bool? accepted,
bool? deleted,
bool? hidden,
bool? mine,
bool? unseen,
}) {
return DiscussionSummary(
questionId: questionId ?? this.questionId,
publicId: publicId ?? this.publicId,
studysetTitle: studysetTitle ?? this.studysetTitle,
title: title ?? this.title,
asker: asker is _i4q3oto5.FeedbackPerson?
? asker
: this.asker?.copyWith(),
askerIsAuthor: askerIsAuthor ?? this.askerIsAuthor,
target: target ?? this.target.copyWith(),
createdAt: createdAt ?? this.createdAt,
activeAt: activeAt ?? this.activeAt,
answers: answers ?? this.answers,
accepted: accepted ?? this.accepted,
deleted: deleted ?? this.deleted,
hidden: hidden ?? this.hidden,
mine: mine ?? this.mine,
unseen: unseen ?? this.unseen,
);
}
}

View file

@ -0,0 +1,106 @@
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
/* To generate run: "serverpod generate" */
// ignore_for_file: implementation_imports
// ignore_for_file: library_private_types_in_public_api
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: public_member_api_docs
// ignore_for_file: type_literal_in_constant_pattern
// ignore_for_file: use_super_parameters
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:serverpod_client/serverpod_client.dart' as _isc;
/// What a question is about: the published studyset or a topic in it
/// ('studyset'), or one of its cards, notes or steps ('card', 'note',
/// 'step'), by its id in release [version].
abstract class DiscussionTarget
implements _isc.SerializableModel, _isc.ProtocolSerialization {
DiscussionTarget._({
required this.kind,
required this.item,
required this.version,
});
factory DiscussionTarget({
required String kind,
required String item,
required int version,
}) = _DiscussionTargetImpl;
factory DiscussionTarget.fromJson(Map<String, dynamic> jsonSerialization) {
return DiscussionTarget(
kind: jsonSerialization['kind'] as String,
item: jsonSerialization['item'] as String,
version: jsonSerialization['version'] as int,
);
}
String kind;
String item;
int version;
/// Returns a shallow copy of this [DiscussionTarget]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
DiscussionTarget copyWith({
String? kind,
String? item,
int? version,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'DiscussionTarget',
'kind': kind,
'item': item,
'version': version,
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'DiscussionTarget',
'kind': kind,
'item': item,
'version': version,
};
}
@override
String toString() {
return _isc.SerializationManager.encode(this);
}
}
class _DiscussionTargetImpl extends DiscussionTarget {
_DiscussionTargetImpl({
required String kind,
required String item,
required int version,
}) : super._(
kind: kind,
item: item,
version: version,
);
/// Returns a shallow copy of this [DiscussionTarget]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
@override
DiscussionTarget copyWith({
String? kind,
String? item,
int? version,
}) {
return DiscussionTarget(
kind: kind ?? this.kind,
item: item ?? this.item,
version: version ?? this.version,
);
}
}

View file

@ -0,0 +1,236 @@
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
/* To generate run: "serverpod generate" */
// ignore_for_file: implementation_imports
// ignore_for_file: library_private_types_in_public_api
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: public_member_api_docs
// ignore_for_file: type_literal_in_constant_pattern
// ignore_for_file: use_super_parameters
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:halkyon_learn_client/src/protocol/protocol.dart' as _itx5j3f5;
import 'package:serverpod_client/serverpod_client.dart' as _isc;
import '../discussion/discussion_post_view.dart' as _iv1wx9mv;
import '../discussion/discussion_refusal.dart' as _i5ff9exj;
import '../discussion/discussion_target.dart' as _iqswgn91;
/// A discussion as the app shows it: the question, its answers, the accepted
/// one first, then the oldest first, and the comments on both, oldest first.
abstract class DiscussionView
implements _isc.SerializableModel, _isc.ProtocolSerialization {
DiscussionView._({
required this.question,
required this.publicId,
required this.studysetTitle,
required this.location,
required this.target,
required this.answers,
required this.comments,
this.acceptedAnswerId,
required this.asker,
required this.author,
this.refusal,
});
factory DiscussionView({
required _iv1wx9mv.DiscussionPostView question,
required _isc.UuidValue publicId,
required String studysetTitle,
required String location,
required _iqswgn91.DiscussionTarget target,
required List<_iv1wx9mv.DiscussionPostView> answers,
required List<_iv1wx9mv.DiscussionPostView> comments,
_isc.UuidValue? acceptedAnswerId,
required bool asker,
required bool author,
_i5ff9exj.DiscussionRefusal? refusal,
}) = _DiscussionViewImpl;
factory DiscussionView.fromJson(Map<String, dynamic> jsonSerialization) {
return DiscussionView(
question: _itx5j3f5.Protocol().deserialize<_iv1wx9mv.DiscussionPostView>(
jsonSerialization['question'],
),
publicId: _isc.UuidValueJsonExtension.fromJson(
jsonSerialization['publicId'],
),
studysetTitle: jsonSerialization['studysetTitle'] as String,
location: jsonSerialization['location'] as String,
target: _itx5j3f5.Protocol().deserialize<_iqswgn91.DiscussionTarget>(
jsonSerialization['target'],
),
answers: _itx5j3f5.Protocol()
.deserialize<List<_iv1wx9mv.DiscussionPostView>>(
jsonSerialization['answers'],
),
comments: _itx5j3f5.Protocol()
.deserialize<List<_iv1wx9mv.DiscussionPostView>>(
jsonSerialization['comments'],
),
acceptedAnswerId: jsonSerialization['acceptedAnswerId'] == null
? null
: _isc.UuidValueJsonExtension.fromJson(
jsonSerialization['acceptedAnswerId'],
),
asker: _isc.BoolJsonExtension.fromJson(jsonSerialization['asker']),
author: _isc.BoolJsonExtension.fromJson(jsonSerialization['author']),
refusal: jsonSerialization['refusal'] == null
? null
: _i5ff9exj.DiscussionRefusal.fromJson(
(jsonSerialization['refusal'] as String),
),
);
}
_iv1wx9mv.DiscussionPostView question;
/// The published studyset it is on: its public id, title and location.
_isc.UuidValue publicId;
String studysetTitle;
String location;
/// What it is about.
_iqswgn91.DiscussionTarget target;
List<_iv1wx9mv.DiscussionPostView> answers;
List<_iv1wx9mv.DiscussionPostView> comments;
_isc.UuidValue? acceptedAnswerId;
/// Whether the viewer asked it, and so may accept an answer.
bool asker;
/// Whether the viewer is the studyset's author, and so may hide posts.
bool author;
/// Why the viewer may not write in it; null when they may.
_i5ff9exj.DiscussionRefusal? refusal;
/// Returns a shallow copy of this [DiscussionView]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
DiscussionView copyWith({
_iv1wx9mv.DiscussionPostView? question,
_isc.UuidValue? publicId,
String? studysetTitle,
String? location,
_iqswgn91.DiscussionTarget? target,
List<_iv1wx9mv.DiscussionPostView>? answers,
List<_iv1wx9mv.DiscussionPostView>? comments,
_isc.UuidValue? acceptedAnswerId,
bool? asker,
bool? author,
_i5ff9exj.DiscussionRefusal? refusal,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'DiscussionView',
'question': question.toJson(),
'publicId': publicId.toJson(),
'studysetTitle': studysetTitle,
'location': location,
'target': target.toJson(),
'answers': answers.toJson(valueToJson: (v) => v.toJson()),
'comments': comments.toJson(valueToJson: (v) => v.toJson()),
if (acceptedAnswerId != null)
'acceptedAnswerId': acceptedAnswerId?.toJson(),
'asker': asker,
'author': author,
if (refusal != null) 'refusal': refusal?.toJson(),
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'DiscussionView',
'question': question.toJsonForProtocol(),
'publicId': publicId.toJson(),
'studysetTitle': studysetTitle,
'location': location,
'target': target.toJsonForProtocol(),
'answers': answers.toJson(valueToJson: (v) => v.toJsonForProtocol()),
'comments': comments.toJson(valueToJson: (v) => v.toJsonForProtocol()),
if (acceptedAnswerId != null)
'acceptedAnswerId': acceptedAnswerId?.toJson(),
'asker': asker,
'author': author,
if (refusal != null) 'refusal': refusal?.toJson(),
};
}
@override
String toString() {
return _isc.SerializationManager.encode(this);
}
}
class _Undefined {}
class _DiscussionViewImpl extends DiscussionView {
_DiscussionViewImpl({
required _iv1wx9mv.DiscussionPostView question,
required _isc.UuidValue publicId,
required String studysetTitle,
required String location,
required _iqswgn91.DiscussionTarget target,
required List<_iv1wx9mv.DiscussionPostView> answers,
required List<_iv1wx9mv.DiscussionPostView> comments,
_isc.UuidValue? acceptedAnswerId,
required bool asker,
required bool author,
_i5ff9exj.DiscussionRefusal? refusal,
}) : super._(
question: question,
publicId: publicId,
studysetTitle: studysetTitle,
location: location,
target: target,
answers: answers,
comments: comments,
acceptedAnswerId: acceptedAnswerId,
asker: asker,
author: author,
refusal: refusal,
);
/// Returns a shallow copy of this [DiscussionView]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
@override
DiscussionView copyWith({
_iv1wx9mv.DiscussionPostView? question,
_isc.UuidValue? publicId,
String? studysetTitle,
String? location,
_iqswgn91.DiscussionTarget? target,
List<_iv1wx9mv.DiscussionPostView>? answers,
List<_iv1wx9mv.DiscussionPostView>? comments,
Object? acceptedAnswerId = _Undefined,
bool? asker,
bool? author,
Object? refusal = _Undefined,
}) {
return DiscussionView(
question: question ?? this.question.copyWith(),
publicId: publicId ?? this.publicId,
studysetTitle: studysetTitle ?? this.studysetTitle,
location: location ?? this.location,
target: target ?? this.target.copyWith(),
answers: answers ?? this.answers.map((e0) => e0.copyWith()).toList(),
comments: comments ?? this.comments.map((e0) => e0.copyWith()).toList(),
acceptedAnswerId: acceptedAnswerId is _isc.UuidValue?
? acceptedAnswerId
: this.acceptedAnswerId,
asker: asker ?? this.asker,
author: author ?? this.author,
refusal: refusal is _i5ff9exj.DiscussionRefusal? ? refusal : this.refusal,
);
}
}

View file

@ -23,6 +23,8 @@ import 'package:halkyon_learn_client/src/protocol/dictionary/dictionary_key_hit.
as _ii0vb6vg;
import 'package:halkyon_learn_client/src/protocol/dictionary/dictionary_source.dart'
as _i8vj64sp;
import 'package:halkyon_learn_client/src/protocol/discussion/discussion_summary.dart'
as _i7lxdcg2;
import 'package:halkyon_learn_client/src/protocol/explore/explore_entry.dart'
as _iyigb6vc;
import 'package:halkyon_learn_client/src/protocol/feedback/feedback_view.dart'
@ -58,6 +60,12 @@ import 'dictionary/dictionary_hit.dart' as _ivbh47kc;
import 'dictionary/dictionary_key.dart' as _i9gd3xbn;
import 'dictionary/dictionary_key_hit.dart' as _i6b9unou;
import 'dictionary/dictionary_source.dart' as _ie12eb65;
import 'discussion/discussion_post_view.dart' as _ifltqotq;
import 'discussion/discussion_refusal.dart' as _iysrt1r7;
import 'discussion/discussion_refused_exception.dart' as _i1nsp6ib;
import 'discussion/discussion_summary.dart' as _i5mmlb7v;
import 'discussion/discussion_target.dart' as _ijf8cxgd;
import 'discussion/discussion_view.dart' as _ixby8mme;
import 'explore/explore_entry.dart' as _iaxdli27;
import 'explore/explore_query.dart' as _ibl9h9o9;
import 'explore/release_content.dart' as _ievpeb4f;
@ -114,6 +122,12 @@ export 'dictionary/dictionary_hit.dart';
export 'dictionary/dictionary_key.dart';
export 'dictionary/dictionary_key_hit.dart';
export 'dictionary/dictionary_source.dart';
export 'discussion/discussion_post_view.dart';
export 'discussion/discussion_refusal.dart';
export 'discussion/discussion_refused_exception.dart';
export 'discussion/discussion_summary.dart';
export 'discussion/discussion_target.dart';
export 'discussion/discussion_view.dart';
export 'explore/explore_entry.dart';
export 'explore/explore_query.dart';
export 'explore/release_content.dart';
@ -2364,6 +2378,24 @@ class Protocol extends _isd.DatabaseSerializationManager {
if (t == _ie12eb65.DictionarySource) {
return _ie12eb65.DictionarySource.fromJson(data) as T;
}
if (t == _ifltqotq.DiscussionPostView) {
return _ifltqotq.DiscussionPostView.fromJson(data) as T;
}
if (t == _iysrt1r7.DiscussionRefusal) {
return _iysrt1r7.DiscussionRefusal.fromJson(data) as T;
}
if (t == _i1nsp6ib.DiscussionRefusedException) {
return _i1nsp6ib.DiscussionRefusedException.fromJson(data) as T;
}
if (t == _i5mmlb7v.DiscussionSummary) {
return _i5mmlb7v.DiscussionSummary.fromJson(data) as T;
}
if (t == _ijf8cxgd.DiscussionTarget) {
return _ijf8cxgd.DiscussionTarget.fromJson(data) as T;
}
if (t == _ixby8mme.DiscussionView) {
return _ixby8mme.DiscussionView.fromJson(data) as T;
}
if (t == _iaxdli27.ExploreEntry) {
return _iaxdli27.ExploreEntry.fromJson(data) as T;
}
@ -2561,6 +2593,32 @@ class Protocol extends _isd.DatabaseSerializationManager {
return (data != null ? _ie12eb65.DictionarySource.fromJson(data) : null)
as T;
}
if (t == _isc.getType<_ifltqotq.DiscussionPostView?>()) {
return (data != null ? _ifltqotq.DiscussionPostView.fromJson(data) : null)
as T;
}
if (t == _isc.getType<_iysrt1r7.DiscussionRefusal?>()) {
return (data != null ? _iysrt1r7.DiscussionRefusal.fromJson(data) : null)
as T;
}
if (t == _isc.getType<_i1nsp6ib.DiscussionRefusedException?>()) {
return (data != null
? _i1nsp6ib.DiscussionRefusedException.fromJson(data)
: null)
as T;
}
if (t == _isc.getType<_i5mmlb7v.DiscussionSummary?>()) {
return (data != null ? _i5mmlb7v.DiscussionSummary.fromJson(data) : null)
as T;
}
if (t == _isc.getType<_ijf8cxgd.DiscussionTarget?>()) {
return (data != null ? _ijf8cxgd.DiscussionTarget.fromJson(data) : null)
as T;
}
if (t == _isc.getType<_ixby8mme.DiscussionView?>()) {
return (data != null ? _ixby8mme.DiscussionView.fromJson(data) : null)
as T;
}
if (t == _isc.getType<_iaxdli27.ExploreEntry?>()) {
return (data != null ? _iaxdli27.ExploreEntry.fromJson(data) : null) as T;
}
@ -2696,6 +2754,12 @@ class Protocol extends _isd.DatabaseSerializationManager {
.toList()
as T;
}
if (t == List<_ifltqotq.DiscussionPostView>) {
return (data as List)
.map((e) => deserialize<_ifltqotq.DiscussionPostView>(e))
.toList()
as T;
}
if (t == List<_iaswq7ap.ReleaseItem>) {
return (data as List)
.map((e) => deserialize<_iaswq7ap.ReleaseItem>(e))
@ -2765,6 +2829,12 @@ class Protocol extends _isd.DatabaseSerializationManager {
)
as T;
}
if (t == List<_i7lxdcg2.DiscussionSummary>) {
return (data as List)
.map((e) => deserialize<_i7lxdcg2.DiscussionSummary>(e))
.toList()
as T;
}
if (t == List<_iyigb6vc.ExploreEntry>) {
return (data as List)
.map((e) => deserialize<_iyigb6vc.ExploreEntry>(e))
@ -2818,6 +2888,12 @@ class Protocol extends _isd.DatabaseSerializationManager {
_i9gd3xbn.DictionaryKey => 'DictionaryKey',
_i6b9unou.DictionaryKeyHit => 'DictionaryKeyHit',
_ie12eb65.DictionarySource => 'DictionarySource',
_ifltqotq.DiscussionPostView => 'DiscussionPostView',
_iysrt1r7.DiscussionRefusal => 'DiscussionRefusal',
_i1nsp6ib.DiscussionRefusedException => 'DiscussionRefusedException',
_i5mmlb7v.DiscussionSummary => 'DiscussionSummary',
_ijf8cxgd.DiscussionTarget => 'DiscussionTarget',
_ixby8mme.DiscussionView => 'DiscussionView',
_iaxdli27.ExploreEntry => 'ExploreEntry',
_ibl9h9o9.ExploreQuery => 'ExploreQuery',
_ievpeb4f.ReleaseContent => 'ReleaseContent',
@ -2914,6 +2990,18 @@ class Protocol extends _isd.DatabaseSerializationManager {
return 'DictionaryKeyHit';
case _ie12eb65.DictionarySource():
return 'DictionarySource';
case _ifltqotq.DiscussionPostView():
return 'DiscussionPostView';
case _iysrt1r7.DiscussionRefusal():
return 'DiscussionRefusal';
case _i1nsp6ib.DiscussionRefusedException():
return 'DiscussionRefusedException';
case _i5mmlb7v.DiscussionSummary():
return 'DiscussionSummary';
case _ijf8cxgd.DiscussionTarget():
return 'DiscussionTarget';
case _ixby8mme.DiscussionView():
return 'DiscussionView';
case _iaxdli27.ExploreEntry():
return 'ExploreEntry';
case _ibl9h9o9.ExploreQuery():
@ -3077,6 +3165,24 @@ class Protocol extends _isd.DatabaseSerializationManager {
if (dataClassName == 'DictionarySource') {
return deserialize<_ie12eb65.DictionarySource>(data['data']);
}
if (dataClassName == 'DiscussionPostView') {
return deserialize<_ifltqotq.DiscussionPostView>(data['data']);
}
if (dataClassName == 'DiscussionRefusal') {
return deserialize<_iysrt1r7.DiscussionRefusal>(data['data']);
}
if (dataClassName == 'DiscussionRefusedException') {
return deserialize<_i1nsp6ib.DiscussionRefusedException>(data['data']);
}
if (dataClassName == 'DiscussionSummary') {
return deserialize<_i5mmlb7v.DiscussionSummary>(data['data']);
}
if (dataClassName == 'DiscussionTarget') {
return deserialize<_ijf8cxgd.DiscussionTarget>(data['data']);
}
if (dataClassName == 'DiscussionView') {
return deserialize<_ixby8mme.DiscussionView>(data['data']);
}
if (dataClassName == 'ExploreEntry') {
return deserialize<_iaxdli27.ExploreEntry>(data['data']);
}

View file

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

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.",

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:

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';

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';

View file

@ -16,6 +16,9 @@ import 'package:halkyon_learn_flutter/src/feature/card/bloc/card_composer_bloc.d
import 'package:halkyon_learn_flutter/src/feature/card/widget/card_browser_screen.dart';
import 'package:halkyon_learn_flutter/src/feature/card/widget/composer/card_composer_screen.dart';
import 'package:halkyon_learn_flutter/src/feature/dictionary/widget/dictionary_screen.dart';
import 'package:halkyon_learn_flutter/src/feature/discussion/model/discussion.dart';
import 'package:halkyon_learn_flutter/src/feature/discussion/widget/discussion_screen.dart';
import 'package:halkyon_learn_flutter/src/feature/discussion/widget/discussions_screen.dart';
import 'package:halkyon_learn_flutter/src/feature/explore/widget/explore_screen.dart';
import 'package:halkyon_learn_flutter/src/feature/feedback/bloc/inbox_count_bloc.dart';
import 'package:halkyon_learn_flutter/src/feature/home/widget/home_screen.dart';
@ -294,6 +297,29 @@ GoRouter buildRouter({String initialLocation = RoutePaths.home}) => GoRouter(
page: int.tryParse(state.uri.queryParameters['page'] ?? ''),
),
),
// A published studyset's discussions, and each of them (ADR 0013).
GoRoute(
path: '/discussions/:publicId',
builder: (context, state) => FramedScreen(
child: DiscussionsScreen(
publicId: state.pathParameters['publicId']!,
topic: state.uri.queryParameters['topic'],
version: int.tryParse(state.uri.queryParameters['version'] ?? ''),
title: state.uri.queryParameters['title'],
ask: targetFromQuery(state.uri.queryParameters['ask']),
),
),
routes: [
GoRoute(
path: ':questionId',
builder: (context, state) => FramedScreen(
child: DiscussionScreen(
questionId: state.pathParameters['questionId']!,
),
),
),
],
),
GoRoute(
path: RoutePaths.settings,
builder: (context, state) => const FramedScreen(child: SettingsScreen()),

View file

@ -92,9 +92,10 @@ class _PersonAppState() extends State<PersonApp> {
),
// What is new in the inbox, for the Profile tab's badge.
BlocProvider(
create: (context) =>
InboxCountBloc(PersonScope.of(context).feedback)
..add(const InboxCountEventRefreshed()),
create: (context) => InboxCountBloc(
PersonScope.of(context).feedback,
PersonScope.of(context).discussions,
)..add(const InboxCountEventRefreshed()),
),
// The Pomodoro timer goes on across every screen.
BlocProvider(

View file

@ -42,6 +42,8 @@ import 'package:halkyon_learn_flutter/src/feature/dictionary/data/device_diction
import 'package:halkyon_learn_flutter/src/feature/dictionary/data/dictionary_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/dictionary/data/japanese_analyser.dart';
import 'package:halkyon_learn_flutter/src/feature/dictionary/data/serverpod_dictionary_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/discussion/data/discussion_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/discussion/data/serverpod_discussion_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/explore/data/copy_updater.dart';
import 'package:halkyon_learn_flutter/src/feature/explore/data/explore_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/explore/data/release_copier.dart';
@ -429,6 +431,7 @@ Future<PersonDependencies> _openPerson(
);
await studysetTabs.open();
final discussions = DiscussionRepository(ServerpodDiscussionGateway(client));
return PersonDependencies(
sync: sync,
studysets: studysets,
@ -467,6 +470,7 @@ Future<PersonDependencies> _openPerson(
paths,
assets,
),
discussions: discussions,
onDispose: () async {
try {
// Deletions first: those still waiting are dropped, those under way
@ -478,6 +482,7 @@ Future<PersonDependencies> _openPerson(
await pathDeletions.dispose();
await stepDeletions.dispose();
await pdfDeletions.dispose();
await discussions.dispose();
await studysets.dispose();
await tags.dispose();
await settings.dispose();

View file

@ -2,6 +2,7 @@ import 'package:halkyon_learn_flutter/src/core/asset/data/asset_repository.dart'
import 'package:halkyon_learn_flutter/src/core/sync/sync_service.dart';
import 'package:halkyon_learn_flutter/src/feature/card/data/card_deletions.dart';
import 'package:halkyon_learn_flutter/src/feature/card/data/card_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/discussion/data/discussion_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/explore/data/explore_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/feedback/data/feedback_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/learning_path_deletions.dart';
@ -95,6 +96,9 @@ class PersonDependencies({
/// Reports and suggestions on published studysets.
required final FeedbackRepository feedback,
/// Published studysets' discussions, on the instance.
required final DiscussionRepository discussions,
final Future<void> Function()? _onDispose,
}) {
/// Closes what was opened for the person.

View file

@ -102,4 +102,32 @@ abstract final class RoutePaths() {
/// A step of a learning path, to write, over the tabs.
static String pathStep(String studysetId, String pathId, String stepId) =>
'/studysets/$studysetId/paths/$pathId/steps/$stepId';
/// A published studyset's discussions, over the tabs: those within the
/// topic [topic] (a studyset's id in the release [version], which asking
/// about it names), named [title], or all; with [ask]
/// (`<kind>:<item>:<version>`: a card, note or step), asking about it at
/// once.
static String discussions(
String publicId, {
String? topic,
int? version,
String? title,
String? ask,
}) {
final query = {
'topic': ?topic,
if (version != null) 'version': '$version',
'title': ?title,
'ask': ?ask,
};
return Uri(
path: '/discussions/$publicId',
queryParameters: query.isEmpty ? null : query,
).toString();
}
/// A question on a published studyset, with its answers, over the tabs.
static String discussion(String publicId, String questionId) =>
'/discussions/$publicId/$questionId';
}

Some files were not shown because too many files have changed in this diff Show more