feat(base,learn): suggest changes within a shared studyset
ADR 0044's step 6b (D44, D46 to D48): a Viewer, and an agent, suggest changes within a shared studyset, which its Owner and Editors accept or decline. A suggestion or report is written as Explore's are, in its writer's own synced space, with kinds of its own (space-suggestion, space-report) and the space's id in feedback_thread's publicationId, so no synced table changes and older apps, skipping kinds they don't know, keep syncing. Where the studyset is private it is sealed with its key, bound to the space; a reply to it is sealed for its thread. Learn's server gathers a space's threads for its members alone (feedback.threadsInSpace, NotFound for anyone else): only threads and replies by members now, decisions only from rows in the space itself, where the engine lets its Owner and Editors alone write; sealed payloads pass in base64 with their key and nonce. The inbox adds the threads its caller decides on or wrote. The module tells a host a person's spaces and each space's members (sharedSpacesOf, membersOfSpaces). No migration: no table changes. In the app, a Viewer's card and note offer Suggest a change: the fields write into the suggestion alone, sent with a message. A shared studyset's menu lists its Suggestions, where its Owner and Editors accept a change as a revision, or a new card or note into the studyset, and decide in the space. A suggestion naming a file that isn't the studyset's, or bringing files of its own, isn't taken. Whats-New: In a studyset shared with you as a viewer, suggest a change to a card or note; its owner and editors find your suggestions under Suggestions in its menu and accept or decline them. Whats-New-De: In einem Lernset, das du nur ansehen darfst, schlägst du Änderungen an Karteikarten und Notizen vor; Besitzer:in und Bearbeiter:innen finden sie im Menü unter „Vorschläge“ und nehmen sie an oder lehnen sie ab. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
7aee78bd87
commit
82c777b1a5
6 changed files with 2949 additions and 427 deletions
|
|
@ -672,6 +672,18 @@ class EndpointFeedback extends _isc.EndpointRef {
|
|||
'threadsOf',
|
||||
{'publicId': publicId},
|
||||
);
|
||||
|
||||
/// The suggestions and reports within the shared space [space] (Halkyon's
|
||||
/// ADR 0044, D46, D47), for its members alone, the most recently active
|
||||
/// first. Throws [NotFoundException] for anyone else, as for a space that
|
||||
/// doesn't exist.
|
||||
_ida.Future<List<_idpwh21d.FeedbackView>> threadsInSpace(
|
||||
_isc.UuidValue space,
|
||||
) => caller.callServerEndpoint<List<_idpwh21d.FeedbackView>>(
|
||||
'feedback',
|
||||
'threadsInSpace',
|
||||
{'space': space},
|
||||
);
|
||||
}
|
||||
|
||||
/// The person's inbox of reports and suggestions: those on their published
|
||||
|
|
@ -686,7 +698,9 @@ class EndpointInbox extends _isc.EndpointRef {
|
|||
String get name => 'inbox';
|
||||
|
||||
/// The threads on the caller's published studysets and those they wrote,
|
||||
/// the most recently active first.
|
||||
/// and within the shared spaces they decide in, as an Owner or Editor,
|
||||
/// and those they wrote there (Halkyon's ADR 0044, D46), the most
|
||||
/// recently active first.
|
||||
_ida.Future<List<_idpwh21d.FeedbackView>> inbox() =>
|
||||
caller.callServerEndpoint<List<_idpwh21d.FeedbackView>>(
|
||||
'inbox',
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ import 'package:serverpod_database/serverpod_database.dart' as _isd;
|
|||
/// What a published studyset's author decided about a thread of reports and
|
||||
/// suggestions (FeedbackThread): 'closed', 'reopened', 'hidden', 'shown',
|
||||
/// 'accepted' or 'declined' (a suggestion). Written in the author's own
|
||||
/// space, and only the author's count; the latest of each pair (by its id,
|
||||
/// space, and only the author's count; in a shared space, written in that
|
||||
/// space, where only its Owner and Editors write (Halkyon's ADR 0044, D46); the latest of each pair (by its id,
|
||||
/// a UUID v7) stands. Never changed. Synced.
|
||||
abstract class FeedbackDecision
|
||||
implements _isd.TableRow<_isc.UuidValue?>, _isc.ProtocolSerialization {
|
||||
|
|
|
|||
|
|
@ -72,13 +72,15 @@ abstract class FeedbackMessage
|
|||
/// The thread replied in.
|
||||
_isc.UuidValue threadId;
|
||||
|
||||
/// The encryption key's id; null, plaintext, the only value (ADR 0002).
|
||||
/// The encryption key's id; null, plaintext, but in a private shared
|
||||
/// studyset (ADR 0002, ADR 0019).
|
||||
String? keyId;
|
||||
|
||||
/// Its content: UTF-8 JSON with a `schemaVersion` and its text.
|
||||
_idt.ByteData payload;
|
||||
|
||||
/// The encryption nonce; null, as for keyId.
|
||||
/// The encryption nonce, or a readable row's signature; null, as for
|
||||
/// keyId.
|
||||
_idt.ByteData? nonce;
|
||||
|
||||
@override
|
||||
|
|
@ -259,13 +261,15 @@ class FeedbackMessageTable extends _isd.Table<_isc.UuidValue?> {
|
|||
/// The thread replied in.
|
||||
late final _isd.ColumnUuid threadId;
|
||||
|
||||
/// The encryption key's id; null, plaintext, the only value (ADR 0002).
|
||||
/// The encryption key's id; null, plaintext, but in a private shared
|
||||
/// studyset (ADR 0002, ADR 0019).
|
||||
late final _isd.ColumnString keyId;
|
||||
|
||||
/// Its content: UTF-8 JSON with a `schemaVersion` and its text.
|
||||
late final _isd.ColumnByteData payload;
|
||||
|
||||
/// The encryption nonce; null, as for keyId.
|
||||
/// The encryption nonce, or a readable row's signature; null, as for
|
||||
/// keyId.
|
||||
late final _isd.ColumnByteData nonce;
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ abstract class FeedbackMessageView
|
|||
required this.by,
|
||||
required this.fromAuthor,
|
||||
required this.payload,
|
||||
this.keyId,
|
||||
this.nonce,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
|
|
@ -30,6 +32,8 @@ abstract class FeedbackMessageView
|
|||
required _i4q3oto5.FeedbackPerson by,
|
||||
required bool fromAuthor,
|
||||
required String payload,
|
||||
String? keyId,
|
||||
String? nonce,
|
||||
required DateTime createdAt,
|
||||
}) = _FeedbackMessageViewImpl;
|
||||
|
||||
|
|
@ -43,6 +47,8 @@ abstract class FeedbackMessageView
|
|||
jsonSerialization['fromAuthor'],
|
||||
),
|
||||
payload: jsonSerialization['payload'] as String,
|
||||
keyId: jsonSerialization['keyId'] as String?,
|
||||
nonce: jsonSerialization['nonce'] as String?,
|
||||
createdAt: _isc.DateTimeJsonExtension.fromJson(
|
||||
jsonSerialization['createdAt'],
|
||||
),
|
||||
|
|
@ -51,14 +57,21 @@ abstract class FeedbackMessageView
|
|||
|
||||
_isc.UuidValue id;
|
||||
|
||||
/// Who wrote it, and whether that is the published studyset's author.
|
||||
/// Who wrote it, and whether that is the published studyset's author
|
||||
/// (in a shared space, its Owner or an Editor).
|
||||
_i4q3oto5.FeedbackPerson by;
|
||||
|
||||
bool fromAuthor;
|
||||
|
||||
/// What it says: its payload as written.
|
||||
/// What it says: its payload as written; sealed (keyId), in base64.
|
||||
String payload;
|
||||
|
||||
/// The key a reply in a private shared studyset is sealed with, and its
|
||||
/// nonce in base64; null while readable.
|
||||
String? keyId;
|
||||
|
||||
String? nonce;
|
||||
|
||||
/// When, in UTC.
|
||||
DateTime createdAt;
|
||||
|
||||
|
|
@ -70,6 +83,8 @@ abstract class FeedbackMessageView
|
|||
_i4q3oto5.FeedbackPerson? by,
|
||||
bool? fromAuthor,
|
||||
String? payload,
|
||||
String? keyId,
|
||||
String? nonce,
|
||||
DateTime? createdAt,
|
||||
});
|
||||
@override
|
||||
|
|
@ -80,6 +95,8 @@ abstract class FeedbackMessageView
|
|||
'by': by.toJson(),
|
||||
'fromAuthor': fromAuthor,
|
||||
'payload': payload,
|
||||
if (keyId != null) 'keyId': keyId,
|
||||
if (nonce != null) 'nonce': nonce,
|
||||
'createdAt': createdAt.toJson(),
|
||||
};
|
||||
}
|
||||
|
|
@ -92,6 +109,8 @@ abstract class FeedbackMessageView
|
|||
'by': by.toJsonForProtocol(),
|
||||
'fromAuthor': fromAuthor,
|
||||
'payload': payload,
|
||||
if (keyId != null) 'keyId': keyId,
|
||||
if (nonce != null) 'nonce': nonce,
|
||||
'createdAt': createdAt.toJson(),
|
||||
};
|
||||
}
|
||||
|
|
@ -102,18 +121,24 @@ abstract class FeedbackMessageView
|
|||
}
|
||||
}
|
||||
|
||||
class _Undefined {}
|
||||
|
||||
class _FeedbackMessageViewImpl extends FeedbackMessageView {
|
||||
_FeedbackMessageViewImpl({
|
||||
required _isc.UuidValue id,
|
||||
required _i4q3oto5.FeedbackPerson by,
|
||||
required bool fromAuthor,
|
||||
required String payload,
|
||||
String? keyId,
|
||||
String? nonce,
|
||||
required DateTime createdAt,
|
||||
}) : super._(
|
||||
id: id,
|
||||
by: by,
|
||||
fromAuthor: fromAuthor,
|
||||
payload: payload,
|
||||
keyId: keyId,
|
||||
nonce: nonce,
|
||||
createdAt: createdAt,
|
||||
);
|
||||
|
||||
|
|
@ -126,6 +151,8 @@ class _FeedbackMessageViewImpl extends FeedbackMessageView {
|
|||
_i4q3oto5.FeedbackPerson? by,
|
||||
bool? fromAuthor,
|
||||
String? payload,
|
||||
Object? keyId = _Undefined,
|
||||
Object? nonce = _Undefined,
|
||||
DateTime? createdAt,
|
||||
}) {
|
||||
return FeedbackMessageView(
|
||||
|
|
@ -133,6 +160,8 @@ class _FeedbackMessageViewImpl extends FeedbackMessageView {
|
|||
by: by ?? this.by.copyWith(),
|
||||
fromAuthor: fromAuthor ?? this.fromAuthor,
|
||||
payload: payload ?? this.payload,
|
||||
keyId: keyId is String? ? keyId : this.keyId,
|
||||
nonce: nonce is String? ? nonce : this.nonce,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,19 +75,24 @@ abstract class FeedbackThread
|
|||
int? spaceId;
|
||||
|
||||
/// The published studyset it is on (its public id): a column, for the
|
||||
/// server to gather by.
|
||||
/// server to gather by; for a thread in a shared space, the space's id
|
||||
/// (Halkyon's ADR 0044, D46).
|
||||
_isc.UuidValue publicationId;
|
||||
|
||||
/// 'report' or 'suggestion'; a kind this app doesn't know is skipped.
|
||||
/// 'report' or 'suggestion'; 'space-report' or 'space-suggestion' in a
|
||||
/// shared space, sealed with its studyset's key where that is private.
|
||||
/// A kind an app doesn't know is skipped.
|
||||
String kind;
|
||||
|
||||
/// The encryption key's id; null, plaintext, the only value (ADR 0002).
|
||||
/// The encryption key's id; null, plaintext, but in a private shared
|
||||
/// studyset (ADR 0002, ADR 0019).
|
||||
String? keyId;
|
||||
|
||||
/// Its content: UTF-8 JSON with a `schemaVersion`.
|
||||
_idt.ByteData payload;
|
||||
|
||||
/// The encryption nonce; null, as for keyId.
|
||||
/// The encryption nonce, or a readable row's signature; null, as for
|
||||
/// keyId.
|
||||
_idt.ByteData? nonce;
|
||||
|
||||
@override
|
||||
|
|
@ -281,19 +286,24 @@ class FeedbackThreadTable extends _isd.Table<_isc.UuidValue?> {
|
|||
late final _isd.ColumnInt spaceId;
|
||||
|
||||
/// The published studyset it is on (its public id): a column, for the
|
||||
/// server to gather by.
|
||||
/// server to gather by; for a thread in a shared space, the space's id
|
||||
/// (Halkyon's ADR 0044, D46).
|
||||
late final _isd.ColumnUuid publicationId;
|
||||
|
||||
/// 'report' or 'suggestion'; a kind this app doesn't know is skipped.
|
||||
/// 'report' or 'suggestion'; 'space-report' or 'space-suggestion' in a
|
||||
/// shared space, sealed with its studyset's key where that is private.
|
||||
/// A kind an app doesn't know is skipped.
|
||||
late final _isd.ColumnString kind;
|
||||
|
||||
/// The encryption key's id; null, plaintext, the only value (ADR 0002).
|
||||
/// The encryption key's id; null, plaintext, but in a private shared
|
||||
/// studyset (ADR 0002, ADR 0019).
|
||||
late final _isd.ColumnString keyId;
|
||||
|
||||
/// Its content: UTF-8 JSON with a `schemaVersion`.
|
||||
late final _isd.ColumnByteData payload;
|
||||
|
||||
/// The encryption nonce; null, as for keyId.
|
||||
/// The encryption nonce, or a readable row's signature; null, as for
|
||||
/// keyId.
|
||||
late final _isd.ColumnByteData nonce;
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -16,8 +16,9 @@ import '../feedback/feedback_message_view.dart' as _irmjyw1o;
|
|||
import '../feedback/feedback_person.dart' as _i4q3oto5;
|
||||
|
||||
/// A thread of reports and suggestions as the app shows it: on which
|
||||
/// published studyset, by whom, what it says (its payload's JSON), its
|
||||
/// replies, and where it stands.
|
||||
/// published studyset, or in which shared space (Halkyon's ADR 0044, D46),
|
||||
/// by whom, what it says (its payload's JSON, or sealed), its replies, and
|
||||
/// where it stands.
|
||||
abstract class FeedbackView
|
||||
implements _isc.SerializableModel, _isc.ProtocolSerialization {
|
||||
FeedbackView._({
|
||||
|
|
@ -28,6 +29,9 @@ abstract class FeedbackView
|
|||
required this.kind,
|
||||
required this.writer,
|
||||
required this.payload,
|
||||
this.space,
|
||||
this.keyId,
|
||||
this.nonce,
|
||||
required this.createdAt,
|
||||
required this.messages,
|
||||
required this.status,
|
||||
|
|
@ -45,6 +49,9 @@ abstract class FeedbackView
|
|||
required String kind,
|
||||
required _i4q3oto5.FeedbackPerson writer,
|
||||
required String payload,
|
||||
_isc.UuidValue? space,
|
||||
String? keyId,
|
||||
String? nonce,
|
||||
required DateTime createdAt,
|
||||
required List<_irmjyw1o.FeedbackMessageView> messages,
|
||||
required String status,
|
||||
|
|
@ -69,6 +76,11 @@ abstract class FeedbackView
|
|||
jsonSerialization['writer'],
|
||||
),
|
||||
payload: jsonSerialization['payload'] as String,
|
||||
space: jsonSerialization['space'] == null
|
||||
? null
|
||||
: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['space']),
|
||||
keyId: jsonSerialization['keyId'] as String?,
|
||||
nonce: jsonSerialization['nonce'] as String?,
|
||||
createdAt: _isc.DateTimeJsonExtension.fromJson(
|
||||
jsonSerialization['createdAt'],
|
||||
),
|
||||
|
|
@ -86,22 +98,33 @@ abstract class FeedbackView
|
|||
|
||||
_isc.UuidValue threadId;
|
||||
|
||||
/// The published studyset: its public id, title and location.
|
||||
/// The published studyset: its public id, title and location; in a
|
||||
/// shared space, the space's id, and neither title nor location.
|
||||
_isc.UuidValue publicId;
|
||||
|
||||
String studysetTitle;
|
||||
|
||||
String location;
|
||||
|
||||
/// 'report' or 'suggestion'.
|
||||
/// 'report' or 'suggestion'; 'space-report' or 'space-suggestion' in a
|
||||
/// shared space.
|
||||
String kind;
|
||||
|
||||
/// Who wrote it.
|
||||
_i4q3oto5.FeedbackPerson writer;
|
||||
|
||||
/// What it says: its payload as written.
|
||||
/// What it says: its payload as written; sealed (keyId), in base64.
|
||||
String payload;
|
||||
|
||||
/// The shared space it is in (D46); null on a published studyset.
|
||||
_isc.UuidValue? space;
|
||||
|
||||
/// The key a thread in a private shared studyset is sealed with, and
|
||||
/// its nonce in base64; null while readable.
|
||||
String? keyId;
|
||||
|
||||
String? nonce;
|
||||
|
||||
/// When it was written, in UTC.
|
||||
DateTime createdAt;
|
||||
|
||||
|
|
@ -114,7 +137,8 @@ abstract class FeedbackView
|
|||
/// Whether the author hid it: then only its writer and the author see it.
|
||||
bool hidden;
|
||||
|
||||
/// Whether the caller wrote it; whether the caller is the author.
|
||||
/// Whether the caller wrote it; whether the caller is the author (in a
|
||||
/// shared space, its Owner or an Editor, who decide on it).
|
||||
bool mine;
|
||||
|
||||
bool forMe;
|
||||
|
|
@ -133,6 +157,9 @@ abstract class FeedbackView
|
|||
String? kind,
|
||||
_i4q3oto5.FeedbackPerson? writer,
|
||||
String? payload,
|
||||
_isc.UuidValue? space,
|
||||
String? keyId,
|
||||
String? nonce,
|
||||
DateTime? createdAt,
|
||||
List<_irmjyw1o.FeedbackMessageView>? messages,
|
||||
String? status,
|
||||
|
|
@ -152,6 +179,9 @@ abstract class FeedbackView
|
|||
'kind': kind,
|
||||
'writer': writer.toJson(),
|
||||
'payload': payload,
|
||||
if (space != null) 'space': space?.toJson(),
|
||||
if (keyId != null) 'keyId': keyId,
|
||||
if (nonce != null) 'nonce': nonce,
|
||||
'createdAt': createdAt.toJson(),
|
||||
'messages': messages.toJson(valueToJson: (v) => v.toJson()),
|
||||
'status': status,
|
||||
|
|
@ -173,6 +203,9 @@ abstract class FeedbackView
|
|||
'kind': kind,
|
||||
'writer': writer.toJsonForProtocol(),
|
||||
'payload': payload,
|
||||
if (space != null) 'space': space?.toJson(),
|
||||
if (keyId != null) 'keyId': keyId,
|
||||
if (nonce != null) 'nonce': nonce,
|
||||
'createdAt': createdAt.toJson(),
|
||||
'messages': messages.toJson(valueToJson: (v) => v.toJsonForProtocol()),
|
||||
'status': status,
|
||||
|
|
@ -189,6 +222,8 @@ abstract class FeedbackView
|
|||
}
|
||||
}
|
||||
|
||||
class _Undefined {}
|
||||
|
||||
class _FeedbackViewImpl extends FeedbackView {
|
||||
_FeedbackViewImpl({
|
||||
required _isc.UuidValue threadId,
|
||||
|
|
@ -198,6 +233,9 @@ class _FeedbackViewImpl extends FeedbackView {
|
|||
required String kind,
|
||||
required _i4q3oto5.FeedbackPerson writer,
|
||||
required String payload,
|
||||
_isc.UuidValue? space,
|
||||
String? keyId,
|
||||
String? nonce,
|
||||
required DateTime createdAt,
|
||||
required List<_irmjyw1o.FeedbackMessageView> messages,
|
||||
required String status,
|
||||
|
|
@ -213,6 +251,9 @@ class _FeedbackViewImpl extends FeedbackView {
|
|||
kind: kind,
|
||||
writer: writer,
|
||||
payload: payload,
|
||||
space: space,
|
||||
keyId: keyId,
|
||||
nonce: nonce,
|
||||
createdAt: createdAt,
|
||||
messages: messages,
|
||||
status: status,
|
||||
|
|
@ -234,6 +275,9 @@ class _FeedbackViewImpl extends FeedbackView {
|
|||
String? kind,
|
||||
_i4q3oto5.FeedbackPerson? writer,
|
||||
String? payload,
|
||||
Object? space = _Undefined,
|
||||
Object? keyId = _Undefined,
|
||||
Object? nonce = _Undefined,
|
||||
DateTime? createdAt,
|
||||
List<_irmjyw1o.FeedbackMessageView>? messages,
|
||||
String? status,
|
||||
|
|
@ -250,6 +294,9 @@ class _FeedbackViewImpl extends FeedbackView {
|
|||
kind: kind ?? this.kind,
|
||||
writer: writer ?? this.writer.copyWith(),
|
||||
payload: payload ?? this.payload,
|
||||
space: space is _isc.UuidValue? ? space : this.space,
|
||||
keyId: keyId is String? ? keyId : this.keyId,
|
||||
nonce: nonce is String? ? nonce : this.nonce,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
messages: messages ?? this.messages.map((e0) => e0.copyWith()).toList(),
|
||||
status: status ?? this.status,
|
||||
|
|
|
|||
Loading…
Reference in a new issue