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
14 changed files with 2949 additions and 427 deletions
|
|
@ -1,7 +1,8 @@
|
|||
### 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.
|
||||
class: FeedbackDecision
|
||||
table: feedback_decision
|
||||
|
|
|
|||
|
|
@ -31,6 +31,26 @@ class FeedbackEndpoint extends Endpoint {
|
|||
viewer: session.authenticated!.authUserId,
|
||||
publicIds: {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.
|
||||
Future<List<FeedbackView>> threadsInSpace(
|
||||
Session session,
|
||||
UuidValue space,
|
||||
) async {
|
||||
final viewer = session.authenticated!.authUserId;
|
||||
final role = (await sharedSpacesOf(session, viewer))[space];
|
||||
if (role == null) {
|
||||
throw NotFoundException(what: 'space');
|
||||
}
|
||||
return await gatherSpaceFeedback(
|
||||
session,
|
||||
viewer: viewer,
|
||||
spaces: {space: role},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The person's inbox of reports and suggestions: those on their published
|
||||
|
|
@ -42,7 +62,9 @@ class InboxEndpoint extends Endpoint {
|
|||
Set<Scope> get requiredScopes => {identified};
|
||||
|
||||
/// 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.
|
||||
Future<List<FeedbackView>> inbox(Session session) async {
|
||||
final person = session.authenticated!.authUserId;
|
||||
// SELECT "publicId" FROM "published_studyset" WHERE "authUserId" = $1
|
||||
|
|
@ -53,14 +75,37 @@ class InboxEndpoint extends Endpoint {
|
|||
))
|
||||
studyset.publicId,
|
||||
};
|
||||
return await gatherFeedback(
|
||||
final published = await gatherFeedback(
|
||||
session,
|
||||
viewer: person,
|
||||
publicIds: mine,
|
||||
mine: true,
|
||||
);
|
||||
final inSpaces = [
|
||||
for (final view in await gatherSpaceFeedback(
|
||||
session,
|
||||
viewer: person,
|
||||
spaces: await sharedSpacesOf(session, person),
|
||||
mine: true,
|
||||
))
|
||||
if (view.mine || view.forMe) view,
|
||||
];
|
||||
if (inSpaces.isEmpty) {
|
||||
return published;
|
||||
}
|
||||
// Both the most recently active first, by when each was last active.
|
||||
return [...published, ...inSpaces]..sort(
|
||||
(a, b) => _lastActive(b).compareTo(_lastActive(a)),
|
||||
);
|
||||
}
|
||||
|
||||
/// When [view] was last active: written or replied to. A decision's time
|
||||
/// isn't in the view, so the merged inbox orders by the others alone.
|
||||
static DateTime _lastActive(FeedbackView view) => [
|
||||
view.createdAt,
|
||||
for (final message in view.messages) message.createdAt,
|
||||
].reduce((a, b) => a.isAfter(b) ? a : b);
|
||||
|
||||
/// How many threads of the inbox have something new.
|
||||
Future<int> unseen(Session session) async =>
|
||||
(await inbox(session)).where((thread) => thread.unseen).length;
|
||||
|
|
|
|||
|
|
@ -245,6 +245,224 @@ Future<List<FeedbackView>> gatherFeedback(
|
|||
return [for (final (_, view) in views) view];
|
||||
}
|
||||
|
||||
/// The threads within shared spaces [viewer] may see (Halkyon's ADR 0044,
|
||||
/// D44, D46, D47): those in [spaces] (each a space [viewer] is a member
|
||||
/// of), and with [mine] those [viewer] wrote in any space they are in, the
|
||||
/// most recently active first.
|
||||
///
|
||||
/// Rows come from people's own synced spaces, so what counts is decided
|
||||
/// here, as with [gatherFeedback]: a thread only of a space kind, naming
|
||||
/// the space, by someone who is a member now; a reply only from a member;
|
||||
/// a decision only from a row in the space itself, where the engine lets
|
||||
/// its Owner and Editors alone write. A hidden thread shows to its writer
|
||||
/// and the space's Owner and Editors alone. A sealed payload (a private
|
||||
/// shared studyset's) passes as it is, in base64 with its key and nonce,
|
||||
/// which the members' devices open.
|
||||
Future<List<FeedbackView>> gatherSpaceFeedback(
|
||||
Session session, {
|
||||
required UuidValue viewer,
|
||||
required Map<UuidValue, SharedSpaceRole> spaces,
|
||||
bool mine = false,
|
||||
}) async {
|
||||
if (spaces.isEmpty) {
|
||||
return const [];
|
||||
}
|
||||
final viewerSpace = mine ? await _spaceOf(session, viewer) : null;
|
||||
// SELECT … FROM "feedback_thread" WHERE "kind" = ANY($1)
|
||||
// AND ("publicationId" = ANY($2) OR "spaceId" = $3)
|
||||
final threads = await FeedbackThread.db.find(
|
||||
session,
|
||||
where: (t) =>
|
||||
t.kind.inSet(_spaceKinds) &
|
||||
(viewerSpace == null
|
||||
? t.publicationId.inSet(spaces.keys.toSet())
|
||||
: t.publicationId.inSet(spaces.keys.toSet()) |
|
||||
t.spaceId.equals(viewerSpace)),
|
||||
);
|
||||
// Only the spaces the viewer is in: another's thread they wrote counts
|
||||
// no more once they went.
|
||||
final inView = [
|
||||
for (final thread in threads)
|
||||
if (spaces.containsKey(thread.publicationId)) thread,
|
||||
];
|
||||
if (inView.isEmpty) {
|
||||
return const [];
|
||||
}
|
||||
final threadIds = {for (final thread in inView) thread.id!};
|
||||
final members = await membersOfSpaces(session, {
|
||||
for (final thread in inView) thread.publicationId,
|
||||
});
|
||||
|
||||
// SELECT … FROM "feedback_message" WHERE "threadId" = ANY($1)
|
||||
final messages = await FeedbackMessage.db.find(
|
||||
session,
|
||||
where: (t) => t.threadId.inSet(threadIds),
|
||||
);
|
||||
// SELECT … FROM "feedback_decision" WHERE "threadId" = ANY($1)
|
||||
final decisions = await FeedbackDecision.db.find(
|
||||
session,
|
||||
where: (t) => t.threadId.inSet(threadIds),
|
||||
);
|
||||
// Whose each space is: a person's own space bears their id, a shared
|
||||
// space its own.
|
||||
// SELECT … FROM "offline_sync_spaces" WHERE "id" = ANY($1)
|
||||
final uuidOf = {
|
||||
for (final space in await OfflineSyncSpace.db.find(
|
||||
session,
|
||||
where: (t) => t.id.inSet(<int>{
|
||||
for (final thread in inView) thread.spaceId!,
|
||||
for (final message in messages) message.spaceId!,
|
||||
for (final decision in decisions) decision.spaceId!,
|
||||
}),
|
||||
))
|
||||
space.id!: space.uuidSpaceId,
|
||||
};
|
||||
final profiles = await profilesOf(session, {
|
||||
for (final ofSpace in members.values) ...ofSpace.keys,
|
||||
});
|
||||
// SELECT … FROM "feedback_seen" WHERE "authUserId" = $1
|
||||
// AND "threadId" = ANY($2)
|
||||
final seen = {
|
||||
for (final row in await FeedbackSeen.db.find(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(viewer) & t.threadId.inSet(threadIds),
|
||||
))
|
||||
row.threadId: row.seenAt,
|
||||
};
|
||||
|
||||
FeedbackPerson personOf(UuidValue person) => switch (profiles[person]) {
|
||||
final profile? => FeedbackPerson(
|
||||
handle: profile.handle,
|
||||
displayName: profile.displayName,
|
||||
avatarUrl: profile.avatarUrl,
|
||||
),
|
||||
null => FeedbackPerson(handle: '', displayName: ''),
|
||||
};
|
||||
|
||||
final views = <(DateTime, FeedbackView)>[];
|
||||
for (final thread in inView) {
|
||||
final space = thread.publicationId;
|
||||
final roles = members[space] ?? const {};
|
||||
final writer = uuidOf[thread.spaceId];
|
||||
final payload = _passed(thread.payload, sealed: thread.keyId != null);
|
||||
if (writer == null || !roles.containsKey(writer) || payload == null) {
|
||||
continue;
|
||||
}
|
||||
bool decides(UuidValue person) => switch (roles[person]) {
|
||||
SharedSpaceRole.owner || SharedSpaceRole.editor => true,
|
||||
_ => false,
|
||||
};
|
||||
final mineToo = writer == viewer;
|
||||
final forMe = decides(viewer);
|
||||
|
||||
// Decisions written in the space itself, the latest of each pair
|
||||
// standing.
|
||||
var status = 'open';
|
||||
var hidden = false;
|
||||
DateTime? decidedAt;
|
||||
final ofThread = [
|
||||
for (final decision in decisions)
|
||||
if (decision.threadId == thread.id && uuidOf[decision.spaceId] == space)
|
||||
decision,
|
||||
]..sort((a, b) => a.id!.uuid.compareTo(b.id!.uuid));
|
||||
for (final decision in ofThread) {
|
||||
switch (decision.decision) {
|
||||
case 'closed' || 'accepted' || 'declined':
|
||||
status = decision.decision;
|
||||
case 'reopened':
|
||||
status = 'open';
|
||||
case 'hidden':
|
||||
hidden = true;
|
||||
case 'shown':
|
||||
hidden = false;
|
||||
}
|
||||
decidedAt = _timeOf(decision.id!);
|
||||
}
|
||||
if (hidden && !mineToo && !forMe) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final replies = <FeedbackMessageView>[];
|
||||
// When others than the viewer replied.
|
||||
final othersReplied = <DateTime>[];
|
||||
for (final message
|
||||
in messages.where((message) => message.threadId == thread.id).toList()
|
||||
..sort((a, b) => a.id!.uuid.compareTo(b.id!.uuid))) {
|
||||
final by = uuidOf[message.spaceId];
|
||||
final text = _passed(message.payload, sealed: message.keyId != null);
|
||||
if (by == null || !roles.containsKey(by) || text == null) {
|
||||
continue;
|
||||
}
|
||||
replies.add(
|
||||
FeedbackMessageView(
|
||||
id: message.id!,
|
||||
by: personOf(by),
|
||||
fromAuthor: decides(by),
|
||||
payload: text,
|
||||
keyId: message.keyId,
|
||||
nonce: _base64(message.nonce),
|
||||
createdAt: _timeOf(message.id!),
|
||||
),
|
||||
);
|
||||
if (by != viewer) {
|
||||
othersReplied.add(_timeOf(message.id!));
|
||||
}
|
||||
}
|
||||
|
||||
final createdAt = _timeOf(thread.id!);
|
||||
// What others did last: what is new to the viewer.
|
||||
final theirs = [
|
||||
if (!mineToo) createdAt,
|
||||
...othersReplied,
|
||||
if (mineToo && !forMe) ?decidedAt,
|
||||
];
|
||||
final lastSeen = seen[thread.id];
|
||||
final active = [
|
||||
createdAt,
|
||||
for (final reply in replies) reply.createdAt,
|
||||
?decidedAt,
|
||||
].reduce((a, b) => a.isAfter(b) ? a : b);
|
||||
views.add((
|
||||
active,
|
||||
FeedbackView(
|
||||
threadId: thread.id!,
|
||||
publicId: space,
|
||||
studysetTitle: '',
|
||||
location: '',
|
||||
kind: thread.kind,
|
||||
writer: personOf(writer),
|
||||
payload: payload,
|
||||
space: space,
|
||||
keyId: thread.keyId,
|
||||
nonce: _base64(thread.nonce),
|
||||
createdAt: createdAt,
|
||||
messages: replies,
|
||||
status: status,
|
||||
hidden: hidden,
|
||||
mine: mineToo,
|
||||
forMe: forMe,
|
||||
unseen:
|
||||
(mineToo || forMe) &&
|
||||
theirs.any((at) => lastSeen == null || at.isAfter(lastSeen)),
|
||||
),
|
||||
));
|
||||
}
|
||||
views.sort((a, b) => b.$1.compareTo(a.$1));
|
||||
return [for (final (_, view) in views) view];
|
||||
}
|
||||
|
||||
/// The kinds of thread within a shared space (D46).
|
||||
const _spaceKinds = {'space-report', 'space-suggestion'};
|
||||
|
||||
/// A payload as a view passes it: sealed, in base64; readable, its JSON's
|
||||
/// text; null for a blank one or one that isn't readable as it says.
|
||||
String? _passed(ByteData payload, {required bool sealed}) => sealed
|
||||
? (payload.lengthInBytes == 0 ? null : _base64(payload))
|
||||
: _text(payload);
|
||||
|
||||
String? _base64(ByteData? bytes) =>
|
||||
bytes == null ? null : base64Encode(Uint8List.sublistView(bytes));
|
||||
|
||||
/// The id of [person]'s own space; null while they have none.
|
||||
Future<int?> _spaceOf(Session session, UuidValue person) async =>
|
||||
// SELECT "id" FROM "offline_sync_spaces" WHERE "uuidSpaceId" = $1
|
||||
|
|
|
|||
|
|
@ -10,11 +10,13 @@ database: sync
|
|||
fields:
|
||||
### The thread replied in.
|
||||
threadId: UuidValue
|
||||
### 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).
|
||||
keyId: String?
|
||||
### Its content: UTF-8 JSON with a `schemaVersion` and its text.
|
||||
payload: ByteData
|
||||
### The encryption nonce; null, as for keyId.
|
||||
### The encryption nonce, or a readable row's signature; null, as for
|
||||
### keyId.
|
||||
nonce: ByteData?
|
||||
indexes:
|
||||
# Deleting a space finds its rows through this index (see studyset.spy.yaml).
|
||||
|
|
|
|||
|
|
@ -2,10 +2,15 @@
|
|||
class: FeedbackMessageView
|
||||
fields:
|
||||
id: UuidValue
|
||||
### 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).
|
||||
by: FeedbackPerson
|
||||
fromAuthor: bool
|
||||
### What it says: its payload as written.
|
||||
### What it says: its payload as written; sealed (keyId), in base64.
|
||||
payload: String
|
||||
### The key a reply in a private shared studyset is sealed with, and its
|
||||
### nonce in base64; null while readable.
|
||||
keyId: String?
|
||||
nonce: String?
|
||||
### When, in UTC.
|
||||
createdAt: DateTime
|
||||
|
|
|
|||
|
|
@ -11,15 +11,20 @@ table: feedback_thread
|
|||
database: sync
|
||||
fields:
|
||||
### 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).
|
||||
publicationId: UuidValue
|
||||
### '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.
|
||||
kind: String
|
||||
### 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).
|
||||
keyId: String?
|
||||
### Its content: UTF-8 JSON with a `schemaVersion`.
|
||||
payload: ByteData
|
||||
### The encryption nonce; null, as for keyId.
|
||||
### The encryption nonce, or a readable row's signature; null, as for
|
||||
### keyId.
|
||||
nonce: ByteData?
|
||||
indexes:
|
||||
# Deleting a space finds its rows through this index (see studyset.spy.yaml).
|
||||
|
|
|
|||
|
|
@ -1,19 +1,28 @@
|
|||
### 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.
|
||||
class: FeedbackView
|
||||
fields:
|
||||
threadId: UuidValue
|
||||
### 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.
|
||||
publicId: UuidValue
|
||||
studysetTitle: String
|
||||
location: String
|
||||
### 'report' or 'suggestion'.
|
||||
### 'report' or 'suggestion'; 'space-report' or 'space-suggestion' in a
|
||||
### shared space.
|
||||
kind: String
|
||||
### Who wrote it.
|
||||
writer: FeedbackPerson
|
||||
### What it says: its payload as written.
|
||||
### What it says: its payload as written; sealed (keyId), in base64.
|
||||
payload: String
|
||||
### The shared space it is in (D46); null on a published studyset.
|
||||
space: UuidValue?
|
||||
### The key a thread in a private shared studyset is sealed with, and
|
||||
### its nonce in base64; null while readable.
|
||||
keyId: String?
|
||||
nonce: String?
|
||||
### When it was written, in UTC.
|
||||
createdAt: DateTime
|
||||
### Its replies, oldest first.
|
||||
|
|
@ -22,7 +31,8 @@ fields:
|
|||
status: String
|
||||
### Whether the author hid it: then only its writer and the author see it.
|
||||
hidden: bool
|
||||
### 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).
|
||||
mine: bool
|
||||
forMe: bool
|
||||
### Whether something in it is new to the caller.
|
||||
|
|
|
|||
|
|
@ -1041,6 +1041,25 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
params['publicId'],
|
||||
),
|
||||
),
|
||||
'threadsInSpace': _is.MethodConnector(
|
||||
name: 'threadsInSpace',
|
||||
params: {
|
||||
'space': _is.ParameterDescription(
|
||||
name: 'space',
|
||||
type: _is.getType<_is.UuidValue>(),
|
||||
nullable: false,
|
||||
),
|
||||
},
|
||||
call:
|
||||
(
|
||||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
) async => (endpoints['feedback'] as _i3n4p88h.FeedbackEndpoint)
|
||||
.threadsInSpace(
|
||||
session,
|
||||
params['space'],
|
||||
),
|
||||
),
|
||||
},
|
||||
);
|
||||
connectors['inbox'] = _is.EndpointConnector(
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ import 'package:serverpod/serverpod.dart' as _is;
|
|||
/// 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 _is.TableRow<_is.UuidValue?>, _is.ProtocolSerialization {
|
||||
|
|
|
|||
|
|
@ -69,13 +69,15 @@ abstract class FeedbackMessage
|
|||
/// The thread replied in.
|
||||
_is.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
|
||||
|
|
@ -254,13 +256,15 @@ class FeedbackMessageTable extends _is.Table<_is.UuidValue?> {
|
|||
/// The thread replied in.
|
||||
late final _is.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 _is.ColumnString keyId;
|
||||
|
||||
/// Its content: UTF-8 JSON with a `schemaVersion` and its text.
|
||||
late final _is.ColumnByteData payload;
|
||||
|
||||
/// The encryption nonce; null, as for keyId.
|
||||
/// The encryption nonce, or a readable row's signature; null, as for
|
||||
/// keyId.
|
||||
late final _is.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: _is.DateTimeJsonExtension.fromJson(
|
||||
jsonSerialization['createdAt'],
|
||||
),
|
||||
|
|
@ -51,14 +57,21 @@ abstract class FeedbackMessageView
|
|||
|
||||
_is.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 _is.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,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,19 +72,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).
|
||||
_is.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
|
||||
|
|
@ -278,19 +283,24 @@ class FeedbackThreadTable extends _is.Table<_is.UuidValue?> {
|
|||
late final _is.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 _is.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 _is.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 _is.ColumnString keyId;
|
||||
|
||||
/// Its content: UTF-8 JSON with a `schemaVersion`.
|
||||
late final _is.ColumnByteData payload;
|
||||
|
||||
/// The encryption nonce; null, as for keyId.
|
||||
/// The encryption nonce, or a readable row's signature; null, as for
|
||||
/// keyId.
|
||||
late final _is.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 _is.SerializableModel, _is.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,
|
||||
_is.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
|
||||
: _is.UuidValueJsonExtension.fromJson(jsonSerialization['space']),
|
||||
keyId: jsonSerialization['keyId'] as String?,
|
||||
nonce: jsonSerialization['nonce'] as String?,
|
||||
createdAt: _is.DateTimeJsonExtension.fromJson(
|
||||
jsonSerialization['createdAt'],
|
||||
),
|
||||
|
|
@ -86,22 +98,33 @@ abstract class FeedbackView
|
|||
|
||||
_is.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.
|
||||
_is.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.
|
||||
_is.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,
|
||||
_is.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 _is.UuidValue threadId,
|
||||
|
|
@ -198,6 +233,9 @@ class _FeedbackViewImpl extends FeedbackView {
|
|||
required String kind,
|
||||
required _i4q3oto5.FeedbackPerson writer,
|
||||
required String payload,
|
||||
_is.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 _is.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,
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ learningCount:
|
|||
- reportCopies:
|
||||
feedback:
|
||||
- threadsOf:
|
||||
- threadsInSpace:
|
||||
inbox:
|
||||
- inbox:
|
||||
- unseen:
|
||||
|
|
|
|||
Loading…
Reference in a new issue