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

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']);
}